/******************************************************************************* * Copyright (c) 2008-2014 Chair for Applied Software Engineering, * Technische Universitaet Muenchen. * 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: * Otto von Wesendonk, Edgar Mueller - initial API and implementation ******************************************************************************/ package org.eclipse.emf.emfstore.internal.client.model.controller; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.emf.emfstore.client.callbacks.ESUpdateCallback; import org.eclipse.emf.emfstore.client.exceptions.ESProjectNotSharedException; import org.eclipse.emf.emfstore.client.observer.ESUpdateObserver; import org.eclipse.emf.emfstore.internal.client.common.UnknownEMFStoreWorkloadCommand; import org.eclipse.emf.emfstore.internal.client.model.Configuration; import org.eclipse.emf.emfstore.internal.client.model.ESWorkspaceProviderImpl; import org.eclipse.emf.emfstore.internal.client.model.connectionmanager.ServerCall; import org.eclipse.emf.emfstore.internal.client.model.exceptions.ChangeConflictException; import org.eclipse.emf.emfstore.internal.client.model.impl.ProjectSpaceBase; import org.eclipse.emf.emfstore.internal.client.model.util.EMFStoreCommand; import org.eclipse.emf.emfstore.internal.common.model.util.ModelUtil; import org.eclipse.emf.emfstore.internal.server.conflictDetection.ChangeConflictSet; import org.eclipse.emf.emfstore.internal.server.conflictDetection.ConflictDetector; import org.eclipse.emf.emfstore.internal.server.conflictDetection.ModelElementIdToEObjectMappingImpl; import org.eclipse.emf.emfstore.internal.server.impl.api.ESConflictSetImpl; import org.eclipse.emf.emfstore.internal.server.model.versioning.AbstractChangePackage; import org.eclipse.emf.emfstore.internal.server.model.versioning.PrimaryVersionSpec; import org.eclipse.emf.emfstore.internal.server.model.versioning.VersionSpec; import org.eclipse.emf.emfstore.internal.server.model.versioning.Versions; import org.eclipse.emf.emfstore.internal.server.model.versioning.operations.AbstractOperation; import org.eclipse.emf.emfstore.internal.server.model.versioning.operations.util.ChangePackageUtil; import org.eclipse.emf.emfstore.server.ESCloseableIterable; import org.eclipse.emf.emfstore.server.exceptions.ESException; import org.eclipse.emf.emfstore.server.model.ESChangePackage; import com.google.common.collect.Lists; /** * Controller class for updating a project space. * * @author ovonwesen * @author emueller */ public class UpdateController extends ServerCall<PrimaryVersionSpec> { private final VersionSpec version; private final ESUpdateCallback callback; /** * Constructor. * * @param projectSpace * the project space to be updated * @param version * the target version * @param callback * an optional update callback instance * @param progress * a progress monitor that is used to indicate the progress of the update */ public UpdateController(ProjectSpaceBase projectSpace, VersionSpec version, ESUpdateCallback callback, IProgressMonitor progress) { super(projectSpace); if (!projectSpace.isShared()) { throw new ESProjectNotSharedException(); } // SANITY CHECKS if (version == null) { version = Versions.createHEAD(projectSpace.getBaseVersion()); } if (callback == null) { callback = ESUpdateCallback.NOCALLBACK; } this.version = version; this.callback = callback; setProgressMonitor(progress); } /** * * {@inheritDoc} * * @see org.eclipse.emf.emfstore.internal.client.model.connectionmanager.ServerCall#run() */ @Override protected PrimaryVersionSpec run() throws ESException { return doUpdate(version); } private PrimaryVersionSpec doUpdate(VersionSpec version) throws ChangeConflictException, ESException { getProgressMonitor().beginTask(Messages.UpdateController_UpdatingProject, 100); getProgressMonitor().worked(1); getProgressMonitor().subTask(Messages.UpdateController_ResolvingNewVersion); final PrimaryVersionSpec resolvedVersion = getProjectSpace().resolveVersionSpec(version, getProgressMonitor()); if (equalsBaseVersion(resolvedVersion)) { return resolvedVersion; } getProgressMonitor().worked(5); if (getProgressMonitor().isCanceled()) { return getProjectSpace().getBaseVersion(); } getProgressMonitor().subTask(Messages.UpdateController_FetchingChanges); final List<AbstractChangePackage> incomingChanges = getIncomingChanges(resolvedVersion); checkAndRemoveDuplicateOperations(incomingChanges); AbstractChangePackage copiedLocalChangedPackage = ChangePackageUtil.createChangePackage( Configuration.getClientBehavior().useInMemoryChangePackage() ); final ESCloseableIterable<AbstractOperation> operations = getProjectSpace().getLocalChangePackage() .operations(); try { for (final AbstractOperation operation : operations.iterable()) { copiedLocalChangedPackage.add(ModelUtil.clone(operation)); } } finally { operations.close(); } // build a mapping including deleted and create model elements in local and incoming change packages final ModelElementIdToEObjectMappingImpl idToEObjectMapping = new ModelElementIdToEObjectMappingImpl( getProjectSpace().getProject(), incomingChanges); idToEObjectMapping.put(copiedLocalChangedPackage); getProgressMonitor().worked(65); if (getProgressMonitor().isCanceled()) { return getProjectSpace().getBaseVersion(); } getProgressMonitor().subTask(Messages.UpdateController_CheckingForConflicts); final List<ESChangePackage> incomingAPIChangePackages = new ArrayList<ESChangePackage>(); for (final AbstractChangePackage incomingChange : incomingChanges) { incomingAPIChangePackages.add(incomingChange.toAPI()); } // TODO ASYNC review this cancel if (getProgressMonitor().isCanceled() || !callback.inspectChanges( getProjectSpace().toAPI(), incomingAPIChangePackages, idToEObjectMapping.toAPI())) { return getProjectSpace().getBaseVersion(); } ESWorkspaceProviderImpl .getObserverBus() .notify(ESUpdateObserver.class, true) .inspectChanges( getProjectSpace().toAPI(), incomingAPIChangePackages, getProgressMonitor()); if (!getProjectSpace().getLocalChangePackage().isEmpty()) { final ChangeConflictSet changeConflictSet = calcConflicts(copiedLocalChangedPackage, incomingChanges, idToEObjectMapping); if (changeConflictSet.getConflictBuckets().size() > 0) { getProgressMonitor().subTask(Messages.UpdateController_ConflictsDetected); if (callback.conflictOccurred(new ESConflictSetImpl(changeConflictSet), getProgressMonitor())) { final List<AbstractChangePackage> myChanges = Lists.newArrayList(); myChanges.add(copiedLocalChangedPackage); copiedLocalChangedPackage = getProjectSpace().mergeResolvedConflicts( changeConflictSet, myChanges, incomingChanges); // continue with update by applying changes } else { throw new ChangeConflictException(changeConflictSet); } } } getProgressMonitor().worked(15); getProgressMonitor().subTask(Messages.UpdateController_ApplyingChanges); getProjectSpace().applyChanges( resolvedVersion, incomingChanges, copiedLocalChangedPackage, getProgressMonitor(), true); final Date now = new Date(); getProjectSpace().setLastUpdated(now); ESWorkspaceProviderImpl.getObserverBus() .notify(ESUpdateObserver.class, true) .updateCompleted(getProjectSpace().toAPI(), getProgressMonitor()); return getProjectSpace().getBaseVersion(); } private boolean equalsBaseVersion(final PrimaryVersionSpec resolvedVersion) { return resolvedVersion.compareTo(getProjectSpace().getBaseVersion()) == 0; } private List<AbstractChangePackage> getIncomingChanges(final PrimaryVersionSpec resolvedVersion) throws ESException { final List<AbstractChangePackage> changePackages = new UnknownEMFStoreWorkloadCommand<List<AbstractChangePackage>>( getProgressMonitor()) { @Override public List<AbstractChangePackage> run(IProgressMonitor monitor) throws ESException { return getConnectionManager().getChanges(getSessionId(), getProjectSpace().getProjectId(), getProjectSpace().getBaseVersion(), resolvedVersion); } }.execute(); return changePackages; } private ChangeConflictSet calcConflicts(AbstractChangePackage localChanges, List<AbstractChangePackage> changes, ModelElementIdToEObjectMappingImpl idToEObjectMapping) { final ConflictDetector conflictDetector = new ConflictDetector(); return conflictDetector.calculateConflicts( Collections.singletonList(localChanges), changes, idToEObjectMapping); } private void checkAndRemoveDuplicateOperations(List<AbstractChangePackage> incomingChanges) { final int baseVersionDelta = removeFromChangePackages(incomingChanges); if (baseVersionDelta == 0) { return; } // some change have been matched, fix base version and save final PrimaryVersionSpec baseVersion = getProjectSpace().getBaseVersion(); baseVersion.setIdentifier(baseVersion.getIdentifier() + baseVersionDelta); ModelUtil.logError(MessageFormat .format( Messages.UpdateController_ChangePackagesRemoved + Messages.UpdateController_PullingUpBaseVersion, baseVersionDelta, baseVersion.getIdentifier(), baseVersion.getIdentifier() + baseVersionDelta)); getProjectSpace().save(); } /** * Remove duplicate change packages from the change package. * * @param incomingChanges incoming change packages * @return baseVersionDelta */ public int removeFromChangePackages(List<AbstractChangePackage> incomingChanges) { final Iterator<AbstractChangePackage> incomingChangesIterator = incomingChanges.iterator(); int baseVersionDelta = 0; while (incomingChangesIterator.hasNext()) { final AbstractChangePackage incomingChangePackage = incomingChangesIterator.next(); final boolean hasBeenConsumed = removeDuplicateOperations( incomingChangePackage, getProjectSpace().getLocalChangePackage()); if (hasBeenConsumed) { baseVersionDelta += 1; incomingChangesIterator.remove(); } else { break; } } return baseVersionDelta; } /** * Remove duplicate operations. * * @param incomingChanges incoming change package * @param localChanges local change package * @return <code>true</code> when all change packages have been consumed */ public boolean removeDuplicateOperations(AbstractChangePackage incomingChanges, AbstractChangePackage localChanges) { // TODO: cleanup this mess, ensure compatibility with in-memory change package if (localChanges.size() == 0) { return false; } final AbstractChangePackage tempChangePackage = ChangePackageUtil.createChangePackage( Configuration.getClientBehavior().useInMemoryChangePackage() ); final ESCloseableIterable<AbstractOperation> localOperations = localChanges.operations(); final ESCloseableIterable<AbstractOperation> incomingOps = incomingChanges.operations(); final int incomingOpsSize = incomingChanges.size(); int incomingIdx = 0; boolean operationMatchingStarted = false; try { final Iterator<AbstractOperation> localOperationsIterator = localOperations.iterable().iterator(); final Iterator<AbstractOperation> incomingOpsIterator = incomingOps.iterable().iterator(); while (localOperationsIterator.hasNext()) { final AbstractOperation localOp = localOperationsIterator.next(); if (incomingIdx == incomingOpsSize) { new EMFStoreCommand() { @Override protected void doRun() { tempChangePackage.add(ModelUtil.clone(localOp)); } }.run(false); while (localOperationsIterator.hasNext()) { // add all remaining local ops final AbstractOperation next = localOperationsIterator.next(); tempChangePackage.add(ModelUtil.clone(next)); } // incoming change package is fully consumed, continue with next change package return true; } final AbstractOperation incomingOp = incomingOpsIterator.next(); incomingIdx += 1; if (incomingOp.getIdentifier().equals(localOp.getIdentifier())) { operationMatchingStarted = true; } else { tempChangePackage.add(ModelUtil.clone(localOp)); while (localOperationsIterator.hasNext()) { tempChangePackage.add(ModelUtil.clone(localOperationsIterator.next())); } if (operationMatchingStarted) { ModelUtil.logError(Messages.UpdateController_IncomingOperationsOnlyPartlyMatch); throw new IllegalStateException(Messages.UpdateController_IncomingOpsPartlyMatched); } // first operation of incoming change package does not match return false; } } // all incoming and local changes have been consumed if (incomingIdx == incomingOpsSize) { return true; } ModelUtil.logError(Messages.UpdateController_IncomingOperationsNotFullyConsumed); throw new IllegalStateException(Messages.UpdateController_IncomingOpsNotConsumed); } finally { localOperations.close(); incomingOps.close(); if (incomingIdx == incomingOpsSize) { tempChangePackage.attachToProjectSpace(getProjectSpace()); } } } }