blob: 88a869eb86dccf71b73574d718fbb73fe6b01d95 [file] [log] [blame]
/*
* Copyright (c) 1998, 2021 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// Contributors:
// Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.internal.sessions;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.descriptors.changetracking.CollectionChangeEvent;
import org.eclipse.persistence.exceptions.ValidationException;
import org.eclipse.persistence.internal.queries.ContainerPolicy;
/**
* <p>
* <b>Purpose</b>: This class holds the record of the changes made to a collection attribute of
* an object.
* <p>
* <b>Description</b>: Collections must be compared to each other and added and removed objects must
* be recorded separately.
*/
public class CollectionChangeRecord extends DeferrableChangeRecord implements org.eclipse.persistence.sessions.changesets.CollectionChangeRecord {
/**
* Contains the added values to the collection and their corresponding ChangeSets.
*/
protected Map<ObjectChangeSet, ObjectChangeSet> addObjectList;
/**
* Contains the added values to the collection and their corresponding ChangeSets in order.
*/
protected List<ObjectChangeSet> orderedAddObjects;
/**
* Contains the added values index to the collection.
*/
protected Map<ObjectChangeSet, Integer> orderedAddObjectIndices;
/**
* Contains OrderedChangeObjects representing each change made to the collection.
*/
protected List<OrderedChangeObject> orderedChangeObjectList;
/**
* Contains the removed values to the collection and their corresponding ChangeSets.
*/
protected Map<Integer, ObjectChangeSet> orderedRemoveObjects;
/**
* Contains the removed values index to the collection.
*/
protected transient List<Integer> orderedRemoveObjectIndices;
/**
* Contains a list of extra adds. These extra adds are used by attribute change tracking
* to replicate behavior when someone adds the same object to a list and removes it once.
* In this case the object should still appear once in the change set.
*/
protected transient List<ObjectChangeSet> addOverFlow;
/**
* Contains the removed values from the collection and their corresponding ChangeSets.
*/
protected Map<ObjectChangeSet, ObjectChangeSet> removeObjectList;
/**
* Indicates whether IndirectList's order has been repaired.
*/
protected boolean orderHasBeenRepaired;
/**
* This default constructor.
*/
public CollectionChangeRecord() {
super();
}
/**
* Constructor for the ChangeRecord representing a collection mapping
* @param owner the changeSet that uses this record
*/
public CollectionChangeRecord(ObjectChangeSet owner) {
this.owner = owner;
}
/**
* This method takes a Map of objects, converts these into ObjectChangeSets.
*/
public void addAdditionChange(Map objectChanges, ContainerPolicy cp, UnitOfWorkChangeSet changeSet, AbstractSession session) {
Iterator enumtr = objectChanges.values().iterator();
while (enumtr.hasNext()) {
Object object = cp.unwrapElement(enumtr.next());
ObjectChangeSet change = session.getDescriptor(object.getClass()).getObjectBuilder().createObjectChangeSet(object, changeSet, session);
if (change.hasKeys()){
// if change set has keys this is a map comparison. Maps are
// not supported in change tracking so do not need to prevent duplicates
// when map support is added this will have to be refactored
getAddObjectList().put(change, change);
} else {
if (getRemoveObjectList().containsKey(change)) {
getRemoveObjectList().remove(change);
} else {
getAddObjectList().put(change, change);
}
}
}
}
/**
* This method takes a list of objects and converts them into
* ObjectChangeSets. This method should only be called from a
* ListContainerPolicy. Additions to the list are made by index, hence,
* the second Map of objectChangesIndices.
*/
public void addOrderedAdditionChange(List<Object> orderedObjectsToAdd, Map<Object, Integer> objectChangesIndices, UnitOfWorkChangeSet changeSet, AbstractSession session) {
for (Object object : orderedObjectsToAdd) {
ObjectChangeSet change = session.getDescriptor(object.getClass()).getObjectBuilder().createObjectChangeSet(object, changeSet, session);
getOrderedAddObjects().add(change);
getOrderedAddObjectIndices().put(change, objectChangesIndices.get(object));
}
}
/**
* This method takes a map of objects and converts them into
* ObjectChangeSets. This method should only be called from a
* ListContainerPolicy. Deletions from the list is made by index, hence,
* the second Vector of indicesToRemove.
*/
public void addOrderedRemoveChange(List<Integer> indicesToRemove, Map objectChanges, UnitOfWorkChangeSet changeSet, AbstractSession session) {
this.orderedRemoveObjectIndices = indicesToRemove;
for (Integer index : indicesToRemove) {
Object object = objectChanges.get(index);
ObjectChangeSet change = session.getDescriptor(object.getClass()).getObjectBuilder().createObjectChangeSet(object, changeSet, session);
getOrderedRemoveObjects().put(index, change);
}
}
/**
* This method takes a Map of objects, converts these into ObjectChangeSets.
*/
public void addRemoveChange(Map objectChanges, ContainerPolicy cp, UnitOfWorkChangeSet changeSet, AbstractSession session) {
// There is no need to keep track of removed new objects because it will not be in the backup,
// It will not be in the backup because it is new.
if(objectChanges.isEmpty()) {
return;
}
ClassDescriptor descriptor = this.mapping.getReferenceDescriptor();
boolean hasChildren = (descriptor.hasInheritance() && descriptor.getInheritancePolicy().hasChildren())
|| descriptor.hasTablePerClassPolicy();
Iterator enumtr = cp.getChangeValuesFrom(objectChanges);
while (enumtr.hasNext()) {
Object object = cp.unwrapElement(enumtr.next());
if (hasChildren) {
descriptor = getReferenceDescriptor(object, session);
}
ObjectChangeSet change = descriptor.getObjectBuilder().createObjectChangeSet(object, changeSet, session);
if (change.hasKeys()) {
// if change set has keys this is a map comparison. Maps are
// not support in change tracking so do not need to prevent duplicates
// when map support is added this will have to be refactored
getRemoveObjectList().put(change, change);
} else {
if (getAddObjectList().containsKey(change)) {
getAddObjectList().remove(change);
} else {
getRemoveObjectList().put(change, change);
}
}
}
}
/**
* ADVANCED:
* This method returns the collection of ChangeSets that were added to the collection.
*/
@Override
public Map<ObjectChangeSet, ObjectChangeSet> getAddObjectList() {
if (addObjectList == null) {
addObjectList = new IdentityHashMap(10);
}
return addObjectList;
}
/**
* Returns a list of extra adds.
* These extra adds are used by attribute change tracking
* to replicate behavior when someone adds the same object to a list and removes it once.
* In this case the object should still appear once in the change set.
*/
public List<ObjectChangeSet> getAddOverFlow() {
if (addOverFlow == null) {
addOverFlow = new ArrayList();
}
return addOverFlow;
}
/**
* Returns descriptor corresponding to the object.
*/
ClassDescriptor getReferenceDescriptor(Object object, AbstractSession session) {
return session.getClassDescriptor(object);
}
/**
* PUBLIC:
* This method returns the Map that contains the removed values from the collection
* and their corresponding ChangeSets.
*/
@Override
public Map<ObjectChangeSet, ObjectChangeSet> getRemoveObjectList() {
if (removeObjectList == null) {
removeObjectList = new IdentityHashMap();
}
return removeObjectList;
}
/**
* PUBLIC:
* Returns true if the change set has changes.
*/
@Override
public boolean hasChanges() {
return (!( (this.addObjectList == null || this.addObjectList.isEmpty()) &&
(this.removeObjectList == null || this.removeObjectList.isEmpty()) &&
(this.orderedAddObjects == null || this.orderedAddObjects.isEmpty()) &&
(this.orderedRemoveObjects == null || this.orderedRemoveObjects.isEmpty()) &&
(this.orderedChangeObjectList == null || this.orderedChangeObjectList.isEmpty())))
|| getOwner().isNew();
}
/**
* This method will be used to merge one record into another.
*/
@Override
public void mergeRecord(ChangeRecord mergeFromRecord, UnitOfWorkChangeSet mergeToChangeSet, UnitOfWorkChangeSet mergeFromChangeSet) {
if (((DeferrableChangeRecord)mergeFromRecord).isDeferred()){
if (this.hasChanges()){
//merging into existing change record need to combine changes
mergeFromRecord.getMapping().calculateDeferredChanges(mergeFromRecord, mergeToChangeSet.getSession());
}else{
if (! this.isDeferred){
this.originalCollection = ((DeferrableChangeRecord)mergeFromRecord).originalCollection;
}
this.isDeferred = true;
this.latestCollection = ((DeferrableChangeRecord)mergeFromRecord).latestCollection;
return;
}
}
Map<ObjectChangeSet, ObjectChangeSet> changeSets = new HashMap<>();
Iterator<ObjectChangeSet> addEnum = ((CollectionChangeRecord)mergeFromRecord).getAddObjectList().keySet().iterator();
while (addEnum.hasNext()) {
ObjectChangeSet mergingObject = addEnum.next();
ObjectChangeSet localChangeSet = mergeToChangeSet.findOrIntegrateObjectChangeSet(mergingObject, mergeFromChangeSet);
if (getRemoveObjectList().containsKey(localChangeSet)) {
getRemoveObjectList().remove(localChangeSet);
} else {
changeSets.put(localChangeSet, localChangeSet);
}
}
getAddObjectList().putAll(changeSets);
changeSets = new HashMap<>();
Iterator<ObjectChangeSet> removeEnum = ((CollectionChangeRecord)mergeFromRecord).getRemoveObjectList().keySet().iterator();
while (removeEnum.hasNext()) {
ObjectChangeSet mergingObject = removeEnum.next();
ObjectChangeSet localChangeSet = mergeToChangeSet.findOrIntegrateObjectChangeSet(mergingObject, mergeFromChangeSet);
if (getAddObjectList().containsKey(localChangeSet)) {
getAddObjectList().remove(localChangeSet);
} else {
changeSets.put(localChangeSet, localChangeSet);
}
}
getRemoveObjectList().putAll(changeSets);
//237545: merge the changes for ordered list's attribute change tracking. (still need to check if deferred changes need to be merged)
List<OrderedChangeObject> orderedChangeSets = new ArrayList<>();
Iterator<OrderedChangeObject> orderedChangeObjectEnum = ((CollectionChangeRecord)mergeFromRecord).getOrderedChangeObjectList().iterator();
while (orderedChangeObjectEnum.hasNext()) {
OrderedChangeObject changeObject = orderedChangeObjectEnum.next();
ObjectChangeSet mergingObject = changeObject.getChangeSet();
ObjectChangeSet localChangeSet = mergeToChangeSet.findOrIntegrateObjectChangeSet(mergingObject, mergeFromChangeSet);
OrderedChangeObject orderedChangeObject = new OrderedChangeObject(changeObject.getChangeType(), changeObject.getIndex(), localChangeSet);
orderedChangeSets.add(orderedChangeObject);
}
getOrderedChangeObjectList().addAll(orderedChangeSets);
}
/**
* Sets the Added objects list.
*/
public void setAddObjectList(Map<ObjectChangeSet, ObjectChangeSet> objectChangesList) {
this.addObjectList = objectChangesList;
}
/**
* Sets the removed objects list.
*/
public void setRemoveObjectList(Map<ObjectChangeSet, ObjectChangeSet> objectChangesList) {
this.removeObjectList = objectChangesList;
}
/**
* This method will be used to update the objectsChangeSets references.
*/
@Override
public void updateReferences(UnitOfWorkChangeSet mergeToChangeSet, UnitOfWorkChangeSet mergeFromChangeSet) {
Map addList = new IdentityHashMap(this.getAddObjectList().size() + 1);
Map removeList = new IdentityHashMap(this.getRemoveObjectList().size() + 1);
// If we have ordered lists we need to iterate through those.
if (getOrderedAddObjects().size() > 0 || getOrderedRemoveObjectIndices().size() > 0) {
// Do the ordered adds first ...
List<ObjectChangeSet> orderedAddList = new ArrayList(getOrderedAddObjects().size());
Map orderedAddListIndices = new IdentityHashMap(getOrderedAddObjectIndices().size());
for (int i = 0; i < getOrderedAddObjects().size(); i++) {
ObjectChangeSet changeSet = getOrderedAddObjects().get(i);
ObjectChangeSet localChangeSet = mergeToChangeSet.findOrIntegrateObjectChangeSet(changeSet, mergeFromChangeSet);
orderedAddList.add(localChangeSet);
orderedAddListIndices.put(localChangeSet, getOrderedAddObjectIndices().get(changeSet));
// Object was actually added and not moved.
if (getAddObjectList().containsKey(changeSet)) {
addList.put(localChangeSet, localChangeSet);
}
}
setOrderedAddObjects(orderedAddList);
setOrderedAddObjectIndices(orderedAddListIndices);
// Do the ordered removes now ...
Map orderedRemoveList = new HashMap(getOrderedRemoveObjects().size());
for (Object index : getOrderedRemoveObjects().keySet()) {
ObjectChangeSet changeSet = getOrderedRemoveObjects().get(index);
ObjectChangeSet localChangeSet = mergeToChangeSet.findOrIntegrateObjectChangeSet(changeSet, mergeFromChangeSet);
orderedRemoveList.put(index, localChangeSet);
// Object was actually removed and not moved.
if (getRemoveObjectList().containsKey(changeSet)) {
removeList.put(localChangeSet, localChangeSet);
}
}
setOrderedRemoveObjects(orderedRemoveList);
// Don't need to worry about the vector of indices (Integer's), just leave them as is.
} else {
Iterator<ObjectChangeSet> changes = getAddObjectList().values().iterator();
while (changes.hasNext()) {
ObjectChangeSet localChangeSet = mergeToChangeSet.findOrIntegrateObjectChangeSet(changes.next(), mergeFromChangeSet);
addList.put(localChangeSet, localChangeSet);
}
changes = getRemoveObjectList().values().iterator();
while (changes.hasNext()) {
ObjectChangeSet localChangeSet = mergeToChangeSet.findOrIntegrateObjectChangeSet(changes.next(), mergeFromChangeSet);
removeList.put(localChangeSet, localChangeSet);
}
}
setAddObjectList(addList);
setRemoveObjectList(removeList);
}
/**
* This method returns the collection of ChangeSets in the order they were
* added to the collection. This list includes those objects that were
* moved within the collection.
*/
public List<ObjectChangeSet> getOrderedAddObjects() {
if (orderedAddObjects == null) {
orderedAddObjects = new ArrayList();
}
return orderedAddObjects;
}
/**
* This method returns the index of an object added to the collection.
*/
public Integer getOrderedAddObjectIndex(ObjectChangeSet changes) {
return getOrderedAddObjectIndices().get(changes);
}
/**
* This method returns the collection of ChangeSets that they were
* added to the collection.
*/
public Map<ObjectChangeSet, Integer> getOrderedAddObjectIndices() {
if (orderedAddObjectIndices == null) {
orderedAddObjectIndices = new IdentityHashMap();
}
return orderedAddObjectIndices;
}
/**
* This method returns the Vector of OrderedChangeObjects. These objects represent
* all changes made to the collection, and their order in the vector represents the order
* they were performed.
*/
public List<OrderedChangeObject> getOrderedChangeObjectList() {
if (orderedChangeObjectList == null) {
orderedChangeObjectList = new ArrayList();
}
return orderedChangeObjectList;
}
/**
* This method returns the ordered list of indices to remove from the collection.
*/
public List<Integer> getOrderedRemoveObjectIndices() {
if (this.orderedRemoveObjectIndices == null) {
this.orderedRemoveObjectIndices = new ArrayList();
}
return this.orderedRemoveObjectIndices;
}
/**
* This method returns the index of an object removed from the collection.
*/
public Object getOrderedRemoveObject(Integer index) {
return getOrderedRemoveObjects().get(index);
}
/**
* This method returns the collection of ChangeSets of objects removed from
* the collection.
*/
public Map<Integer, ObjectChangeSet> getOrderedRemoveObjects() {
if (this.orderedRemoveObjects == null) {
this.orderedRemoveObjects = new HashMap();
}
return this.orderedRemoveObjects;
}
/**
* Sets collection of ChangeSets (and their respective index) that they
* were added to the collection.
*/
public void setOrderedAddObjectIndices(Map<ObjectChangeSet, Integer> orderedAddObjectIndices) {
this.orderedAddObjectIndices = orderedAddObjectIndices;
}
/**
* Sets collection of ChangeSets that they were added to the collection.
*/
public void setOrderedAddObjects(List<ObjectChangeSet> orderedAddObjects) {
this.orderedAddObjects = orderedAddObjects;
}
public void setOrderedChangeObjectList(List<OrderedChangeObject> orderedChangeObjectList) {
this.orderedChangeObjectList = orderedChangeObjectList;
}
/**
* Sets collection of ChangeSets that they were removed from the collection.
*/
public void setOrderedRemoveObjects(Map<Integer, ObjectChangeSet> orderedRemoveObjects) {
this.orderedRemoveObjects = orderedRemoveObjects;
}
/**
* The same size as original list,
* at the i-th position holds the index of the i-th original object in the current list (-1 if the object was removed):
* for example: {0, -1, 1, -1, 3} means that:
* previous(0) == current(0);
* previous(1) was removed;
* previous(2) == current(1);
* previous(3) was removed;
* previous(4) == current(3);
*/
public List<Integer> getCurrentIndexesOfOriginalObjects(List newList) {
int newSize = newList.size();
List<Integer> currentIndexes = new ArrayList(newSize);
for(int i=0; i < newSize; i++) {
currentIndexes.add(i);
}
if(orderedChangeObjectList != null) {
for (int i = this.orderedChangeObjectList.size() - 1; i>=0; i--) {
OrderedChangeObject orderedChange = orderedChangeObjectList.get(i);
Object obj = orderedChange.getAddedOrRemovedObject();
Integer index = orderedChange.getIndex();
int changeType = orderedChange.getChangeType();
if(changeType == CollectionChangeEvent.ADD) {
// the object was added - remove the corresponding index
if(index == null) {
currentIndexes.remove(currentIndexes.size()-1);
} else {
currentIndexes.remove(index.intValue());
}
} else if(changeType == CollectionChangeEvent.REMOVE) {
// the object was removed - add its index in the new list
if(index == null) {
throw ValidationException.collectionRemoveEventWithNoIndex(getMapping());
} else {
currentIndexes.add(index, newList.indexOf(obj));
}
}
}
}
return currentIndexes;
}
/**
* Recreates the original state of currentCollection.
*/
@Override
public void internalRecreateOriginalCollection(Object currentCollection, AbstractSession session) {
ContainerPolicy cp = this.mapping.getContainerPolicy();
if(orderedChangeObjectList == null || orderedChangeObjectList.isEmpty()) {
if(this.removeObjectList != null) {
Iterator<ObjectChangeSet> it = this.removeObjectList.keySet().iterator();
while(it.hasNext()) {
ObjectChangeSet changeSet = it.next();
cp.addInto(changeSet.getUnitOfWorkClone(), currentCollection, session);
}
}
if(this.addObjectList != null) {
Iterator<ObjectChangeSet> it = this.addObjectList.keySet().iterator();
while(it.hasNext()) {
ObjectChangeSet changeSet = it.next();
cp.removeFrom(changeSet.getUnitOfWorkClone(), currentCollection, session);
}
}
} else {
List originalList = (List)currentCollection;
for (int i = this.orderedChangeObjectList.size() - 1; i>=0; i--) {
OrderedChangeObject orderedChange = this.orderedChangeObjectList.get(i);
Object obj = orderedChange.getAddedOrRemovedObject();
Integer index = orderedChange.getIndex();
int changeType = orderedChange.getChangeType();
if(changeType == CollectionChangeEvent.ADD) {
// the object was added - remove the corresponding index
if(index == null) {
originalList.remove(originalList.size()-1);
} else {
originalList.remove(index.intValue());
}
} else if(changeType == CollectionChangeEvent.REMOVE) {
// the object was removed - add its index in the new list
if(index == null) {
throw ValidationException.collectionRemoveEventWithNoIndex(getMapping());
} else {
originalList.add(index, obj);
}
}
}
}
}
public void setOrderHasBeenRepaired(boolean hasBeenRepaired) {
this.orderHasBeenRepaired = hasBeenRepaired;
}
public boolean orderHasBeenRepaired() {
return this.orderHasBeenRepaired;
}
/**
* Clears info about added / removed objects set by change tracker.
*/
@Override
public void clearChanges() {
if(orderedChangeObjectList != null) {
this.orderedChangeObjectList.clear();
}
if(this.removeObjectList != null) {
this.removeObjectList.clear();
}
if(this.addObjectList != null) {
this.addObjectList.clear();
}
}
}