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 }