package ca.sqlpower.object;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.log4j.Logger;
import ca.sqlpower.dao.PersistedSPOProperty;
import ca.sqlpower.dao.PersistedSPObject;
import ca.sqlpower.dao.PersisterUtils;
import ca.sqlpower.dao.SPPersistenceException;
import ca.sqlpower.dao.SPPersister;
import ca.sqlpower.diff.DiffChunk;
import ca.sqlpower.diff.DiffChunkTreeNode;
import ca.sqlpower.diff.DiffInfo;
import ca.sqlpower.diff.DiffType;
import ca.sqlpower.diff.PropertyChange;
import ca.sqlpower.sqlobject.SQLObjectException;
import ca.sqlpower.util.MonitorableImpl;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
/**
* XXX This is a copy of the Differ class from the enterprise library. Some methods for calculating diffs has been removed
* as the classes required for them are not in this library. We should re-evaluate the classes available in the base library
* and the enterprise library.
* <p>
* This class may be used in conjunction with a {@link RevisionPersister} to calculate diffs
* between two revisions in the {@link RevisionPersister}, and persist them to another {@link SPPersister},
* or grab them as lists from field accessors.
*
* <p>While this class can take either any {@link RevisionPersister} and two revisions, or lists of persisted objects/properties,
* it's original intention is to take revisions from the {@link JCRPersister}, and persist them to the old revision to update it.
*
*/
public class Differ implements SPPersister {
private static final Logger logger = Logger.getLogger(Differ.class);
private static final PersistedSPObject ROOT_PERSIST = new PersistedSPObject("0", "", "0", 0);
/**
* Used by the {@link calcDiff()} method that takes a persister, to temporarily
* store object persist calls that will be put into the old or new object revision lists.
*/
private List<PersistedSPObject> spObjectListToPopulate;
/**
* Used by the {@link calcDiff()} method that takes a persister, to temporarily
* store property persist calls that will be put into the old or new property revision lists.
*/
private List<PersistedSPOProperty> spoPropertyListToPopulate;
/**
* A map of the old objects, stored by their uuids. Kept as a field to be used
* by other classes to avoid reconstructing a hash map.
*/
private HashMap<String, PersistedSPObject> oldObjectMap;
/**
* A map of the new objects, stored by their uuids. Kept as a field to be used
* by other classes to avoid reconstructing a hash map.
*/
private HashMap<String, PersistedSPObject> newObjectMap;
/**
* A map of the old object properties, stored by their uuids and property names.
* Kept as a field to be used by other classes to avoid reconstructing a hash map.
*/
private HashMap<String, PersistedSPOProperty> oldPropertyMap;
/**
* A map of the new object properties, stored by their uuids and property names.
* Kept as a field to be used by other classes to avoid reconstructing a hash map.
*/
private HashMap<String, PersistedSPOProperty> newPropertyMap;
/**
* The persist calls generated by this differ, and subsequently stored in the static cache.
*/
private DifferPersistCalls persistCalls;
/**
* This is used to keep track of which objects were moved and all their descendants
* so that the property loop knows to add the properties of those objects in the diff.
* ie: If you move a column, that is a remove and add call of the column, and add calls
* of all the column's descendants. All the properties of all those objects must be persisted.
*/
private Set<String> needToAddProperties = new HashSet<String>();
/**
* This can be created if needed by getObjectTree(), but is not done so by default.
* It will map object UUIDs to PersistedObjectTreeNode objects.
*/
private HashMap<String, PersistedObjectTreeNode> treeMap = null;
/**
* Tracks if this differ has already been used to calculate the difference between
* two workspaces. Due to the number of lists and maps stored as class level variables
* each instance of a differ object can only calculate a diff once.
*/
private boolean diffCalculated = false;
/**
* Set this parameter if all persist calls are under the same parent object
* but they don't include the parent. Setting this parameter is meant to
* help reduce the number of persist calls generated by the differ,
* particularly if the first object in the list of children is removed as it
* will cause the differ to report all of the other objects need to be
* removed and added again when it is really just a single remove.
*/
private String parentClass;
/**
* A container class to store the persist calls of this differ.
* Created so that it may be stored in the cache, instead of the entire Differ object.
*/
private class DifferPersistCalls {
/**
* A list of object addition persist calls will be stored here by the
* {@link calcDiff()} methods to later be accessed, or persisted by {@link persistTo()}.
*/
protected final List<PersistedSPObject> persistedSPOsToAdd;
/**
* A list of object removal persist calls will be stored here by the
* {@link calcDiff()} methods to later be accessed, or persisted by {@link persistTo()}.
*/
protected final List<PersistedSPObject> persistedSPOsToRemove;
/**
* A list of property persist calls will be stored here by the
* {@link calcDiff()} methods to later be accessed, or persisted by {@link persistTo()}.
*/
protected final List<PersistedSPOProperty> propertyDiffPersists;
protected DifferPersistCalls() {
persistedSPOsToAdd = new ArrayList<PersistedSPObject>();
persistedSPOsToRemove = new ArrayList<PersistedSPObject>();
propertyDiffPersists = new ArrayList<PersistedSPOProperty>();
}
}
/**
* This comparator will define an ordering for objects such that objects with less
* parents in the list given upon construction (objectsToAdd) come before objects with more.
* In case of ties, which there will be plenty of, the ordering is dependant on indices.
*
* This is to sort a list such that no child will come before its parent, and that
* siblings will be in ascending index order.
*
* A result of 0 is not consistent with objects being equal to eachother.
* Therefore, this comparator should only be used for sorting, as intended.
*/
private class PersistedObjectComparator implements Comparator<PersistedSPObject> {
private HashSet<String> uuidsToAdd = new HashSet<String>();
/**
* Map for storing results of the getDepth() method.
*/
private HashMap<String, Integer> depthMap = new HashMap<String, Integer>();
public PersistedObjectComparator(List<PersistedSPObject> objectsToAdd) {
for (PersistedSPObject o : persistCalls.persistedSPOsToAdd) {
uuidsToAdd.add(o.getUUID());
}
}
/**
* Objects that have less depth/parents come before objects that have more.
* In case of a tie, objects with a lower index come first.
*/
public int compare(PersistedSPObject o1, PersistedSPObject o2) {
if (getDepth(o1) < getDepth(o2)) return -1;
else if (getDepth(o1) > getDepth(o2)) return 1;
else return o1.getIndex() - o2.getIndex();
}
/**
* This will determine how many ancestors that are in the added objects list
* that the given object has, and return it. It will also store the result
* of the given object, and the depths of the ancestors so that they may
* be looked up for subsequent calls to this method, instead of recalculating.
*/
private int getDepth(PersistedSPObject o) {
if (!depthMap.containsKey(o.getUUID())) {
if (uuidsToAdd.contains(o.getParentUUID())) {
depthMap.put(o.getUUID(), getDepth(newObjectMap.get(o.getParentUUID())) + 1);
} else {
depthMap.put(o.getUUID(), 0);
}
}
return depthMap.get(o.getUUID());
}
};
/**
* A simple node object for constructing trees.
*/
private static class PersistedObjectTreeNode {
final private PersistedSPObject object;
final private List<PersistedObjectTreeNode> children = new LinkedList<PersistedObjectTreeNode>();
public PersistedObjectTreeNode(PersistedSPObject object) {
this.object = object;
}
private void addNode(PersistedObjectTreeNode child) {
children.add(child);
}
}
public Differ() {
this(null);
}
public Differ(String parentClass) {
this.parentClass = parentClass;
persistCalls = new DifferPersistCalls();
}
/**
* Calculates the lists of {@link PersistedSPObjects} that need to be added/removed
* to/from the old list to make it the same as the new list.
*
* <p>HashMaps are used to map the both the old and new objects and properties,
* and using a set of all these map keys, each object/property can be looked up
* in both the old and new revision to see if they exist and/or have been changed.
* This is done in private methods {@link calcObjectDiff} and {@link calcPropertyDiff}.
*
* <p>Diffs are stored in list-of-persist-calls fields and can be retrieved by
* {@link getPersistedSPOsToAdd()}, {@link getPersistedSPOsToRemove()}, and {@link getPropertyDiffPersists()}.
*/
public synchronized void calcDiff(List<PersistedSPObject> oldPersistedSPOs,
List<PersistedSPObject> newPersistedSPOs,
List<PersistedSPOProperty> oldPersistedSPOPs,
List<PersistedSPOProperty> newPersistedSPOPs) {
calcDiff(oldPersistedSPOs, newPersistedSPOs, oldPersistedSPOPs, newPersistedSPOPs, null);
}
/**
* Calculates the lists of {@link PersistedSPObjects} that need to be
* added/removed to/from the old list to make it the same as the new list.
*
* <p>
* HashMaps are used to map the both the old and new objects and properties,
* and using a set of all these map keys, each object/property can be looked
* up in both the old and new revision to see if they exist and/or have been
* changed. This is done in private methods {@link calcObjectDiff} and
* {@link calcPropertyDiff}.
*
* <p>
* Diffs are stored in list-of-persist-calls fields and can be retrieved by
* {@link getPersistedSPOsToAdd()}, {@link getPersistedSPOsToRemove()}, and
* {@link getPropertyDiffPersists()}.
*
* @param monitor
* An optional monitor to help us tell what the progress is of
* the diff. Handy for large diffs.
*/
public synchronized void calcDiff(List<PersistedSPObject> oldPersistedSPOs,
List<PersistedSPObject> newPersistedSPOs,
List<PersistedSPOProperty> oldPersistedSPOPs,
List<PersistedSPOProperty> newPersistedSPOPs,
MonitorableImpl monitor) {
if (diffCalculated) throw new IllegalStateException("This differ has already calculated its diff. " +
"Calling this method again will cause the previous diff to enter an invalid state.");
diffCalculated = true;
oldObjectMap = makeObjectHashMap(oldPersistedSPOs);
newObjectMap = makeObjectHashMap(newPersistedSPOs);
oldPropertyMap = makePropertyHashMap(oldPersistedSPOPs);
newPropertyMap = makePropertyHashMap(newPersistedSPOPs);
HashSet<String> objectKeys = new HashSet<String>();
objectKeys.addAll(oldObjectMap.keySet());
objectKeys.addAll(newObjectMap.keySet());
HashSet<String> propertyKeys = new HashSet<String>();
propertyKeys.addAll(oldPropertyMap.keySet());
propertyKeys.addAll(newPropertyMap.keySet());
if (monitor != null) {
monitor.setJobSize(objectKeys.size() + propertyKeys.size());
monitor.setProgress(0);
}
calcObjectDiff(oldObjectMap, newObjectMap, objectKeys, monitor);
calcPropertyDiff(oldPersistedSPOPs, newPersistedSPOPs,
oldPropertyMap, newPropertyMap, propertyKeys, monitor);
if (monitor != null) {
monitor.setJobSize(null);
monitor.setProgress(0);
}
if (logger.isDebugEnabled()) {
logger.debug("Differ found " + oldPersistedSPOs.size() + " objects in old revision, " + newPersistedSPOs.size() + " objects in new revision");
logger.debug("\t" + persistCalls.persistedSPOsToAdd.size() + " objects must be added, and " + persistCalls.persistedSPOsToRemove.size() + " must be removed");
}
}
/**
* Constructs a hash map of {@link PersistedSPObject} types, using their uuids as keys.
* This is used by {@link calcDiff()} to construct {@link oldObjectMap} and {@link newObjectMap},
* which are later used to calculate the diff.
*/
private HashMap<String, PersistedSPObject> makeObjectHashMap(List<PersistedSPObject> objects) {
HashMap<String, PersistedSPObject> map = new HashMap<String, PersistedSPObject>();
for (int i = 0; i < objects.size(); i++) {
PersistedSPObject object = objects.get(i);
map.put(object.getUUID(), object);
}
return map;
}
/**
* Constructs a hash map of {@link PersistedSPOProperty} types, using their uuids concatenated with their propertyName, as keys.
* This is used by {@link calcDiff()} to construct {@link oldPropertyMap} and {@link newPropertyMap},
* which are later used to calculate the diff.
*/
private HashMap<String, PersistedSPOProperty> makePropertyHashMap(List<PersistedSPOProperty> properties) {
HashMap<String, PersistedSPOProperty> map = new HashMap<String, PersistedSPOProperty>();
for (int i = 0; i < properties.size(); i++) {
PersistedSPOProperty property = properties.get(i);
map.put(property.getUUID() + property.getPropertyName(), property);
}
return map;
}
/**
* Will return the PersistedObjectTreeNode with the given uuid if it exists.
* If the object tree has not be made yet, it will be made.
*/
private PersistedObjectTreeNode getTreeNode(String uuid) {
if (treeMap == null) createObjectTreeMap();
return treeMap.get(uuid);
}
/**
* Creates the object tree, and maps each node using the treeMap field.
*/
private void createObjectTreeMap() {
treeMap = new HashMap<String, PersistedObjectTreeNode>();
PersistedSPObject root = newObjectMap.values().iterator().next();
while (root.getParentUUID() != null && !root.getParentUUID().equals("") &&
newObjectMap.get(root.getParentUUID()) != null) {
logger.debug(root.getParentUUID());
root = newObjectMap.get(root.getParentUUID());
}
treeMap.put(root.getUUID(), new PersistedObjectTreeNode(root));
for (PersistedSPObject o : newObjectMap.values()) {
addObjectToTree(o);
}
}
/**
* Adds the given object to the tree. If it can find the object's
* parent in the tree, it will add it as a node under that node.
* If not, it will travel up the the newObjectMap to determine ancestry,
* and find an ancestor of the object in the tree. It will then add all ancestors
* of the object as well as the object under the first ancestor in the tree.
* If the object is already in the tree, it will simply return.
*/
private void addObjectToTree(PersistedSPObject object) {
List<PersistedSPObject> ancestors = new LinkedList<PersistedSPObject>();
PersistedSPObject ancestor = object;
while (ancestor != null && !treeMap.containsKey(ancestor.getUUID())) {
ancestors.add(0, ancestor);
ancestor = newObjectMap.get(ancestor.getParentUUID());
}
for (PersistedSPObject o : ancestors) {
PersistedObjectTreeNode node = new PersistedObjectTreeNode(o);
if (treeMap.get(o.getParentUUID()) != null) {
treeMap.get(o.getParentUUID()).addNode(node);
}
treeMap.put(o.getUUID(), node);
}
}
/**
* Creates a tree like mapping of objects where the key is the persisted
* parent and the map connects different types in the parent's allowed child
* type list to persisted objects.
*/
private Map<PersistedSPObject, Multimap<String, PersistedSPObject>> createObjectTree(Map<String, PersistedSPObject> objectMap) {
//Need to consider types when creating the object tree as each object type index will start at 0.
Map<PersistedSPObject, Multimap<String, PersistedSPObject>> objectTree =
new HashMap<PersistedSPObject, Multimap<String,PersistedSPObject>>();
// Remember that we have to respect the allowed child types of the
// parent in case the parent groups some object types together by a super type.
for (Map.Entry<String, PersistedSPObject> entry : objectMap.entrySet()) {
PersistedSPObject parent = objectMap.get(entry.getValue().getParentUUID());
String parentType;
if (parent == null) {
if (parentClass == null) {
continue;
} else {
parentType = parentClass;
parent = ROOT_PERSIST;
}
} else {
parentType = parent.getType();
}
Class<? extends SPObject> allowedType;
try {
allowedType = PersisterUtils.getParentAllowedChildType(entry.getValue().getType(), parentType);
} catch (Exception e) {
throw new RuntimeException(e);
}
Multimap<String, PersistedSPObject> typeMap = objectTree.get(parent);
if (typeMap == null) {
typeMap = ArrayListMultimap.create();
objectTree.put(parent, typeMap);
}
typeMap.put(allowedType.getName(), entry.getValue());
}
return objectTree;
}
/**
* This method is used by the class to find the diffs between the {@link PersistedSPOBjects}.
* It goes through the set of uuids that belong to the old and new revision objects,
* and determines which were added and which were removed using the object maps.
*/
private void calcObjectDiff(
HashMap<String, PersistedSPObject> oldObjectMap,
HashMap<String, PersistedSPObject> newObjectMap,
HashSet<String> objectKeys,
MonitorableImpl monitor) {
Map<PersistedSPObject, Multimap<String, PersistedSPObject>> oldObjectTree = createObjectTree(oldObjectMap);
Map<PersistedSPObject, Multimap<String, PersistedSPObject>> newObjectTree = createObjectTree(newObjectMap);
if (parentClass == null) {
Set<String> rootKeys = new HashSet<String>();
Map<String, PersistedSPObject> oldRootMap = new HashMap<String, PersistedSPObject>();
for (Map.Entry<String, PersistedSPObject> entry : oldObjectMap.entrySet()) {
PersistedSPObject parent = oldObjectMap.get(entry.getValue().getParentUUID());
if (parent == null) {
oldRootMap.put(entry.getKey(), entry.getValue());
rootKeys.add(entry.getKey());
}
}
Map<String, PersistedSPObject> newRootMap = new HashMap<String, PersistedSPObject>();
for (Map.Entry<String, PersistedSPObject> entry : newObjectMap.entrySet()) {
PersistedSPObject parent = newObjectMap.get(entry.getValue().getParentUUID());
if (parent == null) {
newRootMap.put(entry.getKey(), entry.getValue());
rootKeys.add(entry.getKey());
}
}
Iterator<String> rootIterator = rootKeys.iterator();
while (rootIterator.hasNext()) {
String uuid = rootIterator.next();
PersistedSPObject oldPSO = oldRootMap.get(uuid);
PersistedSPObject newPSO = newRootMap.get(uuid);
if (oldPSO == null) {
addPersistsRecursively(newPSO, newObjectTree);
} else if (newPSO == null) {
persistCalls.persistedSPOsToRemove.add(oldPSO);
} else if (!oldPSO.equals(newPSO)) {
persistCalls.persistedSPOsToRemove.add(oldPSO);
addPersistsRecursively(newPSO, newObjectTree);
} else {
calcChildDiff(oldPSO, oldObjectTree, newPSO, newObjectTree);
}
}
} else {
calcChildDiff(ROOT_PERSIST, oldObjectTree, ROOT_PERSIST, newObjectTree);
}
}
private static class IndexComparator implements Comparator<PersistedSPObject> {
@Override
public int compare(PersistedSPObject o1, PersistedSPObject o2) {
if (!o1.getParentUUID().equals(o2.getParentUUID())) throw new IllegalStateException("Can only compare objects under the same parent");
if (o1.getIndex() < o2.getIndex()) return -1;
if (o1.getIndex() > o2.getIndex()) return 1;
if (!o1.getUUID().equals(o2.getUUID())) throw new IllegalStateException("Same index but different ids");
return 0;
}
}
/**
* Calculating the difference between child objects by comparing the
* children of the same type. This method walks each list of children
* looking for differences and deciding if the differences are an add,
* remove, or move. This is done over comparing each persist individually
* because a remove or add near the start of the list causes all of the
* indexes to be changed which causes all of the following objects to be
* removed and added when it could instead be a single add or remove which
* causes the index change.
*/
private void calcChildDiff(PersistedSPObject oldParent,
Map<PersistedSPObject, Multimap<String, PersistedSPObject>> oldObjectTree,
PersistedSPObject newParent,
Map<PersistedSPObject, Multimap<String, PersistedSPObject>> newObjectTree) {
Multimap<String, PersistedSPObject> oldTypeMapping = oldObjectTree.get(oldParent);
Multimap<String, PersistedSPObject> newTypeMapping = newObjectTree.get(newParent);
Set<String> types = new HashSet<String>();
if (oldTypeMapping != null) {
types.addAll(oldTypeMapping.keySet());
}
if (newTypeMapping != null) {
types.addAll(newTypeMapping.keySet());
}
for (String type : types) {
List<PersistedSPObject> oldChildren;
if (oldTypeMapping != null) {
oldChildren = new ArrayList<PersistedSPObject>(oldTypeMapping.get(type));
} else {
oldChildren = Collections.emptyList();
}
List<PersistedSPObject> newChildren;
if (newTypeMapping != null) {
newChildren = new ArrayList<PersistedSPObject>(newTypeMapping.get(type));
} else {
newChildren = Collections.emptyList();
}
Collections.sort(oldChildren, new IndexComparator());
Collections.sort(newChildren, new IndexComparator());
int oldIdx = 0;
int newIdx = 0;
//We track the objects treated as already inserted or removed and skip them so we don't double persist.
Set<PersistedSPObject> oldHandledPersists = new HashSet<PersistedSPObject>();
Set<PersistedSPObject> newHandledPersists = new HashSet<PersistedSPObject>();
while (oldIdx < oldChildren.size() && newIdx < newChildren.size()) {
PersistedSPObject oldObj = oldChildren.get(oldIdx);
while (oldHandledPersists.contains(oldObj)) {
oldIdx++;
oldObj = oldChildren.get(oldIdx);
}
PersistedSPObject newObj = newChildren.get(newIdx);
while (newHandledPersists.contains(newObj)) {
newIdx++;
newObj = newChildren.get(newIdx);
}
if (oldObj.getUUID().equals(newObj.getUUID())) {
calcChildDiff(oldObj, oldObjectTree, newObj, newObjectTree);
oldIdx++;
newIdx++;
} else {
//we need to figure out which side we are going to move.
//The object with the shorter distance gets moved.
int oldShiftIdx = 0;
for (; oldShiftIdx + oldIdx < oldChildren.size(); oldShiftIdx++) {
if (oldChildren.get(oldShiftIdx + oldIdx).getUUID().equals(newObj.getUUID())) break;
}
int newShiftIdx = 0;
for (; newShiftIdx + newIdx < newChildren.size(); newShiftIdx++) {
if (newChildren.get(newShiftIdx + newIdx).getUUID().equals(oldObj.getUUID())) break;
}
boolean incOldIdx = false;
boolean incNewIdx = false;
if (oldShiftIdx + oldIdx >= oldChildren.size()) {
//Couldn't find new in old list. Add object and descendants
addPersistsRecursively(newObj, newObjectTree);
incNewIdx = true;
}
if (newShiftIdx + newIdx >= newChildren.size()) {
//Couldn't find old in new list. remove object
persistCalls.persistedSPOsToRemove.add(oldObj);
incOldIdx = true;
}
if ((oldShiftIdx + oldIdx < oldChildren.size()) && (newShiftIdx + newIdx < newChildren.size())) {
//This is a move. We only have to move the object with the lower value though.
//If equal the rearrangement is a wash for performance.
if (oldShiftIdx <= newShiftIdx) {
persistCalls.persistedSPOsToRemove.add(oldObj);
PersistedSPObject addObj = newChildren.get(newShiftIdx + newIdx);
addPersistsRecursively(addObj, newObjectTree);
newHandledPersists.add(addObj);
incOldIdx = true;
} else {
PersistedSPObject remObj = oldChildren.get(oldShiftIdx + oldIdx);
persistCalls.persistedSPOsToRemove.add(remObj);
addPersistsRecursively(newObj, newObjectTree);
oldHandledPersists.add(remObj);
incNewIdx = true;
}
}
//Increment indexes after we do comparisons.
if (incOldIdx) oldIdx++;
if (incNewIdx) newIdx++;
}
}
while (oldIdx < oldChildren.size()) {
PersistedSPObject oldObj = oldChildren.get(oldIdx);
persistCalls.persistedSPOsToRemove.add(oldObj);
oldIdx++;
}
while (newIdx < newChildren.size()) {
PersistedSPObject newObj = newChildren.get(newIdx);
addPersistsRecursively(newObj, newObjectTree);
newIdx++;
}
}
}
private void addPersistsRecursively(PersistedSPObject object, Map<PersistedSPObject, Multimap<String, PersistedSPObject>> newObjectTree) {
if (!persistCalls.persistedSPOsToAdd.contains(object)) {
persistCalls.persistedSPOsToAdd.add(object);
}
Multimap<String, PersistedSPObject> typeMap = newObjectTree.get(object);
if (typeMap != null) {
for (PersistedSPObject pso : typeMap.values()) {
addPersistsRecursively(pso, newObjectTree);
}
}
}
/**
* This method determines the persist calls required to diff the old and new properties.
* It goes through the set of uuids of the old and new objects, and determines what
* needs to be added, removed, or changed using the property maps. The new object map
* is required in the case of no corresponding new property, to determine if the old
* property was either changed to null, or the object it was a property of was removed.
*/
private void calcPropertyDiff(List<PersistedSPOProperty> oldPersistedSPOPs,
List<PersistedSPOProperty> newPersistedSPOPs,
HashMap<String, PersistedSPOProperty> oldPropertyMap,
HashMap<String, PersistedSPOProperty> newPropertyMap,
HashSet<String> propertyKeys,
MonitorableImpl monitor) {
// Iterate through all the pairs of properties, and determine changes.
Iterator<String> keyIterator = propertyKeys.iterator();
for (int i = 0; i < propertyKeys.size(); i++) {
String key = keyIterator.next();
PersistedSPOProperty oldProperty = oldPropertyMap.get(key);
PersistedSPOProperty newProperty = newPropertyMap.get(key);
if (oldProperty == null) {
// A new property was added, or the old one was changed to a non-null value.
persistCalls.propertyDiffPersists.add(new PersistedSPOProperty(
newProperty.getUUID(), newProperty.getPropertyName(),
newProperty.getDataType(), null, newProperty.getNewValue(), true));
} else if (newProperty == null) {
// The property was either changed to null, or the object was deleted.
if (newObjectMap.containsKey(oldProperty.getUUID())) {
// The corresponding object still exists, so the property was changed to null.
persistCalls.propertyDiffPersists.add(new PersistedSPOProperty(
oldProperty.getUUID(), oldProperty.getPropertyName(),
oldProperty.getDataType(), oldProperty.getNewValue(), null, true));
}
} else if (!oldProperty.equals(newProperty)) {
// A normal property change
persistCalls.propertyDiffPersists.add(new PersistedSPOProperty(
oldProperty.getUUID(), oldProperty.getPropertyName(),
oldProperty.getDataType(), oldProperty.getNewValue(),
newProperty.getNewValue(), true));
} else if (needToAddProperties.contains(newProperty.getUUID())) {
persistCalls.propertyDiffPersists.add(new PersistedSPOProperty(
newProperty.getUUID(), newProperty.getPropertyName(),
newProperty.getDataType(), newProperty.getNewValue(),
newProperty.getNewValue(), true));
}
if (monitor != null) {
monitor.incrementProgress();
}
}
}
public List<PersistedSPObject> getPersistedSPOsToAdd() {
return persistCalls.persistedSPOsToAdd;
}
public List<PersistedSPObject> getPersistedSPOsToRemove() {
return persistCalls.persistedSPOsToRemove;
}
public List<PersistedSPOProperty> getPropertyDiffPersists() {
return persistCalls.propertyDiffPersists;
}
public void begin() {
// don't need to do anything
}
public void commit() {
// don't need to do anything
}
public void persistObject(String parentUUID, String type, String uuid,
int index) throws SPPersistenceException {
spObjectListToPopulate.add(new PersistedSPObject(parentUUID, type, uuid, index));
}
public void persistProperty(String uuid, String propertyName,
DataType propertyType, Object oldValue, Object newValue)
throws SPPersistenceException {
spoPropertyListToPopulate.add(new PersistedSPOProperty(uuid, propertyName,
propertyType, newValue, newValue, false));
}
public void persistProperty(String uuid, String propertyName,
DataType propertyType, Object newValue)
throws SPPersistenceException {
spoPropertyListToPopulate.add(new PersistedSPOProperty(uuid, propertyName,
propertyType, newValue, newValue, false));
}
public void removeObject(String parentUUID, String uuid)
throws SPPersistenceException {
throw new IllegalStateException("JCR Persistor is wanting to remove objects " +
"when it is creating revision.");
}
public void rollback() {
logger.error("JCR Persistor rolled back when creating revision.");
}
public void persistTo(SPPersister p) throws SPPersistenceException {
persistTo(p, true);
}
/**
* Persist the calculated diffs to a {@link SPPersister}. These persist calls
* assume that the {@link calcDiff()} method has been called and the
* {@link persistCalls.persistedSPOsToAdd}, {@link persistCalls.persistedSPOsToRemove}, and {@link persistCalls.propertyDiffPersists}
* lists have been populated as a result of that.
*
* <p>The persister should persist these calls to the old revision, and the calls will update it to the new one.
*
* @param p The {@link SPPersister} to receive the calls.
* @param conditional If true, the Differ will persist only the new value of properties.
* @throws SPPersistenceException
*/
public void persistTo(SPPersister p, boolean justNew) throws SPPersistenceException {
PersistedSPObject object;
p.begin();
// Remove the old objects first because if it was not deleted,
// but moved somewhere else, it would be added and there would exist
// two objects with the same UUID temporarily, which might confuse/break the persister.
for (int i = 0; i < persistCalls.persistedSPOsToRemove.size(); i++) {
object = persistCalls.persistedSPOsToRemove.get(i);
p.removeObject(object.getParentUUID(), object.getUUID());
}
for (int i = 0; i < persistCalls.persistedSPOsToAdd.size(); i++) {
object = persistCalls.persistedSPOsToAdd.get(i);
p.persistObject(object.getParentUUID(), object.getType(),
object.getUUID(), object.getIndex());
}
for (int i = 0; i < persistCalls.propertyDiffPersists.size(); i++) {
PersistedSPOProperty property = persistCalls.propertyDiffPersists.get(i);
if (justNew) {
p.persistProperty(property.getUUID(), property.getPropertyName(),
property.getDataType(), property.getNewValue());
} else {
p.persistProperty(property.getUUID(), property.getPropertyName(),
property.getDataType(), property.getOldValue(), property.getNewValue());
}
}
p.commit();
}
public boolean hasDifferences() {
return !persistCalls.persistedSPOsToRemove.isEmpty() || !persistCalls.persistedSPOsToAdd.isEmpty() || ! persistCalls.propertyDiffPersists.isEmpty();
}
/**
* This method will sort the Differ's persisted object list so that
* no parents come after their children in the list. It will also make
* sure that siblings that are being added are added in ascending
* order of their indices, so as not to screw up the index property in the JCR.
*
* This does not need to be called when persisting to a client because
* the client already sorts incoming json (more thoroughly).
*/
public void sortPersistedObjects() {
Collections.sort(persistCalls.persistedSPOsToAdd,
new PersistedObjectComparator(persistCalls.persistedSPOsToAdd));
}
public HashMap<String, PersistedSPOProperty> getOldPropertyMap() {
return oldPropertyMap;
}
/**
* This will return a property from the old workspace that has been loaded into the Differ.
*
* @param uuid The uuid of the property's object.
* @param pName The name of the property
* @return The property value, or null if either the object or the property type are not found.
*/
public Object getOldPropertyValue(String uuid, String pName) {
return oldPropertyMap.get(uuid + pName).getNewValue();
}
/**
* This will return a property from the new workspace that has been loaded into the Differ.
*
* @param uuid The uuid of the property's object.
* @param pName The name of the property
* @return The property value, or null if either the object or the property type are not found.
*/
public Object getNewPropertyValue(String uuid, String pName) {
return newPropertyMap.get(uuid + pName).getNewValue();
}
/**
* Allows a root object to be omitted from being persisted. Currently used as a hack
* in ArchitectProjectResource to avoid persisting the SQLObjectRoot of a workspace.
* This method will look for the root object with the given parent workspace uuid
* in the objects it plans to persist (not remove). It will also remove its properties
* from the property diffs to be persisted .
*
* @param workspaceUUID The parent uuid of the root object
* @param newRootUUID If not a null string, the children of the root object will have
* their parent UUIDs changed to this value.
* @return A boolean indicating whether the root object could be found or not.
*/
public boolean omitRootObject(String workspaceUUID, String newRootUUID) {
String rootUUID = "";
for (int i = 0; i < persistCalls.persistedSPOsToAdd.size(); i++) {
if (persistCalls.persistedSPOsToAdd.get(i).getParentUUID().equals(workspaceUUID)) {
rootUUID = persistCalls.persistedSPOsToAdd.get(i).getUUID();
persistCalls.persistedSPOsToAdd.remove(i);
break;
}
}
if (rootUUID.equals("")) {
return false;
}
for (int i = 0; i < persistCalls.propertyDiffPersists.size(); i++) {
if (persistCalls.propertyDiffPersists.get(i).getUUID().equals(rootUUID)) {
persistCalls.propertyDiffPersists.remove(i);
i--;
}
}
if (!newRootUUID.equals("")) {
for (int i = 0; i < persistCalls.persistedSPOsToAdd.size(); i++) {
PersistedSPObject child = persistCalls.persistedSPOsToAdd.get(i);
if (child.getParentUUID().equals(rootUUID)) {
persistCalls.persistedSPOsToAdd.add(new PersistedSPObject(
newRootUUID,
child.getType(),
child.getUUID(),
child.getIndex()));
persistCalls.persistedSPOsToAdd.remove(i);
i--;
}
}
}
return true;
}
public HashMap<String, PersistedSPObject> getOldObjectMap() {
return oldObjectMap;
}
public HashMap<String, PersistedSPObject> getNewObjectMap() {
return newObjectMap;
}
/**
* Returns the name of the type of the object with the given id in this Differ's old workspace.
* @param uuid
* @return A string representing the type of the object with the given uuid, or null if it cannot be found.
* @throws SQLObjectException
*/
public String getObjectType(String uuid) throws SQLObjectException {
if (oldObjectMap.get(uuid) != null) {
return oldObjectMap.get(uuid).getSimpleType();
} else if (newObjectMap.get(uuid) != null) {
return newObjectMap.get(uuid).getSimpleType();
} else {
throw new SQLObjectException("Could not find object in map");
}
}
public String getOldObjectName(String uuid) {
return (String) getOldPropertyValue(uuid, "name");
}
public String getNewObjectName(String uuid) {
return (String) getNewPropertyValue(uuid, "name");
}
/**
* Method to have the Differ take its persist calls and convert them to a list of ordered DiffChunks
* that can be used by CompareDMFormatter's generateEnglishDescriptions method.
*
* @param sourceRoot The UUID of the object whose children you want DiffChunks for (excluding the object itself)
* @throws SQLObjectException
*/
public List<DiffChunk<DiffInfo>> getDiffChunks(String rootUUID) throws SQLObjectException {
// A map containing DiffChunk objects for every object relevant to the comparison.
// This means all added/removed/changed objects, and all their ancestors.
Map<String, DiffChunk<DiffInfo>> diffChunks = new HashMap<String, DiffChunk<DiffInfo>>();
// A map of containing for looking up the parent UUIDs of objects that have been added/removed/changed.
Map<String, String> parentMap = new HashMap<String, String>();
// Add the removed objects to the DiffChunk map and map their ancestors.
for (PersistedSPObject o : persistCalls.persistedSPOsToRemove) {
String uuid = o.getUUID();
getObjectType(uuid);
getOldObjectName(uuid);
DiffInfo d = new DiffInfo(getObjectType(uuid), getOldObjectName(uuid));
diffChunks.put(uuid, new DiffChunk<DiffInfo>(d, DiffType.LEFTONLY));
addAncestorsToMap(uuid, rootUUID, oldObjectMap, parentMap);
}
// Add the added objects to the DiffChunk map and map their ancestors.
for (PersistedSPObject o : persistCalls.persistedSPOsToAdd) {
String uuid = o.getUUID();
DiffInfo d = new DiffInfo(getObjectType(uuid), getNewObjectName(uuid));
diffChunks.put(uuid, new DiffChunk<DiffInfo>(d, DiffType.RIGHTONLY));
addAncestorsToMap(uuid, rootUUID, newObjectMap, parentMap);
}
// Add property changes to the DiffChunks if they are already there.
// If not, add new ones to the DiffChunk map and map the ancestors.
for (PersistedSPOProperty p : persistCalls.propertyDiffPersists) {
String uuid = p.getUUID();
if (!diffChunks.containsKey(uuid)) {
DiffInfo d = new DiffInfo(getObjectType(uuid), getOldObjectName(uuid));
diffChunks.put(uuid, new DiffChunk<DiffInfo>(d, DiffType.MODIFIED));
addAncestorsToMap(uuid, rootUUID, oldObjectMap, parentMap);
}
if (diffChunks.get(uuid).getType() == DiffType.MODIFIED) {
try {
Set<String> interestingProperties = PersisterUtils.getInterestingPropertyNames(
oldObjectMap.get(uuid).getType());
if (interestingProperties.contains(p.getPropertyName())) {
String oldValue = String.valueOf(p.getOldValue());
String newValue = String.valueOf(p.getNewValue());
if (p.getDataType() == DataType.STRING) {
if (p.getOldValue() != null) oldValue = "\"" + oldValue + "\"";
if (p.getNewValue() != null) newValue = "\"" + newValue + "\"";
}
PropertyChange change = new PropertyChange(p.getPropertyName(),
oldValue, newValue);
diffChunks.get(p.getUUID()).addPropertyChange(change);
logger.debug("Added change: " + change);
}
} catch (Exception e) {
throw new SQLObjectException("Error looking up interesting property names", e);
}
}
}
diffChunks.remove(rootUUID);
parentMap.remove(rootUUID);
// Add unchanged objects that are ancestors of those changed by searching up from the changed objects.
Iterator<String> i = parentMap.keySet().iterator();
while (i.hasNext()) {
String leafUUID = i.next();
String nextUUID = parentMap.get(leafUUID);
logger.debug("Leaf " + getObjectType(leafUUID) + ": " + leafUUID);
// Loop while the UUID of the object to add is not the root object, and while it has not already been added.
while (!nextUUID.equals(rootUUID) && !diffChunks.containsKey(nextUUID)) {
logger.debug(nextUUID);
DiffInfo d = new DiffInfo(getObjectType(nextUUID), getOldObjectName(nextUUID));
diffChunks.put(nextUUID, new DiffChunk<DiffInfo>(d, DiffType.SAME));
parentMap.put(nextUUID, oldObjectMap.get(nextUUID).getParentUUID());
nextUUID = parentMap.get(nextUUID);
}
}
DiffChunkTreeNode root = new DiffChunkTreeNode(rootUUID, null);
root.constructTree(diffChunks, parentMap);
return root.buildOrderedList();
}
private void addAncestorsToMap(String uuid, String rootUUID,
Map<String, PersistedSPObject> objectMap,
Map<String, String> parentMap) {
String nextUUID = uuid;
while (!nextUUID.equals(rootUUID) && !parentMap.containsKey(nextUUID)) {
parentMap.put(nextUUID, objectMap.get(nextUUID).getParentUUID());
nextUUID = objectMap.get(nextUUID).getParentUUID();
}
}
}