/*
Copyright (c) 2003-2008 ITerative Consulting Pty Ltd. All Rights Reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted
provided that the following conditions are met:
o Redistributions of source code must retain the above copyright notice, this list of conditions and
the following disclaimer.
o Redistributions in binary form must reproduce the above copyright notice, this list of conditions
and the following disclaimer in the documentation and/or other materials provided with the distribution.
o This jcTOOL Helper Class software, whether in binary or source form may not be used within,
or to derive, any other product without the specific prior written permission of the copyright holder
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package GenericDBMS;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.log4j.Logger;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.jdbc.core.RowMapper;
import Framework.DataValue;
import Framework.DateTimeData;
import Framework.OverriddenAttribute;
import Framework.ParameterHolder;
import Framework.UsageException;
/**
* A row mapper that maps columns to bean properties
*/
public class BeanRowMapper implements RowMapper {
private static Logger logger = Logger.getLogger(BeanRowMapper.class);
/**
* This cache caches the property descriptors for classes
*/
private static Map<Class<?>, Map<String, PropertyDescriptor>> cache = new ConcurrentHashMap<Class<?>, Map<String,PropertyDescriptor>>(1024);
private BeanWrapper beanWrapper;
private BeanWrapper beanClassWrapper;
private String[] columnNamesToMap;
private Object bean;
private ResultSetHelper helper;
private int rowCounter = 0;
/**
* @param beanClass
*/
public BeanRowMapper(Class<?> beanClass) {
this(null, beanClass, null);
}
public BeanRowMapper(Object bean) {
this(bean, null, null);
}
public BeanRowMapper(Object bean, Class<?> beanClass) {
this(bean, beanClass, null);
}
public BeanRowMapper(Class<?> beanClass, String[] columnNamesToMap) {
this(null, beanClass, columnNamesToMap);
}
// public BeanRowMapper(List<?> target, Class<T> rowClass) {
// this(target, rowClass, null);
// }
/**
* Create a new instance. If the passed bean is non-null, the results will be read
* into this object. Otherwise, a new instance of the passed class will be created
* to contain the results.
*
* @param bean the bean we are mapping to
* @param beanClass the bean we are mapping into
* @param columnNamesToMap the columns names in the resultset to map
* to bean properties
*/
public BeanRowMapper(Object bean, Class<?> beanClass, String[] columnNamesToMap) {
if (bean == null) {
this.beanWrapper = new BeanWrapperImpl(beanClass);
this.columnNamesToMap = columnNamesToMap;
}
else {
this.bean = bean;
// AD:28/10/2008: JCT-595 Create a beanClassWrapper if the beanClass is different from the bean
if (beanClass != null && !(bean.getClass().equals(beanClass))) {
this.beanClassWrapper = new BeanWrapperImpl(beanClass);
}
this.beanWrapper = new BeanWrapperImpl(bean, null, null);
this.columnNamesToMap = null;
}
}
/**
* Sets the result set helper to the passed value. This helper will be used to map the
* rows onto the underlying object. If the passed helper is null, a new one will be
* created when needed.
* @param helper
*/
public void setResultSetHelper(ResultSetHelper helper) {
this.helper = helper;
}
/**
* Set the wrapped instance of the bean to the passed argument
* @param bean
* @throws UsageException if the passed bean is null
*/
public void setBean(Object bean) {
if (bean == null) {
throw new UsageException("bean should not be null");
}
else {
this.bean = bean;
if (bean.getClass().equals(beanWrapper.getWrappedClass())) {
// No need to clear the column names, they're the same as the existing
this.beanWrapper.setWrappedInstance(bean);
}
else {
this.beanWrapper = new BeanWrapperImpl(bean, null, null);
this.columnNamesToMap = null;
}
}
}
/**
* Does the actual mapping using a bean wrapper.
*
* @param resultSet
* @param rowsExpected
* @return
* @throws java.sql.SQLException
*/
@SuppressWarnings({ "unchecked" })
public Object mapRow(ResultSet resultSet, int rowsExpected) throws SQLException {
if (resultSet == null && helper == null) {
throw new NullPointerException("resultSet must be passed as non-null,or setResultSetHelper() must be called with a valid result set helper.");
}
if (helper == null) {
helper = new ResultSetHelper(resultSet);
}
else if (resultSet == null) {
resultSet = helper.getResultSet();
}
if (columnNamesToMap == null) {
setColumnNamesFromResult(resultSet);
}
if (this.bean == null) {
beanWrapper.setWrappedInstance(BeanUtils.instantiateClass(beanWrapper.getWrappedClass()));
}
else {
beanWrapper.setWrappedInstance(this.bean);
}
if (logger.isDebugEnabled()) {
logger.debug("Mapping Row to Object [" + beanWrapper.getWrappedClass().getName() + "]");
}
for (int i = 0; i < columnNamesToMap.length; i++) {
ParameterHolder beanWrapperHolder = new ParameterHolder(beanWrapper);
PropertyDescriptor descriptor = getDescriptorAndBeanForColumnName(columnNamesToMap[i], beanWrapperHolder);
BeanWrapper localWrapper = ((BeanWrapper) beanWrapperHolder.getObject());
if (descriptor == null) {
logger.error("Skipping unknown property \"" + columnNamesToMap[i] + "\"");
continue;
}
else if (localWrapper == null) {
logger.info("Skipping unknown property \""
+ columnNamesToMap[i] + "\" on " + descriptor.getName()
+ " because the object to map to is null");
continue;
}
Class clazz = descriptor.getPropertyType();
String descriptorName = descriptor.getName();
if (DataValue.class.isAssignableFrom(clazz)) {
localWrapper.setPropertyValue(descriptorName, helper.getDataValue(columnNamesToMap[i], clazz));
}
else {
Object obj = resultSet.getObject(columnNamesToMap[i]);
if (obj instanceof String) {
if (clazz.equals(Integer.TYPE)) {
int theInt = resultSet.getInt(columnNamesToMap[i]);
localWrapper.setPropertyValue(descriptorName, theInt);
}
// TF:6/7/07:Forte automatically right trims strings returned from the database, so do the same.
// TF:24/03/2009:DET-88:Ensure we only trim strings of the correct type
else {
int columnNumber = resultSet.findColumn(columnNamesToMap[i]);
localWrapper.setPropertyValue(descriptorName, this.helper.determineStringToReturn((String)obj, columnNumber));
}
}
else if (obj instanceof BigDecimal) {
if (clazz.equals(Boolean.TYPE)) {
Integer theInt = new Integer(resultSet.getInt(columnNamesToMap[i]));
Boolean bool = Boolean.valueOf((theInt.intValue() != 0));
localWrapper.setPropertyValue(descriptorName, bool);
}
else if (clazz.equals(Integer.TYPE)) {
Integer theInt = new Integer(resultSet.getBigDecimal(columnNamesToMap[i]).toBigInteger().intValue());
localWrapper.setPropertyValue(descriptorName, theInt);
}
else if (clazz.equals(Float.TYPE)) {
Float theFloat = new Float(resultSet.getBigDecimal(columnNamesToMap[i]).floatValue());
localWrapper.setPropertyValue(descriptorName, theFloat);
}
else if (clazz.equals(Double.TYPE)) {
Double theDouble = new Double(resultSet.getBigDecimal(columnNamesToMap[i]).doubleValue());
localWrapper.setPropertyValue(descriptorName, theDouble);
}
else if (clazz.equals(String.class)) {
String theStr = Integer.toString(resultSet.getBigDecimal(columnNamesToMap[i]).toBigInteger().intValue());
localWrapper.setPropertyValue(descriptorName, theStr);
}
else {
localWrapper.setPropertyValue(descriptorName, obj);
}
}
else if (((obj instanceof Short) || (obj instanceof Integer)) && (clazz.equals(Boolean.TYPE))) {
Integer theInt = new Integer(resultSet.getInt(columnNamesToMap[i]));
Boolean bool = Boolean.valueOf((theInt.intValue() != 0));
localWrapper.setPropertyValue(descriptorName, bool);
}
else if (obj instanceof Double && clazz.equals(Float.TYPE)) {
Float theFloat = new Float(resultSet.getBigDecimal(columnNamesToMap[i]).floatValue());
localWrapper.setPropertyValue(descriptorName, theFloat);
}
else if (obj instanceof Date && clazz.equals(String.class)) { // CraigM:05/08/2008 - Map a Date to a String
localWrapper.setPropertyValue(descriptorName, obj.toString());
}
else if (obj instanceof Timestamp && clazz.equals(String.class)) {
DateTimeData dtd = new DateTimeData((Timestamp)obj);
localWrapper.setPropertyValue(descriptorName, dtd.toString());
}
else {
localWrapper.setPropertyValue(descriptorName, obj);
}
}
}
this.rowCounter++;
return beanWrapper.getWrappedInstance();
}
/**
* Map a single row from the result set onto the bean object. This method returns the mapped object.
* Any SQLExceptions thrown by this method are mapped to Spring DataAccessException subclasses and re-thrown
* @param resultSet
* @return
*/
public Object mapRow(ResultSet resultSet) {
try {
return this.mapRow(resultSet, 1);
}
catch (SQLException e) {
throw DBUtilities.translateSQLException(Constants.DB_VT_UNKNOWN, e);
}
}
/**
* Map a single row from the result set onto the bean object. This method returns the mapped object.
* Any SQLExceptions thrown by this method are mapped to Spring DataAccessException subclasses and re-thrown.
* This method will throw a UsageException if a valid ResultSetHelper has not been set via a call to
* {@link #setResultSetHelper(ResultSetHelper)} or a previous call to {@link #mapRow(ResultSet)} or {@link #mapRow(ResultSet, int)}
* @return
*/
public Object mapRow() {
if (this.helper == null) {
throw new UsageException("no valid ResultSetHelper has been defined.");
}
try {
return this.mapRow(null, 1);
}
catch (SQLException e) {
throw DBUtilities.translateSQLException(Constants.DB_VT_UNKNOWN, e);
}
}
/**
* Map a single row from the result set onto the bean object. This method returns the mapped object.
* Any SQLExceptions thrown by this method are mapped to Spring DataAccessException subclasses and re-thrown.
* This method will throw a UsageException if a valid ResultSetHelper has not been set via a call to
* {@link #setResultSetHelper(ResultSetHelper)} or a previous call to {@link #mapRow(ResultSet)} or {@link #mapRow(ResultSet, int)}
* @return
*/
public Object mapRow(int count) {
if (this.helper == null) {
throw new UsageException("no valid ResultSetHelper has been defined.");
}
try {
return this.mapRow(null, count);
}
catch (SQLException e) {
throw DBUtilities.translateSQLException(Constants.DB_VT_UNKNOWN, e);
}
}
private void setColumnNamesFromResult(ResultSet resultSet) throws SQLException {
if (logger.isDebugEnabled()) {
logger.debug("Setting column names to map from resultset meta data");
}
ResultSetMetaData metaData = resultSet.getMetaData();
columnNamesToMap = new String[metaData.getColumnCount()];
for (int i = 0; i < columnNamesToMap.length; i++)
columnNamesToMap[i] = metaData.getColumnName(i + 1);
}
/**
* Find the property descriptor for the column name of the object wrapped in the passed bean wrapper. The
* finding of the property descriptor is case insensitive. If the passed column name is null, the passed
* wrapper is null, or the wrapper does not wrapper an object, null is returned.
* @param columnName
* @param wrapper
* @return
*/
private static PropertyDescriptor findDescriptorForColumnName(String columnName, BeanWrapper wrapper) {
PropertyDescriptor result = null;
if (wrapper != null && columnName != null) {
// Get the property descriptors for this class and cache them
Class<?> clazz = wrapper.getWrappedClass();
if (clazz != null) {
Map<String, PropertyDescriptor> propertyMap = cache.get(clazz);
if (propertyMap == null) {
PropertyDescriptor[] descriptors = wrapper.getPropertyDescriptors();
// Don't need to use anything more fancy than a HashMap here, as we're going to write the values once then only ever
// read them again, so it just needs to be a read-safe map
propertyMap = new HashMap<String, PropertyDescriptor>(descriptors.length);
for (int i = 0; i < descriptors.length; i++) {
// AD:24/10/2008: JCT-595 Check if property has an overridden attribute associated with it
PropertyDescriptor propertyDescriptor = descriptors[i];
Method method = propertyDescriptor.getReadMethod();
// Only consider the readable properties.
if (method != null) {
String propertyName;
OverriddenAttribute annotation = method.getAnnotation(OverriddenAttribute.class);
if (annotation != null && annotation.overridenAttributeName() != null) {
propertyName = annotation.overridenAttributeName();
} else {
propertyName = propertyDescriptor.getName();
}
// Store the property descriptors in a case-insensitive map
propertyMap.put(propertyName.toLowerCase(), propertyDescriptor);
}
}
cache.put(clazz, propertyMap);
}
// Now see if our property is in the resulting map
result = propertyMap.get(columnName.toLowerCase());
}
}
return result;
}
private PropertyDescriptor getDescriptorForColumnName(String columnName, BeanWrapper wrapper) {
if (wrapper == this.beanWrapper && this.beanClassWrapper != null) {
// AD:24/10/2008: JCT-595 if the original beanWrapper is the same as the current one and there
// is a beanClassWrapper then use its descriptors instead
PropertyDescriptor foundPropertyDescriptor = findDescriptorForColumnName( columnName, beanClassWrapper );
return wrapper.getPropertyDescriptor(foundPropertyDescriptor.getName());
} else {
return findDescriptorForColumnName( columnName, wrapper );
}
}
/**
* Sometimes we will need to map to an alias which includes dots in it. For example
* select A.B "A.B"...
* so we must detect if this is the case and handle it appropriately. NB: We cannot use
* the nested properties that Spring supports because these are case sensitive and we
* don't know that our SQL is.
* @param columnName
* @param pBeanHolder
* @return
*/
private PropertyDescriptor getDescriptorAndBeanForColumnName(
String columnName, ParameterHolder pBeanHolder) {
BeanWrapper aWrapper = (BeanWrapper) pBeanHolder.getObject();
int oldIndex = 0;
while (true) {
int index = columnName.indexOf('.', oldIndex);
if (index < 0) {
pBeanHolder.setObject(aWrapper);
return this.getDescriptorForColumnName(columnName
.substring(oldIndex), aWrapper);
} else {
String thisPortion = columnName.substring(oldIndex, index);
oldIndex = index + 1;
PropertyDescriptor descriptor = this
.getDescriptorForColumnName(thisPortion, aWrapper);
if (descriptor == null) {
return null;
} else {
// we need to find a sub-bean of the current bean with this descriptor
aWrapper = new BeanWrapperImpl(aWrapper
.getPropertyValue(descriptor.getName()));
}
}
}
}
public int getRowCount() {
return this.rowCounter;
}
}