/* * 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.envers.internal.entities.mapper.relation; import java.io.Serializable; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.hibernate.collection.internal.PersistentMap; import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.engine.spi.CollectionEntry; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.envers.RevisionType; import org.hibernate.envers.boot.internal.EnversService; import org.hibernate.envers.exception.AuditException; import org.hibernate.envers.internal.entities.PropertyData; import org.hibernate.envers.internal.entities.mapper.PersistentCollectionChangeData; import org.hibernate.envers.internal.entities.mapper.PropertyMapper; import org.hibernate.envers.internal.entities.mapper.relation.lazy.initializor.Initializor; import org.hibernate.envers.internal.reader.AuditReaderImplementor; import org.hibernate.envers.internal.tools.ReflectionTools; import org.hibernate.internal.util.compare.EqualsHelper; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.property.access.spi.Setter; /** * @author Adam Warski (adam at warski dot org) * @author Michal Skowronek (mskowr at o2 dot pl) * @author Chris Cranford */ public abstract class AbstractCollectionMapper<T> implements PropertyMapper { protected final CommonCollectionMapperData commonCollectionMapperData; protected final Class<? extends T> collectionClass; protected final boolean ordinalInId; protected final boolean revisionTypeInId; private final Constructor<? extends T> proxyConstructor; protected AbstractCollectionMapper( CommonCollectionMapperData commonCollectionMapperData, Class<? extends T> collectionClass, Class<? extends T> proxyClass, boolean ordinalInId, boolean revisionTypeInId) { this.commonCollectionMapperData = commonCollectionMapperData; this.collectionClass = collectionClass; this.ordinalInId = ordinalInId; this.revisionTypeInId = revisionTypeInId; try { proxyConstructor = proxyClass.getConstructor( Initializor.class ); } catch (NoSuchMethodException e) { throw new AuditException( e ); } } protected abstract Collection getNewCollectionContent(PersistentCollection newCollection); protected abstract Collection getOldCollectionContent(Serializable oldCollection); /** * Maps the changed collection element to the given map. * * @param idData Map to which composite-id data should be added. * @param data Where to map the data. * @param changed The changed collection element to map. */ protected abstract void mapToMapFromObject( SessionImplementor session, Map<String, Object> idData, Map<String, Object> data, Object changed); /** * Creates map for storing identifier data. Ordinal parameter guarantees uniqueness of primary key. * Composite primary key cannot contain embeddable properties since they might be nullable. * * @param ordinal Iteration ordinal. * * @return Map for holding identifier data. */ protected Map<String, Object> createIdMap(int ordinal) { final Map<String, Object> idMap = new HashMap<>(); if ( ordinalInId ) { idMap.put( commonCollectionMapperData.getVerEntCfg().getEmbeddableSetOrdinalPropertyName(), ordinal ); } return idMap; } private void addCollectionChanges( SessionImplementor session, List<PersistentCollectionChangeData> collectionChanges, Set<Object> changed, RevisionType revisionType, Serializable id) { int ordinal = 0; for ( Object changedObj : changed ) { final Map<String, Object> entityData = new HashMap<>(); final Map<String, Object> originalId = createIdMap( ordinal++ ); entityData.put( commonCollectionMapperData.getVerEntCfg().getOriginalIdPropName(), originalId ); collectionChanges.add( new PersistentCollectionChangeData( commonCollectionMapperData.getVersionsMiddleEntityName(), entityData, changedObj ) ); // Mapping the collection owner's id. commonCollectionMapperData.getReferencingIdData().getPrefixedMapper().mapToMapFromId( originalId, id ); // Mapping collection element and index (if present). mapToMapFromObject( session, originalId, entityData, changedObj ); ( revisionTypeInId ? originalId : entityData ).put( commonCollectionMapperData.getVerEntCfg() .getRevisionTypePropName(), revisionType ); } } @Override @SuppressWarnings({"unchecked"}) public List<PersistentCollectionChangeData> mapCollectionChanges( SessionImplementor session, String referencingPropertyName, PersistentCollection newColl, Serializable oldColl, Serializable id) { if ( !commonCollectionMapperData.getCollectionReferencingPropertyData().getName().equals( referencingPropertyName ) ) { return null; } // HHH-11063 final CollectionEntry collectionEntry = session.getPersistenceContext().getCollectionEntry( newColl ); if ( collectionEntry != null ) { // This next block delegates only to the persiter-based collection change code if // the following are true: // 1. New collection is not a PersistentMap. // 2. The collection has a persister. // 3. The collection is not indexed, e.g. @IndexColumn // // In the case of 1 and 3, the collection is transformed into a set of Pair<> elements where the // pair's left element is either the map key or the index. In these cases, the key/index do // affect the change code; hence why they're skipped here and handled at the end. // // For all others, the persister based method uses the collection's ElementType#isSame to calculate // equality between the newColl and oldColl. This enforces the same equality check that core uses // for element types such as @Entity in cases where the hash code does not use the id field but has // the same value in both collections. Using #isSame, these will be seen as differing elements and // changes to the collection will be returned. if ( !( newColl instanceof PersistentMap ) ) { final CollectionPersister collectionPersister = collectionEntry.getCurrentPersister(); if ( collectionPersister != null && !collectionPersister.hasIndex() ) { return mapCollectionChanges( session, newColl, oldColl, id, collectionPersister ); } } } return mapCollectionChanges( session, newColl, oldColl, id ); } @Override public boolean mapToMapFromEntity( SessionImplementor session, Map<String, Object> data, Object newObj, Object oldObj) { // Changes are mapped in the "mapCollectionChanges" method. return false; } @Override public void mapModifiedFlagsToMapFromEntity( SessionImplementor session, Map<String, Object> data, Object newObj, Object oldObj) { final PropertyData propertyData = commonCollectionMapperData.getCollectionReferencingPropertyData(); if ( propertyData.isUsingModifiedFlag() ) { if ( isNotPersistentCollection( newObj ) || isNotPersistentCollection( oldObj ) ) { // Compare POJOs. data.put( propertyData.getModifiedFlagPropertyName(), !EqualsHelper.areEqual( newObj, oldObj ) ); } else if ( isFromNullToEmptyOrFromEmptyToNull( (PersistentCollection) newObj, (Serializable) oldObj ) ) { data.put( propertyData.getModifiedFlagPropertyName(), true ); } else { // HHH-7949 - Performance optimization to avoid lazy-fetching collections that have // not been changed for deriving the modified flags value. final PersistentCollection pc = (PersistentCollection) newObj; if ( ( pc != null && !pc.isDirty() ) || ( newObj == null && oldObj == null ) ) { data.put( propertyData.getModifiedFlagPropertyName(), false ); return; } final List<PersistentCollectionChangeData> changes = mapCollectionChanges( session, commonCollectionMapperData.getCollectionReferencingPropertyData().getName(), pc, (Serializable) oldObj, null ); data.put( propertyData.getModifiedFlagPropertyName(), !changes.isEmpty() ); } } } private boolean isNotPersistentCollection(Object obj) { return obj != null && !(obj instanceof PersistentCollection); } private boolean isFromNullToEmptyOrFromEmptyToNull(PersistentCollection newColl, Serializable oldColl) { // Comparing new and old collection content. final Collection newCollection = getNewCollectionContent( newColl ); final Collection oldCollection = getOldCollectionContent( oldColl ); return oldCollection == null && newCollection != null && newCollection.isEmpty() || newCollection == null && oldCollection != null && oldCollection.isEmpty(); } @Override public void mapModifiedFlagsToMapForCollectionChange(String collectionPropertyName, Map<String, Object> data) { final PropertyData propertyData = commonCollectionMapperData.getCollectionReferencingPropertyData(); if ( propertyData.isUsingModifiedFlag() ) { data.put( propertyData.getModifiedFlagPropertyName(), propertyData.getName().equals( collectionPropertyName ) ); } } protected abstract Initializor<T> getInitializor( EnversService enversService, AuditReaderImplementor versionsReader, Object primaryKey, Number revision, boolean removed); @Override public void mapToEntityFromMap( EnversService enversService, Object obj, Map data, Object primaryKey, AuditReaderImplementor versionsReader, Number revision) { final Setter setter = ReflectionTools.getSetter( obj.getClass(), commonCollectionMapperData.getCollectionReferencingPropertyData(), enversService.getServiceRegistry() ); try { setter.set( obj, proxyConstructor.newInstance( getInitializor( enversService, versionsReader, primaryKey, revision, RevisionType.DEL.equals( data.get( enversService.getAuditEntitiesConfiguration().getRevisionTypePropName() ) ) ) ), null ); } catch (InstantiationException e) { throw new AuditException( e ); } catch (IllegalAccessException e) { throw new AuditException( e ); } catch (InvocationTargetException e) { throw new AuditException( e ); } } /** * Map collection changes using hash identity. * * @param session The session. * @param newColl The new persistent collection. * @param oldColl The old collection. * @param id The owning entity identifier. * @return the persistent collection changes. */ @SuppressWarnings("unchecked") private List<PersistentCollectionChangeData> mapCollectionChanges( SessionImplementor session, PersistentCollection newColl, Serializable oldColl, Serializable id) { final List<PersistentCollectionChangeData> collectionChanges = new ArrayList<PersistentCollectionChangeData>(); // Comparing new and old collection content. final Collection newCollection = getNewCollectionContent( newColl ); final Collection oldCollection = getOldCollectionContent( oldColl ); final Set<Object> added = new HashSet<>(); if ( newColl != null ) { added.addAll( newCollection ); } // Re-hashing the old collection as the hash codes of the elements there may have changed, and the // removeAll in AbstractSet has an implementation that is hashcode-change sensitive (as opposed to addAll). if ( oldColl != null ) { added.removeAll( new HashSet( oldCollection ) ); } addCollectionChanges( session, collectionChanges, added, RevisionType.ADD, id ); final Set<Object> deleted = new HashSet<>(); if ( oldColl != null ) { deleted.addAll( oldCollection ); } // The same as above - re-hashing new collection. if ( newColl != null ) { deleted.removeAll( new HashSet( newCollection ) ); } addCollectionChanges( session, collectionChanges, deleted, RevisionType.DEL, id ); return collectionChanges; } /** * Map collection changes using the collection element type equality functionality. * * @param session The session. * @param newColl The new persistent collection. * @param oldColl The old collection. * @param id The owning entity identifier. * @param collectionPersister The collection persister. * @return the persistent collection changes. */ private List<PersistentCollectionChangeData> mapCollectionChanges( SessionImplementor session, PersistentCollection newColl, Serializable oldColl, Serializable id, CollectionPersister collectionPersister) { final List<PersistentCollectionChangeData> collectionChanges = new ArrayList<PersistentCollectionChangeData>(); // Comparing new and old collection content. final Collection newCollection = getNewCollectionContent( newColl ); final Collection oldCollection = getOldCollectionContent( oldColl ); // take the new collection and remove any that exist in the old collection. // take the resulting Set<> and generate ADD changes. final Set<Object> added = new HashSet<>(); if ( newColl != null ) { added.addAll( newCollection ); } if ( oldColl != null && collectionPersister != null ) { for ( Object object : oldCollection ) { for ( Iterator addedIt = added.iterator(); addedIt.hasNext(); ) { Object object2 = addedIt.next(); if ( collectionPersister.getElementType().isSame( object, object2 ) ) { addedIt.remove(); break; } } } } addCollectionChanges( session, collectionChanges, added, RevisionType.ADD, id ); // take the old collection and remove any that exist in the new collection. // take the resulting Set<> and generate DEL changes. final Set<Object> deleted = new HashSet<>(); if ( oldColl != null ) { deleted.addAll( oldCollection ); } if ( newColl != null && collectionPersister != null ) { for ( Object object : newCollection ) { for ( Iterator deletedIt = deleted.iterator(); deletedIt.hasNext(); ) { Object object2 = deletedIt.next(); if ( collectionPersister.getElementType().isSame( object, object2 ) ) { deletedIt.remove(); break; } } } } addCollectionChanges( session, collectionChanges, deleted, RevisionType.DEL, id ); return collectionChanges; } @Override public boolean hasPropertiesWithModifiedFlag() { if ( commonCollectionMapperData != null ) { final PropertyData propertyData = commonCollectionMapperData.getCollectionReferencingPropertyData(); return propertyData != null && propertyData.isUsingModifiedFlag(); } return false; } }