/* * JBoss, Home of Professional Open Source. * Copyright 2008, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.ejb.plugins.cmp.jdbc; import java.lang.reflect.Method; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.List; import java.util.Set; import javax.ejb.EJBException; import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCCMPFieldMetaData; import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCCMPFieldPropertyMetaData; import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCTypeMappingMetaData; import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCValueClassMetaData; import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCValuePropertyMetaData; import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCUserTypeMappingMetaData; import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCMappingMetaData; import org.jboss.deployment.DeploymentException; //import org.jboss.logging.Logger; /** * JDBCTypeFactory mapps Java Classes to JDBCType objects. The main job of * this class is to flatten the JDBCValueClassMetaData into columns. * * @author <a href="mailto:dain@daingroup.com">Dain Sundstrom</a> * @author <a href="mailto:alex@jboss.org">Alexey Loubyansky</a> * @version $Revision: 81030 $ */ public final class JDBCTypeFactory { //private static final Logger log = Logger.getLogger(JDBCTypeFactory.class); // // Default CMPFieldStateFactory implementations // /** * This implementation uses field's value as its state. */ public static final CMPFieldStateFactory EQUALS = new CMPFieldStateFactory() { public Object getFieldState(Object fieldValue) { return fieldValue; } public boolean isStateValid(Object state, Object fieldValue) { return state == null ? fieldValue == null : state.equals(fieldValue); } }; /** * This implementation will always suppose that the state is invalid unless * both states are null. */ private static final CMPFieldStateFactory INVALID_UNLESS_NULL = new CMPFieldStateFactory() { public Object getFieldState(Object fieldValue) { return fieldValue; } public boolean isStateValid(Object state, Object fieldValue) { return state == null ? fieldValue == null : false; } }; /** * Field state factory for java.util.Map implementations. The state is * a deep copy of the value. */ private static final CMPFieldStateFactory MAP = new CMPFieldStateFactory() { public Object getFieldState(Object fieldValue) { return cloneValue(fieldValue, Map.class); } public boolean isStateValid(Object state, Object fieldValue) { return (state == null ? fieldValue == null : state.equals(fieldValue)); } }; /** * Field state factory for java.util.List implementations. The state is * a deep copy of the value. */ private static final CMPFieldStateFactory LIST = new CMPFieldStateFactory() { public Object getFieldState(Object fieldValue) { return cloneValue(fieldValue, Collection.class); } public boolean isStateValid(Object state, Object fieldValue) { return (state == null ? fieldValue == null : state.equals(fieldValue)); } }; /** * Field state factory for java.util.Set implementations. The state is * a deep copy of the value. */ private static final CMPFieldStateFactory SET = new CMPFieldStateFactory() { public Object getFieldState(Object fieldValue) { return cloneValue(fieldValue, Collection.class); } public boolean isStateValid(Object state, Object fieldValue) { return (state == null ? fieldValue == null : state.equals(fieldValue)); } }; /** * Field state factory for arrays. The state is a deep copy of the value. */ private static final CMPFieldStateFactory ARRAY = new CMPFieldStateFactory() { public Object getFieldState(Object fieldValue) { Object state = null; if(fieldValue != null) { int length = Array.getLength(fieldValue); state = Array.newInstance(fieldValue.getClass().getComponentType(), length); System.arraycopy(fieldValue, 0, state, 0, length); } return state; } public boolean isStateValid(Object state, Object fieldValue) { boolean valid; if(state == null) { valid = fieldValue == null; } else { if(fieldValue == null) { valid = false; } else { int stateLength = Array.getLength(state); if(stateLength != Array.getLength(fieldValue)) { valid = false; } else { valid = true; for(int i = 0; i < stateLength; ++i) { Object stateEl = Array.get(state, i); Object valueEl = Array.get(fieldValue, i); valid = (stateEl == null ? valueEl == null : stateEl.equals(valueEl)); if(!valid) { break; } } } } } return valid; } }; // // Static // public static final CMPFieldStateFactory getCMPFieldStateFactory(JDBCTypeFactory factory, String implClassName, Class clazz) throws DeploymentException { CMPFieldStateFactory stateFactory; // if the state factory is not provided on the field level use the one from the user type mapping if any if(implClassName == null) { JDBCUserTypeMappingMetaData userMapping = (JDBCUserTypeMappingMetaData)factory.userTypeMappings.get(clazz.getName()); if(userMapping != null) { implClassName = userMapping.getStateFactory(); } } if(implClassName != null) { try { Class implClass = TCLAction.UTIL.getContextClassLoader().loadClass(implClassName); stateFactory = (CMPFieldStateFactory)implClass.newInstance(); } catch(ClassNotFoundException e) { throw new DeploymentException("Could not load state factory class: " + implClassName); } catch(Exception e) { throw new DeploymentException("Failed instantiate state factory: " + implClassName); } } else if(Map.class.isAssignableFrom(clazz)) { stateFactory = MAP; } else if(List.class.isAssignableFrom(clazz)) { stateFactory = LIST; } else if(Set.class.isAssignableFrom(clazz)) { stateFactory = SET; } else if(clazz.isArray()) { stateFactory = ARRAY; } else if(usedWithEqualsStateFactory(clazz)) { stateFactory = EQUALS; } else { stateFactory = INVALID_UNLESS_NULL; } return stateFactory; } public static final boolean checkDirtyAfterGet(JDBCTypeFactory factory, byte checkDirtyAfterGet, Class fieldType) { boolean result; if(checkDirtyAfterGet == JDBCCMPFieldMetaData.CHECK_DIRTY_AFTER_GET_NOT_PRESENT) { JDBCUserTypeMappingMetaData userMapping = (JDBCUserTypeMappingMetaData)factory. userTypeMappings.get(fieldType.getName()); if(userMapping != null && userMapping.checkDirtyAfterGet() != JDBCCMPFieldMetaData.CHECK_DIRTY_AFTER_GET_NOT_PRESENT) { result = userMapping.checkDirtyAfterGet() == JDBCCMPFieldMetaData.CHECK_DIRTY_AFTER_GET_TRUE; } else { result = !isDefaultImmutable(fieldType); } } else { result = checkDirtyAfterGet == JDBCCMPFieldMetaData.CHECK_DIRTY_AFTER_GET_TRUE; } return result; } private static Object cloneValue(Object fieldValue, Class argType) { if(fieldValue == null) { return null; } Class valueType = fieldValue.getClass(); Constructor ctor; try { ctor = valueType.getConstructor(new Class[]{argType}); } catch(NoSuchMethodException e) { throw new IllegalStateException( "Failed to find a ctor in " + valueType + " that takes an instance of " + argType + " as an argument." ); } try { return ctor.newInstance(new Object[]{fieldValue}); } catch(Exception e) { throw new IllegalStateException( "Failed to create an instance of " + valueType + " with the " + fieldValue + " as a ctor argument" ); } } private static final boolean usedWithEqualsStateFactory(Class clazz) { return isDefaultImmutable(clazz) || clazz == java.util.Date.class || clazz == java.sql.Date.class || clazz == java.sql.Time.class || clazz == java.sql.Timestamp.class; } private static final boolean isDefaultImmutable(Class clazz) { boolean result = false; if(clazz.isPrimitive() || clazz == Boolean.class || clazz == Byte.class || clazz == Short.class || clazz == Integer.class || clazz == Long.class || clazz == Float.class || clazz == Double.class || clazz == Character.class || clazz == String.class || clazz == java.math.BigInteger.class || clazz == java.math.BigDecimal.class ) { result = true; } return result; } // // Attributes // // the type mapping to use with the specified database private final JDBCTypeMappingMetaData typeMapping; // all known complex types by java class type private final Map complexTypes = new HashMap(); private final Map mappedSimpleTypes = new HashMap(); /** user types mappings */ private final Map userTypeMappings; public JDBCTypeFactory(JDBCTypeMappingMetaData typeMapping, Collection valueClasses, Map userTypeMappings) throws DeploymentException { this.typeMapping = typeMapping; this.userTypeMappings = userTypeMappings; HashMap valueClassesByType = new HashMap(); for(Iterator i = valueClasses.iterator(); i.hasNext();) { JDBCValueClassMetaData valueClass = (JDBCValueClassMetaData)i.next(); valueClassesByType.put(valueClass.getJavaType(), valueClass); } // convert the value class meta data to a jdbc complex type for(Iterator i = valueClasses.iterator(); i.hasNext();) { JDBCValueClassMetaData valueClass = (JDBCValueClassMetaData)i.next(); JDBCTypeComplex type = createTypeComplex(valueClass, valueClassesByType); complexTypes.put(valueClass.getJavaType(), type); } Iterator i = typeMapping.getMappings().iterator(); while(i.hasNext()) { JDBCMappingMetaData mapping = (JDBCMappingMetaData)i.next(); String sqlType = mapping.getSqlType(); int jdbcType = mapping.getJdbcType(); Class javaType = loadClass(mapping.getJavaType()); boolean notNull = javaType.isPrimitive(); boolean autoIncrement = false; JDBCParameterSetter paramSetter; if(mapping.getParamSetter() != null) { paramSetter = (JDBCParameterSetter)newInstance(mapping.getParamSetter()); } else { paramSetter = JDBCUtil.getParameterSetter(jdbcType, javaType); } JDBCResultSetReader resultReader; if(mapping.getResultReader() != null) { resultReader = (JDBCResultSetReader)newInstance(mapping.getResultReader()); } else { resultReader = JDBCUtil.getResultSetReader(jdbcType, javaType); } JDBCTypeSimple type = new JDBCTypeSimple( null, javaType, jdbcType, sqlType, notNull, autoIncrement, null, paramSetter, resultReader ); mappedSimpleTypes.put(javaType, type); } } public JDBCType getJDBCType(Class javaType) { if(complexTypes.containsKey(javaType)) { return (JDBCTypeComplex)complexTypes.get(javaType); } else { JDBCTypeSimple type = (JDBCTypeSimple)mappedSimpleTypes.get(javaType); if(type == null) { JDBCUserTypeMappingMetaData userTypeMapping = (JDBCUserTypeMappingMetaData)userTypeMappings.get(javaType.getName()); Mapper mapper = null; if(userTypeMapping != null) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); try { javaType = cl.loadClass(userTypeMapping.getMappedType()); } catch(ClassNotFoundException e) { throw new IllegalStateException("Failed to load mapped type: " + userTypeMapping.getMappedType()); } try { mapper = (Mapper)newInstance(userTypeMapping.getMapper()); } catch(DeploymentException e) { throw new IllegalStateException("Failed to create Mapper instance of " + userTypeMapping.getMapper()); } } JDBCMappingMetaData typeMappingMD = typeMapping.getTypeMappingMetaData(javaType); String sqlType = typeMappingMD.getSqlType(); int jdbcType = typeMappingMD.getJdbcType(); boolean notNull = javaType.isPrimitive(); boolean autoIncrement = false; JDBCParameterSetter paramSetter; if(typeMappingMD.getParamSetter() != null) { try { paramSetter = (JDBCParameterSetter)newInstance(typeMappingMD.getParamSetter()); } catch(DeploymentException e) { throw new IllegalStateException(e.getMessage()); } } else { paramSetter = JDBCUtil.getParameterSetter(jdbcType, javaType); } JDBCResultSetReader resultReader; if(typeMappingMD.getResultReader() != null) { try { resultReader = (JDBCResultSetReader)newInstance(typeMappingMD.getResultReader()); } catch(DeploymentException e) { throw new IllegalStateException(e.getMessage()); } } else { resultReader = JDBCUtil.getResultSetReader(jdbcType, javaType); } type = new JDBCTypeSimple( null, javaType, jdbcType, sqlType, notNull, autoIncrement, mapper, paramSetter, resultReader ); } return type; } } public JDBCType getJDBCType(JDBCCMPFieldMetaData cmpField) throws DeploymentException { JDBCType fieldJDBCType; final Class fieldType = cmpField.getFieldType(); if(complexTypes.containsKey(fieldType)) { fieldJDBCType = createTypeComplex(cmpField); } else { fieldJDBCType = createTypeSimple(cmpField); } return fieldJDBCType; } public int getJDBCTypeForJavaType(Class clazz) { return typeMapping.getTypeMappingMetaData(clazz).getJdbcType(); } public JDBCTypeMappingMetaData getTypeMapping() { return typeMapping; } private JDBCTypeComplex createTypeComplex( JDBCValueClassMetaData valueClass, HashMap valueClassesByType) { // get the properties ArrayList propertyList = createComplexProperties(valueClass, valueClassesByType, new PropertyStack()); // transform properties into an array JDBCTypeComplexProperty[] properties = new JDBCTypeComplexProperty[propertyList.size()]; properties = (JDBCTypeComplexProperty[])propertyList.toArray(properties); return new JDBCTypeComplex(properties, valueClass.getJavaType()); } private JDBCTypeSimple createTypeSimple(JDBCCMPFieldMetaData cmpField) throws DeploymentException { String columnName = cmpField.getColumnName(); Class javaType = cmpField.getFieldType(); JDBCMappingMetaData typeMappingMD = typeMapping.getTypeMappingMetaData(javaType); String paramSetter = typeMappingMD.getParamSetter(); String resultReader = typeMappingMD.getResultReader(); int jdbcType; String sqlType = cmpField.getSQLType(); if(sqlType != null) { jdbcType = cmpField.getJDBCType(); } else { // get jdbcType and sqlType from typeMapping sqlType = typeMappingMD.getSqlType(); jdbcType = typeMappingMD.getJdbcType(); } boolean notNull = cmpField.isNotNull(); boolean autoIncrement = cmpField.isAutoIncrement(); Mapper mapper = null; JDBCUserTypeMappingMetaData userTypeMapping = (JDBCUserTypeMappingMetaData)userTypeMappings.get(javaType.getName()); if(userTypeMapping != null) { String mappedTypeStr = userTypeMapping.getMappedType(); try { final ClassLoader contextClassLoader = TCLAction.UTIL.getContextClassLoader(); Class mapperClass = contextClassLoader.loadClass(userTypeMapping.getMapper()); mapper = (Mapper)mapperClass.newInstance(); javaType = contextClassLoader.loadClass(mappedTypeStr); if(cmpField.getSQLType() == null) { JDBCMappingMetaData mappingMD = typeMapping.getTypeMappingMetaData(javaType); sqlType = mappingMD.getSqlType(); jdbcType = mappingMD.getJdbcType(); paramSetter = mappingMD.getParamSetter(); resultReader = mappingMD.getResultReader(); } } catch(ClassNotFoundException e) { throw new DeploymentException("Class not found for mapper: " + userTypeMapping.getMapper(), e); } catch(Exception e) { throw new DeploymentException("Could not instantiate mapper: " + userTypeMapping.getMapper(), e); } } JDBCParameterSetter paramSetterImpl; if(paramSetter == null) { paramSetterImpl = JDBCUtil.getParameterSetter(jdbcType, javaType); } else { paramSetterImpl = (JDBCParameterSetter)newInstance(paramSetter); } JDBCResultSetReader resultReaderImpl; if(resultReader == null) { resultReaderImpl = JDBCUtil.getResultSetReader(jdbcType, javaType); } else { resultReaderImpl = (JDBCResultSetReader)newInstance(resultReader); } return new JDBCTypeSimple( columnName, javaType, jdbcType, sqlType, notNull, autoIncrement, mapper, paramSetterImpl, resultReaderImpl ); } private JDBCTypeComplex createTypeComplex(JDBCCMPFieldMetaData cmpField) { // get the default properties for a field of its type JDBCTypeComplex type = (JDBCTypeComplex)complexTypes.get(cmpField.getFieldType()); JDBCTypeComplexProperty[] defaultProperties = type.getProperties(); // create a map of the overrides based on flat property name HashMap overrides = new HashMap(); for(int i = 0; i < cmpField.getPropertyOverrides().size(); ++i) { JDBCCMPFieldPropertyMetaData p = (JDBCCMPFieldPropertyMetaData)cmpField.getPropertyOverrides().get(i); overrides.put(p.getPropertyName(), p); } // array that will hold the final properites after overrides JDBCTypeComplexProperty[] finalProperties = new JDBCTypeComplexProperty[defaultProperties.length]; // override property default values for(int i = 0; i < defaultProperties.length; i++) { // pop off the override, if present JDBCCMPFieldPropertyMetaData override; override = (JDBCCMPFieldPropertyMetaData)overrides.remove(defaultProperties[i].getPropertyName()); if(override == null) { finalProperties[i] = defaultProperties[i]; finalProperties[i] = new JDBCTypeComplexProperty( defaultProperties[i], cmpField.getColumnName() + "_" + defaultProperties[i].getColumnName(), defaultProperties[i].getJDBCType(), defaultProperties[i].getSQLType(), cmpField.isNotNull() || defaultProperties[i].isNotNull()); } else { // columnName String columnName = override.getColumnName(); if(columnName == null) { columnName = cmpField.getColumnName() + "_" + defaultProperties[i].getColumnName(); } // sql and jdbc type String sqlType = override.getSQLType(); int jdbcType; if(sqlType != null) { jdbcType = override.getJDBCType(); } else { sqlType = defaultProperties[i].getSQLType(); jdbcType = defaultProperties[i].getJDBCType(); } boolean notNull = cmpField.isNotNull() || override.isNotNull() || defaultProperties[i].isNotNull(); finalProperties[i] = new JDBCTypeComplexProperty( defaultProperties[i], columnName, jdbcType, sqlType, notNull); } } // did we find all overriden properties if(overrides.size() > 0) { String propertyName = (String)overrides.keySet().iterator().next(); throw new EJBException("Property " + propertyName + " in field " + cmpField.getFieldName() + " is not a property of value object " + cmpField.getFieldType().getName()); } // return the new complex type return new JDBCTypeComplex(finalProperties, cmpField.getFieldType()); } private ArrayList createComplexProperties( JDBCValueClassMetaData valueClass, HashMap valueClassesByType, PropertyStack propertyStack) { ArrayList properties = new ArrayList(); // add the properties each property to the list java.util.List valueClassProperties = valueClass.getProperties(); for(int i = 0; i < valueClassProperties.size(); ++i) { JDBCValuePropertyMetaData propertyMetaData = (JDBCValuePropertyMetaData)valueClassProperties.get(i); properties.addAll(createComplexProperties(propertyMetaData, valueClassesByType, propertyStack)); } return properties; } private ArrayList createComplexProperties( JDBCValuePropertyMetaData propertyMetaData, HashMap valueClassesByType, PropertyStack propertyStack) { // push my data onto the stack propertyStack.pushPropertyMetaData(propertyMetaData); ArrayList properties = new ArrayList(); Class javaType = propertyMetaData.getPropertyType(); if(!valueClassesByType.containsKey(javaType)) { // this property is a simple type // which makes this the end of the line for recursion String propertyName = propertyStack.getPropertyName(); String columnName = propertyStack.getColumnName(); String sqlType = propertyMetaData.getSqlType(); int jdbcType; if(sqlType != null) { jdbcType = propertyMetaData.getJDBCType(); } else { // get jdbcType and sqlType from typeMapping JDBCMappingMetaData typeMappingMD = typeMapping.getTypeMappingMetaData(javaType); sqlType = typeMappingMD.getSqlType(); jdbcType = typeMappingMD.getJdbcType(); } boolean notNull = propertyStack.isNotNull(); Method[] getters = propertyStack.getGetters(); Method[] setters = propertyStack.getSetters(); properties.add(new JDBCTypeComplexProperty( propertyName, columnName, javaType, jdbcType, sqlType, notNull, getters, setters)); } else { // this property is a value object, recurse JDBCValueClassMetaData valueClass = (JDBCValueClassMetaData)valueClassesByType.get(javaType); properties.addAll(createComplexProperties( valueClass, valueClassesByType, propertyStack)); } // pop my data, back off propertyStack.popPropertyMetaData(); return properties; } private static final class PropertyStack { final ArrayList properties = new ArrayList(); final ArrayList propertyNames = new ArrayList(); final ArrayList columnNames = new ArrayList(); final ArrayList notNulls = new ArrayList(); final ArrayList getters = new ArrayList(); final ArrayList setters = new ArrayList(); public PropertyStack() { } public final void pushPropertyMetaData( JDBCValuePropertyMetaData propertyMetaData) { propertyNames.add(propertyMetaData.getPropertyName()); columnNames.add(propertyMetaData.getColumnName()); notNulls.add(new Boolean(propertyMetaData.isNotNull())); getters.add(propertyMetaData.getGetter()); setters.add(propertyMetaData.getSetter()); if(properties.contains(propertyMetaData)) { throw new EJBException("Circular reference discoverd at " + "property: " + getPropertyName()); } properties.add(propertyMetaData); } public final void popPropertyMetaData() { propertyNames.remove(propertyNames.size() - 1); columnNames.remove(columnNames.size() - 1); notNulls.remove(notNulls.size() - 1); getters.remove(getters.size() - 1); setters.remove(setters.size() - 1); properties.remove(properties.size() - 1); } public final String getPropertyName() { StringBuffer buf = new StringBuffer(); for(int i = 0; i < propertyNames.size(); i++) { if(i > 0) { buf.append("."); } buf.append((String)propertyNames.get(i)); } return buf.toString(); } public final String getColumnName() { StringBuffer buf = new StringBuffer(); for(int i = 0; i < columnNames.size(); i++) { if(i > 0) { buf.append("_"); } buf.append((String)columnNames.get(i)); } return buf.toString(); } public final boolean isNotNull() { for(int i = 0; i < notNulls.size(); i++) { if(((Boolean)notNulls.get(i)).booleanValue()) { return true; } } return false; } public final Method[] getGetters() { return (Method[])getters.toArray(new Method[getters.size()]); } public final Method[] getSetters() { return (Method[])setters.toArray(new Method[setters.size()]); } } private Object newInstance(String className) throws DeploymentException { Class clazz = loadClass(className); try { return clazz.newInstance(); } catch(Exception e) { throw new DeploymentException("Failed to instantiate " + className, e); } } private Class loadClass(String className) throws DeploymentException { try { final ClassLoader contextClassLoader = TCLAction.UTIL.getContextClassLoader(); return contextClassLoader.loadClass(className); } catch(ClassNotFoundException e) { throw new DeploymentException("Failed to load class: " + className, e); } } }