Coverage Summary for Class: Reader (net.sf.persism)

Class Method, % Line, %
Reader 100% (8/8) 98.9% (183/185)
Reader$1 100% (1/1) 100% (1/1)
Total 100% (9/9) 98.9% (184/186)


1 package net.sf.persism; 2  3 import net.sf.persism.annotations.NotTable; 4  5 import java.beans.ConstructorProperties; 6 import java.io.ByteArrayOutputStream; 7 import java.io.IOException; 8 import java.io.InputStream; 9 import java.io.StringWriter; 10 import java.lang.reflect.*; 11 import java.math.BigDecimal; 12 import java.math.BigInteger; 13 import java.sql.*; 14 import java.util.*; 15 import java.util.stream.Collectors; 16  17 final class Reader { 18  19  private static final Log log = Log.getLogger(Reader.class); 20  21  private Connection connection; 22  private MetaData metaData; 23  private Converter converter; 24  25  Reader(Session session) { 26  this.connection = session.getConnection(); 27  this.metaData = session.getMetaData(); 28  this.converter = session.getConverter(); 29  } 30  31  <T> T readObject(T object, ResultSet rs) throws IllegalAccessException, SQLException, InvocationTargetException, IOException { 32  33  Class<?> objectClass = object.getClass(); 34  35  // We should never call this method with a primitive type. 36  assert Types.getType(objectClass) == null; 37  38  Map<String, PropertyInfo> properties; 39  if (objectClass.getAnnotation(NotTable.class) == null) { 40  properties = metaData.getTableColumnsPropertyInfo(objectClass, connection); 41  } else { 42  properties = metaData.getQueryColumnsPropertyInfo(objectClass, rs); 43  } 44  45  // Test if all properties have column mapping and throw PersismException if not 46  // This block verifies that the object is fully initialized. 47  // Any properties not marked by NotColumn should have been set (or if they have a getter only) 48  // If not throw a PersismException 49  Collection<PropertyInfo> allProperties = MetaData.getPropertyInfo(objectClass); 50  if (properties.values().size() < allProperties.size()) { 51  Set<PropertyInfo> missing = new HashSet<>(allProperties.size()); 52  missing.addAll(allProperties); 53  missing.removeAll(properties.values()); 54  55  StringBuilder sb = new StringBuilder(); 56  String sep = ""; 57  for (PropertyInfo prop : missing) { 58  sb.append(sep).append(prop.propertyName); 59  sep = ","; 60  } 61  throw new PersismException(Messages.ObjectNotProperlyInitialized.message(objectClass, sb)); 62  } 63  64  ResultSetMetaData rsmd = rs.getMetaData(); 65  int columnCount = rsmd.getColumnCount(); 66  List<String> foundColumns = new ArrayList<>(columnCount); 67  68  for (int j = 1; j <= columnCount; j++) { 69  70  String columnName = rsmd.getColumnLabel(j); 71  PropertyInfo columnProperty = getPropertyInfo(columnName, properties); //properties.get(columnName); 72  73  if (columnProperty != null) { 74  Class<?> returnType = columnProperty.getter.getReturnType(); 75  76  Object value = readColumn(rs, j, returnType); 77  78  foundColumns.add(columnName); 79  80  if (value != null) { 81  try { 82  if (columnProperty.readOnly) { 83  // set the value on the field directly 84  columnProperty.field.setAccessible(true); 85  columnProperty.field.set(object, value); 86  columnProperty.field.setAccessible(false); 87  } else { 88  columnProperty.setter.invoke(object, value); 89  } 90  } catch (IllegalArgumentException e) { 91  throw new PersismException(Messages.IllegalArgumentReadingColumn.message(columnProperty.propertyName, objectClass, columnName, returnType, value.getClass(), value), e); 92  } 93  } 94  } 95  } 96  97  // This is doing a similar check to above but on the ResultSet itself. 98  // This tests for when a user writes their own SQL and forgets a column. 99  if (foundColumns.size() < properties.keySet().size()) { 100  101  Set<String> missing = new HashSet<>(columnCount); 102  missing.addAll(properties.keySet()); 103  foundColumns.forEach(missing::remove); 104  105  throw new PersismException(Messages.ObjectNotProperlyInitializedByQuery.message(objectClass, foundColumns, missing)); 106  } 107  108  if (object instanceof Persistable) { 109  // Save this object initial state to later detect changed properties 110  ((Persistable<?>) object).saveReadState(); 111  } 112  113  return object; 114  } 115  116  <T> T readRecord(Class<T> objectClass, ResultSet rs) throws SQLException, IOException { 117  // resultset may not have columns in the proper order 118  // resultset may not have all columns 119  // step 1 - get column order based on which properties are found 120  // read 121  // Note: We can't use this method to read Objects without default constructors using the Record styled conventions 122  // since Java 8 does not have the constructor parameter names. 123  124  Map<String, PropertyInfo> propertiesByColumn; 125  if (objectClass.getAnnotation(NotTable.class) == null) { 126  propertiesByColumn = metaData.getTableColumnsPropertyInfo(objectClass, connection); 127  } else { 128  propertiesByColumn = metaData.getQueryColumnsPropertyInfo(objectClass, rs); 129  } 130  131  List<String> propertyNames = metaData.getPropertyNames(objectClass); 132  133  Constructor<?> selectedConstructor = findConstructor(objectClass, propertyNames); 134  135  // now read resultset by property order 136  Map<String, PropertyInfo> propertyInfoByConstructorOrder = new LinkedHashMap<>(selectedConstructor.getParameterCount()); 137  for (String paramName : propertyNames) { 138  for (String col : propertiesByColumn.keySet()) { 139  if (paramName.equals(propertiesByColumn.get(col).field.getName())) { 140  propertyInfoByConstructorOrder.put(col, propertiesByColumn.get(col)); 141  } 142  } 143  } 144  145  // now read by this order 146  List<Object> constructorParams = new ArrayList<>(12); 147  List<Class<?>> constructorTypes = new ArrayList<>(12); 148  149  ResultSetMetaData rsmd = rs.getMetaData(); 150  Map<String, Integer> ordinals = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 151  for (int j = 1; j <= rsmd.getColumnCount(); j++) { 152  ordinals.put(rsmd.getColumnLabel(j), j); 153  } 154  for (String col : propertyInfoByConstructorOrder.keySet()) { 155  if (ordinals.containsKey(col)) { 156  Field field = propertyInfoByConstructorOrder.get(col).field; 157  Object value = readColumn(rs, ordinals.get(col), field.getType()); 158  if (value == null && field.getType().isPrimitive()) { 159  // Set null primitives to their default, otherwise the constructor will not be found 160  value = Types.getDefaultValue(field.getType()); 161  } 162  163  constructorParams.add(value); 164  constructorTypes.add(propertyInfoByConstructorOrder.get(col).field.getType()); 165  } else { 166  throw new PersismException(Messages.ReadRecordColumnNotFound.message(objectClass, col)); 167  } 168  } 169  170  try { 171  // Select the constructor to double check we have the correct one 172  // This would be a double check on the types rather than just the names 173  Constructor<?> constructor = objectClass.getConstructor(constructorTypes.toArray(new Class<?>[0])); 174  //noinspection unchecked 175  return (T) constructor.newInstance(constructorParams.toArray()); 176  177  } catch (Exception e) { 178  throw new PersismException(Messages.ReadRecordCouldNotInstantiate.message(objectClass, constructorParams, constructorTypes)); 179  } 180  } 181  182  // https://stackoverflow.com/questions/67126109/is-there-a-way-to-recognise-a-java-16-records-canonical-constructor-via-reflect 183  // Can't be used with Java 8 184 // private static <T> Constructor<T> getCanonicalConstructor(Class<T> recordClass) 185 // throws NoSuchMethodException, SecurityException { 186 // Class<?>[] componentTypes = Arrays.stream(recordClass.getRecordComponents()) 187 // .map(RecordComponent::getType) 188 // .toArray(Class<?>[]::new); 189 // return recordClass.getDeclaredConstructor(componentTypes); 190 // } 191  192  private Constructor<?> findConstructor(Class<?> objectClass, List<String> propertyNames) { 193  Constructor<?>[] constructors = objectClass.getConstructors(); 194  Constructor<?> selectedConstructor = null; 195  196  for (Constructor<?> constructor : constructors) { 197  198  // Check with canonical or maybe -parameters 199  List<String> parameterNames = Arrays.stream(constructor.getParameters()). 200  map(Parameter::getName).collect(Collectors.toList()); 201  202  if (listEqualsIgnoreOrder(propertyNames, parameterNames)) { 203  // re-arrange the propertyNames to match parameterNames 204  propertyNames.clear(); 205  propertyNames.addAll(parameterNames); 206  selectedConstructor = constructor; 207  break; 208  } 209  210  // Check with ConstructorProperties 211  ConstructorProperties constructorProperties = constructor.getAnnotation(ConstructorProperties.class); 212  if (constructorProperties != null) { 213  parameterNames = Arrays.asList(constructorProperties.value()); 214  if (listEqualsIgnoreOrder(propertyNames, parameterNames)) { 215  // re-arrange the propertyNames to match parameterNames 216  propertyNames.clear(); 217  propertyNames.addAll(parameterNames); 218  selectedConstructor = constructor; 219  break; 220  } 221  } 222  } 223  224  log.debug("findConstructor: %s", selectedConstructor); 225  if (selectedConstructor == null) { 226  throw new PersismException(Messages.CouldNotFindConstructorForRecord.message(objectClass, propertyNames)); 227  } 228  return selectedConstructor; 229  } 230  231  // https://stackoverflow.com/questions/1075656/simple-way-to-find-if-two-different-lists-contain-exactly-the-same-elements 232  private static <T> boolean listEqualsIgnoreOrder(List<T> list1, List<T> list2) { 233  return new HashSet<>(list1).equals(new HashSet<>(list2)); 234  } 235  236  <T> T readColumn(ResultSet rs, int column, Class<?> returnType) throws SQLException, IOException { 237  ResultSetMetaData rsmd = rs.getMetaData(); 238  int sqlColumnType = rsmd.getColumnType(column); 239  String columnName = rsmd.getColumnLabel(column); 240  241  if (returnType.isEnum()) { 242  // Some DBs may read an enum type as other 1111 - we can tell it here to read it as a string. 243  sqlColumnType = java.sql.Types.CHAR; 244  } 245  246  Object value = null; 247  248  Types columnType = Types.convert(sqlColumnType); // note this could be null if we can't match a type 249  if (columnType != null) { 250  251  switch (columnType) { 252  253  case BooleanType: 254  case booleanType: 255  if (returnType.equals(Boolean.class) || returnType.equals(boolean.class)) { 256  value = rs.getBoolean(column); 257  } else { 258  value = rs.getByte(column); 259  } 260  break; 261  262  case TimestampType: 263  if (returnType.equals(String.class)) { // JTDS 264  value = rs.getString(column); 265  } else { 266  // work around to Oracle reading a oracle.sql.TIMESTAMP class with getObject 267  value = rs.getTimestamp(column); 268  } 269  break; 270  271  case ByteArrayType: 272  case byteArrayType: 273  value = rs.getBytes(column); 274  break; 275  276  case ClobType: 277  Clob clob = rs.getClob(column); 278  if (clob != null) { 279  try (InputStream in = clob.getAsciiStream()) { 280  StringWriter writer = new StringWriter(); 281  282  int c = -1; 283  while ((c = in.read()) != -1) { 284  writer.write(c); 285  } 286  writer.flush(); 287  value = writer.toString(); 288  } 289  } 290  break; 291  292  case BlobType: 293  byte[] buffer = new byte[1024]; 294  Blob blob = rs.getBlob(column); 295  if (blob != null) { 296  try (InputStream in = blob.getBinaryStream()) { 297  ByteArrayOutputStream bos = new ByteArrayOutputStream((int) blob.length()); 298  for (int len; (len = in.read(buffer)) != -1; ) { 299  bos.write(buffer, 0, len); 300  } 301  value = bos.toByteArray(); 302  } 303  } 304  break; 305  306  case IntegerType: 307  // stupid SQLite reports LONGS as Integers for date types which WRAPS past Integer.MAX - Clowns. 308  if (metaData.getConnectionType() == ConnectionTypes.SQLite) { 309  value = rs.getObject(column); 310  if (value != null) { 311  if (value instanceof Long) { 312  value = rs.getLong(column); 313  } else { 314  value = rs.getInt(column); 315  } 316  } 317  } else { 318  value = rs.getObject(column) == null ? null : rs.getInt(column); 319  } 320  break; 321  322  case LongType: 323  value = rs.getObject(column) == null ? null : rs.getLong(column); 324  break; 325  326  case FloatType: 327  value = rs.getObject(column) == null ? null : rs.getFloat(column); 328  break; 329  330  case DoubleType: 331  value = rs.getObject(column) == null ? null : rs.getDouble(column); 332  break; 333  334  case BigIntegerType: 335  case BigDecimalType: 336  value = null; 337  if (returnType.equals(BigInteger.class)) { 338  BigDecimal bd = rs.getBigDecimal(column); 339  if (bd != null) { 340  value = bd.toBigInteger(); 341  } 342  } else { 343  value = rs.getBigDecimal(column); 344  } 345  break; 346  347  case TimeType: 348  value = rs.getTime(column); 349  break; 350  351 // We can't assume rs.getDate will work. SQLITE actually has a long value in here. 352 // We can live with rs.getObject and the convert method will handle it. 353 // case SQLDateType: 354 // case UtilDateType: 355 // value = rs.getDate(column); 356 // break; 357  358  case StringType: 359  if (returnType.equals(Character.class) || returnType.equals(char.class)) { 360  String s = rs.getString(column); 361  if (s != null && s.length() > 0) { 362  value = s.charAt(0); 363  } 364  break; 365  } 366  367  value = rs.getString(column); 368  break; 369  370  default: 371  value = rs.getObject(column); 372  } 373  374  } else { 375  log.warn(Messages.ColumnTypeNotKnownForSQLType.message(sqlColumnType), new Throwable()); 376  value = rs.getObject(column); 377  } 378  379  // If value is null or column type is unknown - no need to try to convert anything. 380  if (value != null && columnType != null) { 381  value = converter.convert(value, returnType, columnName); 382  } 383  384  //noinspection unchecked 385  return (T) value; 386  } 387  388  // Poor man's case insensitive linked hash map ;) 389  private PropertyInfo getPropertyInfo(String col, Map<String, PropertyInfo> properties) { 390  for (String key : properties.keySet()) { 391  if (key.equalsIgnoreCase(col)) { 392  return properties.get(key); 393  } 394  } 395  return null; 396  } 397  398 }