/* * 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.metadata; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import org.jboss.deployment.DeploymentException; import org.jboss.metadata.MetaData; import org.w3c.dom.Element; /** * Imutable class which holds all the information jbosscmp-jdbc needs to know * about a CMP field It loads its data from standardjbosscmp-jdbc.xml and * jbosscmp-jdbc.xml * * @author <a href="mailto:dain@daingroup.com">Dain Sundstrom</a> * @author <a href="sebastien.alborini@m4x.org">Sebastien Alborini</a> * @author <a href="mailto:dirk@jboss.de">Dirk Zimmermann</a> * @author <a href="mailto:vincent.harcq@hubmethods.com">Vincent Harcq</a> * @author <a href="mailto:loubyansky@hotmail.com">Alex Loubyansky</a> * @author <a href="mailto:heiko.rupp@cellent.de">Heiko W.Rupp</a> * * @version $Revision: 81030 $ */ public final class JDBCCMPFieldMetaData { public static final byte CHECK_DIRTY_AFTER_GET_TRUE = 1; public static final byte CHECK_DIRTY_AFTER_GET_FALSE = 2; public static final byte CHECK_DIRTY_AFTER_GET_NOT_PRESENT = 4; /** The entity on which this field is defined. */ private final JDBCEntityMetaData entity; /** The name of this field. */ private final String fieldName; /** The java type of this field */ private final Class fieldType; /** The column name in the table */ private final String columnName; /** * The jdbc type (see java.sql.Types), used in PreparedStatement.setParameter * default value used is intended to cause an exception if used */ private final int jdbcType; /** The sql type, used for table creation. */ private final String sqlType; /** Is this field read only? */ private final boolean readOnly; /** How long is read valid */ private final int readTimeOut; /** Is this field a memeber of the primary keys or the sole prim-key-field. */ private final boolean primaryKeyMember; /** Should null values not be allowed for this field. */ private final boolean notNull; /** Should an index for this field be generated? */ private final boolean genIndex; /** * The Field object in the primary key class for this * cmp field, or null if this field is the prim-key-field. */ private final Field primaryKeyField; /** property overrides */ private final List propertyOverrides = new ArrayList(); /** indicates whether this is an unknown pk field */ private final boolean unknownPkField; /** auto-increment flag */ private final boolean autoIncrement; /** whether this field is a relation table key field*/ private final boolean relationTableField; /** If true, the field should be checked for dirty state after its get method was invoked */ private final byte checkDirtyAfterGet; /** Fully qualified class name of implementation of CMPFieldStateFactory */ private final String stateFactory; private static byte readCheckDirtyAfterGet(Element element, byte defaultValue) throws DeploymentException { byte checkDirtyAfterGet; String dirtyAfterGetStr = MetaData.getOptionalChildContent(element, "check-dirty-after-get"); if(dirtyAfterGetStr == null) { checkDirtyAfterGet = defaultValue; } else { checkDirtyAfterGet = (Boolean.valueOf(dirtyAfterGetStr).booleanValue() ? CHECK_DIRTY_AFTER_GET_TRUE : CHECK_DIRTY_AFTER_GET_FALSE); } return checkDirtyAfterGet; } public static byte readCheckDirtyAfterGet(Element element) throws DeploymentException { return readCheckDirtyAfterGet(element, CHECK_DIRTY_AFTER_GET_NOT_PRESENT); } /** * This constructor is added especially for unknown primary key field */ public JDBCCMPFieldMetaData(JDBCEntityMetaData entity) { this.entity = entity; fieldName = entity.getName() + "_upk"; fieldType = entity.getPrimaryKeyClass(); // java.lang.Object.class columnName = entity.getName() + "_upk"; jdbcType = Integer.MIN_VALUE; sqlType = null; readOnly = entity.isReadOnly(); readTimeOut = entity.getReadTimeOut(); primaryKeyMember = true; notNull = true; primaryKeyField = null; genIndex = false; unknownPkField = true; autoIncrement = false; relationTableField = false; checkDirtyAfterGet = CHECK_DIRTY_AFTER_GET_NOT_PRESENT; stateFactory = null; } /** * Constructs cmp field meta data for a field on the specified entity with * the specified fieldName. * * @param fieldName name of the field for which the meta data will be loaded * @param entity entity on which this field is defined * @throws DeploymentException if data in the entity is inconsistent with field type */ public JDBCCMPFieldMetaData(JDBCEntityMetaData entity, String fieldName) throws DeploymentException { this.entity = entity; this.fieldName = fieldName; fieldType = loadFieldType(entity, fieldName); columnName = fieldName; jdbcType = Integer.MIN_VALUE; sqlType = null; readOnly = entity.isReadOnly(); readTimeOut = entity.getReadTimeOut(); genIndex = false; // initialize primary key info String pkFieldName = entity.getPrimaryKeyFieldName(); if(pkFieldName != null) { // single-valued key so field is null primaryKeyField = null; // is this the pk field if(pkFieldName.equals(fieldName)) { // verify field type if(!entity.getPrimaryKeyClass().equals(fieldType)) { throw new DeploymentException("primkey-field must be the same type as prim-key-class"); } // we are the pk primaryKeyMember = true; } else { primaryKeyMember = false; } } else { // this is a multi-valued key Field[] fields = entity.getPrimaryKeyClass().getFields(); boolean pkMember = false; Field pkField = null; for(int i = 0; i < fields.length; i++) { final Field field = fields[i]; if(field.getName().equals(fieldName)) { // verify field type if(!field.getType().equals(fieldType)) { throw new DeploymentException("Field " + fieldName + " in prim-key-class must be of the same type."); } if(pkField != null) { if(field.getDeclaringClass().equals(entity.getPrimaryKeyClass())) { pkField = field; } org.jboss.logging.Logger.getLogger(getClass().getName() + '.' + entity.getName()).warn( "PK field " + fieldName + " was found more than once in class hierarchy of " + entity.getPrimaryKeyClass().getName() + ". Will use the one from " + pkField.getDeclaringClass().getName() ); } else { pkField = field; } // we are a pk member pkMember = true; } } primaryKeyMember = pkMember; primaryKeyField = pkField; } notNull = fieldType.isPrimitive() || primaryKeyMember; unknownPkField = false; autoIncrement = false; relationTableField = false; checkDirtyAfterGet = CHECK_DIRTY_AFTER_GET_NOT_PRESENT; stateFactory = null; } public JDBCCMPFieldMetaData(JDBCEntityMetaData entity, JDBCCMPFieldMetaData defaultValues) { this.entity = entity; fieldName = defaultValues.getFieldName(); fieldType = defaultValues.getFieldType(); columnName = defaultValues.getColumnName(); jdbcType = defaultValues.getJDBCType(); sqlType = defaultValues.getSQLType(); readOnly = entity.isReadOnly(); readTimeOut = entity.getReadTimeOut(); primaryKeyMember = defaultValues.isPrimaryKeyMember(); primaryKeyField = defaultValues.getPrimaryKeyField(); notNull = defaultValues.isNotNull(); unknownPkField = defaultValues.isUnknownPkField(); autoIncrement = defaultValues.isAutoIncrement(); genIndex = false; // If <dbindex/> is not given on a field, no index is wanted. relationTableField = defaultValues.isRelationTableField(); checkDirtyAfterGet = defaultValues.getCheckDirtyAfterGet(); stateFactory = defaultValues.getStateFactory(); } /** * Constructs cmp field meta data with the data contained in the cmp-field * xml element from a jbosscmp-jdbc xml file. Optional values of the xml * element that are not present are instead loaded from the defalutValues * parameter. * * @param element the xml Element which contains the metadata about * this field * @param defaultValues the JDBCCMPFieldMetaData which contains the values * for optional elements of the element * @throws DeploymentException if the xml element is not semantically correct */ public JDBCCMPFieldMetaData(JDBCEntityMetaData entity, Element element, JDBCCMPFieldMetaData defaultValues) throws DeploymentException { this.entity = entity; // unknown primary key this.unknownPkField = defaultValues.isUnknownPkField(); // Field name // if field-name is specified for unknown-pk, it's set here String unknownFieldName = MetaData.getOptionalChildContent(element, "field-name"); if(unknownPkField && unknownFieldName != null) { fieldName = unknownFieldName; } else { fieldName = defaultValues.getFieldName(); } // Field type // must be set for unknow-pk String unknownPkClass = MetaData.getOptionalChildContent(element, "unknown-pk-class"); if(unknownPkClass == null) { fieldType = defaultValues.getFieldType(); } else { try { fieldType = entity.getClassLoader().loadClass(unknownPkClass); } catch(ClassNotFoundException e) { throw new DeploymentException("could not load the class for " + " unknown primary key: " + unknownPkClass); } } // Column name String columnStr = MetaData.getOptionalChildContent(element, "column-name"); if(columnStr != null) { columnName = columnStr; } else { columnName = defaultValues.getColumnName(); } // JDBC Type String jdbcStr = MetaData.getOptionalChildContent(element, "jdbc-type"); if(jdbcStr != null) { jdbcType = JDBCMappingMetaData.getJdbcTypeFromName(jdbcStr); // SQL Type sqlType = MetaData.getUniqueChildContent(element, "sql-type"); } else { jdbcType = defaultValues.getJDBCType(); sqlType = defaultValues.getSQLType(); } // read-only String readOnlyStr = MetaData.getOptionalChildContent(element, "read-only"); if(readOnlyStr != null) { readOnly = Boolean.valueOf(readOnlyStr).booleanValue(); } else { readOnly = defaultValues.isReadOnly(); } // read-time-out String readTimeOutStr = MetaData.getOptionalChildContent(element, "read-time-out"); if(readTimeOutStr != null) { try { readTimeOut = Integer.parseInt(readTimeOutStr); } catch(NumberFormatException e) { throw new DeploymentException("Invalid number format in " + "read-time-out '" + readTimeOutStr + "': " + e); } } else { readTimeOut = defaultValues.getReadTimeOut(); } // primary key member? this.primaryKeyMember = defaultValues.isPrimaryKeyMember(); // field object of the primary key primaryKeyField = defaultValues.getPrimaryKeyField(); // not-null Element notNullElement = MetaData.getOptionalChild(element, "not-null"); notNull = fieldType.isPrimitive() || primaryKeyMember || (notNullElement != null); // property overrides Iterator iterator = MetaData.getChildrenByTagName(element, "property"); while(iterator.hasNext()) { propertyOverrides.add(new JDBCCMPFieldPropertyMetaData(this, (Element)iterator.next())); } // is the field auto-increment? autoIncrement = MetaData.getOptionalChild(element, "auto-increment") != null; // should an index for this field be generated? if(MetaData.getOptionalChild(element, "dbindex") == null) genIndex = false; else genIndex = true; relationTableField = defaultValues.isRelationTableField(); checkDirtyAfterGet = readCheckDirtyAfterGet(element, defaultValues.getCheckDirtyAfterGet()); String stateFactoryStr = MetaData.getOptionalChildContent(element, "state-factory"); if(stateFactoryStr == null) stateFactory = defaultValues.getStateFactory(); else stateFactory = stateFactoryStr; } /** * Constructs cmp field meta data with the data contained in the cmp-field * xml element from a jbosscmp-jdbc xml file. Optional values of the xml * element that are not present are instead loaded from the defalutValues * parameter. * * This constructor form is used to create cmp field meta data for use as * foreign keys. The primaryKeyMember parameter is very important in this * context because a foreign key is not a primary key member but used a pk * member as the default value. If we did not have the primary key member * parameter this JDBCCMPFieldMetaData would get the value from the * defaultValues and be declared a memeber. */ public JDBCCMPFieldMetaData(JDBCEntityMetaData entity, Element element, JDBCCMPFieldMetaData defaultValues, boolean primaryKeyMember, boolean notNull, boolean readOnly, int readTimeOut, boolean relationTableField) throws DeploymentException { this.entity = entity; fieldName = defaultValues.getFieldName(); fieldType = defaultValues.getFieldType(); String columnStr = MetaData.getOptionalChildContent(element, "column-name"); if(columnStr != null) { columnName = columnStr; } else { columnName = defaultValues.getColumnName(); } // JDBC Type String jdbcStr = MetaData.getOptionalChildContent(element, "jdbc-type"); if(jdbcStr != null) { jdbcType = JDBCMappingMetaData.getJdbcTypeFromName(jdbcStr); sqlType = MetaData.getUniqueChildContent(element, "sql-type"); } else { jdbcType = defaultValues.getJDBCType(); sqlType = defaultValues.getSQLType(); } // read-only this.readOnly = readOnly; // read-time-out this.readTimeOut = readTimeOut; // primary key member? this.primaryKeyMember = primaryKeyMember; // not-null this.notNull = notNull; // field object of the primary key primaryKeyField = defaultValues.getPrimaryKeyField(); // property overrides Iterator iterator = MetaData.getChildrenByTagName(element, "property"); while(iterator.hasNext()) { propertyOverrides.add(new JDBCCMPFieldPropertyMetaData(this, (Element)iterator.next())); } this.unknownPkField = defaultValues.isUnknownPkField(); autoIncrement = MetaData.getOptionalChild(element, "auto-increment") != null; if(MetaData.getOptionalChild(element, "dbindex") == null) genIndex = false; else genIndex = true; this.relationTableField = relationTableField; String dirtyAfterGetStr = MetaData.getOptionalChildContent(element, "check-dirty-after-get"); if(dirtyAfterGetStr == null) { checkDirtyAfterGet = defaultValues.getCheckDirtyAfterGet(); } else { checkDirtyAfterGet = (Boolean.valueOf(dirtyAfterGetStr).booleanValue() ? CHECK_DIRTY_AFTER_GET_TRUE : CHECK_DIRTY_AFTER_GET_FALSE); } String stateFactoryStr = MetaData.getOptionalChildContent(element, "state-factory"); if(stateFactoryStr == null) stateFactory = defaultValues.getStateFactory(); else stateFactory = stateFactoryStr; } /** * Constructs a foreign key or a relation table key field. */ public JDBCCMPFieldMetaData(JDBCEntityMetaData entity, JDBCCMPFieldMetaData defaultValues, String columnName, boolean primaryKeyMember, boolean notNull, boolean readOnly, int readTimeOut, boolean relationTableField) { this.entity = entity; fieldName = defaultValues.getFieldName(); fieldType = defaultValues.getFieldType(); this.columnName = columnName; jdbcType = defaultValues.getJDBCType(); sqlType = defaultValues.getSQLType(); this.readOnly = readOnly; this.readTimeOut = readTimeOut; this.primaryKeyMember = primaryKeyMember; primaryKeyField = defaultValues.getPrimaryKeyField(); this.notNull = notNull; for(Iterator i = defaultValues.propertyOverrides.iterator(); i.hasNext();) { propertyOverrides.add(new JDBCCMPFieldPropertyMetaData( this, (JDBCCMPFieldPropertyMetaData)i.next())); } this.unknownPkField = defaultValues.isUnknownPkField(); autoIncrement = false; genIndex = false; this.relationTableField = relationTableField; checkDirtyAfterGet = defaultValues.getCheckDirtyAfterGet(); stateFactory = defaultValues.getStateFactory(); } /** * Constructs a field that is used as an optimistic lock */ public JDBCCMPFieldMetaData(JDBCEntityMetaData entity, String fieldName, Class fieldType, String columnName, int jdbcType, String sqlType) throws DeploymentException { this.entity = entity; this.fieldName = fieldName; this.fieldType = fieldType; this.columnName = columnName; this.jdbcType = jdbcType; this.sqlType = sqlType; readOnly = false; readTimeOut = -1; primaryKeyMember = false; notNull = true; primaryKeyField = null; unknownPkField = false; autoIncrement = false; genIndex = false; relationTableField = false; checkDirtyAfterGet = CHECK_DIRTY_AFTER_GET_NOT_PRESENT; stateFactory = null; } /** * Gets the entity on which this field is defined * @return the entity on which this field is defined */ public JDBCEntityMetaData getEntity() { return entity; } /** * Gets the name of the field. * @return the name of this field */ public String getFieldName() { return fieldName; } /** * Gets the java Class type of this field. * @return the Class type of this field */ public Class getFieldType() { return fieldType; } /** * Gets the column name the property should use or null if the * column name is not overriden. * @return the name to which this field is persisted or null if the * column name is not overriden */ public String getColumnName() { return columnName; } /** * Gets the JDBC type the property should use or Integer.MIN_VALUE * if not overriden. * @return the jdbc type of this field */ public int getJDBCType() { return jdbcType; } /** * Gets the SQL type the property should use or null * if not overriden. * @return the sql data type string used in create table statements */ public String getSQLType() { return sqlType; } /** * Gets the property overrides. Property overrides change the default * mapping of Dependent Value Object properties. If there are no property * overrides this method returns an empty list. * @return an unmodifiable list of the property overrides. */ public List getPropertyOverrides() { return Collections.unmodifiableList(propertyOverrides); } /** * Is this field read only. A read only field will never be persisted * * @return true if this field is read only */ public boolean isReadOnly() { return readOnly; } /** * Gets the length of time (ms) that a read valid or -1 if data must * always be reread from the database * @return the length of time that data read database is valid, or -1 * if data must always be reread from the database */ public int getReadTimeOut() { return readTimeOut; } /** * Is this field one of the primary key fields? * @return true if this field is one of the primary key fields */ public boolean isPrimaryKeyMember() { return primaryKeyMember; } /** * Should this field allow null values? * @return true if this field will not allow a null value. */ public boolean isNotNull() { return notNull; } /** * Should an index for this field be generated? * Normally this should be false for primary key fields * But it seems there are databases that do not automatically * put indices on primary keys *sigh* * @return true if an index should be generated on this field */ public boolean isIndexed() { return genIndex; } /** * Gets the Field of the primary key object which contains the value of * this field. Returns null, if this field is not a member of the primary * key, or if the primray key is single valued. * @return the Field of the primary key which contains the * value of this field */ public Field getPrimaryKeyField() { return primaryKeyField; } /** * Is this field an unknown primary key field? * @return true if the field is an unknown primary key field */ public boolean isUnknownPkField() { return unknownPkField; } /** * @return true if the key is auto incremented by the database */ public boolean isAutoIncrement() { return autoIncrement; } public boolean isRelationTableField() { return relationTableField; } public byte getCheckDirtyAfterGet() { return checkDirtyAfterGet; } public String getStateFactory() { return stateFactory; } /** * Compares this JDBCCMPFieldMetaData against the specified object. Returns * true if the objects are the same. Two JDBCCMPFieldMetaData are the same * if they both have the same name and are defined on the same entity. * @param o the reference object with which to compare * @return true if this object is the same as the object argument; false * otherwise */ public boolean equals(Object o) { if(o instanceof JDBCCMPFieldMetaData) { JDBCCMPFieldMetaData cmpField = (JDBCCMPFieldMetaData)o; return fieldName.equals(cmpField.fieldName) && entity.equals(cmpField.entity); } return false; } /** * Returns a hashcode for this JDBCCMPFieldMetaData. The hashcode is computed * based on the hashCode of the declaring entity and the hashCode of the * fieldName * @return a hash code value for this object */ public int hashCode() { int result = 17; result = 37 * result + entity.hashCode(); result = 37 * result + fieldName.hashCode(); return result; } /** * Returns a string describing this JDBCCMPFieldMetaData. The exact details * of the representation are unspecified and subject to change, but the * following may be regarded as typical: * * "[JDBCCMPFieldMetaData: fieldName=name, [JDBCEntityMetaData: * entityName=UserEJB]]" * * @return a string representation of the object */ public String toString() { return "[JDBCCMPFieldMetaData : fieldName=" + fieldName + ", " + entity + "]"; } /** * Loads the java type of this field from the entity bean class. If this * bean uses, cmp 1.x persistence, the field type is loaded from the field * in the bean class with the same name as this field. If this bean uses, * cmp 2.x persistence, the field type is loaded from the abstract getter * or setter method for field in the bean class. */ private Class loadFieldType(JDBCEntityMetaData entity, String fieldName) throws DeploymentException { if(entity.isCMP1x()) { // CMP 1.x field Style try { return entity.getEntityClass().getField(fieldName).getType(); } catch(NoSuchFieldException e) { throw new DeploymentException("No field named '" + fieldName + "' found in entity class." + entity.getEntityClass().getName()); } } else { // CMP 2.x abstract accessor style String baseName = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1); String getName = "get" + baseName; String setName = "set" + baseName; Method[] methods = entity.getEntityClass().getMethods(); for(int i = 0; i < methods.length; i++) { // is this a public abstract method? if(Modifier.isPublic(methods[i].getModifiers()) && Modifier.isAbstract(methods[i].getModifiers())) { // get accessor if(getName.equals(methods[i].getName()) && methods[i].getParameterTypes().length == 0 && !methods[i].getReturnType().equals(Void.TYPE)) { return methods[i].getReturnType(); } // set accessor if(setName.equals(methods[i].getName()) && methods[i].getParameterTypes().length == 1 && methods[i].getReturnType().equals(Void.TYPE)) { return methods[i].getParameterTypes()[0]; } } } throw new DeploymentException("No abstract accessors for field " + "named '" + fieldName + "' found in entity class " + entity.getEntityClass().getName()); } } }