/* * 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.synchronization.work; import java.io.Serializable; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.envers.RevisionType; import org.hibernate.envers.boot.internal.EnversService; import org.hibernate.envers.internal.entities.RelationDescription; /** * A work unit that handles "fake" bidirectional one-to-many relations (mapped with {@code @OneToMany+@JoinColumn} and * {@code @ManyToOne+@Column(insertable=false, updatable=false)}. * * @author Adam Warski (adam at warski dot org) */ public class FakeBidirectionalRelationWorkUnit extends AbstractAuditWorkUnit implements AuditWorkUnit { private final Map<String, FakeRelationChange> fakeRelationChanges; /* * The work unit responsible for generating the "raw" entity data to be saved. */ private final AuditWorkUnit nestedWorkUnit; public FakeBidirectionalRelationWorkUnit( SessionImplementor sessionImplementor, String entityName, EnversService enversService, Serializable id, String referencingPropertyName, Object owningEntity, RelationDescription rd, RevisionType revisionType, Object index, AuditWorkUnit nestedWorkUnit) { super( sessionImplementor, entityName, enversService, id, revisionType ); this.nestedWorkUnit = nestedWorkUnit; // Adding the change for the relation. fakeRelationChanges = new HashMap<>(); fakeRelationChanges.put( referencingPropertyName, new FakeRelationChange( owningEntity, rd, revisionType, index ) ); } public FakeBidirectionalRelationWorkUnit( FakeBidirectionalRelationWorkUnit original, Map<String, FakeRelationChange> fakeRelationChanges, AuditWorkUnit nestedWorkUnit) { super( original.sessionImplementor, original.entityName, original.enversService, original.id, original.revisionType ); this.fakeRelationChanges = fakeRelationChanges; this.nestedWorkUnit = nestedWorkUnit; } public FakeBidirectionalRelationWorkUnit(FakeBidirectionalRelationWorkUnit original, AuditWorkUnit nestedWorkUnit) { super( original.sessionImplementor, original.entityName, original.enversService, original.id, original.revisionType ); this.nestedWorkUnit = nestedWorkUnit; fakeRelationChanges = new HashMap<>( original.getFakeRelationChanges() ); } public AuditWorkUnit getNestedWorkUnit() { return nestedWorkUnit; } public Map<String, FakeRelationChange> getFakeRelationChanges() { return fakeRelationChanges; } @Override public boolean containsWork() { return true; } @Override public Map<String, Object> generateData(Object revisionData) { // Generating data with the nested work unit. This data contains all data except the fake relation. // Making a defensive copy not to modify the data held by the nested work unit. final Map<String, Object> nestedData = new HashMap<>( nestedWorkUnit.generateData( revisionData ) ); // Now adding data for all fake relations. for ( FakeRelationChange fakeRelationChange : fakeRelationChanges.values() ) { fakeRelationChange.generateData( sessionImplementor, nestedData ); } return nestedData; } @Override public AuditWorkUnit merge(AddWorkUnit second) { return merge( this, nestedWorkUnit, second ); } @Override public AuditWorkUnit merge(ModWorkUnit second) { return merge( this, nestedWorkUnit, second ); } @Override public AuditWorkUnit merge(DelWorkUnit second) { return second; } @Override public AuditWorkUnit merge(CollectionChangeWorkUnit second) { return this; } @Override public AuditWorkUnit merge(FakeBidirectionalRelationWorkUnit second) { // First merging the nested work units. final AuditWorkUnit mergedNested = second.getNestedWorkUnit().dispatch( nestedWorkUnit ); // Now merging the fake relation changes from both work units. final Map<String, FakeRelationChange> secondFakeRelationChanges = second.getFakeRelationChanges(); final Map<String, FakeRelationChange> mergedFakeRelationChanges = new HashMap<>(); final Set<String> allPropertyNames = new HashSet<>( fakeRelationChanges.keySet() ); allPropertyNames.addAll( secondFakeRelationChanges.keySet() ); for ( String propertyName : allPropertyNames ) { mergedFakeRelationChanges.put( propertyName, FakeRelationChange.merge( fakeRelationChanges.get( propertyName ), secondFakeRelationChanges.get( propertyName ) ) ); } return new FakeBidirectionalRelationWorkUnit( this, mergedFakeRelationChanges, mergedNested ); } @Override public AuditWorkUnit dispatch(WorkUnitMergeVisitor first) { return first.merge( this ); } public static AuditWorkUnit merge( FakeBidirectionalRelationWorkUnit frwu, AuditWorkUnit nestedFirst, AuditWorkUnit nestedSecond) { final AuditWorkUnit nestedMerged = nestedSecond.dispatch( nestedFirst ); // Creating a new fake relation work unit with the nested merged data return new FakeBidirectionalRelationWorkUnit( frwu, nestedMerged ); } /** * Describes a change to a single fake bidirectional relation. */ private static class FakeRelationChange { private final Object owningEntity; private final RelationDescription rd; private final RevisionType revisionType; private final Object index; public FakeRelationChange( Object owningEntity, RelationDescription rd, RevisionType revisionType, Object index) { this.owningEntity = owningEntity; this.rd = rd; this.revisionType = revisionType; this.index = index; } public RevisionType getRevisionType() { return revisionType; } public void generateData(SessionImplementor sessionImplementor, Map<String, Object> data) { // If the revision type is "DEL", it means that the object is removed from the collection. Then the // new owner will in fact be null. rd.getFakeBidirectionalRelationMapper().mapToMapFromEntity( sessionImplementor, data, revisionType == RevisionType.DEL ? null : owningEntity, null ); rd.getFakeBidirectionalRelationMapper().mapModifiedFlagsToMapFromEntity( sessionImplementor, data, revisionType == RevisionType.DEL ? null : owningEntity, null ); // Also mapping the index, if the collection is indexed. if ( rd.getFakeBidirectionalRelationIndexMapper() != null ) { rd.getFakeBidirectionalRelationIndexMapper().mapToMapFromEntity( sessionImplementor, data, revisionType == RevisionType.DEL ? null : index, null ); rd.getFakeBidirectionalRelationIndexMapper().mapModifiedFlagsToMapFromEntity( sessionImplementor, data, revisionType == RevisionType.DEL ? null : index, null ); } } public static FakeRelationChange merge(FakeRelationChange first, FakeRelationChange second) { if ( first == null ) { return second; } if ( second == null ) { return first; } /* * The merging rules are the following (revision types of the first and second changes): * - DEL, DEL - return any (the work units are the same) * - DEL, ADD - return ADD (points to new owner) * - ADD, DEL - return ADD (points to new owner) * - ADD, ADD - return second (points to newer owner) */ if ( first.getRevisionType() == RevisionType.DEL || second.getRevisionType() == RevisionType.ADD ) { return second; } else { return first; } } } }