| /* |
| * 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); |
| } |
| } |
| } |