427 lines
16 KiB
Java
427 lines
16 KiB
Java
|
package com.moparisthebest.jdbc;
|
||
|
|
||
|
/*
|
||
|
* 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.
|
||
|
*
|
||
|
* $Header:$
|
||
|
*/
|
||
|
|
||
|
import java.lang.reflect.*;
|
||
|
import java.sql.ResultSet;
|
||
|
import java.sql.ResultSetMetaData;
|
||
|
import java.sql.SQLException;
|
||
|
import java.util.Calendar;
|
||
|
import java.util.HashMap;
|
||
|
import java.util.Map;
|
||
|
|
||
|
import static com.moparisthebest.jdbc.UpdateableDTO.YES;
|
||
|
import static com.moparisthebest.jdbc.UpdateableDTO.NO;
|
||
|
|
||
|
/**
|
||
|
* Map a ResultSet row to an Object. This mapper uses Java reflection to perform the mapping.
|
||
|
* <p/>
|
||
|
* This class is modified from org.apache.beehive.controls.system.jdbc.RowToObjectMapper
|
||
|
* <p/>
|
||
|
* The column names are compared, case insensitive, and with and without underscores (_), to the set* methods
|
||
|
* and fields of the given class to map the fields. For example:
|
||
|
* <p/>
|
||
|
* USERID would prefer method setUserId(), and fall back to field userId if the method doesn't exist
|
||
|
* USER_ID would act the same as the above, but also additionally try to match method setUser_Id(), or field user_id
|
||
|
* <p/>
|
||
|
* First, this class will look for a constructor that takes a ResultSet as a parameter, if it finds one, it will
|
||
|
* instantiate it with that constructor, sending in the ResultSet. Otherwise, this will only try to use public
|
||
|
* setters, but will set fields regardless of specified access, even private fields.
|
||
|
*
|
||
|
* @author Travis Burtrum (modifications from beehive)
|
||
|
*/
|
||
|
public class RowToObjectMapper<T> extends RowMapper {
|
||
|
|
||
|
public static final int TYPE_BOOLEAN = _tmf.getTypeId(Boolean.TYPE);//TypeMappingsFactory.TYPE_BOOLEAN; // not public?
|
||
|
public static final int TYPE_BOOLEAN_OBJ = _tmf.getTypeId(Boolean.class);//TypeMappingsFactory.TYPE_BOOLEAN_OBJ; // not public?
|
||
|
|
||
|
protected final int _columnCount;
|
||
|
protected final Constructor<T> resultSetConstructor;
|
||
|
protected final Class<? extends T> _returnTypeClass; // over-ride non-generic version of this in super class
|
||
|
|
||
|
// only non-null when _returnTypeClass is an array, or a map
|
||
|
protected final Class<?> componentType;
|
||
|
protected final boolean returnMap;
|
||
|
|
||
|
protected AccessibleObject[] _fields;
|
||
|
protected int[] _fieldTypes;
|
||
|
|
||
|
protected final Object[] _args = new Object[1];
|
||
|
|
||
|
public RowToObjectMapper(ResultSet resultSet, Class<T> returnTypeClass) {
|
||
|
this(resultSet, returnTypeClass, null, null);
|
||
|
}
|
||
|
|
||
|
public RowToObjectMapper(ResultSet resultSet, Class<T> returnTypeClass, Class<?> mapValType) {
|
||
|
this(resultSet, returnTypeClass, null, mapValType);
|
||
|
}
|
||
|
|
||
|
public RowToObjectMapper(ResultSet resultSet, Class<T> returnTypeClass, Calendar cal) {
|
||
|
this(resultSet, returnTypeClass, cal, null);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a new RowToObjectMapper.
|
||
|
*
|
||
|
* @param resultSet ResultSet to map
|
||
|
* @param returnTypeClass Class to map to.
|
||
|
* @param cal Calendar instance for date/time mappings.
|
||
|
*/
|
||
|
public RowToObjectMapper(ResultSet resultSet, Class<T> returnTypeClass, Calendar cal, Class<?> mapValType) {
|
||
|
super(resultSet, returnTypeClass, cal);
|
||
|
returnMap = Map.class.isAssignableFrom(returnTypeClass);
|
||
|
if(returnMap){
|
||
|
_returnTypeClass = ResultSetMapper.getConcreteClass(returnTypeClass, HashMap.class);
|
||
|
componentType = mapValType;
|
||
|
}else{
|
||
|
_returnTypeClass = returnTypeClass;
|
||
|
// detect if we want an array back
|
||
|
componentType = returnTypeClass.getComponentType();
|
||
|
}
|
||
|
|
||
|
_fields = null;
|
||
|
|
||
|
// detect if returnTypeClass has a constructor that takes a ResultSet, if so, our job couldn't be easier...
|
||
|
Constructor<T> resultSetConstructor = null;
|
||
|
try {
|
||
|
resultSetConstructor = returnTypeClass.getConstructor(ResultSet.class);
|
||
|
if (!resultSetConstructor.isAccessible())
|
||
|
resultSetConstructor.setAccessible(true);
|
||
|
} catch (Throwable e) {
|
||
|
// do nothing, no such constructor
|
||
|
}
|
||
|
this.resultSetConstructor = resultSetConstructor;
|
||
|
|
||
|
try {
|
||
|
_columnCount = resultSet.getMetaData().getColumnCount();
|
||
|
} catch (SQLException e) {
|
||
|
throw new MapperException("RowToObjectMapper: SQLException: " + e.getMessage(), e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Do the mapping.
|
||
|
*
|
||
|
* @return An object instance.
|
||
|
*/
|
||
|
@SuppressWarnings({"unchecked"})
|
||
|
public T mapRowToReturnType() {
|
||
|
|
||
|
if (resultSetConstructor != null)
|
||
|
try {
|
||
|
return resultSetConstructor.newInstance(_resultSet);
|
||
|
} catch (Throwable e) {
|
||
|
throw new MapperException(e.getClass().getName() + " when trying to create instance of : "
|
||
|
+ _returnTypeClass.getName() + " sending in a ResultSet object as a parameter", e);
|
||
|
}
|
||
|
|
||
|
if(returnMap) // we want a map
|
||
|
try {
|
||
|
final Map<String, Object> ret = (Map<String, Object>)_returnTypeClass.newInstance();
|
||
|
final ResultSetMetaData md = _resultSet.getMetaData();
|
||
|
final int columnLength = _columnCount+1;
|
||
|
if(componentType != null && componentType != Object.class){ // we want a specific value type
|
||
|
int typeId = _tmf.getTypeId(componentType);
|
||
|
for(int x = 1; x < columnLength; ++x)
|
||
|
ret.put(md.getColumnName(x).toLowerCase(), extractColumnValue(x, typeId));
|
||
|
} else // we want a generic object type
|
||
|
for(int x = 1; x < columnLength; ++x)
|
||
|
ret.put(md.getColumnName(x).toLowerCase(), _resultSet.getObject(x));
|
||
|
return _returnTypeClass.cast(ret);
|
||
|
} catch (Throwable e) {
|
||
|
throw new MapperException(e.getClass().getName() + " when trying to create a Map<String, "
|
||
|
+ (componentType == null ? "java.lang.Object" : componentType.getName()) + "> from a ResultSet row" +
|
||
|
", all columns must be of the map value type", e);
|
||
|
}
|
||
|
else if(componentType != null) // we want an array
|
||
|
try {
|
||
|
final Object[] ret = (Object[])Array.newInstance(componentType, _columnCount);
|
||
|
final int typeId = _tmf.getTypeId(componentType);
|
||
|
for(int x = 0; x < _columnCount;)
|
||
|
ret[x] = extractColumnValue(++x, typeId);
|
||
|
return _returnTypeClass.cast(ret);
|
||
|
} catch (Throwable e) {
|
||
|
throw new MapperException(e.getClass().getName() + " when trying to create a "
|
||
|
+ componentType.getName() + "[] from a ResultSet row, all columns must be of that type", e);
|
||
|
}
|
||
|
|
||
|
|
||
|
T resultObject = null;
|
||
|
|
||
|
// if the ResultSet only contains a single column we may be able to map directly
|
||
|
// to the return type -- if so we don't need to build any structures to support
|
||
|
// mapping
|
||
|
if (_columnCount == 1) {
|
||
|
|
||
|
final int typeId = _tmf.getTypeId(_returnTypeClass);
|
||
|
|
||
|
try {
|
||
|
if (typeId != TypeMappingsFactory.TYPE_UNKNOWN) {
|
||
|
return _returnTypeClass.cast(extractColumnValue(1, typeId));
|
||
|
} else {
|
||
|
// we still might want a single value (i.e. java.util.Date)
|
||
|
Object val = extractColumnValue(1, typeId);
|
||
|
if (_returnTypeClass.isAssignableFrom(val.getClass())) {
|
||
|
return _returnTypeClass.cast(val);
|
||
|
}
|
||
|
}
|
||
|
} catch (Exception e) {
|
||
|
throw new MapperException(e.getMessage(), e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (_fields == null) {
|
||
|
try {
|
||
|
getFieldMappings();
|
||
|
} catch (SQLException e) {
|
||
|
throw new MapperException(e.getMessage(), e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
resultObject = _returnTypeClass.newInstance();
|
||
|
} catch (Throwable e) {
|
||
|
throw new MapperException(e.getClass().getName() + " when trying to create instance of : "
|
||
|
+ _returnTypeClass.getName(), e);
|
||
|
}
|
||
|
|
||
|
for (int i = 1; i < _fields.length; i++) {
|
||
|
AccessibleObject f = _fields[i];
|
||
|
|
||
|
try {
|
||
|
_args[0] = extractColumnValue(i, _fieldTypes[i]);
|
||
|
//System.out.printf("field: '%s' obj: '%s' fieldType: '%s'\n", _fields[i], _args[0], _fieldTypes[i]);
|
||
|
// custom hacked-in support for enums, can do better when we scrap org.apache.beehive.controls.system.jdbc.TypeMappingsFactory
|
||
|
if(_fieldTypes[i] == 0 && _args[0] instanceof String){
|
||
|
Class<?> fieldType = f instanceof Field ? ((Field)f).getType() : ((Method)f).getParameterTypes()[0];
|
||
|
if(Enum.class.isAssignableFrom(fieldType))
|
||
|
_args[0] = Enum.valueOf((Class<? extends Enum>)fieldType, (String)_args[0]);
|
||
|
}
|
||
|
if (f instanceof Field) {
|
||
|
((Field) f).set(resultObject, _args[0]);
|
||
|
} else {
|
||
|
((Method) f).invoke(resultObject, _args);
|
||
|
}
|
||
|
} catch (SQLException e) {
|
||
|
throw new MapperException(e.getMessage(), e);
|
||
|
} catch (IllegalArgumentException iae) {
|
||
|
|
||
|
try {
|
||
|
ResultSetMetaData md = _resultSet.getMetaData();
|
||
|
if (f instanceof Field) {
|
||
|
throw new MapperException("The declared Java type for field " + ((Field) f).getName()
|
||
|
+ ((Field) f).getType().toString()
|
||
|
+ " is incompatible with the SQL format of column " + i + " '" + md.getColumnName(i)
|
||
|
+ "' (" + md.getColumnTypeName(i)
|
||
|
+ ") which returns objects of type " + _args[0].getClass().getName());
|
||
|
} else {
|
||
|
throw new MapperException("The declared Java type for method " + ((Method) f).getName()
|
||
|
+ ((Method) f).getParameterTypes()[0].toString()
|
||
|
+ " is incompatible with the SQL format of column " + i + " '" + md.getColumnName(i)
|
||
|
+ "' (" + md.getColumnTypeName(i)
|
||
|
+ ") which returns objects of type " + _args[0].getClass().getName());
|
||
|
}
|
||
|
} catch (SQLException e) {
|
||
|
throw new MapperException(e.getMessage(), e);
|
||
|
}
|
||
|
|
||
|
} catch (IllegalAccessException e) {
|
||
|
if (f instanceof Field) {
|
||
|
throw new MapperException("IllegalAccessException when trying to access field " + ((Field) f).getName(), e);
|
||
|
} else {
|
||
|
throw new MapperException("IllegalAccessException when trying to access method " + ((Method) f).getName(), e);
|
||
|
}
|
||
|
} catch (InvocationTargetException e) {
|
||
|
throw new MapperException("InvocationTargetException when trying to access method " + ((Method) f).getName(), e);
|
||
|
}
|
||
|
}
|
||
|
// if this resultObject is Finishable, call finish()
|
||
|
if (resultObject instanceof Finishable)
|
||
|
try {
|
||
|
((Finishable) resultObject).finish(_resultSet);
|
||
|
} catch (SQLException e) {
|
||
|
throw new MapperException(e.getMessage(), e);
|
||
|
}
|
||
|
return resultObject;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Provided so we can extract a column value to use for the key of a map
|
||
|
*
|
||
|
* @param index
|
||
|
* @param classType
|
||
|
* @return
|
||
|
* @throws SQLException
|
||
|
*/
|
||
|
@SuppressWarnings({"unchecked"})
|
||
|
public <E> E extractColumnValue(int index, Class<E> classType) throws SQLException {
|
||
|
return classType.cast(extractColumnValue(index, _tmf.getTypeId(classType)));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Provided so we know whether to map all values to a type, or just the second one
|
||
|
*
|
||
|
* @return
|
||
|
*/
|
||
|
public int getColumnCount() {
|
||
|
return this._columnCount;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Build the structures necessary to do the mapping
|
||
|
*
|
||
|
* @throws SQLException on error.
|
||
|
*/
|
||
|
protected void getFieldMappings()
|
||
|
throws SQLException {
|
||
|
|
||
|
final String[] keys = getKeysFromResultSet();
|
||
|
//System.out.println("keys: "+ Arrays.toString(keys));
|
||
|
|
||
|
// added this to handle stripping '_' from keys
|
||
|
Map<String, String> strippedKeys = new HashMap<String, String>();
|
||
|
for (final String key : keys) {
|
||
|
String strippedKey = key;
|
||
|
if (key != null) {
|
||
|
strippedKey = key.replaceAll("_", "");
|
||
|
if (key.equals(strippedKey))
|
||
|
continue;
|
||
|
strippedKeys.put(strippedKey, key);
|
||
|
}
|
||
|
}
|
||
|
//System.out.println("strippedKeys: "+strippedKeys);
|
||
|
//
|
||
|
// find fields or setters for return class
|
||
|
//
|
||
|
HashMap<String, AccessibleObject> mapFields = new HashMap<String, AccessibleObject>(_columnCount * 2);
|
||
|
for (int i = 1; i <= _columnCount; i++) {
|
||
|
mapFields.put(keys[i], null);
|
||
|
}
|
||
|
|
||
|
// public methods
|
||
|
Method[] classMethods = _returnTypeClass.getMethods();
|
||
|
for (Method m : classMethods) {
|
||
|
//System.out.printf("method: '%s', isSetterMethod: '%s'\n", m, isSetterMethod(m));
|
||
|
if (isSetterMethod(m)) {
|
||
|
String fieldName = m.getName().substring(3).toUpperCase();
|
||
|
//System.out.println("METHOD-fieldName: "+fieldName);
|
||
|
if (!mapFields.containsKey(fieldName)) {
|
||
|
fieldName = strippedKeys.get(fieldName);
|
||
|
if (fieldName == null)
|
||
|
continue;
|
||
|
}
|
||
|
// check for overloads
|
||
|
Object field = mapFields.get(fieldName);
|
||
|
if (field == null) {
|
||
|
mapFields.put(fieldName, m);
|
||
|
} else {
|
||
|
throw new MapperException("Unable to choose between overloaded methods " + m.getName()
|
||
|
+ " on the " + _returnTypeClass.getName() + " class. Mapping is done using "
|
||
|
+ "a case insensitive comparison of SQL ResultSet columns to field "
|
||
|
+ "names and public setter methods on the return class. Columns are also "
|
||
|
+ "stripped of '_' and compared if no match is found with them.");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// fix for 8813: include inherited and non-public fields
|
||
|
for (Class clazz = _returnTypeClass; clazz != null && clazz != Object.class; clazz = clazz.getSuperclass()) {
|
||
|
//System.out.println("searching for fields in class: "+clazz.getName());
|
||
|
Field[] classFields = clazz.getDeclaredFields();
|
||
|
//System.out.println("fields in class: "+Arrays.toString(classFields));
|
||
|
for (Field f : classFields) {
|
||
|
if (Modifier.isStatic(f.getModifiers())) continue;
|
||
|
//if (!Modifier.isPublic(f.getModifiers())) continue; // commented this out to work on all types of fields
|
||
|
String fieldName = f.getName().toUpperCase();
|
||
|
//System.out.println("fieldName: "+fieldName);
|
||
|
if (!mapFields.containsKey(fieldName)) {
|
||
|
fieldName = strippedKeys.get(fieldName);
|
||
|
if (fieldName == null)
|
||
|
continue;
|
||
|
}
|
||
|
Object field = mapFields.get(fieldName);
|
||
|
if (field == null) {
|
||
|
mapFields.put(fieldName, f);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// finally actually init the fields array
|
||
|
_fields = new AccessibleObject[_columnCount + 1];
|
||
|
_fieldTypes = new int[_columnCount + 1];
|
||
|
|
||
|
for (int i = 1; i < _fields.length; i++) {
|
||
|
AccessibleObject f = mapFields.get(keys[i]);
|
||
|
if (f == null) {
|
||
|
throw new MapperException("Unable to map the SQL column " + keys[i]
|
||
|
+ " to a field on the " + _returnTypeClass.getName() +
|
||
|
" class. Mapping is done using a case insensitive comparision of SQL ResultSet "
|
||
|
+ "columns to field "
|
||
|
+ "names and public setter methods on the return class. Columns are also "
|
||
|
+ "stripped of '_' and compared if no match is found with them.");
|
||
|
}
|
||
|
f.setAccessible(true);
|
||
|
_fields[i] = f;
|
||
|
if (f instanceof Field) {
|
||
|
_fieldTypes[i] = _tmf.getTypeId(((Field) f).getType());
|
||
|
} else {
|
||
|
_fieldTypes[i] = _tmf.getTypeId(((Method) f).getParameterTypes()[0]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected Object extractColumnValue(int index, int resultType) throws SQLException {
|
||
|
try{
|
||
|
if (resultType != TYPE_BOOLEAN && resultType != TYPE_BOOLEAN_OBJ)
|
||
|
return super.extractColumnValue(index, resultType);
|
||
|
else {
|
||
|
// do some special handling to convert a database string to a boolean
|
||
|
boolean ret;
|
||
|
try {
|
||
|
// try to get an actual boolean from the database
|
||
|
ret = _resultSet.getBoolean(index);
|
||
|
// null seems to get returned as false above, so String code won't run if its null
|
||
|
if (_resultSet.wasNull())
|
||
|
if (resultType == TYPE_BOOLEAN_OBJ)
|
||
|
return null; // only return null for Boolean object
|
||
|
else
|
||
|
throw new MapperException(String.format("Implicit conversion of database string to boolean failed on column '%d'. Returned string needs to be 'Y' or 'N' and was instead 'null'. If you want to accept null values, make it an object Boolean instead of primitive boolean.", index));
|
||
|
} catch (SQLException e) {
|
||
|
// if we are here, it wasn't a boolean or null, so try to grab a string instead
|
||
|
String bool = _resultSet.getString(index);//.toUpperCase(); // do we want it case-insensitive?
|
||
|
ret = YES.equals(bool);
|
||
|
if (!ret && !NO.equals(bool))
|
||
|
throw new MapperException(String.format("Implicit conversion of database string to boolean failed on column '%d'. Returned string needs to be 'Y' or 'N' and was instead '%s'.", index, bool));
|
||
|
//throw e;
|
||
|
}
|
||
|
return ret ? Boolean.TRUE : Boolean.FALSE;
|
||
|
}
|
||
|
}catch(SQLException e){
|
||
|
throw new SQLExceptionColumnNum(e, index);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static <T> T fixNull(Class<T> returnType) {
|
||
|
return returnType.cast(_tmf.fixNull(returnType));
|
||
|
}
|
||
|
}
|
||
|
|