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 }