blob: 185c44af74867736af8b009b8cb5ab72e55353da [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011, 2013 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;
import static com.google.common.collect.Sets.difference;
import static com.google.common.collect.Sets.intersection;
import static com.google.common.collect.Sets.newLinkedHashSet;
import static org.eclipse.emf.compare.ide.ui.internal.util.PlatformElementUtil.adaptAs;
import static org.eclipse.emf.compare.ide.utils.ResourceUtil.createURIFor;
import static org.eclipse.emf.compare.ide.utils.ResourceUtil.hasContentType;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.IResourceVisitor;
import org.eclipse.core.resources.IStorage;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.content.IContentType;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.compare.ide.ui.internal.EMFCompareIDEUIMessages;
import org.eclipse.emf.compare.ide.ui.internal.EMFCompareIDEUIPlugin;
import org.eclipse.emf.compare.ide.ui.logical.IModelResolver;
import org.eclipse.emf.compare.ide.ui.logical.IStorageProvider;
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;
/**
* This implementation of an {@link IModelResolver} will look up in the whole project contained by the
* "starting points" of the models to resolve in order to check for referencing parents. Once this is done,
* we'll know the whole logical model for the "local" resource. The right and origin (if any) resources will
* be provided with the same traversal of resources, expanded with a "top-down" approach : load all models of
* the traversal from the remote side, then resolve their containment tree to check whether there are new
* remote resources in the logical model.
*
* @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a>
*/
public class ProjectModelResolver extends LogicalModelResolver {
/** Content types of the files to consider as potential models. */
private static final String[] MODEL_CONTENT_TYPES = new String[] {
"org.eclipse.emf.compare.content.type", "org.eclipse.emf.ecore", //$NON-NLS-1$ //$NON-NLS-2$
"org.eclipse.emf.ecore.xmi", }; //$NON-NLS-1$
/**
* Keeps track of the discovered dependency graph. Model resolvers are created from the extension point
* registry, we can thus keep this graph around to avoid multiple crawlings of the same IProject. Team,
* and the EMFResourceMapping, tend to be over-enthusiast with the resolution of model traversals. For
* example, a single "right-click -> compare with -> commit..." with EGit ends up calling 8 distinct times
* for the resource traversal of the selected resource.
*/
private Graph<URI> dependencyGraph;
/**
* This resolver will keep a resource listener over the workspace in order to keep its dependencies graph
* in sync.
*/
private ModelResourceListener resourceListener;
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.ide.ui.logical.AbstractModelResolver#initialize()
*/
@Override
public void initialize() {
super.initialize();
this.dependencyGraph = new Graph<URI>();
this.resourceListener = new ModelResourceListener();
ResourcesPlugin.getWorkspace().addResourceChangeListener(this.resourceListener);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.ide.ui.logical.AbstractModelResolver#dispose()
*/
@Override
public void dispose() {
ResourcesPlugin.getWorkspace().removeResourceChangeListener(this.resourceListener);
this.resourceListener = null;
this.dependencyGraph = null;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.ide.ui.internal.logical.LogicalModelResolver#resolveLocalModels(org.eclipse.core.resources.IResource,
* org.eclipse.core.resources.IResource, org.eclipse.core.resources.IResource,
* org.eclipse.core.runtime.IProgressMonitor)
*/
@Override
public SynchronizationModel resolveLocalModels(IResource left, IResource right, IResource origin,
IProgressMonitor monitor) {
if (!(left instanceof IFile) || !(right instanceof IFile)) {
return super.resolveLocalModels(left, right, origin, monitor);
}
updateChangedDependencies(monitor);
updateDependencies((IFile)left, monitor);
updateDependencies((IFile)right, monitor);
if (origin instanceof IFile) {
updateDependencies((IFile)origin, monitor);
}
final Set<IFile> startingPoints;
if (origin != null) {
startingPoints = ImmutableSet.of((IFile)left, (IFile)right, (IFile)origin);
} else {
startingPoints = ImmutableSet.of((IFile)left, (IFile)right);
}
final Set<IStorage> leftTraversal = resolveTraversal((IFile)left, difference(startingPoints,
Collections.singleton(left)), monitor);
final Set<IStorage> rightTraversal = resolveTraversal((IFile)right, difference(startingPoints,
Collections.singleton(right)), monitor);
final Set<IStorage> originTraversal;
if (origin instanceof IFile) {
originTraversal = resolveTraversal((IFile)origin, difference(startingPoints, Collections
.singleton(origin)), monitor);
} else {
originTraversal = Collections.emptySet();
}
/*
* If one resource of the logical model was pointing to both (or "all three") of our starting
* elements, we'll have way too many things in our traversal. We need to remove the intersection
* before going any further.
*/
Set<IStorage> intersection = intersection(leftTraversal, rightTraversal);
if (!originTraversal.isEmpty()) {
intersection = intersection(intersection, originTraversal);
}
logCoherenceThreats(startingPoints, intersection);
final Set<IStorage> actualLeft = newLinkedHashSet(difference(leftTraversal, intersection));
final Set<IStorage> actualRight = newLinkedHashSet(difference(rightTraversal, intersection));
final Set<IStorage> actualOrigin = newLinkedHashSet(difference(originTraversal, intersection));
return new SynchronizationModel(new StorageTraversal(actualLeft), new StorageTraversal(actualRight),
new StorageTraversal(actualOrigin));
}
/**
* When executing local comparisons, we resolve the full logical model of both (or "all three of") the
* compared files.
* <p>
* If there is one resource in the scope that references all of these starting points, then we'll have
* perfectly identical logical models for all comparison sides. Because of that, we need to constrain the
* logical model of each starting point to only parts that are not accessible from other starting points.
* This might cause coherence issues as merging could thus "break" references from other files to our
* compared ones.
* </p>
* <p>
* This method will be used to browse the files that are removed from the logical model, and log a warning
* for the files that are removed even though they are "parents" of one of the starting points.
* </p>
*
* @param startingPoints
* Starting points of the comparison.
* @param removedFromModel
* All files that have been removed from the comparison scope.
*/
private void logCoherenceThreats(Set<IFile> startingPoints, Set<IStorage> removedFromModel) {
final Set<URI> coherenceThreats = new LinkedHashSet<URI>();
for (IStorage start : startingPoints) {
final URI startURI = createURIFor(start);
for (IStorage removed : removedFromModel) {
final URI removedURI = createURIFor(removed);
if (dependencyGraph.hasChild(removedURI, startURI)) {
coherenceThreats.add(removedURI);
}
}
}
if (!coherenceThreats.isEmpty()) {
final String message = EMFCompareIDEUIMessages.getString("ModelResolver.coherenceWarning"); //$NON-NLS-1$
final String details = Iterables.toString(coherenceThreats);
EMFCompareIDEUIPlugin.getDefault().getLog().log(
new Status(IStatus.WARNING, EMFCompareIDEUIPlugin.PLUGIN_ID, message + '\n' + details));
}
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.ide.ui.internal.logical.LogicalModelResolver#resolveModels(org.eclipse.emf.compare.ide.ui.logical.IStorageProviderAccessor,
* org.eclipse.core.resources.IStorage, org.eclipse.core.resources.IStorage,
* org.eclipse.core.resources.IStorage, org.eclipse.core.runtime.IProgressMonitor)
*/
@Override
public SynchronizationModel resolveModels(IStorageProviderAccessor storageAccessor, IStorage left,
IStorage right, IStorage origin, IProgressMonitor monitor) {
final IFile leftFile = adaptAs(left, IFile.class);
if (leftFile == null) {
return super.resolveModels(storageAccessor, leftFile, right, origin, monitor);
}
updateChangedDependencies(monitor);
updateDependencies(leftFile, storageAccessor, monitor);
final Set<IStorage> leftTraversal = resolveLocalTraversal(storageAccessor, leftFile, monitor);
final Set<IStorage> rightTraversal = resolveTraversal(storageAccessor, DiffSide.REMOTE,
leftTraversal, monitor);
final Set<IStorage> originTraversal;
if (origin != null) {
originTraversal = resolveTraversal(storageAccessor, DiffSide.ORIGIN, leftTraversal, monitor);
} else {
originTraversal = Collections.emptySet();
}
return new SynchronizationModel(new StorageTraversal(leftTraversal), new StorageTraversal(
rightTraversal), new StorageTraversal(originTraversal));
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.ide.ui.internal.logical.LogicalModelResolver#resolveLocalModel(org.eclipse.core.resources.IResource,
* org.eclipse.core.runtime.IProgressMonitor)
*/
@Override
public StorageTraversal resolveLocalModel(IResource start, IProgressMonitor monitor) {
if (!(start instanceof IFile)) {
return super.resolveLocalModel(start, monitor);
}
updateChangedDependencies(monitor);
updateDependencies((IFile)start, monitor);
return new StorageTraversal(resolveTraversal((IFile)start, monitor));
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.ide.ui.internal.logical.LogicalModelResolver#canResolve(org.eclipse.core.resources.IStorage)
*/
@Override
public boolean canResolve(IStorage sourceStorage) {
if (sourceStorage instanceof IFile) {
IFile file = (IFile)sourceStorage;
return file.getProject().isAccessible() && ((IFile)sourceStorage).exists();
}
return false;
}
/**
* Updates the dependency graph for the given file.
*
* @param file
* File which dependencies we are to update.
* @param monitor
* Monitor to report progress on.
*/
private void updateDependencies(IFile file, IProgressMonitor monitor) {
final URI startURI = createURIFor(file);
if (!dependencyGraph.contains(startURI)) {
final IProject project = file.getProject();
final ModelResourceVisitor modelVisitor = new ModelResourceVisitor(dependencyGraph, monitor);
try {
project.accept(modelVisitor);
} catch (CoreException e) {
Throwables.propagate(e);
}
}
}
/**
* Updates the dependency graph for the given file.
*
* @param file
* File which dependencies we are to update.
* @param storageAccessor
* The accessor that can be used to retrieve synchronization information between our resources.
* @param monitor
* Monitor to report progress on.
*/
private void updateDependencies(IFile file, IStorageProviderAccessor storageAccessor,
IProgressMonitor monitor) {
final URI leftURI = createURIFor(file);
if (!dependencyGraph.contains(leftURI)) {
final IProject project = file.getProject();
final ModelResourceVisitor modelVisitor = new ModelResourceVisitor(storageAccessor,
DiffSide.SOURCE, dependencyGraph, monitor);
try {
project.accept(modelVisitor);
} catch (CoreException e) {
// TODO log
}
}
}
/**
* Checks all dependencies that have changed since we last checked (as returned by the
* {@link #resourceListener}).
*
* @param monitor
* Monitor to report progress on.
*/
private void updateChangedDependencies(IProgressMonitor monitor) {
final Set<URI> removedURIs = resourceListener.popRemovedURIs();
final Set<URI> changedURIs = Sets.difference(resourceListener.popChangedURIs(), removedURIs);
dependencyGraph.removeAll(removedURIs);
dependencyGraph.removeAll(changedURIs);
for (URI changed : changedURIs) {
if (!dependencyGraph.contains(changed)) {
final IFile file = getFileAt(changed);
updateDependencies(file, monitor);
}
}
}
/**
* Checks whether the given file has one of the content types described in {@link #MODEL_CONTENT_TYPES}.
*
* @param file
* The file which contents are to be checked.
* @return <code>true</code> if this file has one of the "model" content types.
*/
protected static final boolean hasModelType(IFile file) {
boolean isModel = false;
final IContentType[] contentTypes = ResourceUtil.getContentTypes(file);
for (int i = 0; i < MODEL_CONTENT_TYPES.length && !isModel; i++) {
isModel = hasContentType(MODEL_CONTENT_TYPES[i], contentTypes);
}
return isModel;
}
/**
* This will be used to resolve the traversal of a file's logical model, according to
* {@link #dependencyGraph}.
*
* @param resource
* The resource for which we need the full logical model.
* @param monitor
* Monitor on which to report progress to the user.
* @return The set of all storages that compose the logical model of <code>resource</code>.
*/
private Set<IStorage> resolveTraversal(IFile resource, IProgressMonitor monitor) {
final Set<IStorage> traversal = new LinkedHashSet<IStorage>();
final URI startURI = createURIFor(resource);
final Iterable<URI> uris = dependencyGraph.getSubgraphOf(startURI);
for (URI uri : uris) {
traversal.add(getFileAt(uri));
}
return traversal;
}
/**
* This will be used in case of remote comparisons to resolve the local side's traversal (if there is a
* local side).
*
* @param storageAccessor
* The accessor that can be used to retrieve synchronization information between our resources.
* @param resource
* The resource for which we need the full logical model.
* @param monitor
* Monitor on which to report progress to the user.
* @return The set of all storages that compose the logical model of <code>resource</code>.
*/
private Set<IStorage> resolveLocalTraversal(IStorageProviderAccessor storageAccessor, IFile resource,
IProgressMonitor monitor) {
final Set<IStorage> traversal = new LinkedHashSet<IStorage>();
final URI startURI = createURIFor(resource);
final Iterable<URI> uris = dependencyGraph.getSubgraphOf(startURI);
for (URI uri : uris) {
final IFile file = getFileAt(uri);
try {
if (!storageAccessor.isInSync(file)) {
traversal.add(file);
}
} catch (CoreException e) {
// swallow
}
}
return traversal;
}
/**
* This will be used to resolve the traversal of a file's logical model, according to
* {@link #dependencyGraph}.
*
* @param resource
* The resource for which we need the full logical model.
* @param bounds
* The resources constituting starting points of "other" logical models. This will be used to
* constrain the dependency sub-graph.
* @param monitor
* Monitor on which to report progress to the user.
* @return The set of all storages that compose the logical model of <code>resource</code>.
*/
private Set<IStorage> resolveTraversal(IFile resource, Set<IFile> bounds, IProgressMonitor monitor) {
final Set<IStorage> traversal = new LinkedHashSet<IStorage>();
final URI startURI = createURIFor(resource);
final Set<URI> uriBounds = new LinkedHashSet<URI>(bounds.size());
for (IFile bound : bounds) {
uriBounds.add(createURIFor(bound));
}
final Iterable<URI> uris = dependencyGraph.getBoundedSubgraphOf(startURI, uriBounds);
for (URI uri : uris) {
traversal.add(getFileAt(uri));
}
return traversal;
}
/**
* Returns the IFile located at the given URI.
*
* @param uri
* URI we need the file for.
* @return The IFile located at the given URI.
*/
private IFile getFileAt(URI uri) {
final StringBuilder path = new StringBuilder();
List<String> segments = uri.segmentsList();
if (uri.isPlatformResource()) {
segments = segments.subList(1, segments.size());
}
for (String segment : segments) {
path.append(URI.decode(segment)).append('/');
}
return ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(path.toString()));
}
/**
* This will be used to resolve the logical model of a remote resource variant. Since we have no direct
* access to the resources themselves, we cannot simply browse one of their container for the traversal.
* Instead of that, we'll use the local traversal as a "reference", load remote variants of all of these
* local files, and re-resolve them in a "top-down" approach in case there are new resources on the remote
* side.
*
* @param storageAccessor
* The accessor that can be used to retrieve synchronization information between our resources.
* @param side
* Side of the logical model to resolve. Used in conjunction with the storage accessor to
* retrieve appropriate contents for the remote variants.
* @param localTraversal
* Traversal resolved for the logical model of the local file.
* @param monitor
* Monitor on which to report progress to the user.
* @return The set of all remote storages composing the same logical model as the given local traversal.
*/
private Set<IStorage> resolveTraversal(IStorageProviderAccessor storageAccessor, DiffSide side,
Set<IStorage> localTraversal, IProgressMonitor monitor) {
final DependencyResourceSet resourceSet = new DependencyResourceSet(dependencyGraph);
final StorageURIConverter converter = new RevisionedURIConverter(resourceSet.getURIConverter(),
storageAccessor, side);
resourceSet.setURIConverter(converter);
final Set<IStorage> storages = Sets.newLinkedHashSet();
for (IStorage local : localTraversal) {
final IFile localFile = adaptAs(local, IFile.class);
try {
final IStorageProvider remoteStorageProvider = storageAccessor.getStorageProvider(localFile,
side);
if (remoteStorageProvider != null) {
final IStorage start = remoteStorageProvider.getStorage(monitor);
if (resourceSet.resolveAll(start, monitor)) {
if (!contains(storages, start)) {
storages.add(start);
}
for (IStorage loaded : converter.getLoadedRevisions()) {
if (!contains(storages, loaded)) {
storages.add(loaded);
}
}
} else {
// failed to load a remote version of this resource
}
} else {
// file only exist locally
}
} catch (CoreException e) {
// failed to load a remote version of this resource
}
}
return storages;
}
/**
* This implementation of a resource visitor will allow us to browse all models in a given hierarchy.
*
* @author <a href="mailto:laurent.goubet@obeo.fr">laurent Goubet</a>
*/
private static class ModelResourceVisitor implements IResourceVisitor {
/** Resource Set in which we should load the temporary resources. */
private final DependencyResourceSet resourceSet;
/** Monitor to report progress on. */
// FIXME logarithmic progress
private final IProgressMonitor monitor;
/**
* Instantiates a resource visitor.
*
* @param graph
* The dependency graph that is to be populated/completed.
* @param monitor
* The monitor to report progress on.
*/
public ModelResourceVisitor(Graph<URI> graph, IProgressMonitor monitor) {
this.resourceSet = new DependencyResourceSet(graph);
this.monitor = monitor;
final StorageURIConverter converter = new StorageURIConverter(resourceSet.getURIConverter());
this.resourceSet.setURIConverter(converter);
}
/**
* Instantiates a resource visitor given the storage accessor to use for I/O operations.
*
* @param storageAccessor
* The accessor to use for all I/O operations to fetch resource content.
* @param side
* Side of the resources. Used in conjunction with the storage accessor to fetch proper
* content.
* @param graph
* The dependency graph that is to be populated/completed.
* @param monitor
* The monitor to report progress on.
*/
public ModelResourceVisitor(IStorageProviderAccessor storageAccessor, DiffSide side,
Graph<URI> graph, IProgressMonitor monitor) {
this.resourceSet = new DependencyResourceSet(graph);
this.monitor = monitor;
final StorageURIConverter converter = new RevisionedURIConverter(resourceSet.getURIConverter(),
storageAccessor, side);
this.resourceSet.setURIConverter(converter);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.core.resources.IResourceVisitor#visit(org.eclipse.core.resources.IResource)
*/
public boolean visit(IResource resource) throws CoreException {
if (resource instanceof IFile) {
IFile file = (IFile)resource;
if (hasModelType(file)) {
resourceSet.resolveAll(file, monitor);
return true;
}
return false;
}
return true;
}
}
/**
* This will listen to workspace changes and react to all changes on "model" resources as determined by
* {@link ProjectModelResolver#MODEL_CONTENT_TYPES}.
*
* @author <a href="mailto:laurent.goubet@obeo.fr">laurent Goubet</a>
*/
private static class ModelResourceListener implements IResourceChangeListener {
/** Keeps track of the URIs that need to be reparsed when next we need the dependencies graph . */
protected Set<URI> changedURIs;
/** Tracks the files that have been removed. */
protected Set<URI> removedURIs;
/** Initializes this listener. */
public ModelResourceListener() {
this.changedURIs = new LinkedHashSet<URI>();
this.removedURIs = new LinkedHashSet<URI>();
}
/**
* {@inheritDoc}
*
* @see org.eclipse.core.resources.IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent)
*/
public void resourceChanged(IResourceChangeEvent event) {
final IResourceDelta delta = event.getDelta();
if (delta == null) {
return;
}
try {
delta.accept(new ModelResourceDeltaVisitor());
} catch (CoreException e) {
EMFCompareIDEUIPlugin.getDefault().log(e);
}
}
/**
* Retrieves the set of all changed URIs since we last updated the dependencies graph, and clears it
* for subsequent calls.
*
* @return The set of all changed URIs since we last updated the dependencies graph.
*/
public Set<URI> popChangedURIs() {
final Set<URI> changed;
synchronized(changedURIs) {
changed = ImmutableSet.copyOf(changedURIs);
changedURIs.clear();
}
return changed;
}
/**
* Retrieves the set of all removed URIs since we last updated the dependencies graph, and clears it
* for subsequent calls.
*
* @return The set of all removed URIs since we last updated the dependencies graph.
*/
public Set<URI> popRemovedURIs() {
final Set<URI> removed;
synchronized(removedURIs) {
removed = ImmutableSet.copyOf(removedURIs);
removedURIs.clear();
}
return removed;
}
/**
* Visits a resource delta to collect the changed and removed files' URIs.
*
* @author <a href="mailto:laurent.goubet@obeo.fr">laurent Goubet</a>
*/
private class ModelResourceDeltaVisitor implements IResourceDeltaVisitor {
/**
* {@inheritDoc}
*
* @see org.eclipse.core.resources.IResourceDeltaVisitor#visit(org.eclipse.core.resources.IResourceDelta)
*/
public boolean visit(IResourceDelta delta) throws CoreException {
if (delta.getFlags() == IResourceDelta.MARKERS
|| delta.getResource().getType() != IResource.FILE) {
return true;
}
final IFile file = (IFile)delta.getResource();
final URI fileURI = createURIFor(file);
// We can't check the content type of a removed resource
if (delta.getKind() == IResourceDelta.REMOVED) {
synchronized(removedURIs) {
removedURIs.add(fileURI);
}
} else if (hasModelType(file)) {
if ((delta.getKind() & (IResourceDelta.CHANGED | IResourceDelta.ADDED)) != 0) {
synchronized(changedURIs) {
changedURIs.add(fileURI);
}
}
}
return true;
}
}
}
}