blob: 9273a167711c7f22b57821194be9d509eccbd421 [file] [log] [blame]
/*******************************************************************************
* 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());
}
}
}
}