Revision: 3484
Author: silva.josemanuel1
Date: Fri Apr 30 15:31:53 2010
Log: Added lots of conflict detection to NetworkConflictResolver. To see what constitutes a conflict, see the ConflictCases enum within NetworkConflictResolver, and the checkForSimultaneousEdit. Not all the cases have been tested.
http://code.google.com/p/power-architect/source/detail?r=3484

Modified:
/trunk/src/main/java/ca/sqlpower/architect/enterprise/NetworkConflictResolver.java
 /trunk/src/main/java/ca/sqlpower/architect/swingui/PlayPen.java
 /trunk/src/main/java/ca/sqlpower/architect/swingui/PlayPenContentPane.java

=======================================
--- /trunk/src/main/java/ca/sqlpower/architect/enterprise/NetworkConflictResolver.java Wed Apr 28 15:04:19 2010 +++ /trunk/src/main/java/ca/sqlpower/architect/enterprise/NetworkConflictResolver.java Fri Apr 30 15:31:53 2010
@@ -20,8 +20,15 @@

 import java.net.URI;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;

 import org.apache.http.client.HttpClient;
@@ -35,9 +42,9 @@
 import org.springframework.security.AccessDeniedException;

 import ca.sqlpower.architect.ArchitectSession;
+import ca.sqlpower.architect.swingui.PlayPenComponent;
+import ca.sqlpower.architect.swingui.PlayPenContentPane;
 import ca.sqlpower.dao.MessageSender;
-import ca.sqlpower.dao.PersistedObjectEntry;
-import ca.sqlpower.dao.PersistedPropertiesEntry;
 import ca.sqlpower.dao.PersistedSPOProperty;
 import ca.sqlpower.dao.PersistedSPObject;
 import ca.sqlpower.dao.RemovedObjectEntry;
@@ -48,6 +55,7 @@
 import ca.sqlpower.dao.json.SPJSONMessageDecoder;
 import ca.sqlpower.dao.session.SessionPersisterSuperConverter;
 import ca.sqlpower.object.SPObject;
+import ca.sqlpower.sqlobject.SQLRelationship.ColumnMapping;
 import ca.sqlpower.util.SQLPowerUtils;
 import ca.sqlpower.util.UserPrompter.UserPromptOptions;
 import ca.sqlpower.util.UserPrompter.UserPromptResponse;
@@ -84,17 +92,13 @@

     private JSONArray messageBuffer = new JSONArray();

- private List<PersistedSPObject> inboundObjectsToAdd = new LinkedList<PersistedSPObject>(); + private HashMap<String, PersistedSPObject> inboundObjectsToAdd = new HashMap<String, PersistedSPObject>(); private Multimap<String, PersistedSPOProperty> inboundPropertiesToChange = LinkedListMultimap.create(); - private List<RemovedObjectEntry> inboundObjectsToRemove = new LinkedList<RemovedObjectEntry>();
-
- private List<PersistedSPObject> outboundObjectsToAdd = new LinkedList<PersistedSPObject>(); + private HashMap<String, RemovedObjectEntry> inboundObjectsToRemove = new HashMap<String, RemovedObjectEntry>();
+
+ private Map<String, PersistedSPObject> outboundObjectsToAdd = new LinkedHashMap<String, PersistedSPObject>(); private Multimap<String, PersistedSPOProperty> outboundPropertiesToChange = LinkedListMultimap.create(); - private List<RemovedObjectEntry> outboundObjectsToRemove = new LinkedList<RemovedObjectEntry>();
-
- private List<PersistedObjectEntry> outboundObjectsToAddRollbackList = new LinkedList<PersistedObjectEntry>(); - private List<PersistedPropertiesEntry> outboundPropertiesToChangeRollbackList = new LinkedList<PersistedPropertiesEntry>(); - private List<RemovedObjectEntry> outboundObjectsToRemoveRollbackList = new LinkedList<RemovedObjectEntry>(); + private Map<String, RemovedObjectEntry> outboundObjectsToRemove = new LinkedHashMap<String, RemovedObjectEntry>();

private List<UpdateListener> updateListeners = new ArrayList<UpdateListener>();

@@ -172,14 +176,12 @@
                 return;
             }
             if (response.isSuccessful()) {
-                // Sent json message without conflict.
+                // Sent json message without conflict.
                 try {
currentRevision = (new JSONObject(response.getBody())).getInt("currentRevision");
                 } catch (JSONException e) {
throw new RuntimeException("Could not update current revision" + e.getMessage());
                 }
-                // Prepare for next send ...
-                clear(reflush);
             } else {
// Did not successfully post json, we must update ourselves, and then try again if we can.
                 if (!reflush) {
@@ -226,23 +228,35 @@
                 // Try to apply update
                 decodeMessage(json, newRev);
// We need an additional step here for checking for special case conflicts
-                if (detectConflict()) {
- throw new RuntimeException("There is a conflict between our state and the server's, our changes will be lost");
-                } else {
+                List<ConflictMessage> conflicts = detectConflicts();
+                if (conflicts.size() == 0) {
// Try to return the persisted objects to their state pre-update.
                     try {
SPSessionPersister.redoForSession(session.getWorkspace(), - outboundObjectsToAdd, outboundPropertiesToChange,
-                                outboundObjectsToRemove, converter);
+ new LinkedList<PersistedSPObject>(outboundObjectsToAdd.values()),
+                                outboundPropertiesToChange,
+ new LinkedList<RemovedObjectEntry>(outboundObjectsToRemove.values()), converter); // We want to re-send our changes, but only if we were able to restore them
                         flush(true);
                     } catch (Exception ex) {
throw new RuntimeException("Reflush failed on rollforward", ex);
                     }
+                } else {
+                    String message = "";
+ message += "Your changes have been discarded due to a conflict between you and another user: \n";
+                    for (ConflictMessage conflict : conflicts) {
+                        message += conflict.getMessage() + "\n";
+                    }
+                    session.createUserPrompter(message,
+                            UserPromptType.MESSAGE,
+                            UserPromptOptions.OK,
+                            UserPromptResponse.OK,
+                            "OK", "OK").promptUser("");
                 }
             }
         } finally {
             postingJSON.set(false);
+            clear(true);
         }
     }

@@ -261,10 +275,6 @@
             outboundObjectsToAdd.clear();
             outboundPropertiesToChange.clear();
             outboundObjectsToRemove.clear();
-
-            outboundObjectsToAddRollbackList.clear();
-            outboundPropertiesToChangeRollbackList.clear();
-            outboundObjectsToRemoveRollbackList.clear();
         }
     }

@@ -393,22 +403,13 @@

     private void fillOutboundPersistedLists() {
         for (PersistedSPObject obj : listener.getPersistedObjects()) {
-            outboundObjectsToAdd.add(obj);
+            outboundObjectsToAdd.put(obj.getUUID(), obj);
         }
for (PersistedSPOProperty prop : listener.getPersistedProperties()) {
             outboundPropertiesToChange.put(prop.getUUID(), prop);
         }
         for (RemovedObjectEntry rem : listener.getObjectsToRemove()) {
-            outboundObjectsToRemove.add(rem);
-        }
- for (PersistedObjectEntry poe : listener.getObjectsRollbackList()) {
-            outboundObjectsToAddRollbackList.add(poe);
-        }
- for (PersistedPropertiesEntry ppe : listener.getPropertiesRollbackList()) {
-            outboundPropertiesToChangeRollbackList.add(ppe);
-        }
-        for (RemovedObjectEntry roe : listener.getRemovedRollbackList()) {
-            outboundObjectsToRemoveRollbackList.add(roe);
+ outboundObjectsToRemove.put(rem.getRemovedChild().getUUID(), rem);
         }
     }

@@ -425,7 +426,7 @@
                     String uuid = obj.getString("uuid");
                     int index = obj.getInt("index");

- inboundObjectsToAdd.add(new PersistedSPObject(parentUUID, type, uuid, index)); + inboundObjectsToAdd.put(uuid, new PersistedSPObject(parentUUID, type, uuid, index));

} else if (obj.getString("method").equals("persistProperty")) {

@@ -453,7 +454,7 @@
                     String uuid = obj.getString("uuid");
SPObject objectToRemove = SQLPowerUtils.findByUuid(session.getWorkspace(), uuid, SPObject.class);

- inboundObjectsToRemove.add(new RemovedObjectEntry(parentUUID, objectToRemove, + inboundObjectsToRemove.put(uuid, new RemovedObjectEntry(parentUUID, objectToRemove, objectToRemove.getParent().getChildren().indexOf(objectToRemove)));
                 }
             }
@@ -462,23 +463,448 @@
         }
     }

-    private boolean detectConflict() {
-        // XXX : Special cases are to be looked for here.
-        if (checkForSimultaneousEdit()) return true;
-        // No special cases found.
-        return false;
+    private List<ConflictMessage> detectConflicts() {
+        List<ConflictMessage> conflicts = checkForSimultaneousEdit();
+        // ----- Special cases -----
+        allowSimultaneousAdditionsUnderDB(conflicts);
+        disallowColumnMappingsPointingToSameColumn(conflicts);
+        return conflicts;
+    }
+
+    /**
+ * This method will make sure that column mappings of the same relationship
+     * do not point to the same column of a table, since this is illegal.
+     */
+ private void disallowColumnMappingsPointingToSameColumn(List<ConflictMessage> conflicts) {
+        /**
+         * Stores the uuids of columns that are being pointed to
+ * by column mappings. Will store them like so: relationshipId:columnId
+         */
+ Set<String> duplicates = getColumnMappingChanges(outboundPropertiesToChange); + duplicates.retainAll(getColumnMappingChanges(inboundPropertiesToChange));
+        for (String duplicate : duplicates) {
+            String[] ids = duplicate.split("\\:");
+            String relationshipId = ids[0];
+            String columnId = ids[1];
+ String relationshipName = session.getWorkspace().getObjectInTree(relationshipId).getName(); + String columnName = session.getWorkspace().getObjectInTree(columnId).getName(); + String message = "More than one column mapping of relationship " + + relationshipName + " points to the column " + columnName;
+            conflicts.add(new ConflictMessage(
+ message, ConflictCase.SPECIAL_CASE, relationshipId, columnId));
+        }
+
     }

-    private boolean checkForSimultaneousEdit() {
-        String targetDBuuid = session.getWorkspace().getUUID();
-        for (PersistedSPObject inSpo : inboundObjectsToAdd) {
-            for (PersistedSPObject outSpo : outboundObjectsToAdd) {
- if (inSpo.getParentUUID().equals(outSpo.getParentUUID()) && !inSpo.equals(targetDBuuid)) {
-                    return true;
-                }
+    /**
+     * Returns a set of strings indicating which columns were pointed to
+ * as a result of column mapping property changes, found in the given properties map.
+     * The format of the strings are {relationshipId}:{columnId}
+     */
+ private Set<String> getColumnMappingChanges(Multimap<String, PersistedSPOProperty> properties) {
+        Set<String> changes = new HashSet<String>();
+        for (String uuid : properties.keySet()) {
+            Class type;
+            String parentId;
+            SPObject spo = session.getWorkspace().getObjectInTree(uuid);
+            PersistedSPObject o = outboundObjectsToAdd.get(uuid);
+            try {
+                if (spo != null) {
+                    type = spo.getClass();
+                    parentId = spo.getParent().getUUID();
+                } else if (o != null) {
+                    type = Class.forName(o.getType());
+                    parentId = o.getParentUUID();
+                } else {
+                    continue;
+                }
+                if (ColumnMapping.class.isAssignableFrom(type)) {
+                    for (PersistedSPOProperty p : properties.get(uuid)) {
+                        if (p.getDataType() == DataType.REFERENCE) {
+ changes.add(parentId + ":" + (String) p.getNewValue());
+                        }
+                    }
+                }
+            } catch (ClassNotFoundException e) {
+                throw new RuntimeException(e);
             }
         }
-        return false;
+        return changes;
+    }
+
+    /**
+ * This method will iterate over the given conflicts looking for those of + * type simulatenous addition. If this conflict was due to a simulatenous
+     * addition under the target database or the play pen content pane,
+ * the conflict is removed from the list and the index of the call is fixed.
+     */
+ private void allowSimultaneousAdditionsUnderDB(List<ConflictMessage> conflicts) {
+        Iterator<ConflictMessage> iterator = conflicts.iterator();
+ List<PersistedSPObject> indexUpdates = new LinkedList<PersistedSPObject>();
+        while (iterator.hasNext()) {
+            ConflictMessage conflict = iterator.next();
+ if (conflict.getConflictCase() == ConflictCase.SIMULTANEOUS_ADDITION) { + PersistedSPObject o = outboundObjectsToAdd.get(conflict.getObjectId(0)); + SPObject parent = session.getWorkspace().getObjectInTree(o.getParentUUID());
+                if (parent == session.getTargetDatabase()) {
+                    iterator.remove();
+ int size = session.getTargetDatabase().getChildren().size();
+                    indexUpdates.add(new PersistedSPObject(
+ o.getParentUUID(), o.getType(), o.getUUID(), size)); + } else if (parent == session.getWorkspace().getPlayPenContentPane()) {
+                    iterator.remove();
+ PlayPenContentPane cp = session.getWorkspace().getPlayPenContentPane();
+                    try {
+ Class<PlayPenComponent> type = (Class<PlayPenComponent>) Class.forName(o.getType());
+                        int newIndex = -1;
+ if (PlayPenContentPane.isDependentComponentType(type)) { + if (o.getIndex() < cp.getFirstDependentComponentIndex()) {
+                                newIndex = cp.getChildren().size();
+                            }
+                        } else {
+ if (o.getIndex() >= cp.getFirstDependentComponentIndex()
+                                    && o.getIndex() > 0) {
+ newIndex = cp.getFirstDependentComponentIndex() - 1;
+                            }
+                        }
+                        if (newIndex > -1) {
+                            indexUpdates.add(new PersistedSPObject(
+ o.getParentUUID(), o.getType(), o.getUUID(), newIndex));
+                        }
+                    } catch (ClassNotFoundException e) {
+                        throw new RuntimeException(e);
+                    }
+                }
+            }
+        }
+        for (PersistedSPObject o : indexUpdates) {
+            outboundObjectsToAdd.put(o.getUUID(), o);
+        }
+    }
+
+    /**
+     * Goes through all the inbound and outbound change lists and
+ * determines whether the outbound changes should be allowed to continue.
+     * The reasons to prevent the outbound changes are usually cases where
+     * as a result of the incoming change, the outbound change would not be
+ * possible through the UI anymore, and/or are impossible in such a state.
+     *
+ * See ConflictCase for all the cases that are looked for in this method.
+     * A Google Docs spreadsheet called Conflict rules has been shared
+     * with the psc group. For more information, see that.
+     */
+    private List<ConflictMessage> checkForSimultaneousEdit() {
+
+ List<ConflictMessage> conflicts = new LinkedList<ConflictMessage>();
+
+        Set<String> inboundAddedObjectParents = new HashSet<String>();
+        Set<String> inboundRemovedObjectParents = new HashSet<String>();
+
+        Set<String> inboundChangedObjects = new HashSet<String>();
+ HashMap<String, String> inboundCreatedDependencies = new HashMap<String, String>();
+
+        Set<String> duplicateMoves = new HashSet<String>();
+
+
+        // ----- Populate the inbound sets / maps -----
+
+        for (String uuid : inboundPropertiesToChange.keys()) {
+            inboundChangedObjects.add(uuid);
+ for (PersistedSPOProperty p : inboundPropertiesToChange.get(uuid)) {
+                if (p.getDataType() == DataType.REFERENCE) {
+ inboundCreatedDependencies.put((String) p.getNewValue(), p.getUUID());
+                }
+            }
+        }
+
+        for (PersistedSPObject o : inboundObjectsToAdd.values()) {
+            inboundAddedObjectParents.add(o.getParentUUID());
+        }
+
+        for (RemovedObjectEntry o : inboundObjectsToRemove.values()) {
+            inboundRemovedObjectParents.add(o.getParentUUID());
+        }
+
+        // ----- Iterate through outbound additions -----
+
+        Set<String> checkedIfCanAddToTree = new HashSet<String>();
+ Iterator<PersistedSPObject> addedObjects = outboundObjectsToAdd.values().iterator();
+        while (addedObjects.hasNext()) {
+            PersistedSPObject o = addedObjects.next();
+
+ // Can't add object to a parent that already had a child added or removed. + // This will also include incoming and/or outgoing moves, which are conflicts too.
+            if (inboundAddedObjectParents.contains(o.getParentUUID()) ||
+ inboundRemovedObjectParents.contains(o.getParentUUID())) { + conflicts.add(new ConflictMessage(ConflictCase.SIMULTANEOUS_ADDITION,
+                        o.getUUID(), getPersistedObjectName(o)));
+            }
+
+            // Can't add an object if the direct parent was changed.
+            if (inboundChangedObjects.contains(o.getParentUUID())) {
+ conflicts.add(new ConflictMessage(ConflictCase.ADDITION_UNDER_CHANGE,
+                        o.getUUID(), getPersistedObjectName(o),
+ o.getParentUUID(), session.getWorkspace().getObjectInTree(o.getParentUUID()).getName()));
+            }
+
+ // Make sure we are not adding an object that had an ancestor removed. + // First iterate up ancestors that are being added in the same transaction.
+            PersistedSPObject highestAddition = o;
+ while (outboundObjectsToAdd.containsKey(highestAddition.getParentUUID()) && + !checkedIfCanAddToTree.contains(highestAddition.getParentUUID())) {
+                checkedIfCanAddToTree.add(highestAddition.getUUID());
+ highestAddition = outboundObjectsToAdd.get(highestAddition.getParentUUID());
+            }
+            checkedIfCanAddToTree.add(highestAddition.getUUID());
+ if (checkedIfCanAddToTree.add(highestAddition.getParentUUID()) && + session.getWorkspace().getObjectInTree(highestAddition.getParentUUID()) == null) { + conflicts.add(new ConflictMessage(ConflictCase.ADDITION_UNDER_REMOVAL, + highestAddition.getUUID(), getPersistedObjectName(highestAddition)));
+            }
+
+            // Check if both clients are adding the same object.
+            // It could mean they both undid a deletion of this object,
+            // or are both trying to move the same object.
+ // If they are identical, remove the outbound add from this list.
+            // If it was a move and has a corresponding remove call, that
+ // must be taken care of in the following outbound removals loop.
+            if (inboundObjectsToAdd.containsKey(o.getUUID())) {
+                if (inboundObjectsToAdd.get(o.getUUID()).equals(o)) {
+                    addedObjects.remove();
+                    outboundPropertiesToChange.removeAll(o.getUUID());
+                    duplicateMoves.add(o.getUUID());
+                } else {
+ conflicts.add(new ConflictMessage(ConflictCase.DIFFERENT_MOVE,
+                            o.getUUID(), getPersistedObjectName(o)));
+                }
+            }
+        }
+
+
+        // ----- Iterate through outbound removals -----
+
+ Iterator<RemovedObjectEntry> removedObjects = outboundObjectsToRemove.values().iterator();
+        while (removedObjects.hasNext()) {
+            RemovedObjectEntry object = removedObjects.next();
+            final String uuid = object.getRemovedChild().getUUID();
+
+ // Check if the object the outbound client is trying to remove does not exist. + SPObject removedObject = session.getWorkspace().getObjectInTree(uuid);
+            if (removedObject == null) {
+ // Check if this remove has a corresponding add, meaning it is a move.
+                // The incoming remove will override the outgoing move.
+                if (outboundObjectsToAdd.containsKey(uuid)) {
+ conflicts.add(new ConflictMessage(ConflictCase.MOVE_OF_REMOVED, + object.getRemovedChild().getUUID(), object.getRemovedChild().getName()));
+                } else {
+ // Both clients removed the same object, either directly or indirectly.
+                    removedObjects.remove();
+                }
+            } else if (inboundCreatedDependencies.containsKey(uuid)) {
+                // Can't remove an object that was just made a dependency
+ String uuidOfDependent = inboundCreatedDependencies.get(uuid); + conflicts.add(new ConflictMessage(ConflictCase.REMOVAL_OF_DEPENDENCY,
+                        uuid, removedObject.getName(),
+ uuidOfDependent, session.getWorkspace().getObjectInTree(uuidOfDependent).getName()));
+            } else if (duplicateMoves.contains(uuid)) {
+                removedObjects.remove();
+            }
+
+        }
+
+
+        // ----- Iterate through outbound properties -----
+
+        for (String uuid : outboundPropertiesToChange.keys()) {
+ SPObject changedObject = session.getWorkspace().getObjectInTree(uuid);
+
+ // If this object is being newly added, the rest of the loop body does not matter.
+            if (outboundObjectsToAdd.containsKey(uuid)) continue;
+
+ // Cannot change a property on an object that no longer exists (due to inbound removal).
+            if (changedObject == null) {
+ conflicts.add(new ConflictMessage(ConflictCase.CHANGE_OF_REMOVED, uuid, uuid));
+                continue;
+            }
+
+ // Cannot change the property of an object whose direct parent was also changed.
+            if (changedObject.getParent() != null &&
+ inboundChangedObjects.contains(changedObject.getParent().getUUID())) { + conflicts.add(new ConflictMessage(ConflictCase.CHANGE_UNDER_CHANGE,
+                        uuid, changedObject.getName(),
+ changedObject.getParent().getUUID(), changedObject.getParent().getName()));
+            }
+
+ // You cannot change the property of an object that had a property already changed, + // unless any and all property changes are identical, in which case the duplicate
+            // property changes will be removed from the outgoing list.
+
+            if (inboundChangedObjects.contains(uuid)) {
+ ConflictMessage message = new ConflictMessage(ConflictCase.SIMULTANEOUS_OBJECT_CHANGE, + uuid, session.getWorkspace().getObjectInTree(uuid).getName());
+
+                HashMap<String, Object> inboundPropertiesMap =
+                    new HashMap<String, Object>();
+ for (PersistedSPOProperty p : inboundPropertiesToChange.get(uuid)) { + inboundPropertiesMap.put(p.getPropertyName(), p.getNewValue());
+                }
+
+ Iterator<PersistedSPOProperty> properties = outboundPropertiesToChange.get(uuid).iterator();
+                while (properties.hasNext()) {
+                    PersistedSPOProperty p = properties.next();
+                    // Check if there is a corresponding inbound property.
+ // If not, this is a conflict since there are non-identical properties. + if (inboundPropertiesMap.containsKey(p.getPropertyName())) { + if (inboundPropertiesMap.get(p.getPropertyName()).equals(p.getNewValue())) {
+                            properties.remove();
+                        } else {
+                            conflicts.add(message);
+                            break;
+                        }
+                    } else {
+                        conflicts.add(message);
+                        break;
+                    }
+                }
+            }
+
+ // Cannot change the property of a parent whose direct child was either:
+            for (SPObject child : changedObject.getChildren()) {
+                // also changed
+                if (inboundChangedObjects.contains(child.getUUID())) {
+ conflicts.add(new ConflictMessage(ConflictCase.CHANGE_UNDER_CHANGE,
+                            uuid, changedObject.getName(),
+                            child.getUUID(), child.getName()));
+                }
+
+                // or just added (moved is okay, though).
+                if (inboundObjectsToAdd.containsKey(child.getUUID()) &&
+                        !inboundObjectsToRemove.containsKey(child.getUUID())){
+ conflicts.add(new ConflictMessage(ConflictCase.CHANGE_AFTER_ADDITION,
+                            uuid, changedObject.getName(),
+                            child.getUUID(), child.getName()));
+                }
+            }
+        }
+        return conflicts;
+    }
+
+    private String getPersistedObjectName(PersistedSPObject o) {
+ for (PersistedSPOProperty p : outboundPropertiesToChange.get(o.getUUID())) {
+            if (p.getPropertyName().equals("name")) {
+                return (String) p.getNewValue();
+            }
+        }
+ throw new IllegalArgumentException("Given persisted object has no corresponding name property!");
+    }
+
+    /**
+     * A class that will take a ConflictCase and parameters insert
+     * them into the ConflictCase's message.
+     */
+    private class ConflictMessage {
+
+        private final ConflictCase conflict;
+        private final String message;
+        private final List<String> objectIds = new LinkedList<String>();
+        private final List<String> objectNames = new LinkedList<String>();
+
+        /**
+         * Create a conflict message using the ConflictCase's message
+         * and String.format() to put the given arguments in
+         * @param conflict
+ * @param uuidsAndNames A list of the relevant object ids and names, in pairs.
+         * ie: "id1", "table1", "id2", "table2"
+         */
+ public ConflictMessage(ConflictCase conflict, String ... uuidsAndNames) {
+            this.conflict = conflict;
+            for (int i = 0; i < uuidsAndNames.length; i += 2) {
+                objectIds.add(uuidsAndNames[i]);
+                objectNames.add(uuidsAndNames[i+1]);
+            }
+            if (objectIds.size() != conflict.numArgs()) {
+                throw new IllegalArgumentException(
+ "Number of arguments passed in does not match number requested by conflict type");
+            }
+            String b = String.format("%s %s", "test", "not");
+            try {
+ message = String.format(conflict.message, objectNames.toArray());
+            } catch (Throwable t) {
+                throw new RuntimeException(t);
+            }
+        }
+
+        /**
+         * This constructor is used for custom messages.
+         * Will not call String.format().
+         * @param message
+         * @param conflict
+         * @param uuids
+         */
+ public ConflictMessage(String message, ConflictCase conflict, String ... uuids) {
+            this.message = message;
+            this.conflict = conflict;
+            objectIds.addAll(Arrays.asList(uuids));
+        }
+
+        public String getObjectId(int index) {
+            return objectIds.get(index);
+        }
+
+        public ConflictCase getConflictCase() {
+            return conflict;
+        }
+
+        public String getMessage() {
+            return message;
+        }
+
+        public String toString() {
+            return message;
+        }
+
+    }
+
+    /**
+     * Defines conflict cases as well as messages for each.
+     */
+    private static enum ConflictCase {
+        NO_CONFLICT ("There were no conflicts"),
+
+ ADDITION_UNDER_REMOVAL ("The %s you tried adding could not be added because its ancestor was removed"),
+
+        MOVE_OF_REMOVED ("Could not move %s because it was removed"),
+
+        CHANGE_OF_REMOVED ("Could not change %s because it was removed"),
+
+ SIMULTANEOUS_ADDITION ("Could not add %s because a sibling was added/removed"),
+
+ ADDITION_UNDER_CHANGE ("Could not add %s because its parent %s was modified"),
+
+ CHANGE_AFTER_ADDITION ("Could not change %s because child %s was added"),
+
+ CHANGE_UNDER_CHANGE ("Could not change %s because its parent/child %s was modified"),
+
+ REMOVAL_OF_DEPENDENCY ("Could not remove %s because another object %s is now dependent on it"),
+
+ SIMULTANEOUS_OBJECT_CHANGE ("Could not change %s because it was changed by another user"),
+
+ DIFFERENT_MOVE ("Could not move %s because it was moved somewhere else"),
+
+        SPECIAL_CASE ("");
+
+        private final String message;
+
+        ConflictCase(String s) {
+            message = s;
+        }
+
+        /**
+ * Returns the number of parameters the enum expects in its message.
+         */
+        public int numArgs() {
+            return message.length() - message.replace("%s", "1").length();
+        }
     }

     /**
=======================================
--- /trunk/src/main/java/ca/sqlpower/architect/swingui/PlayPen.java Thu Apr 22 14:28:36 2010 +++ /trunk/src/main/java/ca/sqlpower/architect/swingui/PlayPen.java Fri Apr 30 15:31:53 2010
@@ -1711,7 +1711,7 @@

PlayPenComponent ppc = removedComponents.get(child.getUUID());
                 if (ppc != null) {
- if (contentPane.isDependentComponentType(ppc.getClass())) { + if (PlayPenContentPane.isDependentComponentType(ppc.getClass())) { contentPane.addChild(ppc, contentPane.getFirstDependentComponentIndex());
                     } else {
                         contentPane.addChild(ppc, 0);
=======================================
--- /trunk/src/main/java/ca/sqlpower/architect/swingui/PlayPenContentPane.java Thu Apr 29 08:28:11 2010 +++ /trunk/src/main/java/ca/sqlpower/architect/swingui/PlayPenContentPane.java Fri Apr 30 15:31:53 2010
@@ -204,7 +204,7 @@
     }

     @NonBound
- public boolean isDependentComponentType(Class<? extends PlayPenComponent> componentType) { + public static boolean isDependentComponentType(Class<? extends PlayPenComponent> componentType) { for (Class<? extends PlayPenComponent> dependentType : dependentComponentTypes) {
             if (dependentType.isAssignableFrom(componentType)) return true;
         }

Reply via email to