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