1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.dbutils;
18
19 import java.beans.BeanInfo;
20 import java.beans.IntrospectionException;
21 import java.beans.Introspector;
22 import java.beans.PropertyDescriptor;
23 import java.lang.reflect.Field;
24 import java.lang.reflect.InvocationTargetException;
25 import java.lang.reflect.Method;
26 import java.sql.ResultSet;
27 import java.sql.ResultSetMetaData;
28 import java.sql.SQLException;
29 import java.util.ArrayList;
30 import java.util.Arrays;
31 import java.util.HashMap;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.ServiceLoader;
35
36 /**
37 * <p>
38 * {@code BeanProcessor} matches column names to bean property names
39 * and converts {@code ResultSet} columns into objects for those bean
40 * properties. Subclasses should override the methods in the processing chain
41 * to customize behavior.
42 * </p>
43 *
44 * <p>
45 * This class is thread-safe.
46 * </p>
47 *
48 * @see BasicRowProcessor
49 *
50 * @since 1.1
51 */
52 public class BeanProcessor {
53
54 /**
55 * Special array value used by {@code mapColumnsToProperties} that
56 * indicates there is no bean property that matches a column from a
57 * {@code ResultSet}.
58 */
59 protected static final int PROPERTY_NOT_FOUND = -1;
60
61 /**
62 * Set a bean's primitive properties to these defaults when SQL NULL
63 * is returned. These are the same as the defaults that ResultSet get*
64 * methods return in the event of a NULL column.
65 */
66 private static final Map<Class<?>, Object> PRIMITIVE_DEFAULTS = new HashMap<>();
67
68 private static final List<ColumnHandler<?>> COLUMN_HANDLERS = new ArrayList<>();
69
70 private static final List<PropertyHandler> PROPERTY_HANDLERS = new ArrayList<>();
71
72 static {
73 PRIMITIVE_DEFAULTS.put(Integer.TYPE, Integer.valueOf(0));
74 PRIMITIVE_DEFAULTS.put(Short.TYPE, Short.valueOf((short) 0));
75 PRIMITIVE_DEFAULTS.put(Byte.TYPE, Byte.valueOf((byte) 0));
76 PRIMITIVE_DEFAULTS.put(Float.TYPE, Float.valueOf(0f));
77 PRIMITIVE_DEFAULTS.put(Double.TYPE, Double.valueOf(0d));
78 PRIMITIVE_DEFAULTS.put(Long.TYPE, Long.valueOf(0L));
79 PRIMITIVE_DEFAULTS.put(Boolean.TYPE, Boolean.FALSE);
80 PRIMITIVE_DEFAULTS.put(Character.TYPE, Character.valueOf((char) 0));
81
82 // Use a ServiceLoader to find implementations
83 ServiceLoader.load(ColumnHandler.class).forEach(COLUMN_HANDLERS::add);
84
85 // Use a ServiceLoader to find implementations
86 ServiceLoader.load(PropertyHandler.class).forEach(PROPERTY_HANDLERS::add);
87 }
88
89 /**
90 * ResultSet column to bean property name overrides.
91 */
92 private final Map<String, String> columnToPropertyOverrides;
93
94 /**
95 * Constructor for BeanProcessor.
96 */
97 public BeanProcessor() {
98 this(new HashMap<>());
99 }
100
101 /**
102 * Constructor for BeanProcessor configured with column to property name overrides.
103 *
104 * @param columnToPropertyOverrides ResultSet column to bean property name overrides
105 * @since 1.5
106 */
107 public BeanProcessor(final Map<String, String> columnToPropertyOverrides) {
108 if (columnToPropertyOverrides == null) {
109 throw new IllegalArgumentException("columnToPropertyOverrides map cannot be null");
110 }
111 this.columnToPropertyOverrides = columnToPropertyOverrides;
112 }
113
114 /**
115 * Calls the setter method on the target object for the given property.
116 * If no setter method exists for the property, this method does nothing.
117 * @param target The object to set the property on.
118 * @param prop The property to set.
119 * @param value The value to pass into the setter.
120 * @throws SQLException if an error occurs setting the property.
121 */
122 private void callSetter(final Object target, final PropertyDescriptor prop, Object value)
123 throws SQLException {
124
125 final Method setter = getWriteMethod(target, prop, value);
126
127 if (setter == null || setter.getParameterTypes().length != 1) {
128 return;
129 }
130
131 try {
132 final Class<?> firstParam = setter.getParameterTypes()[0];
133 for (final PropertyHandler handler : PROPERTY_HANDLERS) {
134 if (handler.match(firstParam, value)) {
135 value = handler.apply(firstParam, value);
136 break;
137 }
138 }
139
140 // Don't call setter if the value object isn't the right type
141 if (!this.isCompatibleType(value, firstParam)) {
142 throw new SQLException(
143 "Cannot set " + prop.getName() + ": incompatible types, cannot convert " + value.getClass().getName() + " to " + firstParam.getName());
144 // value cannot be null here because isCompatibleType allows null
145 }
146 setter.invoke(target, value);
147
148 } catch (final IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {
149 throw new SQLException("Cannot set " + prop.getName() + ": " + e.getMessage());
150 }
151 }
152
153 /**
154 * Creates a new object and initializes its fields from the ResultSet.
155 * @param <T> The type of bean to create
156 * @param resultSet The result set.
157 * @param type The bean type (the return type of the object).
158 * @param props The property descriptors.
159 * @param columnToProperty The column indices in the result set.
160 * @return An initialized object.
161 * @throws SQLException if a database error occurs.
162 */
163 private <T> T createBean(final ResultSet resultSet, final Class<T> type, final PropertyDescriptor[] props, final int[] columnToProperty)
164 throws SQLException {
165 return populateBean(resultSet, this.newInstance(type), props, columnToProperty);
166 }
167
168 /**
169 * Get the write method to use when setting {@code value} to the {@code target}.
170 *
171 * @param target Object where the write method will be called.
172 * @param prop BeanUtils information.
173 * @param value The value that will be passed to the write method.
174 * @return The {@link java.lang.reflect.Method} to call on {@code target} to write {@code value} or {@code null} if
175 * there is no suitable write method.
176 */
177 protected Method getWriteMethod(final Object target, final PropertyDescriptor prop, final Object value) {
178 return prop.getWriteMethod();
179 }
180
181 /**
182 * ResultSet.getObject() returns an Integer object for an INT column. The
183 * setter method for the property might take an Integer or a primitive int.
184 * This method returns true if the value can be successfully passed into
185 * the setter method. Remember, Method.invoke() handles the unwrapping
186 * of Integer into an int.
187 *
188 * @param value The value to be passed into the setter method.
189 * @param type The setter's parameter type (non-null)
190 * @return boolean True if the value is compatible (null => true)
191 */
192 private boolean isCompatibleType(final Object value, final Class<?> type) {
193 // Do object check first, then primitives
194 return value == null || type.isInstance(value) || matchesPrimitive(type, value.getClass());
195 }
196
197 /**
198 * The positions in the returned array represent column numbers. The
199 * values stored at each position represent the index in the
200 * {@code PropertyDescriptor[]} for the bean property that matches
201 * the column name. If no bean property was found for a column, the
202 * position is set to {@code PROPERTY_NOT_FOUND}.
203 *
204 * @param rsmd The {@code ResultSetMetaData} containing column
205 * information.
206 *
207 * @param props The bean property descriptors.
208 *
209 * @throws SQLException if a database access error occurs
210 *
211 * @return An int[] with column index to property index mappings. The 0th
212 * element is meaningless because JDBC column indexing starts at 1.
213 */
214 protected int[] mapColumnsToProperties(final ResultSetMetaData rsmd,
215 final PropertyDescriptor[] props) throws SQLException {
216
217 final int cols = rsmd.getColumnCount();
218 final int[] columnToProperty = new int[cols + 1];
219 Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND);
220
221 for (int col = 1; col <= cols; col++) {
222 String columnName = rsmd.getColumnLabel(col);
223 if (null == columnName || 0 == columnName.length()) {
224 columnName = rsmd.getColumnName(col);
225 }
226 String propertyName = columnToPropertyOverrides.get(columnName);
227 if (propertyName == null) {
228 propertyName = columnName;
229 }
230 if (propertyName == null) {
231 propertyName = Integer.toString(col);
232 }
233
234 for (int i = 0; i < props.length; i++) {
235 final PropertyDescriptor prop = props[i];
236 final Method reader = prop.getReadMethod();
237
238 // Check for @Column annotations as explicit marks
239 final Column column;
240 if (reader != null) {
241 column = reader.getAnnotation(Column.class);
242 } else {
243 column = null;
244 }
245
246 final String propertyColumnName;
247 if (column != null) {
248 propertyColumnName = column.name();
249 } else {
250 propertyColumnName = prop.getName();
251 }
252 if (propertyName.equalsIgnoreCase(propertyColumnName)) {
253 columnToProperty[col] = i;
254 break;
255 }
256 }
257 }
258
259 return columnToProperty;
260 }
261
262 /**
263 * Check whether a value is of the same primitive type as {@code targetType}.
264 *
265 * @param targetType The primitive type to target.
266 * @param valueType The value to match to the primitive type.
267 * @return Whether {@code valueType} can be coerced (e.g. autoboxed) into {@code targetType}.
268 */
269 private boolean matchesPrimitive(final Class<?> targetType, final Class<?> valueType) {
270 if (!targetType.isPrimitive()) {
271 return false;
272 }
273
274 try {
275 // see if there is a "TYPE" field. This is present for primitive wrappers.
276 final Field typeField = valueType.getField("TYPE");
277 final Object primitiveValueType = typeField.get(valueType);
278
279 if (targetType == primitiveValueType) {
280 return true;
281 }
282 } catch (final NoSuchFieldException | IllegalAccessException ignored) {
283 // an inaccessible TYPE field is a good sign that we're not working with a primitive wrapper.
284 // nothing to do. we can't match for compatibility
285 }
286 return false;
287 }
288
289 /**
290 * Factory method that returns a new instance of the given Class. This
291 * is called at the start of the bean creation process and may be
292 * overridden to provide custom behavior like returning a cached bean
293 * instance.
294 * @param <T> The type of object to create
295 * @param c The Class to create an object from.
296 * @return A newly created object of the Class.
297 * @throws SQLException if creation failed.
298 */
299 protected <T> T newInstance(final Class<T> c) throws SQLException {
300 try {
301 return c.getDeclaredConstructor().newInstance();
302
303 } catch (final IllegalAccessException | InstantiationException | InvocationTargetException |
304 NoSuchMethodException e) {
305 throw new SQLException("Cannot create " + c.getName() + ": " + e.getMessage());
306 }
307 }
308
309 /**
310 * Initializes the fields of the provided bean from the ResultSet.
311 * @param <T> The type of bean
312 * @param resultSet The result set.
313 * @param bean The bean to be populated.
314 * @return An initialized object.
315 * @throws SQLException if a database error occurs.
316 */
317 public <T> T populateBean(final ResultSet resultSet, final T bean) throws SQLException {
318 final PropertyDescriptor[] props = this.propertyDescriptors(bean.getClass());
319 final ResultSetMetaData rsmd = resultSet.getMetaData();
320 final int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);
321
322 return populateBean(resultSet, bean, props, columnToProperty);
323 }
324
325 /**
326 * This method populates a bean from the ResultSet based upon the underlying meta-data.
327 *
328 * @param <T> The type of bean
329 * @param resultSet The result set.
330 * @param bean The bean to be populated.
331 * @param props The property descriptors.
332 * @param columnToProperty The column indices in the result set.
333 * @return An initialized object.
334 * @throws SQLException if a database error occurs.
335 */
336 private <T> T populateBean(final ResultSet resultSet, final T bean,
337 final PropertyDescriptor[] props, final int[] columnToProperty)
338 throws SQLException {
339
340 for (int i = 1; i < columnToProperty.length; i++) {
341
342 if (columnToProperty[i] == PROPERTY_NOT_FOUND) {
343 continue;
344 }
345
346 final PropertyDescriptor prop = props[columnToProperty[i]];
347 final Class<?> propType = prop.getPropertyType();
348
349 Object value = null;
350 if (propType != null) {
351 value = this.processColumn(resultSet, i, propType);
352
353 if (value == null && propType.isPrimitive()) {
354 value = PRIMITIVE_DEFAULTS.get(propType);
355 }
356 }
357
358 this.callSetter(bean, prop, value);
359 }
360
361 return bean;
362 }
363
364 /**
365 * Convert a {@code ResultSet} column into an object. Simple
366 * implementations could just call {@code rs.getObject(index)} while
367 * more complex implementations could perform type manipulation to match
368 * the column's type to the bean property type.
369 *
370 * <p>
371 * This implementation calls the appropriate {@code ResultSet} getter
372 * method for the given property type to perform the type conversion. If
373 * the property type doesn't match one of the supported
374 * {@code ResultSet} types, {@code getObject} is called.
375 * </p>
376 *
377 * @param resultSet The {@code ResultSet} currently being processed. It is
378 * positioned on a valid row before being passed into this method.
379 *
380 * @param index The current column index being processed.
381 *
382 * @param propType The bean property type that this column needs to be
383 * converted into.
384 *
385 * @throws SQLException if a database access error occurs
386 *
387 * @return The object from the {@code ResultSet} at the given column
388 * index after optional type processing or {@code null} if the column
389 * value was SQL NULL.
390 */
391 protected Object processColumn(final ResultSet resultSet, final int index, final Class<?> propType)
392 throws SQLException {
393
394 Object retval = resultSet.getObject(index);
395
396 if ( !propType.isPrimitive() && retval == null ) {
397 return null;
398 }
399
400 for (final ColumnHandler<?> handler : COLUMN_HANDLERS) {
401 if (handler.match(propType)) {
402 retval = handler.apply(resultSet, index);
403 break;
404 }
405 }
406
407 return retval;
408
409 }
410
411 /**
412 * Returns a PropertyDescriptor[] for the given Class.
413 *
414 * @param c The Class to retrieve PropertyDescriptors for.
415 * @return A PropertyDescriptor[] describing the Class.
416 * @throws SQLException if introspection failed.
417 */
418 private PropertyDescriptor[] propertyDescriptors(final Class<?> c)
419 throws SQLException {
420 // Introspector caches BeanInfo classes for better performance
421 BeanInfo beanInfo = null;
422 try {
423 beanInfo = Introspector.getBeanInfo(c);
424
425 } catch (final IntrospectionException e) {
426 throw new SQLException(
427 "Bean introspection failed: " + e.getMessage());
428 }
429
430 return beanInfo.getPropertyDescriptors();
431 }
432
433 /**
434 * Convert a {@code ResultSet} row into a JavaBean. This
435 * implementation uses reflection and {@code BeanInfo} classes to
436 * match column names to bean property names. Properties are matched to
437 * columns based on several factors:
438 * <br/>
439 * <ol>
440 * <li>
441 * The class has a writable property with the same name as a column.
442 * The name comparison is case insensitive.
443 * </li>
444 *
445 * <li>
446 * The column type can be converted to the property's set method
447 * parameter type with a ResultSet.get* method. If the conversion fails
448 * (ie. the property was an int and the column was a Timestamp) an
449 * SQLException is thrown.
450 * </li>
451 * </ol>
452 *
453 * <p>
454 * Primitive bean properties are set to their defaults when SQL NULL is
455 * returned from the {@code ResultSet}. Numeric fields are set to 0
456 * and booleans are set to false. Object bean properties are set to
457 * {@code null} when SQL NULL is returned. This is the same behavior
458 * as the {@code ResultSet} get* methods.
459 * </p>
460 * @param <T> The type of bean to create
461 * @param rs ResultSet that supplies the bean data
462 * @param type Class from which to create the bean instance
463 * @throws SQLException if a database access error occurs
464 * @return the newly created bean
465 */
466 public <T> T toBean(final ResultSet rs, final Class<? extends T> type) throws SQLException {
467 final T bean = this.newInstance(type);
468 return this.populateBean(rs, bean);
469 }
470
471 /**
472 * Convert a {@code ResultSet} into a {@code List} of JavaBeans.
473 * This implementation uses reflection and {@code BeanInfo} classes to
474 * match column names to bean property names. Properties are matched to
475 * columns based on several factors:
476 * <br/>
477 * <ol>
478 * <li>
479 * The class has a writable property with the same name as a column.
480 * The name comparison is case insensitive.
481 * </li>
482 *
483 * <li>
484 * The column type can be converted to the property's set method
485 * parameter type with a ResultSet.get* method. If the conversion fails
486 * (ie. the property was an int and the column was a Timestamp) an
487 * SQLException is thrown.
488 * </li>
489 * </ol>
490 *
491 * <p>
492 * Primitive bean properties are set to their defaults when SQL NULL is
493 * returned from the {@code ResultSet}. Numeric fields are set to 0
494 * and booleans are set to false. Object bean properties are set to
495 * {@code null} when SQL NULL is returned. This is the same behavior
496 * as the {@code ResultSet} get* methods.
497 * </p>
498 * @param <T> The type of bean to create
499 * @param resultSet ResultSet that supplies the bean data
500 * @param type Class from which to create the bean instance
501 * @throws SQLException if a database access error occurs
502 * @return the newly created List of beans
503 */
504 public <T> List<T> toBeanList(final ResultSet resultSet, final Class<? extends T> type) throws SQLException {
505 final List<T> results = new ArrayList<>();
506
507 if (!resultSet.next()) {
508 return results;
509 }
510
511 final PropertyDescriptor[] props = this.propertyDescriptors(type);
512 final ResultSetMetaData rsmd = resultSet.getMetaData();
513 final int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);
514
515 do {
516 results.add(this.createBean(resultSet, type, props, columnToProperty));
517 } while (resultSet.next());
518
519 return results;
520 }
521
522 }