blob: f3f72b14291cc1ac74d0ffeaec6345158b5a2115 [file] [log] [blame]
/**
* <copyright>
*
* Copyright (c) 2005, 2008 IBM Corporation, Zeligsoft Inc. and others.
* 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:
* IBM - Initial API and implementation
* Geoff Martin - Fix deletion of resource that has markers
* Zeligsoft - Bug 233004
* Christian Vogt - Bug 235634
*
* </copyright>
*
* $Id: WorkspaceSynchronizer.java,v 1.11 2008/08/13 13:24:44 cdamus Exp $
*/
package org.eclipse.egf.core.workspace;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
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.IResourceRuleFactory;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.resources.WorkspaceJob;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
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.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.MultiRule;
import org.eclipse.egf.core.EGFCorePlugin;
import org.eclipse.egf.core.l10n.EGFCoreMessages;
import org.eclipse.emf.common.archive.ArchiveURLConnection;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.common.util.UniqueEList;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.URIConverter;
import org.eclipse.emf.transaction.TransactionalEditingDomain;
/**
* A utility object that listens to workspace resource changes to synchronize
* the state of an EMF resource set with the workspace.
* <p>
* The default behaviour
* on workspace resource deletions is to unload the corresponding EMF resource.
* The default behaviour on resource changes is to unload and reload the
* corresponding EMF resource, unless the resource path has changed (by move
* or rename), in which case it is simply unloaded.
* </p>
* <p>
* To customize the behaviour of the synchronizer, initialize it with a {@link WorkspaceSynchronizer.Delegate delegate} that provides the required
* behaviour. For example, it might be more user-friendly to prompt the user
* before taking drastic measures.
* </p>
* <p>
* Whether implemented by a delegate or not, the synchronization algorithm is
* invoked asynchronously (as a job) and in a read-only transaction on the
* synchronizer's editing domain. This ensures timely completion of the
* workspace's event dispatching and exclusive access to the resource set
* according to the transaction protocol. Also, the job is scheduled on the
* workspace rule, so that the delegate call-backs are free to read or modify
* any resources that they may need.
* </p>
*
* @author Christian W. Damus (cdamus)
*/
public final class EGFWorkspaceSynchronizer {
private final TransactionalEditingDomain domain;
private Delegate delegate;
// we employ a copy-on-write strategy on this collection for thread safety
private static Collection<EGFWorkspaceSynchronizer> synchronizers = new java.util.ArrayList<EGFWorkspaceSynchronizer>();
// we use a single listener to serve all synchronizers.
private static IResourceChangeListener workspaceListener = new WorkspaceListener();
// the default synchronization strategies
static Delegate defaultDelegate = new DefaultDelegate();
/**
* Initializes me with the editing domain for which I synchronize resources,
* using the default change-handling behaviour.
* <p>
* I immediately start listening for workspace resource changes.
* </p>
*
* @param domain
* my domain (must not be <code>null</code>)
*/
public EGFWorkspaceSynchronizer(TransactionalEditingDomain domain) {
this(domain, null);
}
/**
* Initializes me with the editing domain for which I synchronize resources,
* using the specified delegate to handle resource changes.
* <p>
* I immediately start listening for workspace resource changes.
* </p>
*
* @param domain
* my domain (must not be <code>null</code>)
* @param delegate
* the delegate that handles my resource changes, or
* <code>null</code> to get the default behaviour
*/
public EGFWorkspaceSynchronizer(TransactionalEditingDomain domain, Delegate delegate) {
if (domain == null) {
throw new IllegalArgumentException("null domain"); //$NON-NLS-1$
}
if (delegate == null) {
delegate = defaultDelegate;
}
this.domain = domain;
this.delegate = delegate;
startListening(this);
}
/**
* Queries the editing domain whose resources I synchronize with the
* workspace.
*
* @return my editing domain
*/
public TransactionalEditingDomain getEditingDomain() {
return domain;
}
/**
* Obtains the delegate that handles resource changes.
*
* @return my delegate
*/
Delegate getDelegate() {
return delegate;
}
/**
* Disposes me, in particular disconnecting me from the workspace so that
* I no longer respond to resource change events.
*/
public void dispose() {
stopListening(this);
synchronized (this) {
if (!isDisposed()) {
delegate.dispose();
delegate = null;
}
}
}
/**
* Queries whether I am disposed already.
*
* @return whether I am disposed
*
* @since 1.2.1
*/
boolean isDisposed() {
return delegate == null;
}
/**
* Processes a resource delta to determine whether it corresponds to a
* resource in my editing domain and, if so, how to handle removal or
* change of that resource.
*
* @param delta
* the resource change
* @param synchRequests
* accumulates synch requests for the deltas
* @param affectedFiles
* accumulates the files affected by the deltas
*/
void processDelta(IResourceDelta delta, List<EGFSynchRequest> synchRequests, List<IProject> affectedProjects) {
String fullPath = delta.getFullPath().toString();
URI uri = URI.createPlatformResourceURI(fullPath, false);
ResourceSet rset = getEditingDomain().getResourceSet();
// try the unencoded URI first, in case the client doesn't encode
Resource resource = rset.getResource(uri, false);
if (resource == null) {
URI encodedURI = URI.createPlatformPluginURI(fullPath, true);
if (encodedURI.equals(uri) == false) {
// the URI needs to be encoded. Try it, then
uri = encodedURI;
resource = rset.getResource(uri, false);
}
}
if ((resource != null) && resource.isLoaded()) {
IProject project = ((IFile) delta.getResource()).getProject();
switch (delta.getKind()) {
case IResourceDelta.ADDED:
if ((delta.getFlags() & IResourceDelta.MOVED_FROM) != 0) {
affectedProjects.add(project);
} else {
synchRequests.add(new EGFPersistedSynchRequest(this, resource));
affectedProjects.add(project);
}
break;
case IResourceDelta.REMOVED:
if ((delta.getFlags() & IResourceDelta.MOVED_TO) != 0) {
// first, see whether a resource with the new URI already
// exists. If so, then we will use the same URI (whether
// encoded or not) because that seems to be what the
// client prefers. Otherwise, always encode
String newPath = delta.getMovedToPath().toString();
URI newURI = URI.createPlatformPluginURI(newPath, false);
if (rset.getResource(newURI, false) == null) {
// this may be the same, depending on absence of
// special characters
newURI = URI.createPlatformPluginURI(newPath, true);
}
synchRequests.add(new EGFMovedSynchRequest(this, resource, newURI));
} else {
synchRequests.add(new EGFDeletedSynchRequest(this, resource));
}
break;
case IResourceDelta.CHANGED:
// This prevent excessive notifications
if ((delta.getFlags() & IResourceDelta.CONTENT) != 0) {
synchRequests.add(new EGFChangedSynchRequest(this, resource));
affectedProjects.add(project);
}
break;
}
}
}
/**
* Obtains the workspace file corresponding to the specified resource, if
* it has a platform-resource URI. Note that the resulting file, if not
* <code>null</code>, may nonetheless not actually exist (as the file is
* just a handle).
* <p>
* Note that, if the <tt>resource</tt> is in an archive (such as a ZIP file)
* then it does not map to a workspace file. In this case, however, the
* workspace file (if any) corresponding to the containing archive can be
* obtained via the {@link #getUnderlyingFile(Resource)} method.
* </p>
*
* @param resource
* an EMF resource
*
* @return the corresponding workspace file, or <code>null</code> if the
* resource's URI is not a platform-resource URI
*
* @see #getUnderlyingFile(Resource)
*/
public static IFile getFile(Resource resource) {
ResourceSet rset = resource.getResourceSet();
return getFile(resource.getURI(), (rset != null) ? rset.getURIConverter() : null, false);
}
/**
* Obtains the workspace file underlying the specified resource.
* If the resource has an {@link URI#isArchive() archive} scheme, the {@linkplain URI#authority() authority} is considered instead.
* If the URI has a file scheme, it's looked up in the workspace, just as
* in the {@link #getFile(Resource)} method.
* Otherwise, a platform scheme is assumed.
* <p>
* Note that the resulting file, if not
* <code>null</code>, may nonetheless not actually exist (as the file is
* just a handle).
* </p>
*
* @param resource
* an EMF resource
*
* @return the underlying workspace file, or <code>null</code> if the
* resource's URI is not a platform-resource URI
*
* @see #getFile(Resource)
* @since 1.2
*/
public static IFile getUnderlyingFile(Resource resource) {
ResourceSet rset = resource.getResourceSet();
return getFile(resource.getURI(), (rset != null) ? rset.getURIConverter() : null, true);
}
/**
* Finds the file corresponding to the specified URI, using a URI converter
* if necessary (and provided) to normalize it.
*
* @param uri
* a URI
* @param converter
* an optional URI converter (may be <code>null</code>)
*
* @return the file, if available in the workspace
*/
private static IFile getFile(URI uri, URIConverter converter, boolean considerArchives) {
IFile result = null;
if (considerArchives && uri.isArchive()) {
class MyArchiveURLConnection extends ArchiveURLConnection {
public MyArchiveURLConnection(String url) {
super(url);
}
public String getNestedURI() {
try {
return getNestedURL();
} catch (IOException exception) {
return ""; //$NON-NLS-1$
}
}
}
MyArchiveURLConnection archiveURLConnection = new MyArchiveURLConnection(uri.toString());
result = getFile(URI.createURI(archiveURLConnection.getNestedURI()), converter, considerArchives);
} else if (uri.isPlatformResource()) {
IPath path = new Path(uri.toPlatformString(true));
result = ResourcesPlugin.getWorkspace().getRoot().getFile(path);
} else if (uri.isFile() && !uri.isRelative()) {
result = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation(new Path(uri.toFileString()));
} else {
// normalize, to see whether may we can resolve it this time
if (converter != null) {
URI normalized = converter.normalize(uri);
if (!uri.equals(normalized)) {
// recurse on the new URI
result = getFile(normalized, converter, considerArchives);
}
}
}
if ((result == null) && !uri.isRelative()) {
try {
IFile[] files = ResourcesPlugin.getWorkspace().getRoot().findFilesForLocationURI(new java.net.URI(uri.toString()));
if (files.length > 0) {
// set the result to be the first file found
result = files[0];
}
} catch (URISyntaxException e) {
// won't get this because EMF provides a well-formed URI
}
}
return result;
}
/**
* Starts a synchronizer listening to resource change events.
*
* @param synchronizer
* the synchronizer to start
*/
static void startListening(EGFWorkspaceSynchronizer synchronizer) {
// copy-on-write for thread safety
synchronized (synchronizers) {
Collection<EGFWorkspaceSynchronizer> newList = new ArrayList<EGFWorkspaceSynchronizer>(synchronizers.size() + 1);
newList.addAll(synchronizers);
newList.add(synchronizer);
synchronizers = newList;
// ensure that we are listening to the workspace
ResourcesPlugin.getWorkspace().addResourceChangeListener(workspaceListener, IResourceChangeEvent.POST_CHANGE);
}
}
/**
* Stops a synchronizer listening to resource change events.
*
* @param synchronizer
* the synchronizer to stop
*/
static void stopListening(EGFWorkspaceSynchronizer synchronizer) {
// copy-on-write for thread safety
synchronized (synchronizers) {
Collection<EGFWorkspaceSynchronizer> newList = new ArrayList<EGFWorkspaceSynchronizer>(synchronizers);
newList.remove(synchronizer);
synchronizers = newList;
if (synchronizers.isEmpty()) {
// stop listening to the workspace
ResourcesPlugin.getWorkspace().removeResourceChangeListener(workspaceListener);
}
}
}
/**
* Obtains the synchronizers that need to process a resource change event.
*
* @return the currently active synchronizers
*/
static Collection<EGFWorkspaceSynchronizer> getSynchronizers() {
// does not need synchronization because we copy on write
return synchronizers;
}
/**
* Call-back interface for an object to which a {@link WorkspaceSynchronizer} delegates the algorithms for handling different kinds of resource
* changes.
* <p>
* Every call-back is invoked asynchronously in a read-only transaction on
* the synchronizer's editing domain. Any model changes that the
* receiver wishes to make must be scheduled asynchronously, although
* workspace changes are permitted as the calling thread has the
* workspace lock. The call-backs are not actually required to handle the
* resource change; they can defer to the default behaviour.
* </p>
*
* @author Christian W. Damus (cdamus)
*/
public static interface Delegate {
/**
* Optionally handles the persistence of the physical workspace resource
* behind the specified EMF resource.
*
* @param resource
* a resource whose storage has been persisted
*
* @return <code>true</code> if I handled the resource persistence;
* <code>false</code> to defer to the workspace synchronizer's
* default algorithm
*/
boolean handleResourcePersisted(Resource resource);
/**
* Optionally handles the deletion of the physical workspace resource
* behind the specified EMF resource.
*
* @param resource
* a resource whose storage has been deleted
*
* @return <code>true</code> if I handled the resource deletion;
* <code>false</code> to defer to the workspace synchronizer's
* default algorithm
*/
boolean handleResourceDeleted(Resource resource);
/**
* Optionally handles the move of the physical workspace resource
* behind the specified EMF resource. Both in-place renames of a
* resource and relocations of a resource to another container are
* considered as moves.
*
* @param resource
* a resource whose storage has been moved
* @param newURI
* the new URI of the moved resource
*
* @return <code>true</code> if I handled the resource deletion;
* <code>false</code> to defer to the workspace synchronizer's
* default algorithm
*/
boolean handleResourceMoved(Resource resource, URI newURI);
/**
* Optionally handles a change to the physical workspace resource
* behind the specified EMF resource.
*
* @param resource
* a resource whose storage has been changed
*
* @return <code>true</code> if I handled the resource change;
* <code>false</code> to defer to the workspace synchronizer's
* default algorithm
*/
boolean handleResourceChanged(Resource resource);
/**
* Disposes me. This is called by the synchronizer when it is disposed.
*/
void dispose();
}
/**
* The single shared workspace listener that passes workspace changes
* along to the currently active synchronizers.
*
* @author Christian W. Damus (cdamus)
*/
private static class WorkspaceListener implements IResourceChangeListener {
public void resourceChanged(IResourceChangeEvent event) {
IResourceDelta delta = event.getDelta();
try {
final List<EGFSynchRequest> synchRequests = new ArrayList<EGFSynchRequest>();
final List<IProject> affectedProjects = new UniqueEList<IProject>();
delta.accept(new IResourceDeltaVisitor() {
public boolean visit(IResourceDelta innerDelta) {
if (innerDelta.getResource().getType() == IResource.FILE) {
switch (innerDelta.getKind()) {
case IResourceDelta.CHANGED:
if (innerDelta.getFlags() == IResourceDelta.MARKERS) {
break;
}
case IResourceDelta.ADDED:
case IResourceDelta.REMOVED:
processDelta(innerDelta, synchRequests, affectedProjects);
break;
}
}
return true;
}
});
if (synchRequests.isEmpty() == false) {
new ResourceSynchJob(synchRequests, affectedProjects).schedule();
}
} catch (CoreException e) {
EGFCorePlugin.getDefault().logError(e);
}
}
/**
* Passes the delta to all available synchronizers, to process it.
*
* @param delta
* the delta to process
* @param synchRequests
* accumulates synch requests for the deltas
* @param affectedFiles
* accumulates files affected by the deltas
*/
private void processDelta(IResourceDelta delta, List<EGFSynchRequest> synchRequests, List<IProject> affectedProjects) {
for (EGFWorkspaceSynchronizer next : getSynchronizers()) {
next.processDelta(delta, synchRequests, affectedProjects);
}
}
}
/**
* The default algorithms for handling workspace resource changes.
*
* @author Christian W. Damus (cdamus)
*/
private static class DefaultDelegate implements Delegate {
public boolean handleResourcePersisted(Resource resource) {
// Nothing to do
return true;
}
public boolean handleResourceDeleted(Resource resource) {
resource.unload();
return true;
}
public boolean handleResourceMoved(Resource resource, URI newURI) {
resource.unload();
return true;
}
public boolean handleResourceChanged(Resource resource) {
resource.unload();
try {
resource.load(resource.getResourceSet().getLoadOptions());
} catch (IOException e) {
EGFCorePlugin.getDefault().logError(e);
}
return true;
}
public void dispose() {
// nothing to dispose (especially as I am shared)
}
}
/**
* A job that runs under the workspace scheduling rule to process one or
* more resource synchronization requests.
*
* @author Christian W. Damus (cdamus)
*/
private static class ResourceSynchJob extends WorkspaceJob {
private final List<EGFSynchRequest> synchRequests;
/**
* Initializes me with the list of resources changes that I am to
* process.
*
* @param synchRequests
* the resource synchronization requests
* @param affectedResources
* the resources affected by the workspace changes
*/
ResourceSynchJob(List<EGFSynchRequest> synchRequests, List<IProject> affectedProjects) {
super(EGFCoreMessages.synchJobName);
this.synchRequests = synchRequests;
setRule(getRule(affectedProjects));
}
/**
* Processes my queued resource synchronization requests.
*/
@Override
public IStatus runInWorkspace(IProgressMonitor monitor) {
try {
for (EGFSynchRequest next : synchRequests) {
try {
synchronized (next.getLock()) {
if (next.isDisposed() == false) {
next.perform();
}
}
} catch (RuntimeException e) {
EGFCorePlugin.getDefault().logError(e);
}
}
} catch (InterruptedException e) {
return Status.CANCEL_STATUS;
}
return Status.OK_STATUS;
}
/**
* Obtains a scheduling rule to schedule myself on to give my delegate
* access to the specified affected projects.
*
* @param affectedProjects
* @return the appropriate scheduling rule, or <code>null</code> if
* none is required
*/
private ISchedulingRule getRule(List<IProject> affectedProjects) {
ISchedulingRule result = null;
if (affectedProjects.isEmpty() == false) {
IResourceRuleFactory factory = ResourcesPlugin.getWorkspace().getRuleFactory();
for (IResource next : affectedProjects) {
result = MultiRule.combine(result, factory.modifyRule(next));
}
}
return result;
}
}
}