blob: d99338d7e06396bb3752409661b835d4499ab3f2 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015 Obeo.
* 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:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.emf.compare.ide.ui.internal.logical.resolver;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.concat;
import static com.google.common.collect.Iterables.transform;
import static org.eclipse.emf.compare.ide.ui.internal.util.PlatformElementUtil.adaptAs;
import static org.eclipse.emf.compare.ide.utils.ResourceUtil.asURI;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.Callable;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IStorage;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.compare.ide.ui.internal.util.ThreadSafeProgressMonitor;
import org.eclipse.emf.compare.ide.ui.logical.IStorageProviderAccessor;
import org.eclipse.emf.compare.ide.ui.logical.IStorageProviderAccessor.DiffSide;
import org.eclipse.emf.compare.ide.ui.logical.SynchronizationModel;
import org.eclipse.emf.compare.ide.utils.ResourceUtil;
import org.eclipse.emf.compare.ide.utils.StorageTraversal;
import org.eclipse.emf.compare.ide.utils.StorageURIConverter;
/**
* Computation that resolves 2 or 3 storages (left, right and potentially origin).
*
* @author <a href="mailto:laurent.delaigue@obeo.fr">Laurent Delaigue</a>
*/
public class ModelsResolution extends AbstractResolution {
/** Storage Accessor. */
private final IStorageProviderAccessor storageAccessor;
/** The left storage. */
private final IStorage left;
/** The right storage. */
private final IStorage right;
/** The ancestor storage, can be <code>null</code>. */
private final IStorage origin;
/** The local resolver. */
private final IResourceDependencyLocalResolver localResolver;
/** The remote resolver. */
private final IResourceDependencyRemoteResolver remoteResolver;
/**
* At least one of {@link #left}, {@link #right} and {@link #origin} must be non-null.
*
* @param dependencyProvider
* The dependency provider
* @param scheduler
* The muti-thread support to use
* @param eventBus
* The event bus
* @param monitor
* The progress monitor to use to report progress
* @param storageAccessor
* The storage accessor, must not be {@code null}
* @param left
* The left storage, can be {@code null}
* @param right
* The right storage, can be {@code null}
* @param origin
* The ancestor storage, can be {@code null}
*/
public ModelsResolution(IResolutionContext context, IProgressMonitor monitor,
IStorageProviderAccessor storageAccessor, IStorage left, IStorage right, IStorage origin) {
super(context, monitor);
this.localResolver = context.getLocalResolver();
this.remoteResolver = context.getRemoteResolver();
this.storageAccessor = checkNotNull(storageAccessor);
this.left = left;
this.right = right;
this.origin = origin;
checkArgument(left != null || right != null || origin != null);
}
/**
* Executes the resolution.
*
* @return The logical model to use to compare the given storages
* @throws InterruptedException
* If the treatment is interrupted.
*/
public SynchronizationModel run() throws InterruptedException {
if (logger.isDebugEnabled()) {
logger.debug("run() - START"); //$NON-NLS-1$
}
return call(new Callable<SynchronizationModel>() {
public SynchronizationModel call() throws Exception {
final IFile leftFile = adaptAs(left, IFile.class);
final SynchronizationModel synchronizationModel;
if (leftFile != null) {
synchronizationModel = resolveModelsWithLocal(leftFile, new ThreadSafeProgressMonitor(
monitor));
} else {
synchronizationModel = resolveRemoteModels(new ThreadSafeProgressMonitor(monitor));
}
if (logger.isDebugEnabled()) {
logger.debug("run() - FINISH"); //$NON-NLS-1$
}
return synchronizationModel;
}
});
}
/**
* Overridden to set the work remaining to zero on the progress monitor used.
*/
@Override
protected Runnable getFinalizeResolvingRunnable() {
return new Runnable() {
public void run() {
ModelsResolution.super.getFinalizeResolvingRunnable().run();
if (monitor != null) {
monitor.setWorkRemaining(0);
}
}
};
}
/**
* The 'left' model we've been fed is a local file. We'll assume that the whole 'left' side of this
* comparison is local and resolve everything for that side as we would for local comparisons : update the
* dependency graph according to our resource listener, lookup for cross-references to/from the left
* resource according to the {@link #getResolutionScope() resolution scope}... Once we've resolved the
* local traversal, we'll use that as a base to infer the two remote sides, then "augment" it with the
* cross-references of the remote variants of these resources.
*
* @param leftFile
* File corresponding to the left side of this comparison.
* @param tspm
* Monitor on which to report progress to the user.
* @return The SynchronizationModel describing the traversals of all three sides of this logical model.
* @throws InterruptedException
* Thrown if the resolution is cancelled or interrupted one way or another.
*/
private SynchronizationModel resolveModelsWithLocal(final IFile leftFile,
final ThreadSafeProgressMonitor tspm) throws InterruptedException {
if (logger.isDebugEnabled()) {
logger.debug("resolveModelsWithLocal() - updating dependencies"); //$NON-NLS-1$
}
// Update changes and compute dependencies for left
// Then load the same set of resources for the remote sides, completing it top-down
localResolver.updateDependencies(monitor, diagnostic, leftFile);
if (tspm.isCanceled()) {
throw new OperationCanceledException();
}
if (logger.isDebugEnabled()) {
logger.debug("resolveModelsWithLocal() - resolving traversal"); //$NON-NLS-1$
}
final Set<IStorage> leftTraversal = resolveTraversal(leftFile, Collections.<URI> emptySet());
if (logger.isDebugEnabled()) {
logger.debug("resolveModelsWithLocal() - resolving remote traversal"); //$NON-NLS-1$
}
return resolveRemoteTraversals(leftTraversal, tspm);
}
/**
* All three sides we've been fed are remote. We'll resolve all three with a simple a top-down algorithm
* (detect only outgoing cross-references).
*
* @param tspm
* Monitor on which to report progress to the user.
* @return The SynchronizationModel describing the traversals of all three sides of this logical model.
* @throws InterruptedException
* Thrown if the resolution is cancelled or interrupted one way or another.
*/
private SynchronizationModel resolveRemoteModels(ThreadSafeProgressMonitor tspm)
throws InterruptedException {
if (logger.isDebugEnabled()) {
logger.debug("resolveRemoteModels() - resolving left remote traversal"); //$NON-NLS-1$
}
final Set<IStorage> leftTraversal = resolveRemoteTraversal(left, Collections.<URI> emptySet(),
DiffSide.SOURCE, tspm);
if (logger.isDebugEnabled()) {
logger.debug("resolveRemoteModels() - resolving other remote traversals"); //$NON-NLS-1$
}
return resolveRemoteTraversals(leftTraversal, tspm);
}
/**
* Resolve the remote sides (right and origin, or right alone in case of two-way) of this comparison,
* inferring a "starting traversal" from the left side.
* <p>
* Do note that {@code leftTraversal} <b>will be changed</b> as a result of this call if the right and/or
* origin sides contain a reference to another resource that was not found from the left
* cross-referencing, yet does exist in the left side.
* </p>
*
* @param leftTraversal
* The already resolved left traversal, to be augmented if right and/or origin have some new
* resources in their logical model.
* @param tspm
* Monitor on which to report progress to the user.
* @return The SynchronizationModel describing the traversals of all three sides of this logical model.
* @throws InterruptedException
* Thrown if the resolution is cancelled or interrupted one way or another.
*/
private SynchronizationModel resolveRemoteTraversals(Set<IStorage> leftTraversal,
ThreadSafeProgressMonitor tspm) throws InterruptedException {
if (logger.isDebugEnabled()) {
logger.debug("resolveRemotetraversals() - START"); //$NON-NLS-1$
}
final Set<IStorage> rightTraversal = resolveRemoteTraversal(right, Iterables.transform(leftTraversal,
asURI()), DiffSide.REMOTE, tspm);
final Set<IStorage> differenceRightLeft = difference(rightTraversal, asURISet(leftTraversal));
loadAdditionalRemoteStorages(leftTraversal, rightTraversal, differenceRightLeft, tspm);
final Set<IStorage> originTraversal;
if (origin != null) {
final Set<URI> unionLeftRight = Sets.newLinkedHashSet(Iterables.transform(Sets.union(
leftTraversal, rightTraversal), asURI()));
originTraversal = resolveRemoteTraversal(origin, unionLeftRight, DiffSide.ORIGIN, tspm);
Set<IStorage> differenceOriginLeft = difference(originTraversal, asURISet(leftTraversal));
Set<IStorage> differenceOriginRight = difference(originTraversal, asURISet(rightTraversal));
Set<IStorage> additional = symmetricDifference(differenceOriginLeft, differenceOriginRight);
loadAdditionalRemoteStorages(leftTraversal, rightTraversal, originTraversal, additional, tspm);
} else {
originTraversal = Collections.emptySet();
}
final SynchronizationModel synchronizationModel = new SynchronizationModel(new StorageTraversal(
leftTraversal), new StorageTraversal(rightTraversal), new StorageTraversal(originTraversal),
diagnostic.getDiagnostic());
if (logger.isDebugEnabled()) {
logger.debug("resolveRemotetraversals() - FINISH"); //$NON-NLS-1$
}
return synchronizationModel;
}
/**
* If we found some storages in the right traversal that were not part of the left traversal, we need to
* check whether they exist in the left, since in such a case they must be considered as part of the same
* logical model.
* <p>
* <b>Important</b> : note that the input {@code left} and {@code right} sets <b>will be modified</b> as a
* result of this call if there are any additional storages to load on these sides.
* </p>
*
* @param leftSet
* Traversal of the left logical model.
* @param rightSet
* Traversal of the right logical model.
* @param additional
* the addition storages we are to lookup in left.
* @param tspm
* Monitor on which to report progress to the user.
* @return The set of all additional resources (both on left and right) that have been loaded as a result
* of this call.
* @throws InterruptedException
* Thrown if the resolution is cancelled or interrupted one way or another.
*/
private Set<IStorage> loadAdditionalRemoteStorages(Set<IStorage> leftSet, Set<IStorage> rightSet,
Set<IStorage> additional, ThreadSafeProgressMonitor tspm) throws InterruptedException {
/*
* This loop will be extremely costly at best, but we hope the case to be sufficiently rare (and the
* new resources well spread when it happens) not to pose an issue in the most frequent cases.
*/
final Set<IStorage> additionalStorages = new LinkedHashSet<IStorage>();
final Set<URI> additionalURIs = new LinkedHashSet<URI>();
// Have we found new resources in the right as compared to the left?
Set<IStorage> differenceRightLeft = additional;
while (!differenceRightLeft.isEmpty()) {
// There's at least one resource in the right that was not found in the left.
/*
* This might be a new resource added on the right side... but it might also be a cross-reference
* that's been either removed from left or added in right. In this second case, we need the
* resource to be present in both traversals to make sure we'll be able to properly detect
* potential conflicts. However, since this resource could itself be a part of a larger logical
* model, we need to start the resolving again with it.
*/
final Set<IStorage> additionalLeft = findAdditionalRemoteTraversal(leftSet, differenceRightLeft,
DiffSide.SOURCE, tspm);
leftSet.addAll(additionalLeft);
for (IStorage storage : additionalLeft) {
final URI newURI = asURI().apply(storage);
if (additionalURIs.add(newURI)) {
additionalStorages.add(storage);
}
}
/*
* have we only loaded the resources that were present in the right but not in the left, or have
* we found even more?
*/
final Set<IStorage> differenceAdditionalLeftRight = difference(additionalLeft, asURISet(rightSet));
// If so, we once more need to augment the right traversal
final Set<IStorage> additionalRight = findAdditionalRemoteTraversal(rightSet,
differenceAdditionalLeftRight, DiffSide.REMOTE, tspm);
rightSet.addAll(additionalRight);
for (IStorage storage : additionalRight) {
final URI newURI = asURI().apply(storage);
if (additionalURIs.add(newURI)) {
additionalStorages.add(storage);
}
}
// Start this loop anew if we once again augmented the right further than what we had in
// left
differenceRightLeft = difference(additionalRight, asURISet(leftSet));
}
return additionalStorages;
}
/**
* If we found some storages in the origin traversal that were part of neither the left nor the right
* traversals, we need to check whether they exist in them, since in such a case they must be considered
* as part of the same logical model.
* <p>
* <b>Important</b> : note that the input {@code left}, {@code right} and {@code origin} sets <b>will be
* modified</b> as a result of this call if there are any additional storages to load on either side.
* </p>
*
* @param leftSet
* Traversal of the left logical model.
* @param rightSet
* Traversal of the right logical model.
* @param originSet
* Traversal of the origin logical model.
* @param additional
* the set of additional storages we are to lookup in right and left.
* @param tspm
* Monitor on which to report progress to the user.
* @throws InterruptedException
* Thrown if the resolution is cancelled or interrupted one way or another.
*/
private void loadAdditionalRemoteStorages(Set<IStorage> leftSet, Set<IStorage> rightSet,
Set<IStorage> originSet, Set<IStorage> additional, ThreadSafeProgressMonitor tspm)
throws InterruptedException {
/*
* This loop will be extremely costly at best, but we hope the case to be sufficiently rare (and the
* new resources well spread when it happens) not to pose an issue in the most frequent cases.
*/
Set<IStorage> additionalStorages = additional;
while (!additionalStorages.isEmpty()) {
// There's at least one resource that is in the origin set yet neither in left nor in
// right.
final Set<IStorage> additionalLeftRightComparedToOrigin = loadAdditionalRemoteStorages(leftSet,
rightSet, additionalStorages, tspm);
/*
* Have we found even more resources to add to the traversal? If so, augment the origin
* accordingly.
*/
final Set<IStorage> additionalOrigin = findAdditionalRemoteTraversal(originSet,
additionalLeftRightComparedToOrigin, DiffSide.ORIGIN, tspm);
originSet.addAll(additionalOrigin);
// If we once again found new storages in the origin, restart the loop.
final Set<IStorage> differenceOriginLeft = difference(additionalOrigin, asURISet(leftSet));
final Set<IStorage> differenceOriginRight = difference(additionalOrigin, asURISet(rightSet));
additionalStorages = symmetricDifference(differenceOriginRight, differenceOriginLeft);
// Differences between left/right and origin could come from resources that are present in
// the origin, but were deleted in one of the sides. As these resources already exist in
// the origin, they need to be removed from the additionalStorages
additionalStorages.removeAll(originSet);
}
}
/**
* Tries and resolve the given set of additional storages (as compared to {@code alreadyLoaded}) on the
* given side.
* <p>
* If the storages from {@code additionalStorages} do not (or no longer) exist on the given side, this
* will have no effect. Otherwise, they'll be loaded and resolved in order to determine whether they are
* part of a larger model. Whether they're part of a larger model or not, they will be returned by this
* method as long as they exist on the given side.
* </p>
*
* @param alreadyLoaded
* All storages that have already been loaded on the given side. This will prevent us from
* resolving the same model more than once.
* @param additionalStorages
* The set of additional storages we are to find and resolve on the given side.
* @param side
* Side on which we seek to load additional storages in the traversal.
* @param tspm
* Monitor on which to report progress to the user.
* @return The set of additional storages that are to be added to the traversal of the given side.
* @throws InterruptedException
* Thrown if the resolution is cancelled or interrupted one way or another.
*/
private Set<IStorage> findAdditionalRemoteTraversal(Set<IStorage> alreadyLoaded,
Set<IStorage> additionalStorages, DiffSide side, final ThreadSafeProgressMonitor tspm)
throws InterruptedException {
final SynchronizedResourceSet resourceSet = remoteResolver.getResourceSetForRemoteResolution(
diagnostic, tspm);
final StorageURIConverter converter = new RevisionedURIConverter(resourceSet.getURIConverter(),
storageAccessor, side);
resourceSet.setURIConverter(converter);
ResourceComputationScheduler<URI> scheduler = context.getScheduler();
scheduler.setComputedElements(transform(converter.getLoadedRevisions(), asURI()));
Iterable<URI> urisToResolve = transform(additionalStorages, asURI());
scheduler.computeAll(transform(urisToResolve, resolveRemoteURI(tspm, resourceSet)));
if (tspm.isCanceled()) {
throw new OperationCanceledException();
}
scheduler.clearComputedElements();
return converter.getLoadedRevisions();
}
/**
* Returns the set of all elements that are contained neither in set1 nor in set2.
*
* @param set1
* First of the two sets.
* @param set2
* Second of the two sets.
* @return The set of all elements that are contained neither in set1 nor in set2.
*/
private Set<IStorage> symmetricDifference(Set<IStorage> set1, Set<IStorage> set2) {
final Set<URI> uris1 = Sets.newLinkedHashSet(Iterables.transform(set1, asURI()));
final Set<URI> uris2 = Sets.newLinkedHashSet(Iterables.transform(set2, asURI()));
final Set<IStorage> symmetricDifference = new LinkedHashSet<IStorage>();
for (IStorage storage1 : set1) {
if (!uris2.contains(asURI().apply(storage1))) {
symmetricDifference.add(storage1);
}
}
for (IStorage storage2 : set2) {
if (!uris1.contains(asURI().apply(storage2))) {
symmetricDifference.add(storage2);
}
}
return symmetricDifference;
}
/**
* Returns the set of all elements that are contained in {@code set1} but not in {@code set2}.
*
* @param set1
* First of the two sets.
* @param set2
* Second of the two sets.
* @return The set of all elements that are contained in {@code set1} but not in {@code set2}.
*/
private Set<IStorage> difference(Set<IStorage> set1, Set<URI> set2) {
final Set<IStorage> difference = new LinkedHashSet<IStorage>();
for (IStorage storage1 : set1) {
final URI uri = asURI().apply(storage1);
if (!set2.contains(uri)) {
difference.add(storage1);
}
}
return difference;
}
/**
* Resolve the remote traversal of the given storage.
*
* @param start
* Storage to resolve
* @param knownVariants
* Iterable over all the currently known {@link URI}s that are part of the local logical model
* on any side
* @param side
* The side
* @param tspm
* Monitor to report progress
* @return The traversal of the storage on the given side
* @throws InterruptedException
* If interrupted.
*/
private Set<IStorage> resolveRemoteTraversal(IStorage start, Iterable<URI> knownVariants, DiffSide side,
final ThreadSafeProgressMonitor tspm) throws InterruptedException {
// we can't call ResourceUtil.createURIFor(start) if start is null
// but the returned Set must be changeable so Collections.emptySet() won't do
if (start == null) {
return Sets.newLinkedHashSet();
}
if (logger.isDebugEnabled()) {
logger.debug("resolveRemotetraversal() - START for " + start); //$NON-NLS-1$
}
final SynchronizedResourceSet resourceSet = remoteResolver.getResourceSetForRemoteResolution(
diagnostic, tspm);
final StorageURIConverter converter = new RevisionedURIConverter(resourceSet.getURIConverter(),
storageAccessor, side);
resourceSet.setURIConverter(converter);
ResourceComputationScheduler<URI> scheduler = context.getScheduler();
scheduler.clearComputedElements();
final URI startURI = ResourceUtil.createURIFor(start);
Iterable<URI> urisToResolve = concat(knownVariants, Collections.singleton(startURI));
scheduler.computeAll(transform(urisToResolve, resolveRemoteURI(tspm, resourceSet)));
if (tspm.isCanceled()) {
throw new OperationCanceledException();
}
scheduler.clearComputedElements();
if (logger.isDebugEnabled()) {
logger.debug("resolveRemotetraversal() - END for " + start); //$NON-NLS-1$
}
return converter.getLoadedRevisions();
}
/**
* Provides a {@link Function} that converts a givn URI into a Computation that can be run by a
* {@link ResourceComputationScheduler}.
*
* @param tspm
* The progress monitor to use
* @param resourceSet
* The resource set to use
* @return A {@link Function}, never {@code null}, that can be used to remotely resolvea given URI.
*/
protected Function<URI, IComputation<URI>> resolveRemoteURI(final ThreadSafeProgressMonitor tspm,
final SynchronizedResourceSet resourceSet) {
return new Function<URI, IComputation<URI>>() {
public IComputation<URI> apply(final URI uri) {
return remoteResolver.getRemoteResolveComputation(resourceSet, uri, diagnostic, tspm);
}
};
}
}