    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}