http://git-wip-us.apache.org/repos/asf/commons-dbutils/blob/084f286a/src/main/java/org/apache/commons/dbutils/BeanProcessor.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/commons/dbutils/BeanProcessor.java b/src/main/java/org/apache/commons/dbutils/BeanProcessor.java index e18dc01..b15920f 100644 --- a/src/main/java/org/apache/commons/dbutils/BeanProcessor.java +++ b/src/main/java/org/apache/commons/dbutils/BeanProcessor.java @@ -1,534 +1,534 @@ -/* - * 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 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</code> matches column names to bean property names - * and converts <code>ResultSet</code> 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</code> that - * indicates there is no bean property that matches a column from a - * <code>ResultSet</code>. - */ - 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<Class<?>, Object>(); - - private static final List<ColumnHandler> columnHandlers = new ArrayList<ColumnHandler>(); - - private static final List<PropertyHandler> propertyHandlers = new ArrayList<PropertyHandler>(); - - /** - * 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 (ColumnHandler handler : ServiceLoader.load(ColumnHandler.class)) { - columnHandlers.add(handler); - } - - // Use a ServiceLoader to find implementations - for (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(Map<String, String> columnToPropertyOverrides) { - super(); - if (columnToPropertyOverrides == null) { - throw new IllegalArgumentException("columnToPropertyOverrides map cannot be null"); - } - this.columnToPropertyOverrides = columnToPropertyOverrides; - } - - /** - * Convert a <code>ResultSet</code> row into a JavaBean. This - * implementation uses reflection and <code>BeanInfo</code> 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</code>. Numeric fields are set to 0 - * and booleans are set to false. Object bean properties are set to - * <code>null</code> when SQL NULL is returned. This is the same behavior - * as the <code>ResultSet</code> 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(ResultSet rs, Class<? extends T> type) throws SQLException { - T bean = this.newInstance(type); - return this.populateBean(rs, bean); - } - - /** - * Convert a <code>ResultSet</code> into a <code>List</code> of JavaBeans. - * This implementation uses reflection and <code>BeanInfo</code> 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</code>. Numeric fields are set to 0 - * and booleans are set to false. Object bean properties are set to - * <code>null</code> when SQL NULL is returned. This is the same behavior - * as the <code>ResultSet</code> 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(ResultSet rs, Class<? extends T> type) throws SQLException { - List<T> results = new ArrayList<T>(); - - if (!rs.next()) { - return results; - } - - PropertyDescriptor[] props = this.propertyDescriptors(type); - ResultSetMetaData rsmd = rs.getMetaData(); - 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(ResultSet rs, Class<T> type, - PropertyDescriptor[] props, int[] columnToProperty) - throws SQLException { - - 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(ResultSet rs, T bean) throws SQLException { - PropertyDescriptor[] props = this.propertyDescriptors(bean.getClass()); - ResultSetMetaData rsmd = rs.getMetaData(); - 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(ResultSet rs, T bean, - PropertyDescriptor[] props, int[] columnToProperty) - throws SQLException { - - for (int i = 1; i < columnToProperty.length; i++) { - - if (columnToProperty[i] == PROPERTY_NOT_FOUND) { - continue; - } - - PropertyDescriptor prop = props[columnToProperty[i]]; - 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(Object target, PropertyDescriptor prop, Object value) - throws SQLException { - - Method setter = getWriteMethod(target, prop, value); - - if (setter == null || setter.getParameterTypes().length != 1) { - return; - } - - try { - Class<?> firstParam = setter.getParameterTypes()[0]; - for (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 (IllegalArgumentException e) { - throw new SQLException( - "Cannot set " + prop.getName() + ": " + e.getMessage()); - - } catch (IllegalAccessException e) { - throw new SQLException( - "Cannot set " + prop.getName() + ": " + e.getMessage()); - - } catch (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(Object value, 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</code>. - * - * @param targetType The primitive type to target. - * @param valueType The value to match to the primitive type. - * @return Whether <code>valueType</code> can be coerced (e.g. autoboxed) into <code>targetType</code>. - */ - private boolean matchesPrimitive(Class<?> targetType, Class<?> valueType) { - if (!targetType.isPrimitive()) { - return false; - } - - try { - // see if there is a "TYPE" field. This is present for primitive wrappers. - Field typeField = valueType.getField("TYPE"); - Object primitiveValueType = typeField.get(valueType); - - if (targetType == primitiveValueType) { - return true; - } - } catch (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 (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(Object target, PropertyDescriptor prop, Object value) { - 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(Class<T> c) throws SQLException { - try { - return c.newInstance(); - - } catch (InstantiationException e) { - throw new SQLException( - "Cannot create " + c.getName() + ": " + e.getMessage()); - - } catch (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(Class<?> c) - throws SQLException { - // Introspector caches BeanInfo classes for better performance - BeanInfo beanInfo = null; - try { - beanInfo = Introspector.getBeanInfo(c); - - } catch (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[]</code> 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</code>. - * - * @param rsmd The <code>ResultSetMetaData</code> 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(ResultSetMetaData rsmd, - PropertyDescriptor[] props) throws SQLException { - - int cols = rsmd.getColumnCount(); - 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++) { - - if (propertyName.equalsIgnoreCase(props[i].getName())) { - columnToProperty[col] = i; - break; - } - } - } - - return columnToProperty; - } - - /** - * Convert a <code>ResultSet</code> column into an object. Simple - * implementations could just call <code>rs.getObject(index)</code> 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</code> 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</code> types, <code>getObject</code> is called. - * </p> - * - * @param rs The <code>ResultSet</code> 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</code> at the given column - * index after optional type processing or <code>null</code> if the column - * value was SQL NULL. - */ - protected Object processColumn(ResultSet rs, int index, Class<?> propType) - throws SQLException { - - Object retval = rs.getObject(index); - - if ( !propType.isPrimitive() && retval == null ) { - return null; - } - - for (ColumnHandler handler : columnHandlers) { - if (handler.match(propType)) { - retval = handler.apply(rs, index); - break; - } - } - - return retval; - - } - -} +/* + * 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 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</code> matches column names to bean property names + * and converts <code>ResultSet</code> 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</code> that + * indicates there is no bean property that matches a column from a + * <code>ResultSet</code>. + */ + 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<Class<?>, Object>(); + + private static final List<ColumnHandler> columnHandlers = new ArrayList<ColumnHandler>(); + + private static final List<PropertyHandler> propertyHandlers = new ArrayList<PropertyHandler>(); + + /** + * 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 (ColumnHandler handler : ServiceLoader.load(ColumnHandler.class)) { + columnHandlers.add(handler); + } + + // Use a ServiceLoader to find implementations + for (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(Map<String, String> columnToPropertyOverrides) { + super(); + if (columnToPropertyOverrides == null) { + throw new IllegalArgumentException("columnToPropertyOverrides map cannot be null"); + } + this.columnToPropertyOverrides = columnToPropertyOverrides; + } + + /** + * Convert a <code>ResultSet</code> row into a JavaBean. This + * implementation uses reflection and <code>BeanInfo</code> 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</code>. Numeric fields are set to 0 + * and booleans are set to false. Object bean properties are set to + * <code>null</code> when SQL NULL is returned. This is the same behavior + * as the <code>ResultSet</code> 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(ResultSet rs, Class<? extends T> type) throws SQLException { + T bean = this.newInstance(type); + return this.populateBean(rs, bean); + } + + /** + * Convert a <code>ResultSet</code> into a <code>List</code> of JavaBeans. + * This implementation uses reflection and <code>BeanInfo</code> 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</code>. Numeric fields are set to 0 + * and booleans are set to false. Object bean properties are set to + * <code>null</code> when SQL NULL is returned. This is the same behavior + * as the <code>ResultSet</code> 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(ResultSet rs, Class<? extends T> type) throws SQLException { + List<T> results = new ArrayList<T>(); + + if (!rs.next()) { + return results; + } + + PropertyDescriptor[] props = this.propertyDescriptors(type); + ResultSetMetaData rsmd = rs.getMetaData(); + 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(ResultSet rs, Class<T> type, + PropertyDescriptor[] props, int[] columnToProperty) + throws SQLException { + + 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(ResultSet rs, T bean) throws SQLException { + PropertyDescriptor[] props = this.propertyDescriptors(bean.getClass()); + ResultSetMetaData rsmd = rs.getMetaData(); + 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(ResultSet rs, T bean, + PropertyDescriptor[] props, int[] columnToProperty) + throws SQLException { + + for (int i = 1; i < columnToProperty.length; i++) { + + if (columnToProperty[i] == PROPERTY_NOT_FOUND) { + continue; + } + + PropertyDescriptor prop = props[columnToProperty[i]]; + 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(Object target, PropertyDescriptor prop, Object value) + throws SQLException { + + Method setter = getWriteMethod(target, prop, value); + + if (setter == null || setter.getParameterTypes().length != 1) { + return; + } + + try { + Class<?> firstParam = setter.getParameterTypes()[0]; + for (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 (IllegalArgumentException e) { + throw new SQLException( + "Cannot set " + prop.getName() + ": " + e.getMessage()); + + } catch (IllegalAccessException e) { + throw new SQLException( + "Cannot set " + prop.getName() + ": " + e.getMessage()); + + } catch (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(Object value, 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</code>. + * + * @param targetType The primitive type to target. + * @param valueType The value to match to the primitive type. + * @return Whether <code>valueType</code> can be coerced (e.g. autoboxed) into <code>targetType</code>. + */ + private boolean matchesPrimitive(Class<?> targetType, Class<?> valueType) { + if (!targetType.isPrimitive()) { + return false; + } + + try { + // see if there is a "TYPE" field. This is present for primitive wrappers. + Field typeField = valueType.getField("TYPE"); + Object primitiveValueType = typeField.get(valueType); + + if (targetType == primitiveValueType) { + return true; + } + } catch (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 (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(Object target, PropertyDescriptor prop, Object value) { + 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(Class<T> c) throws SQLException { + try { + return c.newInstance(); + + } catch (InstantiationException e) { + throw new SQLException( + "Cannot create " + c.getName() + ": " + e.getMessage()); + + } catch (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(Class<?> c) + throws SQLException { + // Introspector caches BeanInfo classes for better performance + BeanInfo beanInfo = null; + try { + beanInfo = Introspector.getBeanInfo(c); + + } catch (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[]</code> 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</code>. + * + * @param rsmd The <code>ResultSetMetaData</code> 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(ResultSetMetaData rsmd, + PropertyDescriptor[] props) throws SQLException { + + int cols = rsmd.getColumnCount(); + 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++) { + + if (propertyName.equalsIgnoreCase(props[i].getName())) { + columnToProperty[col] = i; + break; + } + } + } + + return columnToProperty; + } + + /** + * Convert a <code>ResultSet</code> column into an object. Simple + * implementations could just call <code>rs.getObject(index)</code> 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</code> 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</code> types, <code>getObject</code> is called. + * </p> + * + * @param rs The <code>ResultSet</code> 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</code> at the given column + * index after optional type processing or <code>null</code> if the column + * value was SQL NULL. + */ + protected Object processColumn(ResultSet rs, int index, Class<?> propType) + throws SQLException { + + Object retval = rs.getObject(index); + + if ( !propType.isPrimitive() && retval == null ) { + return null; + } + + for (ColumnHandler handler : columnHandlers) { + if (handler.match(propType)) { + retval = handler.apply(rs, index); + break; + } + } + + return retval; + + } + +}
http://git-wip-us.apache.org/repos/asf/commons-dbutils/blob/084f286a/src/test/java/org/apache/commons/dbutils/AsyncQueryRunnerTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/commons/dbutils/AsyncQueryRunnerTest.java b/src/test/java/org/apache/commons/dbutils/AsyncQueryRunnerTest.java index 24f3900..3a00aa9 100644 --- a/src/test/java/org/apache/commons/dbutils/AsyncQueryRunnerTest.java +++ b/src/test/java/org/apache/commons/dbutils/AsyncQueryRunnerTest.java @@ -1,496 +1,496 @@ -/* - * 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 static org.junit.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.sql.Connection; -import java.sql.ParameterMetaData; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; - -import javax.sql.DataSource; - -import org.apache.commons.dbutils.handlers.ArrayHandler; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SuppressWarnings("boxing") // test code -public class AsyncQueryRunnerTest { - AsyncQueryRunner runner; - ArrayHandler handler; - - @Mock DataSource dataSource; - @Mock Connection conn; - @Mock PreparedStatement stmt; - @Mock ParameterMetaData meta; - @Mock ResultSet results; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - - when(dataSource.getConnection()).thenReturn(conn); - when(conn.prepareStatement(any(String.class))).thenReturn(stmt); - when(stmt.getParameterMetaData()).thenReturn(meta); - when(stmt.getResultSet()).thenReturn(results); - when(stmt.executeQuery()).thenReturn(results); - when(results.next()).thenReturn(false); - - handler = new ArrayHandler(); - runner = new AsyncQueryRunner(Executors.newFixedThreadPool(1), new QueryRunner(dataSource)); - } - - // - // Batch test cases - // - private void callGoodBatch(Connection conn, Object[][] params) throws Exception { - when(meta.getParameterCount()).thenReturn(2); - Future<int[]> future = runner.batch(conn, "select * from blah where ? = ?", params); - - future.get(); - - verify(stmt, times(2)).addBatch(); - verify(stmt, times(1)).executeBatch(); - verify(stmt, times(1)).close(); // make sure we closed the statement - verify(conn, times(0)).close(); // make sure we closed the connection - } - - private void callGoodBatch(Object[][] params) throws Exception { - when(meta.getParameterCount()).thenReturn(2); - Future<int[]> future = runner.batch("select * from blah where ? = ?", params); - - future.get(); - - verify(stmt, times(2)).addBatch(); - verify(stmt, times(1)).executeBatch(); - verify(stmt, times(1)).close(); // make sure we closed the statement - verify(conn, times(1)).close(); // make sure we closed the connection - } - - @Test - public void testGoodBatch() throws Exception { - String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; - - callGoodBatch(params); - } - - @SuppressWarnings("deprecation") // deliberate test of deprecated code - @Test - public void testGoodBatchPmdTrue() throws Exception { - runner = new AsyncQueryRunner(dataSource, true, Executors.newFixedThreadPool(1)); - String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; - - callGoodBatch(params); - } - - @Test - public void testGoodBatchDefaultConstructor() throws Exception { - runner = new AsyncQueryRunner(Executors.newFixedThreadPool(1)); - String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; - - callGoodBatch(conn, params); - } - - @Test - public void testNullParamsBatch() throws Exception { - String[][] params = new String[][] { { null, "unit" }, { "test", null } }; - - callGoodBatch(params); - } - - - - // helper method for calling batch when an exception is expected - private void callBatchWithException(String sql, Object[][] params) throws Exception { - Future<int[]> future = null; - boolean caught = false; - - try { - future = runner.batch(sql, params); - - future.get(); - - verify(stmt, times(2)).addBatch(); - verify(stmt, times(1)).executeBatch(); - verify(stmt, times(1)).close(); // make sure the statement is closed - verify(conn, times(1)).close(); // make sure the connection is closed - } catch(Exception e) { - caught = true; - } - - if(!caught) { - fail("Exception never thrown, but expected"); - } - } - - @Test - public void testTooFewParamsBatch() throws Exception { - String[][] params = new String[][] { { "unit" }, { "test" } }; - - callBatchWithException("select * from blah where ? = ?", params); - } - - @Test - public void testTooManyParamsBatch() throws Exception { - String[][] params = new String[][] { { "unit", "unit", "unit" }, { "test", "test", "test" } }; - - callBatchWithException("select * from blah where ? = ?", params); - } - - @Test(expected=ExecutionException.class) - public void testNullConnectionBatch() throws Exception { - String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; - - when(meta.getParameterCount()).thenReturn(2); - when(dataSource.getConnection()).thenReturn(null); - - runner.batch("select * from blah where ? = ?", params).get(); - } - - @Test(expected=ExecutionException.class) - public void testNullSqlBatch() throws Exception { - String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; - - when(meta.getParameterCount()).thenReturn(2); - - runner.batch(null, params).get(); - } - - @Test(expected=ExecutionException.class) - public void testNullParamsArgBatch() throws Exception { - when(meta.getParameterCount()).thenReturn(2); - - runner.batch("select * from blah where ? = ?", null).get(); - } - - @Test - public void testAddBatchException() throws Exception { - String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; - - doThrow(new SQLException()).when(stmt).addBatch(); - - callBatchWithException("select * from blah where ? = ?", params); - } - - @Test - public void testExecuteBatchException() throws Exception { - String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; - - doThrow(new SQLException()).when(stmt).executeBatch(); - - callBatchWithException("select * from blah where ? = ?", params); - } - - - // - // Query test cases - // - private void callGoodQuery(Connection conn) throws Exception { - when(meta.getParameterCount()).thenReturn(2); - runner.query(conn, "select * from blah where ? = ?", handler, "unit", "test").get(); - - verify(stmt, times(1)).executeQuery(); - verify(results, times(1)).close(); - verify(stmt, times(1)).close(); // make sure we closed the statement - verify(conn, times(0)).close(); // make sure we closed the connection - - // call the other variation of query - when(meta.getParameterCount()).thenReturn(0); - runner.query(conn, "select * from blah", handler).get(); - - verify(stmt, times(2)).executeQuery(); - verify(results, times(2)).close(); - verify(stmt, times(2)).close(); // make sure we closed the statement - verify(conn, times(0)).close(); // make sure we closed the connection - } - - private void callGoodQuery() throws Exception { - when(meta.getParameterCount()).thenReturn(2); - runner.query("select * from blah where ? = ?", handler, "unit", "test").get(); - - verify(stmt, times(1)).executeQuery(); - verify(results, times(1)).close(); - verify(stmt, times(1)).close(); // make sure we closed the statement - verify(conn, times(1)).close(); // make sure we closed the connection - - // call the other variation of query - when(meta.getParameterCount()).thenReturn(0); - runner.query("select * from blah", handler).get(); - - verify(stmt, times(2)).executeQuery(); - verify(results, times(2)).close(); - verify(stmt, times(2)).close(); // make sure we closed the statement - verify(conn, times(2)).close(); // make sure we closed the connection - } - - @Test - public void testGoodQuery() throws Exception { - callGoodQuery(); - } - - @SuppressWarnings("deprecation") // deliberate test of deprecated code - @Test - public void testGoodQueryPmdTrue() throws Exception { - runner = new AsyncQueryRunner(true, Executors.newFixedThreadPool(1)); - callGoodQuery(conn); - } - - @Test - public void testGoodQueryDefaultConstructor() throws Exception { - runner = new AsyncQueryRunner(Executors.newFixedThreadPool(1)); - callGoodQuery(conn); - } - - - // helper method for calling batch when an exception is expected - private void callQueryWithException(Object... params) throws Exception { - boolean caught = false; - - try { - when(meta.getParameterCount()).thenReturn(2); - runner.query("select * from blah where ? = ?", handler, params).get(); - - verify(stmt, times(1)).executeQuery(); - verify(results, times(1)).close(); - verify(stmt, times(1)).close(); // make sure we closed the statement - verify(conn, times(1)).close(); // make sure we closed the connection - } catch(Exception e) { - caught = true; - } - - if(!caught) { - fail("Exception never thrown, but expected"); - } - } - - @Test - public void testNoParamsQuery() throws Exception { - callQueryWithException(); - } - - @Test - public void testTooFewParamsQuery() throws Exception { - callQueryWithException("unit"); - } - - @Test - public void testTooManyParamsQuery() throws Exception { - callQueryWithException("unit", "test", "fail"); - } - - @Test(expected=ExecutionException.class) - public void testNullConnectionQuery() throws Exception { - when(meta.getParameterCount()).thenReturn(2); - when(dataSource.getConnection()).thenReturn(null); - - runner.query("select * from blah where ? = ?", handler, "unit", "test").get(); - } - - @Test(expected=ExecutionException.class) - public void testNullSqlQuery() throws Exception { - when(meta.getParameterCount()).thenReturn(2); - - runner.query(null, handler).get(); - } - - @Test(expected=ExecutionException.class) - public void testNullHandlerQuery() throws Exception { - when(meta.getParameterCount()).thenReturn(2); - - runner.query("select * from blah where ? = ?", null).get(); - } - - @Test - public void testExecuteQueryException() throws Exception { - doThrow(new SQLException()).when(stmt).executeQuery(); - - callQueryWithException(handler, "unit", "test"); - } - - - // - // Update test cases - // - private void callGoodUpdate(Connection conn) throws Exception { - when(meta.getParameterCount()).thenReturn(2); - runner.update(conn, "update blah set ? = ?", "unit", "test").get(); - - verify(stmt, times(1)).executeUpdate(); - verify(stmt, times(1)).close(); // make sure we closed the statement - verify(conn, times(0)).close(); // make sure we closed the connection - - // call the other variation - when(meta.getParameterCount()).thenReturn(0); - runner.update(conn, "update blah set unit = test").get(); - - verify(stmt, times(2)).executeUpdate(); - verify(stmt, times(2)).close(); // make sure we closed the statement - verify(conn, times(0)).close(); // make sure we closed the connection - - // call the other variation - when(meta.getParameterCount()).thenReturn(1); - runner.update(conn, "update blah set unit = ?", "test").get(); - - verify(stmt, times(3)).executeUpdate(); - verify(stmt, times(3)).close(); // make sure we closed the statement - verify(conn, times(0)).close(); // make sure we closed the connection - } - - private void callGoodUpdate() throws Exception { - when(meta.getParameterCount()).thenReturn(2); - runner.update("update blah set ? = ?", "unit", "test").get(); - - verify(stmt, times(1)).executeUpdate(); - verify(stmt, times(1)).close(); // make sure we closed the statement - verify(conn, times(1)).close(); // make sure we closed the connection - - // call the other variation - when(meta.getParameterCount()).thenReturn(0); - runner.update("update blah set unit = test").get(); - - verify(stmt, times(2)).executeUpdate(); - verify(stmt, times(2)).close(); // make sure we closed the statement - verify(conn, times(2)).close(); // make sure we closed the connection - - // call the other variation - when(meta.getParameterCount()).thenReturn(1); - runner.update("update blah set unit = ?", "test").get(); - - verify(stmt, times(3)).executeUpdate(); - verify(stmt, times(3)).close(); // make sure we closed the statement - verify(conn, times(3)).close(); // make sure we closed the connection - } - - @Test - public void testGoodUpdate() throws Exception { - callGoodUpdate(); - } - - @SuppressWarnings("deprecation") // deliberate test of deprecated code - @Test - public void testGoodUpdatePmdTrue() throws Exception { - runner = new AsyncQueryRunner(true, Executors.newFixedThreadPool(1)); - callGoodUpdate(conn); - } - - @Test - public void testGoodUpdateDefaultConstructor() throws Exception { - runner = new AsyncQueryRunner(Executors.newFixedThreadPool(1)); - callGoodUpdate(conn); - } - - // helper method for calling batch when an exception is expected - private void callUpdateWithException(Object... params) throws Exception { - boolean caught = false; - - try { - when(meta.getParameterCount()).thenReturn(2); - runner.update("select * from blah where ? = ?", params).get(); - - verify(stmt, times(1)).executeUpdate(); - verify(stmt, times(1)).close(); // make sure we closed the statement - verify(conn, times(1)).close(); // make sure we closed the connection - } catch(Exception e) { - caught = true; - } - - if(!caught) { - fail("Exception never thrown, but expected"); - } - } - - @Test - public void testNoParamsUpdate() throws Exception { - callUpdateWithException(); - } - - @Test - public void testTooFewParamsUpdate() throws Exception { - callUpdateWithException("unit"); - } - - @Test - public void testTooManyParamsUpdate() throws Exception { - callUpdateWithException("unit", "test", "fail"); - } - - @Test - public void testInsertUsesGivenQueryRunner() throws Exception { - QueryRunner mockQueryRunner = mock(QueryRunner.class - , org.mockito.Mockito.withSettings().verboseLogging() // debug for Continuum - ); - runner = new AsyncQueryRunner(Executors.newSingleThreadExecutor(), mockQueryRunner); - - runner.insert("1", handler); - runner.insert("2", handler, "param1"); - runner.insert(conn, "3", handler); - runner.insert(conn, "4", handler, "param1"); - - // give the Executor time to submit all insert statements. Otherwise the following verify statements will fail from time to time. - TimeUnit.MILLISECONDS.sleep(50); - - verify(mockQueryRunner).insert("1", handler); - verify(mockQueryRunner).insert("2", handler, "param1"); - verify(mockQueryRunner).insert(conn, "3", handler); - verify(mockQueryRunner).insert(conn, "4", handler, "param1"); - } - - @Test(expected=ExecutionException.class) - public void testNullConnectionUpdate() throws Exception { - when(meta.getParameterCount()).thenReturn(2); - when(dataSource.getConnection()).thenReturn(null); - - runner.update("select * from blah where ? = ?", "unit", "test").get(); - } - - @Test(expected=ExecutionException.class) - public void testNullSqlUpdate() throws Exception { - when(meta.getParameterCount()).thenReturn(2); - - runner.update(null).get(); - } - - @Test - public void testExecuteUpdateException() throws Exception { - doThrow(new SQLException()).when(stmt).executeUpdate(); - - callUpdateWithException("unit", "test"); - } - - // - // Random tests - // - @Test(expected=ExecutionException.class) - public void testBadPrepareConnection() throws Exception { - runner = new AsyncQueryRunner(Executors.newFixedThreadPool(1)); - runner.update("update blah set unit = test").get(); - } -} +/* + * 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 static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.sql.Connection; +import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import javax.sql.DataSource; + +import org.apache.commons.dbutils.handlers.ArrayHandler; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SuppressWarnings("boxing") // test code +public class AsyncQueryRunnerTest { + AsyncQueryRunner runner; + ArrayHandler handler; + + @Mock DataSource dataSource; + @Mock Connection conn; + @Mock PreparedStatement stmt; + @Mock ParameterMetaData meta; + @Mock ResultSet results; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + when(dataSource.getConnection()).thenReturn(conn); + when(conn.prepareStatement(any(String.class))).thenReturn(stmt); + when(stmt.getParameterMetaData()).thenReturn(meta); + when(stmt.getResultSet()).thenReturn(results); + when(stmt.executeQuery()).thenReturn(results); + when(results.next()).thenReturn(false); + + handler = new ArrayHandler(); + runner = new AsyncQueryRunner(Executors.newFixedThreadPool(1), new QueryRunner(dataSource)); + } + + // + // Batch test cases + // + private void callGoodBatch(Connection conn, Object[][] params) throws Exception { + when(meta.getParameterCount()).thenReturn(2); + Future<int[]> future = runner.batch(conn, "select * from blah where ? = ?", params); + + future.get(); + + verify(stmt, times(2)).addBatch(); + verify(stmt, times(1)).executeBatch(); + verify(stmt, times(1)).close(); // make sure we closed the statement + verify(conn, times(0)).close(); // make sure we closed the connection + } + + private void callGoodBatch(Object[][] params) throws Exception { + when(meta.getParameterCount()).thenReturn(2); + Future<int[]> future = runner.batch("select * from blah where ? = ?", params); + + future.get(); + + verify(stmt, times(2)).addBatch(); + verify(stmt, times(1)).executeBatch(); + verify(stmt, times(1)).close(); // make sure we closed the statement + verify(conn, times(1)).close(); // make sure we closed the connection + } + + @Test + public void testGoodBatch() throws Exception { + String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; + + callGoodBatch(params); + } + + @SuppressWarnings("deprecation") // deliberate test of deprecated code + @Test + public void testGoodBatchPmdTrue() throws Exception { + runner = new AsyncQueryRunner(dataSource, true, Executors.newFixedThreadPool(1)); + String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; + + callGoodBatch(params); + } + + @Test + public void testGoodBatchDefaultConstructor() throws Exception { + runner = new AsyncQueryRunner(Executors.newFixedThreadPool(1)); + String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; + + callGoodBatch(conn, params); + } + + @Test + public void testNullParamsBatch() throws Exception { + String[][] params = new String[][] { { null, "unit" }, { "test", null } }; + + callGoodBatch(params); + } + + + + // helper method for calling batch when an exception is expected + private void callBatchWithException(String sql, Object[][] params) throws Exception { + Future<int[]> future = null; + boolean caught = false; + + try { + future = runner.batch(sql, params); + + future.get(); + + verify(stmt, times(2)).addBatch(); + verify(stmt, times(1)).executeBatch(); + verify(stmt, times(1)).close(); // make sure the statement is closed + verify(conn, times(1)).close(); // make sure the connection is closed + } catch(Exception e) { + caught = true; + } + + if(!caught) { + fail("Exception never thrown, but expected"); + } + } + + @Test + public void testTooFewParamsBatch() throws Exception { + String[][] params = new String[][] { { "unit" }, { "test" } }; + + callBatchWithException("select * from blah where ? = ?", params); + } + + @Test + public void testTooManyParamsBatch() throws Exception { + String[][] params = new String[][] { { "unit", "unit", "unit" }, { "test", "test", "test" } }; + + callBatchWithException("select * from blah where ? = ?", params); + } + + @Test(expected=ExecutionException.class) + public void testNullConnectionBatch() throws Exception { + String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; + + when(meta.getParameterCount()).thenReturn(2); + when(dataSource.getConnection()).thenReturn(null); + + runner.batch("select * from blah where ? = ?", params).get(); + } + + @Test(expected=ExecutionException.class) + public void testNullSqlBatch() throws Exception { + String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; + + when(meta.getParameterCount()).thenReturn(2); + + runner.batch(null, params).get(); + } + + @Test(expected=ExecutionException.class) + public void testNullParamsArgBatch() throws Exception { + when(meta.getParameterCount()).thenReturn(2); + + runner.batch("select * from blah where ? = ?", null).get(); + } + + @Test + public void testAddBatchException() throws Exception { + String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; + + doThrow(new SQLException()).when(stmt).addBatch(); + + callBatchWithException("select * from blah where ? = ?", params); + } + + @Test + public void testExecuteBatchException() throws Exception { + String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; + + doThrow(new SQLException()).when(stmt).executeBatch(); + + callBatchWithException("select * from blah where ? = ?", params); + } + + + // + // Query test cases + // + private void callGoodQuery(Connection conn) throws Exception { + when(meta.getParameterCount()).thenReturn(2); + runner.query(conn, "select * from blah where ? = ?", handler, "unit", "test").get(); + + verify(stmt, times(1)).executeQuery(); + verify(results, times(1)).close(); + verify(stmt, times(1)).close(); // make sure we closed the statement + verify(conn, times(0)).close(); // make sure we closed the connection + + // call the other variation of query + when(meta.getParameterCount()).thenReturn(0); + runner.query(conn, "select * from blah", handler).get(); + + verify(stmt, times(2)).executeQuery(); + verify(results, times(2)).close(); + verify(stmt, times(2)).close(); // make sure we closed the statement + verify(conn, times(0)).close(); // make sure we closed the connection + } + + private void callGoodQuery() throws Exception { + when(meta.getParameterCount()).thenReturn(2); + runner.query("select * from blah where ? = ?", handler, "unit", "test").get(); + + verify(stmt, times(1)).executeQuery(); + verify(results, times(1)).close(); + verify(stmt, times(1)).close(); // make sure we closed the statement + verify(conn, times(1)).close(); // make sure we closed the connection + + // call the other variation of query + when(meta.getParameterCount()).thenReturn(0); + runner.query("select * from blah", handler).get(); + + verify(stmt, times(2)).executeQuery(); + verify(results, times(2)).close(); + verify(stmt, times(2)).close(); // make sure we closed the statement + verify(conn, times(2)).close(); // make sure we closed the connection + } + + @Test + public void testGoodQuery() throws Exception { + callGoodQuery(); + } + + @SuppressWarnings("deprecation") // deliberate test of deprecated code + @Test + public void testGoodQueryPmdTrue() throws Exception { + runner = new AsyncQueryRunner(true, Executors.newFixedThreadPool(1)); + callGoodQuery(conn); + } + + @Test + public void testGoodQueryDefaultConstructor() throws Exception { + runner = new AsyncQueryRunner(Executors.newFixedThreadPool(1)); + callGoodQuery(conn); + } + + + // helper method for calling batch when an exception is expected + private void callQueryWithException(Object... params) throws Exception { + boolean caught = false; + + try { + when(meta.getParameterCount()).thenReturn(2); + runner.query("select * from blah where ? = ?", handler, params).get(); + + verify(stmt, times(1)).executeQuery(); + verify(results, times(1)).close(); + verify(stmt, times(1)).close(); // make sure we closed the statement + verify(conn, times(1)).close(); // make sure we closed the connection + } catch(Exception e) { + caught = true; + } + + if(!caught) { + fail("Exception never thrown, but expected"); + } + } + + @Test + public void testNoParamsQuery() throws Exception { + callQueryWithException(); + } + + @Test + public void testTooFewParamsQuery() throws Exception { + callQueryWithException("unit"); + } + + @Test + public void testTooManyParamsQuery() throws Exception { + callQueryWithException("unit", "test", "fail"); + } + + @Test(expected=ExecutionException.class) + public void testNullConnectionQuery() throws Exception { + when(meta.getParameterCount()).thenReturn(2); + when(dataSource.getConnection()).thenReturn(null); + + runner.query("select * from blah where ? = ?", handler, "unit", "test").get(); + } + + @Test(expected=ExecutionException.class) + public void testNullSqlQuery() throws Exception { + when(meta.getParameterCount()).thenReturn(2); + + runner.query(null, handler).get(); + } + + @Test(expected=ExecutionException.class) + public void testNullHandlerQuery() throws Exception { + when(meta.getParameterCount()).thenReturn(2); + + runner.query("select * from blah where ? = ?", null).get(); + } + + @Test + public void testExecuteQueryException() throws Exception { + doThrow(new SQLException()).when(stmt).executeQuery(); + + callQueryWithException(handler, "unit", "test"); + } + + + // + // Update test cases + // + private void callGoodUpdate(Connection conn) throws Exception { + when(meta.getParameterCount()).thenReturn(2); + runner.update(conn, "update blah set ? = ?", "unit", "test").get(); + + verify(stmt, times(1)).executeUpdate(); + verify(stmt, times(1)).close(); // make sure we closed the statement + verify(conn, times(0)).close(); // make sure we closed the connection + + // call the other variation + when(meta.getParameterCount()).thenReturn(0); + runner.update(conn, "update blah set unit = test").get(); + + verify(stmt, times(2)).executeUpdate(); + verify(stmt, times(2)).close(); // make sure we closed the statement + verify(conn, times(0)).close(); // make sure we closed the connection + + // call the other variation + when(meta.getParameterCount()).thenReturn(1); + runner.update(conn, "update blah set unit = ?", "test").get(); + + verify(stmt, times(3)).executeUpdate(); + verify(stmt, times(3)).close(); // make sure we closed the statement + verify(conn, times(0)).close(); // make sure we closed the connection + } + + private void callGoodUpdate() throws Exception { + when(meta.getParameterCount()).thenReturn(2); + runner.update("update blah set ? = ?", "unit", "test").get(); + + verify(stmt, times(1)).executeUpdate(); + verify(stmt, times(1)).close(); // make sure we closed the statement + verify(conn, times(1)).close(); // make sure we closed the connection + + // call the other variation + when(meta.getParameterCount()).thenReturn(0); + runner.update("update blah set unit = test").get(); + + verify(stmt, times(2)).executeUpdate(); + verify(stmt, times(2)).close(); // make sure we closed the statement + verify(conn, times(2)).close(); // make sure we closed the connection + + // call the other variation + when(meta.getParameterCount()).thenReturn(1); + runner.update("update blah set unit = ?", "test").get(); + + verify(stmt, times(3)).executeUpdate(); + verify(stmt, times(3)).close(); // make sure we closed the statement + verify(conn, times(3)).close(); // make sure we closed the connection + } + + @Test + public void testGoodUpdate() throws Exception { + callGoodUpdate(); + } + + @SuppressWarnings("deprecation") // deliberate test of deprecated code + @Test + public void testGoodUpdatePmdTrue() throws Exception { + runner = new AsyncQueryRunner(true, Executors.newFixedThreadPool(1)); + callGoodUpdate(conn); + } + + @Test + public void testGoodUpdateDefaultConstructor() throws Exception { + runner = new AsyncQueryRunner(Executors.newFixedThreadPool(1)); + callGoodUpdate(conn); + } + + // helper method for calling batch when an exception is expected + private void callUpdateWithException(Object... params) throws Exception { + boolean caught = false; + + try { + when(meta.getParameterCount()).thenReturn(2); + runner.update("select * from blah where ? = ?", params).get(); + + verify(stmt, times(1)).executeUpdate(); + verify(stmt, times(1)).close(); // make sure we closed the statement + verify(conn, times(1)).close(); // make sure we closed the connection + } catch(Exception e) { + caught = true; + } + + if(!caught) { + fail("Exception never thrown, but expected"); + } + } + + @Test + public void testNoParamsUpdate() throws Exception { + callUpdateWithException(); + } + + @Test + public void testTooFewParamsUpdate() throws Exception { + callUpdateWithException("unit"); + } + + @Test + public void testTooManyParamsUpdate() throws Exception { + callUpdateWithException("unit", "test", "fail"); + } + + @Test + public void testInsertUsesGivenQueryRunner() throws Exception { + QueryRunner mockQueryRunner = mock(QueryRunner.class + , org.mockito.Mockito.withSettings().verboseLogging() // debug for Continuum + ); + runner = new AsyncQueryRunner(Executors.newSingleThreadExecutor(), mockQueryRunner); + + runner.insert("1", handler); + runner.insert("2", handler, "param1"); + runner.insert(conn, "3", handler); + runner.insert(conn, "4", handler, "param1"); + + // give the Executor time to submit all insert statements. Otherwise the following verify statements will fail from time to time. + TimeUnit.MILLISECONDS.sleep(50); + + verify(mockQueryRunner).insert("1", handler); + verify(mockQueryRunner).insert("2", handler, "param1"); + verify(mockQueryRunner).insert(conn, "3", handler); + verify(mockQueryRunner).insert(conn, "4", handler, "param1"); + } + + @Test(expected=ExecutionException.class) + public void testNullConnectionUpdate() throws Exception { + when(meta.getParameterCount()).thenReturn(2); + when(dataSource.getConnection()).thenReturn(null); + + runner.update("select * from blah where ? = ?", "unit", "test").get(); + } + + @Test(expected=ExecutionException.class) + public void testNullSqlUpdate() throws Exception { + when(meta.getParameterCount()).thenReturn(2); + + runner.update(null).get(); + } + + @Test + public void testExecuteUpdateException() throws Exception { + doThrow(new SQLException()).when(stmt).executeUpdate(); + + callUpdateWithException("unit", "test"); + } + + // + // Random tests + // + @Test(expected=ExecutionException.class) + public void testBadPrepareConnection() throws Exception { + runner = new AsyncQueryRunner(Executors.newFixedThreadPool(1)); + runner.update("update blah set unit = test").get(); + } +} http://git-wip-us.apache.org/repos/asf/commons-dbutils/blob/084f286a/src/test/java/org/apache/commons/dbutils/BaseTestCase.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/commons/dbutils/BaseTestCase.java b/src/test/java/org/apache/commons/dbutils/BaseTestCase.java index 913397f..dc5e479 100644 --- a/src/test/java/org/apache/commons/dbutils/BaseTestCase.java +++ b/src/test/java/org/apache/commons/dbutils/BaseTestCase.java @@ -1,138 +1,138 @@ -/* - * 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 java.math.BigInteger; -import java.sql.ResultSet; -import java.sql.ResultSetMetaData; -import java.sql.Timestamp; -import java.util.Date; - -import junit.framework.TestCase; - -/** - * BaseTestCase is the base class for all test cases as well as the "all tests" - * runner. - */ -public class BaseTestCase extends TestCase { - - private static final String[] columnNames = - new String[] { - "one", - "two", - "three", - "notInBean", - "intTest", - "integerTest", - "nullObjectTest", - "nullPrimitiveTest", - "notDate", - "columnProcessorDoubleTest" }; - - /** - * The number of columns in the MockResultSet. - */ - protected static final int COLS = columnNames.length; - - protected static final ResultSetMetaData metaData = - MockResultSetMetaData.create(columnNames); - - /** - * A Timestamp for test purposes having 9 decimals - */ - static final Timestamp ts789456123; - - static { - ts789456123 = new Timestamp(new Date().getTime()); - ts789456123.setNanos(789456123); - } - - private static final Object[] row1 = - new Object[] { - "1", - "2", - "THREE", - " notInBean ", - Integer.valueOf(1), - Integer.valueOf(2), - null, - null, - new Date(), - BigInteger.valueOf(13)}; - - private static final Object[] row2 = - new Object[] { - "4", - "5", - "SIX", - " notInBean ", - Integer.valueOf(3), - Integer.valueOf(4), - null, - null, - ts789456123, - BigInteger.valueOf(13)}; - - private static final Object[][] rows = new Object[][] { row1, row2 }; - - /** - * The number of rows in the MockResultSet. - */ - protected static final int ROWS = rows.length; - - /** - * The ResultSet all test methods will use. - */ - protected ResultSet rs = null; - - /** - * A ResultSet with 0 rows. - */ - protected ResultSet emptyResultSet = null; - - /** - * This is called before each test method so ResultSet will be fresh each - * time. - * @see junit.framework.TestCase#setUp() - */ - @Override - protected void setUp() throws Exception { - super.setUp(); - - rs = this.createMockResultSet(); - emptyResultSet = MockResultSet.create(metaData, null); - } - - /** - * Creates a freshly initialized ResultSet. - */ - protected ResultSet createMockResultSet() { - return MockResultSet.create(metaData, rows); - } - - // Test which allows Eclipse to be run on full project (avoids no tests found) - // check that the rows are valid for the column definition - public void testCheckDataSizes() { - assertEquals("Row 1 must contain correct number of columns", columnNames.length, row1.length); - assertEquals("Row 1 must contain correct number of columns", columnNames.length, row2.length); - } - - public void testResultSets() throws Exception { - assertFalse("emptyResultSet should be empty", emptyResultSet.next()); - // fails in SqlNullCheckedResultSetTest assertTrue("rs should not be empty", rs.next()); - } -} +/* + * 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 java.math.BigInteger; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.Timestamp; +import java.util.Date; + +import junit.framework.TestCase; + +/** + * BaseTestCase is the base class for all test cases as well as the "all tests" + * runner. + */ +public class BaseTestCase extends TestCase { + + private static final String[] columnNames = + new String[] { + "one", + "two", + "three", + "notInBean", + "intTest", + "integerTest", + "nullObjectTest", + "nullPrimitiveTest", + "notDate", + "columnProcessorDoubleTest" }; + + /** + * The number of columns in the MockResultSet. + */ + protected static final int COLS = columnNames.length; + + protected static final ResultSetMetaData metaData = + MockResultSetMetaData.create(columnNames); + + /** + * A Timestamp for test purposes having 9 decimals + */ + static final Timestamp ts789456123; + + static { + ts789456123 = new Timestamp(new Date().getTime()); + ts789456123.setNanos(789456123); + } + + private static final Object[] row1 = + new Object[] { + "1", + "2", + "THREE", + " notInBean ", + Integer.valueOf(1), + Integer.valueOf(2), + null, + null, + new Date(), + BigInteger.valueOf(13)}; + + private static final Object[] row2 = + new Object[] { + "4", + "5", + "SIX", + " notInBean ", + Integer.valueOf(3), + Integer.valueOf(4), + null, + null, + ts789456123, + BigInteger.valueOf(13)}; + + private static final Object[][] rows = new Object[][] { row1, row2 }; + + /** + * The number of rows in the MockResultSet. + */ + protected static final int ROWS = rows.length; + + /** + * The ResultSet all test methods will use. + */ + protected ResultSet rs = null; + + /** + * A ResultSet with 0 rows. + */ + protected ResultSet emptyResultSet = null; + + /** + * This is called before each test method so ResultSet will be fresh each + * time. + * @see junit.framework.TestCase#setUp() + */ + @Override + protected void setUp() throws Exception { + super.setUp(); + + rs = this.createMockResultSet(); + emptyResultSet = MockResultSet.create(metaData, null); + } + + /** + * Creates a freshly initialized ResultSet. + */ + protected ResultSet createMockResultSet() { + return MockResultSet.create(metaData, rows); + } + + // Test which allows Eclipse to be run on full project (avoids no tests found) + // check that the rows are valid for the column definition + public void testCheckDataSizes() { + assertEquals("Row 1 must contain correct number of columns", columnNames.length, row1.length); + assertEquals("Row 1 must contain correct number of columns", columnNames.length, row2.length); + } + + public void testResultSets() throws Exception { + assertFalse("emptyResultSet should be empty", emptyResultSet.next()); + // fails in SqlNullCheckedResultSetTest assertTrue("rs should not be empty", rs.next()); + } +}