| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.commons.dbutils; |
| |
| import org.apache.commons.dbutils.annotations.Column; |
| |
| import java.beans.BeanInfo; |
| import java.beans.IntrospectionException; |
| import java.beans.Introspector; |
| import java.beans.PropertyDescriptor; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.sql.ResultSet; |
| import java.sql.ResultSetMetaData; |
| import java.sql.SQLException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.ServiceLoader; |
| |
| /** |
| * <p> |
| * {@code BeanProcessor} matches column names to bean property names |
| * and converts {@code ResultSet} columns into objects for those bean |
| * properties. Subclasses should override the methods in the processing chain |
| * to customize behavior. |
| * </p> |
| * |
| * <p> |
| * This class is thread-safe. |
| * </p> |
| * |
| * @see BasicRowProcessor |
| * |
| * @since DbUtils 1.1 |
| */ |
| public class BeanProcessor { |
| |
| /** |
| * Special array value used by {@code mapColumnsToProperties} that |
| * indicates there is no bean property that matches a column from a |
| * {@code ResultSet}. |
| */ |
| protected static final int PROPERTY_NOT_FOUND = -1; |
| |
| /** |
| * Set a bean's primitive properties to these defaults when SQL NULL |
| * is returned. These are the same as the defaults that ResultSet get* |
| * methods return in the event of a NULL column. |
| */ |
| private static final Map<Class<?>, Object> primitiveDefaults = new HashMap<>(); |
| |
| private static final List<ColumnHandler> columnHandlers = new ArrayList<>(); |
| |
| private static final List<PropertyHandler> propertyHandlers = new ArrayList<>(); |
| |
| /** |
| * ResultSet column to bean property name overrides. |
| */ |
| private final Map<String, String> columnToPropertyOverrides; |
| |
| static { |
| primitiveDefaults.put(Integer.TYPE, Integer.valueOf(0)); |
| primitiveDefaults.put(Short.TYPE, Short.valueOf((short) 0)); |
| primitiveDefaults.put(Byte.TYPE, Byte.valueOf((byte) 0)); |
| primitiveDefaults.put(Float.TYPE, Float.valueOf(0f)); |
| primitiveDefaults.put(Double.TYPE, Double.valueOf(0d)); |
| primitiveDefaults.put(Long.TYPE, Long.valueOf(0L)); |
| primitiveDefaults.put(Boolean.TYPE, Boolean.FALSE); |
| primitiveDefaults.put(Character.TYPE, Character.valueOf((char) 0)); |
| |
| // Use a ServiceLoader to find implementations |
| for (final ColumnHandler handler : ServiceLoader.load(ColumnHandler.class)) { |
| columnHandlers.add(handler); |
| } |
| |
| // Use a ServiceLoader to find implementations |
| for (final PropertyHandler handler : ServiceLoader.load(PropertyHandler.class)) { |
| propertyHandlers.add(handler); |
| } |
| } |
| |
| /** |
| * Constructor for BeanProcessor. |
| */ |
| public BeanProcessor() { |
| this(new HashMap<String, String>()); |
| } |
| |
| /** |
| * Constructor for BeanProcessor configured with column to property name overrides. |
| * |
| * @param columnToPropertyOverrides ResultSet column to bean property name overrides |
| * @since 1.5 |
| */ |
| public BeanProcessor(final Map<String, String> columnToPropertyOverrides) { |
| super(); |
| if (columnToPropertyOverrides == null) { |
| throw new IllegalArgumentException("columnToPropertyOverrides map cannot be null"); |
| } |
| this.columnToPropertyOverrides = columnToPropertyOverrides; |
| } |
| |
| /** |
| * Convert a {@code ResultSet} row into a JavaBean. This |
| * implementation uses reflection and {@code BeanInfo} classes to |
| * match column names to bean property names. Properties are matched to |
| * columns based on several factors: |
| * <br/> |
| * <ol> |
| * <li> |
| * The class has a writable property with the same name as a column. |
| * The name comparison is case insensitive. |
| * </li> |
| * |
| * <li> |
| * The column type can be converted to the property's set method |
| * parameter type with a ResultSet.get* method. If the conversion fails |
| * (ie. the property was an int and the column was a Timestamp) an |
| * SQLException is thrown. |
| * </li> |
| * </ol> |
| * |
| * <p> |
| * Primitive bean properties are set to their defaults when SQL NULL is |
| * returned from the {@code ResultSet}. Numeric fields are set to 0 |
| * and booleans are set to false. Object bean properties are set to |
| * {@code null} when SQL NULL is returned. This is the same behavior |
| * as the {@code ResultSet} get* methods. |
| * </p> |
| * @param <T> The type of bean to create |
| * @param rs ResultSet that supplies the bean data |
| * @param type Class from which to create the bean instance |
| * @throws SQLException if a database access error occurs |
| * @return the newly created bean |
| */ |
| public <T> T toBean(final ResultSet rs, final Class<? extends T> type) throws SQLException { |
| final T bean = this.newInstance(type); |
| return this.populateBean(rs, bean); |
| } |
| |
| /** |
| * Convert a {@code ResultSet} into a {@code List} of JavaBeans. |
| * This implementation uses reflection and {@code BeanInfo} classes to |
| * match column names to bean property names. Properties are matched to |
| * columns based on several factors: |
| * <br/> |
| * <ol> |
| * <li> |
| * The class has a writable property with the same name as a column. |
| * The name comparison is case insensitive. |
| * </li> |
| * |
| * <li> |
| * The column type can be converted to the property's set method |
| * parameter type with a ResultSet.get* method. If the conversion fails |
| * (ie. the property was an int and the column was a Timestamp) an |
| * SQLException is thrown. |
| * </li> |
| * </ol> |
| * |
| * <p> |
| * Primitive bean properties are set to their defaults when SQL NULL is |
| * returned from the {@code ResultSet}. Numeric fields are set to 0 |
| * and booleans are set to false. Object bean properties are set to |
| * {@code null} when SQL NULL is returned. This is the same behavior |
| * as the {@code ResultSet} get* methods. |
| * </p> |
| * @param <T> The type of bean to create |
| * @param rs ResultSet that supplies the bean data |
| * @param type Class from which to create the bean instance |
| * @throws SQLException if a database access error occurs |
| * @return the newly created List of beans |
| */ |
| public <T> List<T> toBeanList(final ResultSet rs, final Class<? extends T> type) throws SQLException { |
| final List<T> results = new ArrayList<>(); |
| |
| if (!rs.next()) { |
| return results; |
| } |
| |
| final PropertyDescriptor[] props = this.propertyDescriptors(type); |
| final ResultSetMetaData rsmd = rs.getMetaData(); |
| final int[] columnToProperty = this.mapColumnsToProperties(rsmd, props); |
| |
| do { |
| results.add(this.createBean(rs, type, props, columnToProperty)); |
| } while (rs.next()); |
| |
| return results; |
| } |
| |
| /** |
| * Creates a new object and initializes its fields from the ResultSet. |
| * @param <T> The type of bean to create |
| * @param rs The result set. |
| * @param type The bean type (the return type of the object). |
| * @param props The property descriptors. |
| * @param columnToProperty The column indices in the result set. |
| * @return An initialized object. |
| * @throws SQLException if a database error occurs. |
| */ |
| private <T> T createBean(final ResultSet rs, final Class<T> type, |
| final PropertyDescriptor[] props, final int[] columnToProperty) |
| throws SQLException { |
| |
| final T bean = this.newInstance(type); |
| return populateBean(rs, bean, props, columnToProperty); |
| } |
| |
| /** |
| * Initializes the fields of the provided bean from the ResultSet. |
| * @param <T> The type of bean |
| * @param rs The result set. |
| * @param bean The bean to be populated. |
| * @return An initialized object. |
| * @throws SQLException if a database error occurs. |
| */ |
| public <T> T populateBean(final ResultSet rs, final T bean) throws SQLException { |
| final PropertyDescriptor[] props = this.propertyDescriptors(bean.getClass()); |
| final ResultSetMetaData rsmd = rs.getMetaData(); |
| final int[] columnToProperty = this.mapColumnsToProperties(rsmd, props); |
| |
| return populateBean(rs, bean, props, columnToProperty); |
| } |
| |
| /** |
| * This method populates a bean from the ResultSet based upon the underlying meta-data. |
| * |
| * @param <T> The type of bean |
| * @param rs The result set. |
| * @param bean The bean to be populated. |
| * @param props The property descriptors. |
| * @param columnToProperty The column indices in the result set. |
| * @return An initialized object. |
| * @throws SQLException if a database error occurs. |
| */ |
| private <T> T populateBean(final ResultSet rs, final T bean, |
| final PropertyDescriptor[] props, final int[] columnToProperty) |
| throws SQLException { |
| |
| for (int i = 1; i < columnToProperty.length; i++) { |
| |
| if (columnToProperty[i] == PROPERTY_NOT_FOUND) { |
| continue; |
| } |
| |
| final PropertyDescriptor prop = props[columnToProperty[i]]; |
| final Class<?> propType = prop.getPropertyType(); |
| |
| Object value = null; |
| if(propType != null) { |
| value = this.processColumn(rs, i, propType); |
| |
| if (value == null && propType.isPrimitive()) { |
| value = primitiveDefaults.get(propType); |
| } |
| } |
| |
| this.callSetter(bean, prop, value); |
| } |
| |
| return bean; |
| } |
| |
| /** |
| * Calls the setter method on the target object for the given property. |
| * If no setter method exists for the property, this method does nothing. |
| * @param target The object to set the property on. |
| * @param prop The property to set. |
| * @param value The value to pass into the setter. |
| * @throws SQLException if an error occurs setting the property. |
| */ |
| private void callSetter(final Object target, final PropertyDescriptor prop, Object value) |
| throws SQLException { |
| |
| final Method setter = getWriteMethod(target, prop, value); |
| |
| if (setter == null || setter.getParameterTypes().length != 1) { |
| return; |
| } |
| |
| try { |
| final Class<?> firstParam = setter.getParameterTypes()[0]; |
| for (final PropertyHandler handler : propertyHandlers) { |
| if (handler.match(firstParam, value)) { |
| value = handler.apply(firstParam, value); |
| break; |
| } |
| } |
| |
| // Don't call setter if the value object isn't the right type |
| if (this.isCompatibleType(value, firstParam)) { |
| setter.invoke(target, new Object[]{value}); |
| } else { |
| throw new SQLException( |
| "Cannot set " + prop.getName() + ": incompatible types, cannot convert " |
| + value.getClass().getName() + " to " + firstParam.getName()); |
| // value cannot be null here because isCompatibleType allows null |
| } |
| |
| } catch (final IllegalArgumentException e) { |
| throw new SQLException( |
| "Cannot set " + prop.getName() + ": " + e.getMessage()); |
| |
| } catch (final IllegalAccessException e) { |
| throw new SQLException( |
| "Cannot set " + prop.getName() + ": " + e.getMessage()); |
| |
| } catch (final InvocationTargetException e) { |
| throw new SQLException( |
| "Cannot set " + prop.getName() + ": " + e.getMessage()); |
| } |
| } |
| |
| /** |
| * ResultSet.getObject() returns an Integer object for an INT column. The |
| * setter method for the property might take an Integer or a primitive int. |
| * This method returns true if the value can be successfully passed into |
| * the setter method. Remember, Method.invoke() handles the unwrapping |
| * of Integer into an int. |
| * |
| * @param value The value to be passed into the setter method. |
| * @param type The setter's parameter type (non-null) |
| * @return boolean True if the value is compatible (null => true) |
| */ |
| private boolean isCompatibleType(final Object value, final Class<?> type) { |
| // Do object check first, then primitives |
| if (value == null || type.isInstance(value) || matchesPrimitive(type, value.getClass())) { |
| return true; |
| |
| } |
| return false; |
| |
| } |
| |
| /** |
| * Check whether a value is of the same primitive type as {@code targetType}. |
| * |
| * @param targetType The primitive type to target. |
| * @param valueType The value to match to the primitive type. |
| * @return Whether {@code valueType} can be coerced (e.g. autoboxed) into {@code targetType}. |
| */ |
| private boolean matchesPrimitive(final Class<?> targetType, final Class<?> valueType) { |
| if (!targetType.isPrimitive()) { |
| return false; |
| } |
| |
| try { |
| // see if there is a "TYPE" field. This is present for primitive wrappers. |
| final Field typeField = valueType.getField("TYPE"); |
| final Object primitiveValueType = typeField.get(valueType); |
| |
| if (targetType == primitiveValueType) { |
| return true; |
| } |
| } catch (final NoSuchFieldException e) { |
| // lacking the TYPE field is a good sign that we're not working with a primitive wrapper. |
| // we can't match for compatibility |
| } catch (final IllegalAccessException e) { |
| // an inaccessible TYPE field is a good sign that we're not working with a primitive wrapper. |
| // nothing to do. we can't match for compatibility |
| } |
| return false; |
| } |
| |
| /** |
| * Get the write method to use when setting {@code value} to the {@code target}. |
| * |
| * @param target Object where the write method will be called. |
| * @param prop BeanUtils information. |
| * @param value The value that will be passed to the write method. |
| * @return The {@link java.lang.reflect.Method} to call on {@code target} to write {@code value} or {@code null} if |
| * there is no suitable write method. |
| */ |
| protected Method getWriteMethod(final Object target, final PropertyDescriptor prop, final Object value) { |
| final Method method = prop.getWriteMethod(); |
| return method; |
| } |
| |
| /** |
| * Factory method that returns a new instance of the given Class. This |
| * is called at the start of the bean creation process and may be |
| * overridden to provide custom behavior like returning a cached bean |
| * instance. |
| * @param <T> The type of object to create |
| * @param c The Class to create an object from. |
| * @return A newly created object of the Class. |
| * @throws SQLException if creation failed. |
| */ |
| protected <T> T newInstance(final Class<T> c) throws SQLException { |
| try { |
| return c.newInstance(); |
| |
| } catch (final InstantiationException e) { |
| throw new SQLException( |
| "Cannot create " + c.getName() + ": " + e.getMessage()); |
| |
| } catch (final IllegalAccessException e) { |
| throw new SQLException( |
| "Cannot create " + c.getName() + ": " + e.getMessage()); |
| } |
| } |
| |
| /** |
| * Returns a PropertyDescriptor[] for the given Class. |
| * |
| * @param c The Class to retrieve PropertyDescriptors for. |
| * @return A PropertyDescriptor[] describing the Class. |
| * @throws SQLException if introspection failed. |
| */ |
| private PropertyDescriptor[] propertyDescriptors(final Class<?> c) |
| throws SQLException { |
| // Introspector caches BeanInfo classes for better performance |
| BeanInfo beanInfo = null; |
| try { |
| beanInfo = Introspector.getBeanInfo(c); |
| |
| } catch (final IntrospectionException e) { |
| throw new SQLException( |
| "Bean introspection failed: " + e.getMessage()); |
| } |
| |
| return beanInfo.getPropertyDescriptors(); |
| } |
| |
| /** |
| * The positions in the returned array represent column numbers. The |
| * values stored at each position represent the index in the |
| * {@code PropertyDescriptor[]} for the bean property that matches |
| * the column name. If no bean property was found for a column, the |
| * position is set to {@code PROPERTY_NOT_FOUND}. |
| * |
| * @param rsmd The {@code ResultSetMetaData} containing column |
| * information. |
| * |
| * @param props The bean property descriptors. |
| * |
| * @throws SQLException if a database access error occurs |
| * |
| * @return An int[] with column index to property index mappings. The 0th |
| * element is meaningless because JDBC column indexing starts at 1. |
| */ |
| protected int[] mapColumnsToProperties(final ResultSetMetaData rsmd, |
| final PropertyDescriptor[] props) throws SQLException { |
| |
| final int cols = rsmd.getColumnCount(); |
| final int[] columnToProperty = new int[cols + 1]; |
| Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND); |
| |
| for (int col = 1; col <= cols; col++) { |
| String columnName = rsmd.getColumnLabel(col); |
| if (null == columnName || 0 == columnName.length()) { |
| columnName = rsmd.getColumnName(col); |
| } |
| String propertyName = columnToPropertyOverrides.get(columnName); |
| if (propertyName == null) { |
| propertyName = columnName; |
| } |
| for (int i = 0; i < props.length; i++) { |
| |
| PropertyDescriptor prop = props[i]; |
| Column column = prop.getReadMethod().getAnnotation(Column.class); |
| String propertyColumnName = null; |
| if (column != null) { |
| propertyColumnName = column.name(); |
| } else { |
| propertyColumnName = prop.getName(); |
| } |
| if (propertyName.equalsIgnoreCase(propertyColumnName)) { |
| columnToProperty[col] = i; |
| break; |
| } |
| } |
| } |
| |
| return columnToProperty; |
| } |
| |
| /** |
| * Convert a {@code ResultSet} column into an object. Simple |
| * implementations could just call {@code rs.getObject(index)} while |
| * more complex implementations could perform type manipulation to match |
| * the column's type to the bean property type. |
| * |
| * <p> |
| * This implementation calls the appropriate {@code ResultSet} getter |
| * method for the given property type to perform the type conversion. If |
| * the property type doesn't match one of the supported |
| * {@code ResultSet} types, {@code getObject} is called. |
| * </p> |
| * |
| * @param rs The {@code ResultSet} currently being processed. It is |
| * positioned on a valid row before being passed into this method. |
| * |
| * @param index The current column index being processed. |
| * |
| * @param propType The bean property type that this column needs to be |
| * converted into. |
| * |
| * @throws SQLException if a database access error occurs |
| * |
| * @return The object from the {@code ResultSet} at the given column |
| * index after optional type processing or {@code null} if the column |
| * value was SQL NULL. |
| */ |
| protected Object processColumn(final ResultSet rs, final int index, final Class<?> propType) |
| throws SQLException { |
| |
| Object retval = rs.getObject(index); |
| |
| if ( !propType.isPrimitive() && retval == null ) { |
| return null; |
| } |
| |
| for (final ColumnHandler handler : columnHandlers) { |
| if (handler.match(propType)) { |
| retval = handler.apply(rs, index); |
| break; |
| } |
| } |
| |
| return retval; |
| |
| } |
| |
| } |