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