/*
* Copyright (c) 2010, 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.enterprise;
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;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.StringEntity;
import org.apache.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.springframework.security.AccessDeniedException;
import ca.sqlpower.dao.PersistedSPOProperty;
import ca.sqlpower.dao.PersistedSPObject;
import ca.sqlpower.dao.RemovedObjectEntry;
import ca.sqlpower.dao.SPPersistenceException;
import ca.sqlpower.dao.SPPersister.DataType;
import ca.sqlpower.dao.SPPersisterListener;
import ca.sqlpower.dao.json.SPJSONMessageDecoder;
import ca.sqlpower.dao.session.SessionPersisterSuperConverter;
import ca.sqlpower.enterprise.client.ProjectLocation;
import ca.sqlpower.object.SPObject;
import ca.sqlpower.sqlobject.SQLObject;
import ca.sqlpower.util.RunnableDispatcher;
import ca.sqlpower.util.SQLPowerUtils;
import ca.sqlpower.util.UserPrompter.UserPromptOptions;
import ca.sqlpower.util.UserPrompter.UserPromptResponse;
import ca.sqlpower.util.UserPrompterFactory;
import ca.sqlpower.util.UserPrompterFactory.UserPromptType;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Multimap;
public abstract class AbstractNetworkConflictResolver extends Thread {
/**
* A class that will take a ConflictCase and parameters insert
* them into the ConflictCase's message.
*/
protected 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");
}
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.
*/
protected 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();
}
}
public static interface UpdateListener {
/**
* Fired when an update from the server has been performed on the client
* @param resolver The NetworkConflictResolver that received the update
* @return true if the listener should be removed from
* listener list and should not receive any more calls
*/
public boolean updatePerformed(AbstractNetworkConflictResolver resolver);
/**
* Called when an exception is thrown from the server on an update. One
* special exception passed in this call is the
* {@link AccessDeniedException}. If the exception is an
* {@link AccessDeniedException} then the user does not have valid
* permissions to do the operation they were attempting and the
* exception is not a fatal one.
*/
public boolean updateException(AbstractNetworkConflictResolver resolver, Throwable t);
/**
* Notifies listeners that the workspace was deleted.
* Swing sessions should listen for this to disable the enterprise session.
* The listener is removed after this method is called.
*/
public void workspaceDeleted();
/**
* Called just before an update will be performed by the
* {@link ArchitectNetworkConflictResolver}. This gives objects the chance to be
* aware of incoming changes from the server if necessary.
*
* @param resolver
* The {@link ArchitectNetworkConflictResolver} that received the
* update.
*/
public void preUpdatePerformed(AbstractNetworkConflictResolver resolver);
}
/**
* If conflicts are found when trying to decide if an incoming change
* conflicts with the last user change only this many conflicting properties
* or reasons will be displayed at max to prevent the dialog from growing
* too large.
*/
protected static final int MAX_CONFLICTS_TO_DISPLAY = 10;
/**
* This is currently a static average wait time for how long each change to
* the server will take. This will let the progress bar update at a decent
* rate.
*/
protected static final int AVG_WAIT_TIME_FOR_PERSIST = 12;
private static final Logger logger = Logger.getLogger(AbstractNetworkConflictResolver.class);
protected AtomicBoolean postingJSON = new AtomicBoolean(false);
protected boolean updating = false;
protected SPPersisterListener listener;
protected SessionPersisterSuperConverter converter;
protected UserPrompterFactory upf;
protected int currentRevision = 0;
protected long serverTimestamp = 0;
protected long retryDelay = 1000;
/**
* This double will store and be updated with the average wait time for each
* persist calls so the progress of the progress bar is on average correct.
* At some point we may want the server to respond with the actual progress
* but this is a start.
*/
protected double currentWaitPerPersist = AVG_WAIT_TIME_FOR_PERSIST;
protected final SPJSONMessageDecoder jsonDecoder;
protected final ProjectLocation projectLocation;
protected final HttpClient outboundHttpClient;
protected final HttpClient inboundHttpClient;
protected String contextRelativePath;
protected volatile boolean cancelled;
protected JSONArray messageBuffer = new JSONArray();
protected HashMap<String, PersistedSPObject> inboundObjectsToAdd = new HashMap<String, PersistedSPObject>();
protected Multimap<String, PersistedSPOProperty> inboundPropertiesToChange = LinkedListMultimap.create();
protected HashMap<String, RemovedObjectEntry> inboundObjectsToRemove = new HashMap<String, RemovedObjectEntry>();
protected Map<String, PersistedSPObject> outboundObjectsToAdd = new LinkedHashMap<String, PersistedSPObject>();
protected Multimap<String, PersistedSPOProperty> outboundPropertiesToChange = LinkedListMultimap.create();
protected Map<String, RemovedObjectEntry> outboundObjectsToRemove = new LinkedHashMap<String, RemovedObjectEntry>();
protected List<UpdateListener> updateListeners = new ArrayList<UpdateListener>();
private final RunnableDispatcher runnable;
public AbstractNetworkConflictResolver(
ProjectLocation projectLocation,
SPJSONMessageDecoder jsonDecoder,
HttpClient inboundHttpClient,
HttpClient outboundHttpClient,
RunnableDispatcher runnable)
{
super("updater-" + projectLocation.getUUID());
this.jsonDecoder = jsonDecoder;
this.projectLocation = projectLocation;
this.inboundHttpClient = inboundHttpClient;
this.outboundHttpClient = outboundHttpClient;
this.runnable = runnable;
contextRelativePath = "/" + ClientSideSessionUtils.REST_TAG + "/project/" + projectLocation.getUUID();
}
public int getRevision() {
return currentRevision;
}
public long getServerTimestamp() {
return serverTimestamp;
}
public void addListener(UpdateListener listener) {
updateListeners.add(listener);
}
protected String getPersistedObjectName(PersistedSPObject o) {
for (PersistedSPOProperty p : outboundPropertiesToChange.get(o.getUUID())) {
if (p.getPropertyName().equals("name")) {
return (String) p.getNewValue();
}
}
throw new IllegalArgumentException("Persisted Object with UUID " + o.getUUID() + " and type " + o.getType() + " has no name property");
}
public void clear() {
clear(false);
}
protected void clear(boolean reflush) {
messageBuffer = new JSONArray();
if (reflush) {
inboundObjectsToAdd.clear();
inboundPropertiesToChange.clear(); // XXX does this cause lists to retain old objects?
inboundObjectsToRemove.clear();
outboundObjectsToAdd.clear();
outboundPropertiesToChange.clear();
outboundObjectsToRemove.clear();
}
}
public void send(JSONObject content) throws SPPersistenceException {
messageBuffer.put(content);
}
public void setUserPrompterFactory(UserPrompterFactory promptSession) {
this.upf = promptSession;
}
public UserPrompterFactory getUserPrompterFactory() {
return upf;
}
public void setListener(SPPersisterListener listener) {
this.listener = listener;
}
public void setConverter(SessionPersisterSuperConverter converter) {
this.converter = converter;
}
public List<UpdateListener> getListeners() {
return updateListeners;
}
public void flush() {
flush(false);
}
/**
* Exists for code reuse.
*
* @param tokener
* {@link JSONTokener} that tokenizes multiple persister calls.
* @param newRevision
* The new revision number.
* @throws SPPersistenceException
*/
protected void decodeMessage(JSONTokener tokener, int newRevision, long timestamp) {
try {
if (currentRevision < newRevision) {
List<UpdateListener> updateListenersCopy = new ArrayList<UpdateListener>(updateListeners);
for (UpdateListener listener : updateListeners) {
listener.preUpdatePerformed(AbstractNetworkConflictResolver.this);
}
// Now we can apply the update ...
jsonDecoder.decode(tokener);
currentRevision = newRevision;
serverTimestamp = timestamp;
if (logger.isDebugEnabled())
logger.debug("Setting currentRevision to: " + currentRevision +
" and serverTimestamp to: " + serverTimestamp);
for (UpdateListener listener : updateListenersCopy) {
if (listener.updatePerformed(this)) {
updateListeners.remove(listener);
}
}
}
} catch (Exception e) {
throw new RuntimeException("Failed to decode the message from the server.", e);
}
}
protected void fillOutboundPersistedLists() {
for (PersistedSPObject obj : listener.getPersistedObjects()) {
outboundObjectsToAdd.put(obj.getUUID(), obj);
}
for (PersistedSPOProperty prop : listener.getPersistedProperties()) {
outboundPropertiesToChange.put(prop.getUUID(), prop);
}
for (RemovedObjectEntry rem : listener.getObjectsToRemove().values()) {
outboundObjectsToRemove.put(rem.getRemovedChild().getUUID(), rem);
}
}
public void interrupt() {
super.interrupt();
cancelled = true;
}
@Override
public void run() {
try {
while (!this.isInterrupted() && !cancelled) {
try {
while (updating) { // this should wait for persisting to server as well.
synchronized (this) {
wait();
}
}
updating = true;
// Request an update from the server using the current revision number.
JSONMessage message = getJsonArray(inboundHttpClient);
// Status 410 (Gone) means the workspace was deleted
if (message.getStatusCode() == 410) {
for (UpdateListener listener : updateListeners) {
listener.workspaceDeleted();
}
updateListeners.clear();
interrupt();
} else if (message.getStatusCode() == 412) { //Precondition failed
upf.createUserPrompter(message.getBody(), UserPromptType.MESSAGE, UserPromptOptions.OK,
UserPromptResponse.OK, null, "OK").promptUser();
continue;
} else if (message.getStatusCode() == 403) { // FORBIDDEN, timestamp is older than server
updateListeners.clear();
interrupt();
if (projectLocation.getUUID().equals("system")) {
upf.createUserPrompter("Server at " + projectLocation.getServiceInfo().getServerAddress() + "has failed since your session began." +
" Please restart the program to synchronize the system workspace with the server." ,
UserPromptType.MESSAGE,
UserPromptOptions.OK,
UserPromptResponse.OK,
null, "OK").promptUser();
} else {
upf.createUserPrompter("Server at " + projectLocation.getServiceInfo().getServerAddress() +
" has failed since your session began." +
" Please use the refresh button to synchronize workspace " + projectLocation.getName() +
" with the server.",
UserPromptType.MESSAGE,
UserPromptOptions.OK,
UserPromptResponse.OK,
null, "OK").promptUser();
}
}
// The updater may have been interrupted/closed/deleted while waiting for an update.
if (this.isInterrupted() || cancelled) break;
JSONObject json = new JSONObject(message.getBody());
final JSONTokener tokener = new JSONTokener(json.getString("data"));
final int jsonRevision = json.getInt("currentRevision");
final long jsonTimestamp = json.getLong("serverTimestamp");
runnable.runInForeground(new Runnable() {
public void run() {
try {
if (!postingJSON.get()) {
decodeMessage(tokener, jsonRevision, jsonTimestamp);
}
} catch (AccessDeniedException e) {
interrupt();
List<UpdateListener> listenersToRemove = new ArrayList<UpdateListener>();
for (UpdateListener listener : updateListeners) {
if (listener.updateException(AbstractNetworkConflictResolver.this, e)) {
listenersToRemove.add(listener);
}
}
updateListeners.removeAll(listenersToRemove);
if (upf != null) {
upf.createUserPrompter(
"You do not have sufficient privileges to perform that action. " +
"Please hit the refresh button to synchronize with the server.",
UserPromptType.MESSAGE,
UserPromptOptions.OK,
UserPromptResponse.OK,
"OK", "OK").promptUser("");
} else {
throw e;
}
} catch (Exception e) {
// TODO: Discard corrupt workspace and start again from scratch.
interrupt();
List<UpdateListener> listenersToRemove = new ArrayList<UpdateListener>();
for (UpdateListener listener : updateListeners) {
if (listener.updateException(AbstractNetworkConflictResolver.this, e)) {
listenersToRemove.add(listener);
}
}
updateListeners.removeAll(listenersToRemove);
throw new RuntimeException("Update from server failed! Unable to decode the message: ", e);
} finally {
synchronized (AbstractNetworkConflictResolver.this) {
updating = false;
AbstractNetworkConflictResolver.this.notify();
}
}
}
});
} catch (Exception ex) {
Throwable root = ex;
while (root != null) {
if (root instanceof SPPersistenceException) {
getUserPrompterFactory().createUserPrompter(
"An exception occurred while updating from the server. See logs for more details.",
UserPromptType.MESSAGE, UserPromptOptions.OK, UserPromptResponse.OK, true, "OK").promptUser();
break;
}
root = root.getCause();
}
logger.error("Failed to contact server. Will retry in " + retryDelay + " ms.", ex);
Thread.sleep(retryDelay);
}
}
} catch (InterruptedException ex) {
logger.info("Updater thread exiting normally due to interruption.");
}
inboundHttpClient.getConnectionManager().shutdown();
}
/**
* Creates and executes an HttpGet request for an update from the server.
* @return A JSONMessage holding the successfulness and message body of the server's response
*/
protected JSONMessage getJsonArray(HttpClient client) {
try {
URI uri = new URI("http", null,
projectLocation.getServiceInfo().getServerAddress(),
projectLocation.getServiceInfo().getPort(),
projectLocation.getServiceInfo().getPath() + contextRelativePath,
"oldRevisionNo=" + currentRevision + "&serverTimestamp=" + serverTimestamp, null);
logger.debug("GETting URI: " + uri.toString());
HttpUriRequest request = new HttpGet(uri);
return client.execute(request, new JSONResponseHandler());
} catch (AccessDeniedException ade) {
throw new AccessDeniedException("Access Denied");
} catch (Exception ex) {
throw new RuntimeException("Unable to get json from server", ex);
}
}
/**
* Creates and executes an HttpPost request containing the json of whatever
* transaction was completed last.
* @param jsonArray Typically created by calling toString() on a JSONArray
* @return A JSONMessage holding the successfulness and message body of the server's response
*/
protected JSONMessage postJsonArray(String jsonArray) {
try {
URI serverURI = new URI("http", null,
projectLocation.getServiceInfo().getServerAddress(),
projectLocation.getServiceInfo().getPort(),
projectLocation.getServiceInfo().getPath() +
"/" + ClientSideSessionUtils.REST_TAG + "/project/" + projectLocation.getUUID(),
"currentRevision=" + currentRevision + "&serverTimestamp=" + serverTimestamp, null);
logger.debug("POSTing URI: " + serverURI.toString());
HttpPost postRequest = new HttpPost(serverURI);
postRequest.setEntity(new StringEntity(jsonArray));
postRequest.setHeader("Content-Type", "application/json");
HttpUriRequest request = postRequest;
return outboundHttpClient.execute(request, new JSONResponseHandler());
} catch (AccessDeniedException ade) {
throw ade;
} catch (Exception ex) {
throw new RuntimeException("Unable to post json to server", ex);
}
}
protected void fillInboundPersistedLists(String json) {
try {
JSONArray array = new JSONArray(json);
for (int i = 0; i < array.length(); i++) {
JSONObject obj = array.getJSONObject(i);
if (obj.getString("method").equals("persistObject")) {
String parentUUID = obj.getString("parentUUID");
String type = obj.getString("type");
String uuid = obj.getString("uuid");
int index = obj.getInt("index");
inboundObjectsToAdd.put(uuid, new PersistedSPObject(parentUUID, type, uuid, index));
} else if (obj.getString("method").equals("persistProperty")) {
String uuid = obj.getString("uuid");
String propertyName = obj.getString("propertyName");
DataType type = DataType.valueOf(obj.getString("type"));
Object oldValue = null;
try {
oldValue = SPJSONMessageDecoder.getWithType(obj, type, "oldValue");
} catch (Exception e) {}
Object newValue = SPJSONMessageDecoder.getWithType(obj, type, "newValue");
boolean unconditional = false;
PersistedSPOProperty property = new PersistedSPOProperty(uuid, propertyName, type, oldValue, newValue, unconditional);
if (inboundPropertiesToChange.keySet().contains(uuid)) {
inboundPropertiesToChange.asMap().get(uuid).add(property);
} else {
inboundPropertiesToChange.put(uuid, property);
}
} else if (obj.getString("method").equals("removeObject")) {
String parentUUID = obj.getString("parentUUID");
String uuid = obj.getString("uuid");
SPObject objectToRemove = SQLPowerUtils.findByUuid(getWorkspace(), uuid, SPObject.class);
inboundObjectsToRemove.put(uuid, new RemovedObjectEntry(parentUUID, objectToRemove,
objectToRemove.getParent().getChildren().indexOf(objectToRemove)));
}
}
} catch (Exception ex) {
throw new RuntimeException("Unable to create persisted lists: ", ex);
}
}
/**
* 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.
*/
protected 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(), SQLPowerUtils.findByUuid(getWorkspace(), o.getParentUUID(), SPObject.class).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()) &&
SQLPowerUtils.findByUuid(getWorkspace(),highestAddition.getParentUUID(), SPObject.class) == 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 = SQLPowerUtils.findByUuid(getWorkspace(), uuid, SPObject.class);
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, SQLPowerUtils.findByUuid(getWorkspace(), uuid, SPObject.class).getName()));
} else if (duplicateMoves.contains(uuid)) {
removedObjects.remove();
}
}
// ----- Iterate through outbound properties -----
for (String uuid : outboundPropertiesToChange.keys()) {
SPObject changedObject = SQLPowerUtils.findByUuid(getWorkspace(),uuid, SPObject.class);
// 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, SQLPowerUtils.findByUuid(getWorkspace(),uuid, SPObject.class).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:
List<SPObject> children = new ArrayList<SPObject>();
if (changedObject instanceof SQLObject) {
children.addAll(((SQLObject) changedObject).getChildrenWithoutPopulating());
} else {
children.addAll(changedObject.getChildren());
}
for (SPObject child : children) {
// 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;
}
protected abstract void flush(boolean reflush);
protected abstract List<ConflictMessage> detectConflicts();
protected abstract SPObject getWorkspace();
/**
* Returns the persister listener used to send changes to the server. This
* is mainly used for testing.
*/
public SPPersisterListener getPersisterListener() {
return listener;
}
}