All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.hibernate.envers.internal.entities.mapper.relation.AbstractCollectionMapper Maven / Gradle / Ivy

There is a newer version: 7.0.0.Beta1
Show newest version
/*
 * 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 .
 */
package org.hibernate.envers.internal.entities.mapper.relation;

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.security.AccessController;
import java.security.PrivilegedAction;
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 implements PropertyMapper {
	protected final CommonCollectionMapperData commonCollectionMapperData;
	protected final Class collectionClass;
	protected final boolean ordinalInId;
	protected final boolean revisionTypeInId;

	private final Constructor proxyConstructor;

	protected AbstractCollectionMapper(
			CommonCollectionMapperData commonCollectionMapperData,
			Class collectionClass, Class 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 idData,
			Map 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 createIdMap(int ordinal) {
		final Map idMap = new HashMap<>();
		if ( ordinalInId ) {
			idMap.put( commonCollectionMapperData.getVerEntCfg().getEmbeddableSetOrdinalPropertyName(), ordinal );
		}
		return idMap;
	}

	private void addCollectionChanges(
			SessionImplementor session, List collectionChanges,
			Set changed, RevisionType revisionType, Serializable id) {
		int ordinal = 0;

		for ( Object changedObj : changed ) {
			final Map entityData = new HashMap<>();
			final Map 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 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 data,
			Object newObj,
			Object oldObj) {
		// Changes are mapped in the "mapCollectionChanges" method.
		return false;
	}

	@Override
	public void mapModifiedFlagsToMapFromEntity(
			SessionImplementor session,
			Map 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 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 data) {
		final PropertyData propertyData = commonCollectionMapperData.getCollectionReferencingPropertyData();
		if ( propertyData.isUsingModifiedFlag() ) {
			data.put(
					propertyData.getModifiedFlagPropertyName(),
					propertyData.getName().equals( collectionPropertyName )
			);
		}
	}

	protected abstract Initializor getInitializor(
			EnversService enversService,
			AuditReaderImplementor versionsReader,
			Object primaryKey,
			Number revision,
			boolean removed);

	@Override
	public void mapToEntityFromMap(
			final EnversService enversService,
			final Object obj,
			final Map data,
			final Object primaryKey,
			final AuditReaderImplementor versionsReader,
			final Number revision) {
		final String revisionTypePropertyName = enversService.getAuditEntitiesConfiguration().getRevisionTypePropName();
		AccessController.doPrivileged(
				new PrivilegedAction() {
					@Override
					public Object run() {
						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( revisionTypePropertyName ) )
											)
									),
									null
							);
						}
						catch (InstantiationException e) {
							throw new AuditException( e );
						}
						catch (IllegalAccessException e) {
							throw new AuditException( e );
						}
						catch (InvocationTargetException e) {
							throw new AuditException( e );
						}

						return null;
					}
				}
		);
	}

	/**
	 * 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 mapCollectionChanges(
			SessionImplementor session,
			PersistentCollection newColl,
			Serializable oldColl,
			Serializable id) {
		final List collectionChanges = new ArrayList();

		// Comparing new and old collection content.
		final Collection newCollection = getNewCollectionContent( newColl );
		final Collection oldCollection = getOldCollectionContent( oldColl );

		final Set 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 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 mapCollectionChanges(
			SessionImplementor session,
			PersistentCollection newColl,
			Serializable oldColl,
			Serializable id,
			CollectionPersister collectionPersister) {

		final List collectionChanges = new ArrayList();

		// 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 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 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;
	}
}