/* * Hibernate, Relational Persistence for Idiomatic Java * * License: GNU Lesser General Public License (LGPL), version 2.1 or later. * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.type; import java.io.Serializable; import java.lang.reflect.Method; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.HashMap; import java.util.Map; import org.hibernate.EntityMode; import org.hibernate.FetchMode; import org.hibernate.HibernateException; import org.hibernate.MappingException; import org.hibernate.PropertyNotFoundException; import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer; import org.hibernate.engine.jdbc.Size; import org.hibernate.engine.spi.CascadeStyle; import org.hibernate.engine.spi.Mapping; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.util.StringHelper; import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.tuple.StandardProperty; import org.hibernate.tuple.ValueGeneration; import org.hibernate.tuple.component.ComponentMetamodel; import org.hibernate.tuple.component.ComponentTuplizer; /** * Handles "component" mappings * * @author Gavin King */ public class ComponentType extends AbstractType implements CompositeType, ProcedureParameterExtractionAware { private final TypeFactory.TypeScope typeScope; private final String[] propertyNames; private final Type[] propertyTypes; private final ValueGeneration[] propertyValueGenerationStrategies; private final boolean[] propertyNullability; protected final int propertySpan; private final CascadeStyle[] cascade; private final FetchMode[] joinedFetch; private final boolean isKey; private boolean hasNotNullProperty; private final boolean createEmptyCompositesEnabled; protected final EntityMode entityMode; protected final ComponentTuplizer componentTuplizer; public ComponentType(TypeFactory.TypeScope typeScope, ComponentMetamodel metamodel) { this.typeScope = typeScope; // for now, just "re-flatten" the metamodel since this is temporary stuff anyway (HHH-1907) this.isKey = metamodel.isKey(); this.propertySpan = metamodel.getPropertySpan(); this.propertyNames = new String[propertySpan]; this.propertyTypes = new Type[propertySpan]; this.propertyValueGenerationStrategies = new ValueGeneration[propertySpan]; this.propertyNullability = new boolean[propertySpan]; this.cascade = new CascadeStyle[propertySpan]; this.joinedFetch = new FetchMode[propertySpan]; for ( int i = 0; i < propertySpan; i++ ) { StandardProperty prop = metamodel.getProperty( i ); this.propertyNames[i] = prop.getName(); this.propertyTypes[i] = prop.getType(); this.propertyNullability[i] = prop.isNullable(); this.cascade[i] = prop.getCascadeStyle(); this.joinedFetch[i] = prop.getFetchMode(); if ( !prop.isNullable() ) { hasNotNullProperty = true; } this.propertyValueGenerationStrategies[i] = prop.getValueGenerationStrategy(); } this.entityMode = metamodel.getEntityMode(); this.componentTuplizer = metamodel.getComponentTuplizer(); this.createEmptyCompositesEnabled = metamodel.isCreateEmptyCompositesEnabled(); } public boolean isKey() { return isKey; } public EntityMode getEntityMode() { return entityMode; } public ComponentTuplizer getComponentTuplizer() { return componentTuplizer; } @Override public int getColumnSpan(Mapping mapping) throws MappingException { int span = 0; for ( int i = 0; i < propertySpan; i++ ) { span += propertyTypes[i].getColumnSpan( mapping ); } return span; } @Override public int[] sqlTypes(Mapping mapping) throws MappingException { //Not called at runtime so doesn't matter if its slow :) int[] sqlTypes = new int[getColumnSpan( mapping )]; int n = 0; for ( int i = 0; i < propertySpan; i++ ) { int[] subtypes = propertyTypes[i].sqlTypes( mapping ); for ( int subtype : subtypes ) { sqlTypes[n++] = subtype; } } return sqlTypes; } @Override public Size[] dictatedSizes(Mapping mapping) throws MappingException { //Not called at runtime so doesn't matter if its slow :) final Size[] sizes = new Size[getColumnSpan( mapping )]; int soFar = 0; for ( Type propertyType : propertyTypes ) { final Size[] propertySizes = propertyType.dictatedSizes( mapping ); System.arraycopy( propertySizes, 0, sizes, soFar, propertySizes.length ); soFar += propertySizes.length; } return sizes; } @Override public Size[] defaultSizes(Mapping mapping) throws MappingException { //Not called at runtime so doesn't matter if its slow :) final Size[] sizes = new Size[getColumnSpan( mapping )]; int soFar = 0; for ( Type propertyType : propertyTypes ) { final Size[] propertySizes = propertyType.defaultSizes( mapping ); System.arraycopy( propertySizes, 0, sizes, soFar, propertySizes.length ); soFar += propertySizes.length; } return sizes; } @Override public final boolean isComponentType() { return true; } public Class getReturnedClass() { return componentTuplizer.getMappedClass(); } @Override public boolean isSame(Object x, Object y) throws HibernateException { if ( x == y ) { return true; } // null value and empty component are considered equivalent Object[] xvalues = getPropertyValues( x, entityMode ); Object[] yvalues = getPropertyValues( y, entityMode ); for ( int i = 0; i < propertySpan; i++ ) { if ( !propertyTypes[i].isSame( xvalues[i], yvalues[i] ) ) { return false; } } return true; } @Override public boolean isEqual(final Object x, final Object y) throws HibernateException { if ( x == y ) { return true; } // null value and empty component are considered equivalent for ( int i = 0; i < propertySpan; i++ ) { if ( !propertyTypes[i].isEqual( getPropertyValue( x, i ), getPropertyValue( y, i ) ) ) { return false; } } return true; } @Override public boolean isEqual(final Object x, final Object y, final SessionFactoryImplementor factory) throws HibernateException { if ( x == y ) { return true; } // null value and empty component are considered equivalent for ( int i = 0; i < propertySpan; i++ ) { if ( !propertyTypes[i].isEqual( getPropertyValue( x, i ), getPropertyValue( y, i ), factory ) ) { return false; } } return true; } @Override public int compare(final Object x, final Object y) { if ( x == y ) { return 0; } for ( int i = 0; i < propertySpan; i++ ) { int propertyCompare = propertyTypes[i].compare( getPropertyValue( x, i ), getPropertyValue( y, i ) ); if ( propertyCompare != 0 ) { return propertyCompare; } } return 0; } public boolean isMethodOf(Method method) { return false; } @Override public int getHashCode(final Object x) { int result = 17; for ( int i = 0; i < propertySpan; i++ ) { Object y = getPropertyValue( x, i ); result *= 37; if ( y != null ) { result += propertyTypes[i].getHashCode( y ); } } return result; } @Override public int getHashCode(final Object x, final SessionFactoryImplementor factory) { int result = 17; for ( int i = 0; i < propertySpan; i++ ) { Object y = getPropertyValue( x, i ); result *= 37; if ( y != null ) { result += propertyTypes[i].getHashCode( y, factory ); } } return result; } @Override public boolean isDirty(final Object x, final Object y, final SharedSessionContractImplementor session) throws HibernateException { if ( x == y ) { return false; } // null value and empty component are considered equivalent for ( int i = 0; i < propertySpan; i++ ) { if ( propertyTypes[i].isDirty( getPropertyValue( x, i ), getPropertyValue( y, i ), session ) ) { return true; } } return false; } public boolean isDirty(final Object x, final Object y, final boolean[] checkable, final SharedSessionContractImplementor session) throws HibernateException { if ( x == y ) { return false; } // null value and empty component are considered equivalent int loc = 0; for ( int i = 0; i < propertySpan; i++ ) { int len = propertyTypes[i].getColumnSpan( session.getFactory() ); if ( len <= 1 ) { final boolean dirty = ( len == 0 || checkable[loc] ) && propertyTypes[i].isDirty( getPropertyValue( x, i ), getPropertyValue( y, i ), session ); if ( dirty ) { return true; } } else { boolean[] subcheckable = new boolean[len]; System.arraycopy( checkable, loc, subcheckable, 0, len ); final boolean dirty = propertyTypes[i].isDirty( getPropertyValue( x, i ), getPropertyValue( y, i ), subcheckable, session ); if ( dirty ) { return true; } } loc += len; } return false; } @Override public boolean isModified( final Object old, final Object current, final boolean[] checkable, final SharedSessionContractImplementor session) throws HibernateException { if ( old == current ) { return false; } // null value and empty components are considered equivalent int loc = 0; for ( int i = 0; i < propertySpan; i++ ) { int len = propertyTypes[i].getColumnSpan( session.getFactory() ); boolean[] subcheckable = new boolean[len]; System.arraycopy( checkable, loc, subcheckable, 0, len ); if ( propertyTypes[i].isModified( getPropertyValue( old, i ), getPropertyValue( current, i ), subcheckable, session ) ) { return true; } loc += len; } return false; } @Override public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException { return resolve( hydrate( rs, names, session, owner ), session, owner ); } @Override public void nullSafeSet(PreparedStatement st, Object value, int begin, SharedSessionContractImplementor session) throws HibernateException, SQLException { Object[] subvalues = nullSafeGetValues( value, entityMode ); for ( int i = 0; i < propertySpan; i++ ) { propertyTypes[i].nullSafeSet( st, subvalues[i], begin, session ); begin += propertyTypes[i].getColumnSpan( session.getFactory() ); } } @Override public void nullSafeSet( PreparedStatement st, Object value, int begin, boolean[] settable, SharedSessionContractImplementor session) throws HibernateException, SQLException { Object[] subvalues = nullSafeGetValues( value, entityMode ); int loc = 0; for ( int i = 0; i < propertySpan; i++ ) { int len = propertyTypes[i].getColumnSpan( session.getFactory() ); //noinspection StatementWithEmptyBody if ( len == 0 ) { //noop } else if ( len == 1 ) { if ( settable[loc] ) { propertyTypes[i].nullSafeSet( st, subvalues[i], begin, session ); begin++; } } else { boolean[] subsettable = new boolean[len]; System.arraycopy( settable, loc, subsettable, 0, len ); propertyTypes[i].nullSafeSet( st, subvalues[i], begin, subsettable, session ); begin += ArrayHelper.countTrue( subsettable ); } loc += len; } } private Object[] nullSafeGetValues(Object value, EntityMode entityMode) throws HibernateException { if ( value == null ) { return new Object[propertySpan]; } else { return getPropertyValues( value, entityMode ); } } @Override public Object nullSafeGet(ResultSet rs, String name, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException { return nullSafeGet( rs, new String[] {name}, session, owner ); } @Override public Object getPropertyValue(Object component, int i, SharedSessionContractImplementor session) throws HibernateException { return getPropertyValue( component, i ); } public Object getPropertyValue(Object component, int i, EntityMode entityMode) throws HibernateException { return getPropertyValue( component, i ); } public Object getPropertyValue(Object component, int i) throws HibernateException { if (component == null) { component = new Object[propertySpan]; } if ( component instanceof Object[] ) { // A few calls to hashCode pass the property values already in an // Object[] (ex: QueryKey hash codes for cached queries). // It's easiest to just check for the condition here prior to // trying reflection. return ( (Object[]) component )[i]; } else { return componentTuplizer.getPropertyValue( component, i ); } } @Override public Object[] getPropertyValues(Object component, SharedSessionContractImplementor session) throws HibernateException { return getPropertyValues( component, entityMode ); } @Override public Object[] getPropertyValues(Object component, EntityMode entityMode) throws HibernateException { if (component == null) { component = new Object[propertySpan]; } if ( component instanceof Object[] ) { // A few calls to hashCode pass the property values already in an // Object[] (ex: QueryKey hash codes for cached queries). // It's easiest to just check for the condition here prior to // trying reflection. return (Object[]) component; } else { return componentTuplizer.getPropertyValues( component ); } } @Override public void setPropertyValues(Object component, Object[] values, EntityMode entityMode) throws HibernateException { componentTuplizer.setPropertyValues( component, values ); } @Override public Type[] getSubtypes() { return propertyTypes; } public ValueGeneration[] getPropertyValueGenerationStrategies() { return propertyValueGenerationStrategies; } @Override public String getName() { return "component" + ArrayHelper.toString( propertyNames ); } @Override public String toLoggableString(Object value, SessionFactoryImplementor factory) throws HibernateException { if ( value == null ) { return "null"; } if ( entityMode == null ) { throw new ClassCastException( value.getClass().getName() ); } Map<String, String> result = new HashMap<>(); Object[] values = getPropertyValues( value, entityMode ); for ( int i = 0; i < propertyTypes.length; i++ ) { if ( values[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY ) { result.put( propertyNames[i], "<uninitialized>" ); } else { result.put( propertyNames[i], propertyTypes[i].toLoggableString( values[i], factory ) ); } } return StringHelper.unqualify( getName() ) + result.toString(); } @Override public String[] getPropertyNames() { return propertyNames; } @Override public Object deepCopy(Object component, SessionFactoryImplementor factory) throws HibernateException { if ( component == null ) { return null; } Object[] values = getPropertyValues( component, entityMode ); for ( int i = 0; i < propertySpan; i++ ) { values[i] = propertyTypes[i].deepCopy( values[i], factory ); } Object result = instantiate( entityMode ); setPropertyValues( result, values, entityMode ); //not absolutely necessary, but helps for some //equals()/hashCode() implementations if ( componentTuplizer.hasParentProperty() ) { componentTuplizer.setParent( result, componentTuplizer.getParent( component ), factory ); } return result; } @Override public Object replace( Object original, Object target, SharedSessionContractImplementor session, Object owner, Map copyCache) throws HibernateException { if ( original == null ) { return null; } //if ( original == target ) return target; final Object result = target == null ? instantiate( owner, session ) : target; Object[] values = TypeHelper.replace( getPropertyValues( original, entityMode ), getPropertyValues( result, entityMode ), propertyTypes, session, owner, copyCache ); setPropertyValues( result, values, entityMode ); return result; } @Override public Object replace( Object original, Object target, SharedSessionContractImplementor session, Object owner, Map copyCache, ForeignKeyDirection foreignKeyDirection) throws HibernateException { if ( original == null ) { return null; } //if ( original == target ) return target; final Object result = target == null ? instantiate( owner, session ) : target; Object[] values = TypeHelper.replace( getPropertyValues( original, entityMode ), getPropertyValues( result, entityMode ), propertyTypes, session, owner, copyCache, foreignKeyDirection ); setPropertyValues( result, values, entityMode ); return result; } /** * This method does not populate the component parent */ public Object instantiate(EntityMode entityMode) throws HibernateException { return componentTuplizer.instantiate(); } public Object instantiate(Object parent, SharedSessionContractImplementor session) throws HibernateException { Object result = instantiate( entityMode ); if ( componentTuplizer.hasParentProperty() && parent != null ) { componentTuplizer.setParent( result, session.getPersistenceContext().proxyFor( parent ), session.getFactory() ); } return result; } @Override public CascadeStyle getCascadeStyle(int i) { return cascade[i]; } @Override public boolean isMutable() { return true; } @Override public Serializable disassemble(Object value, SharedSessionContractImplementor session, Object owner) throws HibernateException { if ( value == null ) { return null; } else { Object[] values = getPropertyValues( value, entityMode ); for ( int i = 0; i < propertyTypes.length; i++ ) { values[i] = propertyTypes[i].disassemble( values[i], session, owner ); } return values; } } @Override public Object assemble(Serializable object, SharedSessionContractImplementor session, Object owner) throws HibernateException { if ( object == null ) { return null; } else { Object[] values = (Object[]) object; Object[] assembled = new Object[values.length]; for ( int i = 0; i < propertyTypes.length; i++ ) { assembled[i] = propertyTypes[i].assemble( (Serializable) values[i], session, owner ); } Object result = instantiate( owner, session ); setPropertyValues( result, assembled, entityMode ); return result; } } @Override public FetchMode getFetchMode(int i) { return joinedFetch[i]; } @Override public Object hydrate( final ResultSet rs, final String[] names, final SharedSessionContractImplementor session, final Object owner) throws HibernateException, SQLException { int begin = 0; boolean notNull = false; Object[] values = new Object[propertySpan]; for ( int i = 0; i < propertySpan; i++ ) { int length = propertyTypes[i].getColumnSpan( session.getFactory() ); String[] range = ArrayHelper.slice( names, begin, length ); //cache this Object val = propertyTypes[i].hydrate( rs, range, session, owner ); if ( val == null ) { if ( isKey ) { return null; //different nullability rules for pk/fk } } else { notNull = true; } values[i] = val; begin += length; } return notNull ? values : null; } @Override public Object resolve(Object value, SharedSessionContractImplementor session, Object owner) throws HibernateException { if ( value != null ) { Object result = instantiate( owner, session ); Object[] values = (Object[]) value; Object[] resolvedValues = new Object[values.length]; //only really need new array during semiresolve! for ( int i = 0; i < values.length; i++ ) { resolvedValues[i] = propertyTypes[i].resolve( values[i], session, owner ); } setPropertyValues( result, resolvedValues, entityMode ); return result; } else if ( isCreateEmptyCompositesEnabled() ) { return instantiate( owner, session ); } else { return null; } } @Override public Object semiResolve(Object value, SharedSessionContractImplementor session, Object owner) throws HibernateException { //note that this implementation is kinda broken //for components with many-to-one associations return resolve( value, session, owner ); } @Override public boolean[] getPropertyNullability() { return propertyNullability; } @Override public boolean[] toColumnNullness(Object value, Mapping mapping) { boolean[] result = new boolean[getColumnSpan( mapping )]; if ( value == null ) { return result; } Object[] values = getPropertyValues( value, EntityMode.POJO ); //TODO!!!!!!! int loc = 0; for ( int i = 0; i < propertyTypes.length; i++ ) { boolean[] propertyNullness = propertyTypes[i].toColumnNullness( values[i], mapping ); System.arraycopy( propertyNullness, 0, result, loc, propertyNullness.length ); loc += propertyNullness.length; } return result; } @Override public boolean isEmbedded() { return false; } @Override public int getPropertyIndex(String name) { String[] names = getPropertyNames(); for ( int i = 0, max = names.length; i < max; i++ ) { if ( names[i].equals( name ) ) { return i; } } throw new PropertyNotFoundException( "Unable to locate property named " + name + " on " + getReturnedClass().getName() ); } private Boolean canDoExtraction; @Override public boolean canDoExtraction() { if ( canDoExtraction == null ) { canDoExtraction = determineIfProcedureParamExtractionCanBePerformed(); } return canDoExtraction; } private boolean determineIfProcedureParamExtractionCanBePerformed() { for ( Type propertyType : propertyTypes ) { if ( !ProcedureParameterExtractionAware.class.isInstance( propertyType ) ) { return false; } if ( !( (ProcedureParameterExtractionAware) propertyType ).canDoExtraction() ) { return false; } } return true; } @Override public Object extract(CallableStatement statement, int startIndex, SharedSessionContractImplementor session) throws SQLException { Object[] values = new Object[propertySpan]; int currentIndex = startIndex; boolean notNull = false; for ( int i = 0; i < propertySpan; i++ ) { // we know this cast is safe from canDoExtraction final Type propertyType = propertyTypes[i]; final Object value = ((ProcedureParameterExtractionAware) propertyType).extract( statement, currentIndex, session ); if ( value == null ) { if ( isKey ) { return null; //different nullability rules for pk/fk } } else { notNull = true; } values[i] = value; currentIndex += propertyType.getColumnSpan( session.getFactory() ); } if ( !notNull ) { values = null; } return resolve( values, session, null ); } @Override public Object extract(CallableStatement statement, String[] paramNames, SharedSessionContractImplementor session) throws SQLException { // for this form to work all sub-property spans must be one (1)... Object[] values = new Object[propertySpan]; int indx = 0; boolean notNull = false; for ( String paramName : paramNames ) { // we know this cast is safe from canDoExtraction final ProcedureParameterExtractionAware propertyType = (ProcedureParameterExtractionAware) propertyTypes[indx]; final Object value = propertyType.extract( statement, new String[] {paramName}, session ); if ( value == null ) { if ( isKey ) { return null; //different nullability rules for pk/fk } } else { notNull = true; } values[indx] = value; } if ( !notNull ) { values = null; } return resolve( values, session, null ); } @Override public boolean hasNotNullProperty() { return hasNotNullProperty; } private boolean isCreateEmptyCompositesEnabled() { return createEmptyCompositesEnabled; } }