/******************************************************************************* * Copyright (c) 1998, 2015 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 v1.0 and Eclipse Distribution License v. 1.0 * which accompanies this distribution. * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * 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 ((DeferrableChangeRecord)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 addEnum = ((CollectionChangeRecord)mergeFromRecord).getAddObjectList().keySet().iterator(); while (addEnum.hasNext()) { ObjectChangeSet mergingObject = (ObjectChangeSet)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 removeEnum = ((CollectionChangeRecord)mergeFromRecord).getRemoveObjectList().keySet().iterator(); while (removeEnum.hasNext()) { ObjectChangeSet mergingObject = (ObjectChangeSet)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 orderedChangeObjectEnum = ((CollectionChangeRecord)mergeFromRecord).getOrderedChangeObjectList().iterator(); while (orderedChangeObjectEnum.hasNext()) { OrderedChangeObject changeObject = (OrderedChangeObject)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 changes = getAddObjectList().values().iterator(); while (changes.hasNext()) { ObjectChangeSet localChangeSet = mergeToChangeSet.findOrIntegrateObjectChangeSet((ObjectChangeSet)changes.next(), mergeFromChangeSet); addList.put(localChangeSet, localChangeSet); } changes = getRemoveObjectList().values().iterator(); while (changes.hasNext()) { ObjectChangeSet localChangeSet = mergeToChangeSet.findOrIntegrateObjectChangeSet((ObjectChangeSet)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.intValue(), 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 it = this.removeObjectList.keySet().iterator(); while(it.hasNext()) { ObjectChangeSet changeSet = (ObjectChangeSet)it.next(); cp.addInto(changeSet.getUnitOfWorkClone(), currentCollection, session); } } if(this.addObjectList != null) { Iterator it = this.addObjectList.keySet().iterator(); while(it.hasNext()) { ObjectChangeSet changeSet = (ObjectChangeSet)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.intValue(), 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(); } } }