/* * Copyright (c) 2013, SQL Power Group Inc. * * This file is part of SQL Power Library. * * SQL Power Library is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * SQL Power Library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package ca.sqlpower.dao; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import ca.sqlpower.object.SPObject; import ca.sqlpower.util.SQLPowerUtils; /** * This comparator is used mainly by the {@link SPSessionPersister} to provide * an ordering to how persist calls for objects should be acted on. The order we * want the persists to appear on is dictated by the class's allowedChildTypes * static variable. This persister will then go depth first in creating the * objects, this way later objects have complete objects created to make * reference to. If a different ordering is needed due to other dependencies * this comparator can be extended. */ public class PersistedObjectComparator implements Comparator<PersistedSPObject> { /** * The root object of the object tree we are persisting to. This root object * will be used to inspect how the tree currently looks to know the order to * persist in. */ private final SPObject root; /** * This maps the objects being created by the id of the object to be * created. We will use this collection of persisted object to understand * how the objects being compared relate to each other. * <p> * We could just include the list of persisted objects but the mapping helps * improve performance. */ private final Map<String, PersistedSPObject> persistedObjectsMap; /** * This cache provides a quick way to look up objects by UUID. */ private final Map<String, SPObject> lookupCache; public PersistedObjectComparator(SPObject root, Map<String, PersistedSPObject> persistedObjectsMap) { this.root = root; this.persistedObjectsMap = persistedObjectsMap; lookupCache = new HashMap<String, SPObject>(SQLPowerUtils.buildIdMap(this.root)); } // If the two objects being compared are of the same type and are // children of the same parent, the one with the lower index should go // first. Otherwise, the one with the smaller ancestor tree should go first // (e.g. Report should go before Page). @Override public int compare(PersistedSPObject o1, PersistedSPObject o2) { if (o1.getParentUUID() == null && o2.getParentUUID() == null) { return 0; } else if (o1.getParentUUID() == null) { return -1; } else if (o2.getParentUUID() == null) { return 1; } if (o1.getParentUUID().equals(o2.getParentUUID()) && o1.getType().equals(o2.getType())) { return Integer.signum(o1.getIndex() - o2.getIndex()); } List<PersistedSPObject> ancestorList1 = buildAncestorListFromPersistedObjects(o1); List<PersistedSPObject> ancestorList2 = buildAncestorListFromPersistedObjects(o2); if (ancestorList1.contains(o2)) { return 1; } else if (ancestorList2.contains(o1)) { return -1; } //This is the shared parent of ancestor1 and ancestor2 PersistedSPObject sharedAncestor = null; //This is the ancestor to o1 which is the last ancestor different to o1 before we reach a shared parent. PersistedSPObject ancestor1 = null; //This is the ancestor to o2 which is the last ancestor different to o2 before we reach a shared parent. PersistedSPObject ancestor2 = null; //If true these ancestor objects should be compared. If false we made it to the end of one of the lists. boolean compareWithAncestor = false; //Looking at the highest ancestor that is different in the list for (int i = 0, j = 0; i < ancestorList1.size() && j < ancestorList2.size(); i++, j++) { ancestor1 = ancestorList1.get(i); ancestor2 = ancestorList2.get(j); if (sharedAncestor != null && !ancestor1.equals(ancestor2)) { compareWithAncestor = true; break; } sharedAncestor = ancestor1; } if (!compareWithAncestor) { if (ancestorList1.size() < ancestorList2.size()) { ancestor1 = o1; ancestor2 = ancestorList2.get(ancestorList1.size()); } else if (ancestorList1.size() > ancestorList2.size()) { ancestor1 = ancestorList1.get(ancestorList2.size()); ancestor2 = o2; } else { ancestor1 = o1; ancestor2 = o2; } } if (ancestor1.equals(ancestor2)) { throw new IllegalStateException("The ancestors of " + o1 + " and " + o2 + " is the same object but should not be by checks above. The ancestor is " + ancestor1); } return compareAncestors(o1, o2, ancestorList1, ancestorList2, sharedAncestor, ancestor1, ancestor2); } /** * This method will compare the ancestors of the objects we are comparing * instead of comparing the objects themselves. The ancestors are being * compared instead of the objects because the objects are so distant in the * tree that we can't compare them directly. * * @param o1 * The first object we are comparing. * @param o2 * The second object we are comparing. * @param ancestorList1 * The list of ancestors for the first object. The root is at the * start of the list and the parent of the object is at the end. * @param ancestorList2 * The list of ancestors for the second object. The root is at * the start of the list and the parent of the object is at the * end. * @param sharedAncestor * This is the parent of ancestor1 and ancestor2. It is the * closest ancestor to o1 and o2 that they share. This value can * be null if o1 and o2 are not in the same tree. * @param ancestor1 * The last ancestor of o1 which is not part of o2's ancestor * tree. * @param ancestor2 * The last ancestor of o2 which is not part of o1's ancestor * tree. * @return -1, 0, or 1 if o1 is less than, equal to, or greater than o2 * respectively. */ protected int compareAncestors(PersistedSPObject o1, PersistedSPObject o2, List<PersistedSPObject> ancestorList1, List<PersistedSPObject> ancestorList2, PersistedSPObject sharedAncestor, PersistedSPObject ancestor1, PersistedSPObject ancestor2) { int c; if (ancestor1.getType().equals(ancestor2.getType())) { c = ancestor1.getIndex() - ancestor2.getIndex(); } else { if (sharedAncestor == null) { if (ancestorList1.isEmpty()) throw new IllegalStateException("The object represented by " + o1 + " is not correctly connected to the model"); if (ancestorList2.isEmpty()) throw new IllegalStateException("The object represented by " + o2 + " is not correctly connected to the model"); throw new NullPointerException("There was an issue comparing " + o1 + " and " + o2 + " which is " + "normally caused by the objects not being in the same tree."); } //Looking at the highest ancestor that is different in the list and finding the order //of these ancestors based on the absolute ordering defined in their shared parent class type. try { int ancestorType1Index = PersisterUtils.getTypePosition(ancestor1.getType(), sharedAncestor.getType()); if (ancestorType1Index == -1) { throw new IllegalStateException("Allowed child types for " + sharedAncestor + " does not contain " + ancestor1); } int ancestorType2Index = PersisterUtils.getTypePosition(ancestor2.getType(), sharedAncestor.getType()); if (ancestorType2Index == -1) { throw new IllegalStateException("Allowed child types for " + sharedAncestor + " does not contain " + ancestor2); } c = ancestorType1Index - ancestorType2Index; if (c == 0 && ancestor1.getParentUUID().equals(ancestor2.getParentUUID())) { //If you reach this point the two objects are subclasses of the same //object type contained in the allowedChildTypes list and their order //is decided by their index locations. c = ancestor1.getIndex() - ancestor2.getIndex(); if (c == 0) { throw new IllegalStateException("The objects " + ancestor1 + " and " + ancestor2 + " are defined as equal but shouldn't be."); } } else if (c == 0) { throw new IllegalStateException("Error comparing " + o1 + " to " + o2); } } catch (Exception e) { throw new RuntimeException(e); } } return Integer.signum(c); } /** * Returns an ancestor list of {@link PersistedSPObject}s from a given child * {@link PersistedSPObject}. The list holds persist objects starting from * the root and going to the parent's persist call. This list does not hold * the persist call for the persist passed in. This list holds persist calls * that are outside of the current transaction of persist calls, it will * create persist calls as necessary. */ private List<PersistedSPObject> buildAncestorListFromPersistedObjects(PersistedSPObject child) { List<PersistedSPObject> resultList = new ArrayList<PersistedSPObject>(); // Iterate through list of persisted SPObjects to build an ancestor // list from objects that do not exist in the root yet. String uuid = child.getParentUUID(); PersistedSPObject pso; while ((pso = persistedObjectsMap.get(uuid)) != null) { resultList.add(0, pso); uuid = pso.getParentUUID(); } SPObject spo = findByUuid(uuid, SPObject.class); List<PersistedSPObject> existingAncestorList = new ArrayList<PersistedSPObject>(); if (spo != null) { existingAncestorList.add(0, PersisterUtils.createPersistedObjectFromSPObject(spo)); List<SPObject> ancestorList = SQLPowerUtils.getAncestorList(spo); Collections.reverse(ancestorList); for (SPObject ancestor : ancestorList) { existingAncestorList.add(0, PersisterUtils.createPersistedObjectFromSPObject(ancestor)); } } resultList.addAll(0, existingAncestorList); return resultList; } /** * This section is originally taken from the SPSessionPersister and we may * need to refactor this to prevent code duplication. * <p> * This uuid lookup uses the cache created in the constructor of this class * to find objects rather than iterating through the tree of objects. */ protected <T extends SPObject> T findByUuid(String uuid, Class<T> expectedType) { if (lookupCache.get(uuid) != null) { SPObject foundObject = lookupCache.get(uuid); if (!expectedType.isAssignableFrom(foundObject.getClass())) { throw new IllegalStateException("The object " + foundObject + " is not of type " + expectedType + " from the cache."); } return expectedType.cast(foundObject); } return null; } }