001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.dbutils;
018
019import java.beans.BeanInfo;
020import java.beans.IntrospectionException;
021import java.beans.Introspector;
022import java.beans.PropertyDescriptor;
023import java.lang.reflect.Field;
024import java.lang.reflect.InvocationTargetException;
025import java.lang.reflect.Method;
026import java.sql.ResultSet;
027import java.sql.ResultSetMetaData;
028import java.sql.SQLException;
029import java.util.ArrayList;
030import java.util.Arrays;
031import java.util.HashMap;
032import java.util.List;
033import java.util.Map;
034import java.util.ServiceLoader;
035
036/**
037 * <p>
038 * {@code BeanProcessor} matches column names to bean property names
039 * and converts {@code ResultSet} columns into objects for those bean
040 * properties.  Subclasses should override the methods in the processing chain
041 * to customize behavior.
042 * </p>
043 *
044 * <p>
045 * This class is thread-safe.
046 * </p>
047 *
048 * @see BasicRowProcessor
049 *
050 * @since 1.1
051 */
052public class BeanProcessor {
053
054    /**
055     * Special array value used by {@code mapColumnsToProperties} that
056     * indicates there is no bean property that matches a column from a
057     * {@code ResultSet}.
058     */
059    protected static final int PROPERTY_NOT_FOUND = -1;
060
061    /**
062     * Set a bean's primitive properties to these defaults when SQL NULL
063     * is returned.  These are the same as the defaults that ResultSet get*
064     * methods return in the event of a NULL column.
065     */
066    private static final Map<Class<?>, Object> PRIMITIVE_DEFAULTS = new HashMap<>();
067
068    private static final List<ColumnHandler<?>> COLUMN_HANDLERS = new ArrayList<>();
069
070    private static final List<PropertyHandler> PROPERTY_HANDLERS = new ArrayList<>();
071
072    static {
073        PRIMITIVE_DEFAULTS.put(Integer.TYPE, Integer.valueOf(0));
074        PRIMITIVE_DEFAULTS.put(Short.TYPE, Short.valueOf((short) 0));
075        PRIMITIVE_DEFAULTS.put(Byte.TYPE, Byte.valueOf((byte) 0));
076        PRIMITIVE_DEFAULTS.put(Float.TYPE, Float.valueOf(0f));
077        PRIMITIVE_DEFAULTS.put(Double.TYPE, Double.valueOf(0d));
078        PRIMITIVE_DEFAULTS.put(Long.TYPE, Long.valueOf(0L));
079        PRIMITIVE_DEFAULTS.put(Boolean.TYPE, Boolean.FALSE);
080        PRIMITIVE_DEFAULTS.put(Character.TYPE, Character.valueOf((char) 0));
081
082        // Use a ServiceLoader to find implementations
083        ServiceLoader.load(ColumnHandler.class).forEach(COLUMN_HANDLERS::add);
084
085        // Use a ServiceLoader to find implementations
086        ServiceLoader.load(PropertyHandler.class).forEach(PROPERTY_HANDLERS::add);
087    }
088
089    /**
090     * ResultSet column to bean property name overrides.
091     */
092    private final Map<String, String> columnToPropertyOverrides;
093
094    /**
095     * Constructor for BeanProcessor.
096     */
097    public BeanProcessor() {
098        this(new HashMap<>());
099    }
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}