View Javadoc
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      * &lt;br/&gt;
439      * &lt;ol&gt;
440      *     &lt;li&gt;
441      *     The class has a writable property with the same name as a column.
442      *     The name comparison is case insensitive.
443      *     &lt;/li&gt;
444      *
445      *     &lt;li&gt;
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      *     &lt;/li&gt;
451      * &lt;/ol&gt;
452      *
453      * &lt;p&gt;
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      * &lt;/p&gt;
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      * &lt;br/&gt;
477      * &lt;ol&gt;
478      *     &lt;li&gt;
479      *     The class has a writable property with the same name as a column.
480      *     The name comparison is case insensitive.
481      *     &lt;/li&gt;
482      *
483      *     &lt;li&gt;
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      *     &lt;/li&gt;
489      * &lt;/ol&gt;
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      * &lt;/p&gt;
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 }