blob: aba446684f6c62e0223bb621c553687e93fd946b [file] [log] [blame]
/*
* Copyright (c) 2015 Eike Stepper (Berlin, Germany) and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Eike Stepper - initial API and implementation
*/
package org.eclipse.oomph.setup.internal.sync;
import org.eclipse.oomph.base.util.BaseUtil;
import org.eclipse.oomph.setup.CompoundTask;
import org.eclipse.oomph.setup.PreferenceTask;
import org.eclipse.oomph.setup.SetupPackage;
import org.eclipse.oomph.setup.SetupTask;
import org.eclipse.oomph.setup.SetupTaskContainer;
import org.eclipse.oomph.setup.internal.sync.DataProvider.NotCurrentException;
import org.eclipse.oomph.setup.internal.sync.Snapshot.WorkingCopy;
import org.eclipse.oomph.setup.sync.RemoteData;
import org.eclipse.oomph.setup.sync.SyncAction;
import org.eclipse.oomph.setup.sync.SyncActionType;
import org.eclipse.oomph.setup.sync.SyncDelta;
import org.eclipse.oomph.setup.sync.SyncDeltaType;
import org.eclipse.oomph.setup.sync.SyncFactory;
import org.eclipse.oomph.setup.sync.SyncPackage;
import org.eclipse.oomph.setup.sync.SyncPolicy;
import org.eclipse.oomph.util.IOUtil;
import org.eclipse.oomph.util.ObjectUtil;
import org.eclipse.oomph.util.StringUtil;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.impl.AdapterImpl;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.EMap;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.util.EcoreUtil;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* @author Eike Stepper
*/
public class Synchronization
{
public static final EClass USER_TYPE = SetupPackage.Literals.USER;
public static final EClass REMOTE_DATA_TYPE = SyncPackage.Literals.REMOTE_DATA;
private final ResourceSet resourceSet = SyncUtil.createResourceSet();
private final Set<String> ids = new HashSet<String>();
private final Map<String, String> preferenceIDs = new HashMap<String, String>();
private final Synchronizer synchronizer;
private final WorkingCopy localWorkingCopy;
private final WorkingCopy remoteWorkingCopy;
private final EMap<String, SyncPolicy> policies;
private final Map<String, SyncAction> actions;
private Map<String, SyncAction> unresolvedActions;
private boolean committed;
private boolean disposed;
private int lastID;
public Synchronization(Synchronizer synchronizer) throws IOException
{
this.synchronizer = synchronizer;
remoteWorkingCopy = createRemoteWorkingCopy();
localWorkingCopy = createLocalWorkingCopy();
policies = getPolicies(remoteWorkingCopy);
// Compute remote deltas first to make sure that new local tasks don't pick remotely existing IDs.
Map<String, SyncDelta> remoteDeltas = computeRemoteDeltas(remoteWorkingCopy);
Map<String, SyncDelta> localDeltas = computeLocalDeltas(localWorkingCopy);
actions = computeSyncActions(localDeltas, remoteDeltas);
for (Map.Entry<String, SyncAction> entry : actions.entrySet())
{
String id = entry.getKey();
SyncAction action = entry.getValue();
new ActionAdapter(action, id);
}
synchronizer.syncStarted();
}
public Synchronizer getSynchronizer()
{
return synchronizer;
}
public EMap<String, SyncPolicy> getRemotePolicies()
{
return policies;
}
private WorkingCopy createRemoteWorkingCopy() throws IOException
{
Snapshot snapshot = synchronizer.getRemoteSnapshot();
return createWorkingCopy(snapshot, REMOTE_DATA_TYPE);
}
private WorkingCopy createLocalWorkingCopy() throws IOException
{
Snapshot snapshot = synchronizer.getLocalSnapshot();
return createWorkingCopy(snapshot, USER_TYPE);
}
private WorkingCopy createWorkingCopy(Snapshot snapshot, EClass eClass) throws IOException
{
WorkingCopy workingCopy = snapshot.createWorkingCopy();
File oldFile = snapshot.getOldFile();
if (!oldFile.exists())
{
SyncUtil.inititalizeFile(oldFile, eClass, resourceSet);
}
File tmpFile = workingCopy.getTmpFile();
if (!tmpFile.exists())
{
File newFile = snapshot.getNewFile();
if (!newFile.exists())
{
SyncUtil.inititalizeFile(tmpFile, eClass, resourceSet);
}
else
{
IOUtil.copyFile(newFile, tmpFile);
}
}
return workingCopy;
}
private EMap<String, SyncPolicy> getPolicies(WorkingCopy remoteWorkingCopy)
{
File file = remoteWorkingCopy.getTmpFile();
RemoteData remoteData = loadObject(file, REMOTE_DATA_TYPE);
return remoteData.getPolicies();
}
private boolean isIncluded(String id)
{
return SyncPolicy.EXCLUDE != policies.get(id);
}
private Map<String, SyncDelta> computeRemoteDeltas(WorkingCopy remoteWorkingCopy)
{
return computeDeltas(remoteWorkingCopy, REMOTE_DATA_TYPE);
}
private Map<String, SyncDelta> computeLocalDeltas(WorkingCopy localWorkingCopy)
{
return computeDeltas(localWorkingCopy, USER_TYPE);
}
private Map<String, SyncDelta> computeDeltas(WorkingCopy workingCopy, EClass eClass)
{
Snapshot snapshot = workingCopy.getSnapshot();
File oldFile = snapshot.getOldFile();
File tmpFile = workingCopy.getTmpFile();
SetupTaskContainer oldData = loadObject(oldFile, eClass);
SetupTaskContainer newData = loadObject(tmpFile, eClass);
return compareTasks(oldData, newData);
}
private Map<String, SyncAction> computeSyncActions(Map<String, SyncDelta> localDeltas, Map<String, SyncDelta> remoteDeltas)
{
Map<String, SyncAction> actions = new HashMap<String, SyncAction>();
for (Map.Entry<String, SyncDelta> localEntry : localDeltas.entrySet())
{
String id = localEntry.getKey();
SyncDelta localDelta = localEntry.getValue();
SyncDelta remoteDelta = remoteDeltas.remove(id);
SyncAction action = compareDeltas(localDelta, remoteDelta);
if (action != null)
{
actions.put(id, action);
}
}
for (SyncDelta remoteDelta : remoteDeltas.values())
{
String id = remoteDelta.getID();
SyncAction action = compareDeltas(null, remoteDelta);
actions.put(id, action);
}
return actions;
}
private SyncAction compareDeltas(SyncDelta localDelta, SyncDelta remoteDelta)
{
SyncDeltaType localDeltaType = localDelta == null ? SyncDeltaType.UNCHANGED : localDelta.getType();
SyncDeltaType remoteDeltaType = remoteDelta == null ? SyncDeltaType.UNCHANGED : remoteDelta.getType();
SyncActionType actionType = compareDeltaTypes(localDeltaType, remoteDeltaType);
if (actionType == SyncActionType.NONE)
{
PreferenceTask localPreference = (PreferenceTask)localDelta.getNewTask();
PreferenceTask remotePreference = (PreferenceTask)remoteDelta.getNewTask();
// The comparison has returned a Changed/Changed delta conflict, so compare the values.
if (ObjectUtil.equals(localPreference.getValue(), remotePreference.getValue()))
{
// Ignore unchanged values.
actionType = null;
}
else
{
actionType = SyncActionType.CONFLICT;
}
}
if (actionType != null)
{
return SyncFactory.eINSTANCE.createSyncAction(localDelta, remoteDelta, actionType);
}
return null;
}
private SyncActionType compareDeltaTypes(SyncDeltaType localDeltaType, SyncDeltaType remoteDeltaType)
{
switch (localDeltaType)
{
case UNCHANGED:
switch (remoteDeltaType)
{
case UNCHANGED:
return null;
case CHANGED:
return SyncActionType.SET_REMOTE;
case REMOVED:
return SyncActionType.REMOVE;
}
break;
case CHANGED:
switch (remoteDeltaType)
{
case UNCHANGED:
return SyncActionType.SET_LOCAL;
case CHANGED:
// Will be changed to CONFLICT or null by the caller.
return SyncActionType.NONE;
case REMOVED:
return SyncActionType.CONFLICT;
}
break;
case REMOVED:
switch (remoteDeltaType)
{
case UNCHANGED:
return SyncActionType.REMOVE;
case CHANGED:
return SyncActionType.CONFLICT;
case REMOVED:
return null;
}
break;
}
throw new IllegalArgumentException();
}
private Map<String, SyncDelta> compareTasks(SetupTaskContainer oldTaskContainer, SetupTaskContainer newTaskContainer)
{
Map<String, SyncDelta> deltas = new HashMap<String, SyncDelta>();
Map<String, SetupTask> oldTasks = collectTasks(oldTaskContainer);
Map<String, SetupTask> newTasks = collectTasks(newTaskContainer);
for (Map.Entry<String, SetupTask> oldEntry : oldTasks.entrySet())
{
String id = oldEntry.getKey();
if (isIncluded(id))
{
SetupTask oldTask = oldEntry.getValue();
SetupTask newTask = newTasks.remove(id);
SyncDelta delta = compareTasks(id, oldTask, newTask);
if (delta != null)
{
deltas.put(id, delta);
}
}
}
for (Map.Entry<String, SetupTask> newEntry : newTasks.entrySet())
{
String id = newEntry.getKey();
if (isIncluded(id))
{
SetupTask newTask = newEntry.getValue();
SyncDelta delta = compareTasks(id, null, newTask);
deltas.put(id, delta);
}
}
return deltas;
}
private SyncDelta compareTasks(String id, SetupTask oldTask, SetupTask newTask)
{
if (oldTask == null)
{
if (newTask == null)
{
return null;
}
return SyncFactory.eINSTANCE.createSyncDelta(id, oldTask, newTask, SyncDeltaType.CHANGED);
}
if (newTask == null)
{
return SyncFactory.eINSTANCE.createSyncDelta(id, oldTask, newTask, SyncDeltaType.REMOVED);
}
PreferenceTask oldPreference = (PreferenceTask)oldTask;
PreferenceTask newPreference = (PreferenceTask)newTask;
if (!ObjectUtil.equals(oldPreference.getKey(), newPreference.getKey()))
{
// Ignore changed keys.
return null;
}
if (ObjectUtil.equals(oldPreference.getValue(), newPreference.getValue()))
{
// Ignore unchanged values.
return null;
}
return SyncFactory.eINSTANCE.createSyncDelta(id, oldPreference, newPreference, SyncDeltaType.CHANGED);
}
private Map<String, SetupTask> collectTasks(SetupTaskContainer taskContainer)
{
Map<String, SetupTask> tasks = new HashMap<String, SetupTask>();
collectTasks(taskContainer.getSetupTasks(), tasks);
return tasks;
}
private void collectTasks(EList<SetupTask> tasks, Map<String, SetupTask> result)
{
for (SetupTask task : tasks)
{
String id = rememberID(task);
if (isSychronizable(task))
{
if (StringUtil.isEmpty(id))
{
id = getPreferenceID(task);
if (StringUtil.isEmpty(id))
{
id = createID();
}
else
{
ids.add(id);
}
task.setID(id);
rememberPreferenceID(task);
}
if (result.put(id, task) != null)
{
throw new DuplicateIDException(id);
}
}
else if (task instanceof CompoundTask)
{
CompoundTask compoundTask = (CompoundTask)task;
collectTasks(compoundTask.getSetupTasks(), result);
}
}
}
private String rememberID(SetupTask task)
{
String id = task.getID();
if (!StringUtil.isEmpty(id))
{
// Make sure existing IDs are not reused.
ids.add(id);
rememberPreferenceID(task);
}
return id;
}
private void rememberPreferenceID(SetupTask task)
{
String id = task.getID();
if (!StringUtil.isEmpty(id))
{
if (task instanceof PreferenceTask)
{
PreferenceTask preferenceTask = (PreferenceTask)task;
String key = preferenceTask.getKey();
if (!StringUtil.isEmpty(key))
{
preferenceIDs.put(key, id);
}
}
}
}
private String getPreferenceID(SetupTask task)
{
if (task instanceof PreferenceTask)
{
PreferenceTask preferenceTask = (PreferenceTask)task;
String key = preferenceTask.getKey();
if (!StringUtil.isEmpty(key))
{
return preferenceIDs.get(key);
}
}
return null;
}
private <T extends EObject> T loadObject(File file, EClass eClass)
{
URI uri = URI.createFileURI(file.getAbsolutePath());
Resource resource = resourceSet.getResource(uri, true);
return BaseUtil.getObjectByType(resource.getContents(), eClass);
}
private boolean isSychronizable(SetupTask task)
{
return task instanceof PreferenceTask;
}
private String createID()
{
for (int i = lastID + 1; i < Integer.MAX_VALUE; i++)
{
String id = "sync" + i;
if (ids.add(id))
{
lastID = i;
return id;
}
}
throw new IllegalStateException("Too many IDs");
}
public String getID(SyncAction action)
{
String id = action.getID();
if (id != null)
{
return id;
}
ActionAdapter adapter = (ActionAdapter)EcoreUtil.getAdapter(action.eAdapters(), ActionAdapter.class);
if (adapter != null)
{
return adapter.getID();
}
return null;
}
public Map<String, SyncAction> getActions()
{
return actions;
}
public Map<String, SyncAction> getUnresolvedActions()
{
if (unresolvedActions == null)
{
unresolvedActions = new HashMap<String, SyncAction>();
for (Map.Entry<String, SyncAction> entry : actions.entrySet())
{
SyncAction action = entry.getValue();
if (action.getEffectiveType() == SyncActionType.CONFLICT)
{
String id = entry.getKey();
unresolvedActions.put(id, action);
}
}
}
return unresolvedActions;
}
public Synchronization resolve(String id, SyncActionType resolvedType)
{
SyncAction action = actions.get(id);
if (action != null)
{
action.setResolvedType(resolvedType);
}
return this;
}
public void commit() throws IOException, NotCurrentException
{
if (!committed && !disposed)
{
committed = true;
doCommit();
}
}
private void doCommit() throws IOException, NotCurrentException
{
synchronizer.commitStarted();
try
{
applyActions(remoteWorkingCopy, REMOTE_DATA_TYPE);
remoteWorkingCopy.commit();
applyActions(localWorkingCopy, USER_TYPE);
localWorkingCopy.commit();
synchronizer.commitFinished(null);
}
catch (IOException ex)
{
synchronizer.commitFinished(ex);
throw ex;
}
catch (RuntimeException ex)
{
synchronizer.commitFinished(ex);
throw ex;
}
catch (Error ex)
{
synchronizer.commitFinished(ex);
throw ex;
}
finally
{
doDispose();
}
}
private void applyActions(WorkingCopy workingCopy, EClass eClass)
{
File file = workingCopy.getTmpFile();
SetupTaskContainer taskContainer = loadObject(file, eClass);
Map<String, SetupTask> tasks = collectTasks(taskContainer);
for (Map.Entry<String, SyncAction> entry : actions.entrySet())
{
String id = entry.getKey();
SyncAction action = entry.getValue();
SyncActionType type = action.getEffectiveType();
switch (type)
{
case CONFLICT:
throw new ConflictException(action);
case SET_LOCAL:
include(id);
applySetAction(taskContainer, tasks, id, action.getLocalDelta());
break;
case SET_REMOTE:
include(id);
applySetAction(taskContainer, tasks, id, action.getRemoteDelta());
break;
case REMOVE:
include(id);
applyRemoveAction(taskContainer, tasks, action.getLocalDelta());
applyRemoveAction(taskContainer, tasks, action.getRemoteDelta());
break;
case EXCLUDE:
exclude(id);
applyRemoveAction(taskContainer, tasks, action.getRemoteDelta());
break;
default:
// Do nothing.
break;
}
}
BaseUtil.saveEObject(taskContainer);
}
private void applySetAction(SetupTaskContainer taskContainer, Map<String, SetupTask> tasks, String id, SyncDelta delta)
{
if (delta != null)
{
SetupTask newTask = delta.getNewTask();
if (newTask != null)
{
newTask = EcoreUtil.copy(newTask);
newTask.setID(id);
newTask.getRestrictions().clear();
newTask.getPredecessors().clear();
newTask.getSuccessors().clear();
SetupTask oldTask = tasks.get(id);
if (oldTask != null)
{
EcoreUtil.replace(oldTask, newTask);
}
else
{
taskContainer.getSetupTasks().add(newTask);
}
}
}
}
private void applyRemoveAction(SetupTaskContainer taskContainer, Map<String, SetupTask> tasks, SyncDelta delta)
{
if (delta != null)
{
String id = delta.getID();
SetupTask oldTask = tasks.get(id);
if (oldTask != null)
{
EcoreUtil.remove(oldTask);
}
}
}
private void include(String id)
{
policies.put(id, SyncPolicy.INCLUDE);
}
private void exclude(String id)
{
policies.put(id, SyncPolicy.EXCLUDE);
}
public void dispose()
{
if (!disposed)
{
doDispose();
}
}
private void doDispose()
{
disposed = true;
try
{
localWorkingCopy.dispose();
}
catch (Throwable ex)
{
SetupSyncPlugin.INSTANCE.log(ex);
}
try
{
remoteWorkingCopy.dispose();
}
catch (Throwable ex)
{
SetupSyncPlugin.INSTANCE.log(ex);
}
try
{
synchronizer.releaseLock();
}
catch (Throwable ex)
{
SetupSyncPlugin.INSTANCE.log(ex);
}
}
/**
* @author Eike Stepper
*/
private final class ActionAdapter extends AdapterImpl
{
private final String id;
public ActionAdapter(SyncAction action, String id)
{
this.id = id;
action.eAdapters().add(this);
}
public String getID()
{
return id;
}
@Override
public void notifyChanged(Notification msg)
{
if (msg.getFeature() == SyncPackage.Literals.SYNC_ACTION__RESOLVED_TYPE && !msg.isTouch())
{
unresolvedActions = null;
SyncAction action = (SyncAction)getTarget();
synchronizer.actionResolved(action, id);
}
}
}
/**
* @author Eike Stepper
*/
public static class DuplicateIDException extends SnychronizerException
{
private static final long serialVersionUID = 1L;
public DuplicateIDException(String id)
{
super("Duplicate ID: " + id);
}
}
/**
* @author Eike Stepper
*/
public static class ConflictException extends SnychronizerException
{
private static final long serialVersionUID = 1L;
public ConflictException(SyncAction action)
{
super("Conflict: " + action);
}
}
}