Skip to content

Instantly share code, notes, and snippets.

@yoshiori
Created July 4, 2011 10:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yoshiori/1063194 to your computer and use it in GitHub Desktop.
Save yoshiori/1063194 to your computer and use it in GitHub Desktop.
/**
*
*/
package org.yoshiori.dbutils;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.dbutils.BeanProcessor;
/**
* @author yoshiori
*
*/
public class YoshioriBeanProcessor extends 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>();
static {
primitiveDefaults.put(Integer.TYPE, 0);
primitiveDefaults.put(Short.TYPE, ((short) 0));
primitiveDefaults.put(Byte.TYPE, ((byte) 0));
primitiveDefaults.put(Float.TYPE, (float) (0));
primitiveDefaults.put(Double.TYPE, (double) (0));
primitiveDefaults.put(Long.TYPE, (0L));
primitiveDefaults.put(Boolean.TYPE, Boolean.FALSE);
primitiveDefaults.put(Character.TYPE, '\u0000');
}
/**
* Constructor for BeanProcessor.
*/
public YoshioriBeanProcessor() {
super();
}
/**
* 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
*/
@Override
public <T> T toBean(ResultSet rs, Class<T> type) throws SQLException {
PropertyDescriptor[] props = this.propertyDescriptors(type);
ResultSetMetaData rsmd = rs.getMetaData();
int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);
return this.createBean(rs, type, props, columnToProperty);
}
/**
* 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
*/
@Override
public <T> List<T> toBeanList(ResultSet rs, Class<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);
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 = this.processColumn(rs, i, propType);
if (propType != null && 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.
*/
@SuppressWarnings("unchecked")
private void callSetter(Object target, PropertyDescriptor prop, Object value)
throws SQLException {
Method setter = prop.getWriteMethod();
if (setter == null) {
return;
}
Class<?>[] params = setter.getParameterTypes();
try {
// convert types for some popular ones
if (value != null) {
if (value instanceof java.util.Date) {
if (params[0].getName().equals("java.sql.Date")) {
value = new java.sql.Date(((java.util.Date) value).getTime());
} else
if (params[0].getName().equals("java.sql.Time")) {
value = new java.sql.Time(((java.util.Date) value).getTime());
} else
if (params[0].getName().equals("java.sql.Timestamp")) {
value = new java.sql.Timestamp(((java.util.Date) value).getTime());
}
}
}
// Don't call setter if the value object isn't the right type
if (this.isCompatibleType(value, params[0])) {
setter.invoke(target, new Object[] { value });
//enum対応追加 ここから
}else if (params[0].isEnum()) {
@SuppressWarnings("rawtypes")
Class clazz = Class.forName(params[0].getName());
setter.invoke(target, Enum.valueOf(clazz, String.class.cast(value)));
//enum対応追加 ここまで
} else {
throw new SQLException(
"Cannot set " + prop.getName() + ": incompatible types.");
}
} catch (ClassNotFoundException e) {
throw new SQLException(
"Cannot set " + prop.getName() + ": " + e.getMessage());
} 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.
* @return boolean True if the value is compatible.
*/
private boolean isCompatibleType(Object value, Class<?> type) {
// Do object check first, then primitives
if (value == null || type.isInstance(value)) {
return true;
} else if (type.equals(Integer.TYPE) && Integer.class.isInstance(value)) {
return true;
} else if (type.equals(Long.TYPE) && Long.class.isInstance(value)) {
return true;
} else if (type.equals(Double.TYPE) && Double.class.isInstance(value)) {
return true;
} else if (type.equals(Float.TYPE) && Float.class.isInstance(value)) {
return true;
} else if (type.equals(Short.TYPE) && Short.class.isInstance(value)) {
return true;
} else if (type.equals(Byte.TYPE) && Byte.class.isInstance(value)) {
return true;
} else if (type.equals(Character.TYPE)
&& Character.class.isInstance(value)) {
return true;
} else if (type.equals(Boolean.TYPE) && Boolean.class.isInstance(value)) {
return true;
} else {
return false;
}
}
/**
* 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.
*/
@Override
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.
*/
/**
* 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.
*/
@Override
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);
}
for (int i = 0; i < props.length; i++) {
// キャッシュとかしたほうがいいのかなぁ……
if (columnName.replace("_", "").equalsIgnoreCase(
props[i].getName().replace("_", ""))) {
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.
*/
@Override
protected Object processColumn(ResultSet rs, int index, Class<?> propType)
throws SQLException {
if (!propType.isPrimitive() && rs.getObject(index) == null) {
return null;
}
if (!propType.isPrimitive() && rs.getObject(index) == null) {
return null;
}
if (propType.equals(String.class)) {
return rs.getString(index);
} else if (propType.equals(Integer.TYPE)
|| propType.equals(Integer.class)) {
return (rs.getInt(index));
} else if (propType.equals(Boolean.TYPE)
|| propType.equals(Boolean.class)) {
return (rs.getBoolean(index));
} else if (propType.equals(Long.TYPE) || propType.equals(Long.class)) {
return (rs.getLong(index));
} else if (propType.equals(Double.TYPE)
|| propType.equals(Double.class)) {
return (rs.getDouble(index));
} else if (propType.equals(Float.TYPE) || propType.equals(Float.class)) {
return (rs.getFloat(index));
} else if (propType.equals(Short.TYPE) || propType.equals(Short.class)) {
return (rs.getShort(index));
} else if (propType.equals(Byte.TYPE) || propType.equals(Byte.class)) {
return (rs.getByte(index));
} else if (propType.equals(Timestamp.class)) {
return rs.getTimestamp(index);
} else {
return rs.getObject(index);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment