
com.arakelian.jdbc.handler.BeanHandler Maven / Gradle / Ivy
The newest version!
/*
* 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 com.arakelian.jdbc.handler;
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.Date;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import com.arakelian.core.utils.DateUtils;
/**
* ResultSetHandler
implementation that converts the next ResultSet
row
* into a JavaBean.
*
* @param
* the type of bean that we return
*/
public class BeanHandler implements ResultSetHandler {
/**
* Special array value used by mapColumnsToProperties
that indicates there is no
* bean property that matches a column from a ResultSet
.
*/
protected static final int PROPERTY_NOT_FOUND = -1;
/**
* Set a bean's primitive properties to these defaults when SQL NULL is returned. These are the
* same as the defaults that ResultSet get* methods return in the event of a NULL column.
*/
private static final Map, Object> primitiveDefaults = new HashMap<>();
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');
}
/**
* The Class of beans produced by this handler.
*/
private final Class type;
private PropertyDescriptor[] props = null;
private int[] columnToProperty = null;
public BeanHandler(final Class type) {
this.type = type;
}
public int[] getColumnToProperty() {
return columnToProperty;
}
public PropertyDescriptor[] getProps() {
return props;
}
public Class getType() {
return type;
}
/**
* Convert the first row of the ResultSet
into a bean with the Class
* given in the constructor.
*
* @param rs
* ResultSet
to process.
* @return An initialized JavaBean or null
if there were no rows in the
* ResultSet
.
*
* @throws SQLException
* if a database access error occurs
*/
@Override
public T handle(final ResultSet rs, final ResultSetMetaData rsmd) throws SQLException {
if (rs.next()) {
if (props == null) {
props = getPropertyDescriptors(type);
}
if (columnToProperty == null) {
columnToProperty = mapColumnsToProperties(rsmd, props);
}
return createBean(rs, type, props, columnToProperty);
} else {
return null;
}
}
@Override
public boolean wasLast(final T result) {
return result == null;
}
/**
* Calls the setter method on the target object for the given property. If no setter method
* exists for the property, this method does nothing.
*
* @param target
* The object to set the property on.
* @param prop
* The property to set.
* @param value
* The value to pass into the setter.
* @throws SQLException
* if an error occurs setting the property.
*/
private void callSetter(final Object target, final PropertyDescriptor prop, Object value)
throws SQLException {
final Method setter = prop.getWriteMethod();
if (setter == null) {
return;
}
final 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 (isCompatibleType(value, params[0])) {
setter.invoke(target, new Object[] { value });
} else {
throw new SQLException("Cannot set " + prop.getName() + ": incompatible types.");
}
} catch (final IllegalArgumentException e) {
throw new SQLException("Cannot set " + prop.getName() + ": " + e.getMessage());
} catch (final IllegalAccessException e) {
throw new SQLException("Cannot set " + prop.getName() + ": " + e.getMessage());
} catch (final InvocationTargetException e) {
throw new SQLException("Cannot set " + prop.getName() + ": " + e.getMessage());
}
}
/**
* Creates a new object and initializes its fields from the ResultSet.
*
* @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 createBean(
final ResultSet rs,
final Class type,
final PropertyDescriptor[] props,
final int[] columnToProperty) throws SQLException {
final T bean = newInstance(type);
final ResultSetMetaData rsmd = rs.getMetaData();
for (int column = 1; column < columnToProperty.length; column++) {
if (columnToProperty[column] == PROPERTY_NOT_FOUND) {
continue;
}
final PropertyDescriptor prop = props[columnToProperty[column]];
final Class> propType = prop.getPropertyType();
Object value = processColumn(rs, rsmd, column, propType);
if (propType != null && value == null && propType.isPrimitive()) {
value = primitiveDefaults.get(propType);
}
callSetter(bean, prop, value);
}
return bean;
}
/**
* 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[] getPropertyDescriptors(final Class> c) throws SQLException {
// Introspector caches BeanInfo classes for better performance
BeanInfo beanInfo = null;
try {
beanInfo = Introspector.getBeanInfo(c);
} catch (final IntrospectionException e) {
throw new SQLException("Bean introspection failed: " + e.getMessage());
}
return beanInfo.getPropertyDescriptors();
}
/**
* 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(final Object value, final 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;
}
}
/**
* The positions in the returned array represent column numbers. The values stored at each
* position represent the index in the PropertyDescriptor[]
for the bean property
* that matches the column name. If no bean property was found for a column, the position is set
* to PROPERTY_NOT_FOUND
.
*
* @param rsmd
* The ResultSetMetaData
containing column information.
*
* @param props
* The bean property descriptors.
*
* @throws SQLException
* if a database access error occurs
*
* @return An int[] with column index to property index mappings. The 0th element is meaningless
* because JDBC column indexing starts at 1.
*/
protected int[] mapColumnsToProperties(final ResultSetMetaData rsmd, final PropertyDescriptor[] props)
throws SQLException {
final int cols = rsmd.getColumnCount();
final int columnToProperty[] = new int[cols + 1];
Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND);
for (int col = 1; col <= cols; col++) {
String columnName = rsmd.getColumnLabel(col);
if (null == columnName || 0 == columnName.length()) {
columnName = rsmd.getColumnName(col);
}
for (int i = 0; i < props.length; i++) {
if (columnName.equalsIgnoreCase(props[i].getName())) {
columnToProperty[col] = i;
break;
}
}
if (columnToProperty[col] == PROPERTY_NOT_FOUND) {
columnName = columnName.replace("_", "");
for (int i = 0; i < props.length; i++) {
if (columnName.equalsIgnoreCase(props[i].getName())) {
columnToProperty[col] = i;
break;
}
}
}
}
return columnToProperty;
}
/**
* 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 c
* The Class to create an object from.
* @return A newly created object of the Class.
* @throws SQLException
* if creation failed.
*/
protected T newInstance(final Class c) throws SQLException {
try {
return c.getConstructor().newInstance();
} catch (final InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
throw new SQLException("Cannot create " + c.getName() + ": " + e.getMessage());
}
}
/**
* Convert a ResultSet
column into an object. Simple implementations could just
* call rs.getObject(index)
while more complex implementations could perform type
* manipulation to match the column's type to the bean property type.
*
*
* This implementation calls the appropriate ResultSet
getter method for the given
* property type to perform the type conversion. If the property type doesn't match one of the
* supported ResultSet
types, getObject
is called.
*
*
* @param rs
* The ResultSet
currently being processed. It is positioned on a valid
* row before being passed into this method.
* @param rsmd
* TODO
* @param column
* 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 ResultSet
at the given column index after optional
* type processing or null
if the column value was SQL NULL.
*/
protected Object processColumn(
final ResultSet rs,
final ResultSetMetaData rsmd,
final int column,
final Class> propType) throws SQLException {
if (!propType.isPrimitive() && rs.getObject(column) == null) {
return null;
}
if (propType.equals(String.class)) {
return rs.getString(column);
} else if (propType.equals(Integer.TYPE) || propType.equals(Integer.class)) {
return rs.getInt(column);
} else if (propType.equals(Boolean.TYPE) || propType.equals(Boolean.class)) {
return rs.getBoolean(column);
} else if (propType.equals(Long.TYPE) || propType.equals(Long.class)) {
return rs.getLong(column);
} else if (propType.equals(Double.TYPE) || propType.equals(Double.class)) {
return rs.getDouble(column);
} else if (propType.equals(Float.TYPE) || propType.equals(Float.class)) {
return rs.getFloat(column);
} else if (propType.equals(Short.TYPE) || propType.equals(Short.class)) {
return rs.getShort(column);
} else if (propType.equals(Byte.TYPE) || propType.equals(Byte.class)) {
return rs.getByte(column);
} else if (propType.equals(Character.TYPE) || propType.equals(Character.class)) {
final String s = rs.getString(column);
if (s != null && s.length() == 1) {
return Character.valueOf(s.charAt(0));
} else {
return null;
}
} else if (propType.equals(Timestamp.class)) {
return rs.getTimestamp(column);
} else if (propType.equals(ZonedDateTime.class)) {
switch (rsmd.getColumnType(column)) {
case Types.DATE:
return DateUtils.toZonedDateTimeUtc(rs.getDate(column));
case Types.TIMESTAMP:
case Types.TIMESTAMP_WITH_TIMEZONE:
return DateUtils.toZonedDateTimeUtc(rs.getTimestamp(column));
default:
return DateUtils.toZonedDateTimeUtc(rs.getString(column));
}
} else if (propType.equals(Date.class)) {
return rs.getDate(column);
} else {
return rs.getObject(column);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy