/*******************************************************************************
* Copyright (c) 2000, 2010 IBM Corporation 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 Corporation - initial API and implementation
* manklu@web.de - fix for bug 156082
* Bert Vingerhoets - fix for bug 169975
* Serge Beauchamp (Freescale Semiconductor) - [229633] Fix Concurency Exception
*******************************************************************************/
package org.eclipse.core.internal.resources;
import java.net.URI;
import java.util.*;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.internal.events.ILifecycleListener;
import org.eclipse.core.internal.events.LifecycleEvent;
import org.eclipse.core.internal.localstore.FileSystemResourceManager;
import org.eclipse.core.internal.utils.Messages;
import org.eclipse.core.internal.utils.Policy;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.osgi.util.NLS;
/**
* An alias is a resource that occupies the same file system location as another resource in the
* workspace. When a resource is modified in a way that affects the file on disk, all aliases need
* to be updated. This class is used to maintain data structures for quickly computing the set of
* aliases for a given resource, and for efficiently updating all aliases when a resource changes on
* disk.
*
* The approach for computing aliases is optimized for alias-free workspaces and alias-free
* projects. That is, if the workspace contains no aliases, then updating should be very quick. If a
* resource is changed in a project that contains no aliases, it should also be very fast.
*
* The data structures maintained by the alias manager can be seen as a cache, that is, they store
* no information that cannot be recomputed from other available information. On shutdown, the alias
* manager discards all state; on startup, the alias manager eagerly rebuilds its state. The
* reasoning is that it's better to incur this cost on startup than on the first attempt to modify a
* resource. After startup, the state is updated incrementally on the following occasions: - when
* projects are deleted, opened, closed, or moved - when linked resources are created, deleted, or
* moved.
*/
public class AliasManager implements IManager, ILifecycleListener, IResourceChangeListener {
public class AddToCollectionDoit implements Doit {
Collection collection;
public void doit(IResource resource) {
collection.add(resource);
}
public void setCollection(Collection collection) {
this.collection= collection;
}
}
interface Doit {
public void doit(IResource resource);
}
class FindAliasesDoit implements Doit {
private int aliasType;
private IPath searchPath;
public void doit(IResource match) {
//don't record the resource we're computing aliases against as a match
if (match.getFullPath().isPrefixOf(searchPath))
return;
IPath aliasPath= null;
switch (match.getType()) {
case IResource.PROJECT:
//first check if there is a linked resource that blocks the project location
if (suffix.segmentCount() > 0) {
IResource testResource= ((IProject)match).findMember(suffix.segment(0));
if (testResource != null && testResource.isLinked())
return;
}
//there is an alias under this project
aliasPath= match.getFullPath().append(suffix);
break;
case IResource.FOLDER:
aliasPath= match.getFullPath().append(suffix);
break;
case IResource.FILE:
if (suffix.segmentCount() == 0)
aliasPath= match.getFullPath();
break;
}
if (aliasPath != null)
if (aliasType == IResource.FILE) {
aliases.add(workspace.getRoot().getFile(aliasPath));
} else {
if (aliasPath.segmentCount() == 1)
aliases.add(workspace.getRoot().getProject(aliasPath.lastSegment()));
else
aliases.add(workspace.getRoot().getFolder(aliasPath));
}
}
/**
* Sets the resource that we are searching for aliases for.
*/
public void setSearchAlias(IResource aliasResource) {
this.aliasType= aliasResource.getType();
this.searchPath= aliasResource.getFullPath();
}
}
/**
* Maintains a mapping of FileStore->IResource, such that multiple resources mapped from the
* same location are tolerated.
*/
class LocationMap {
/**
* Map of FileStore->IResource OR FileStore->ArrayList of (IResource)
*/
private final SortedMap map= new TreeMap(getComparator());
/**
* Adds the given resource to the map, keyed by the given location. Returns true if a new
* entry was added, and false otherwise.
*/
public boolean add(IFileStore location, IResource resource) {
Object oldValue= map.get(location);
if (oldValue == null) {
map.put(location, resource);
return true;
}
if (oldValue instanceof IResource) {
if (resource.equals(oldValue))
return false;//duplicate
ArrayList newValue= new ArrayList(2);
newValue.add(oldValue);
newValue.add(resource);
map.put(location, newValue);
return true;
}
ArrayList list= (ArrayList)oldValue;
if (list.contains(resource))
return false;//duplicate
list.add(resource);
return true;
}
/**
* Method clear.
*/
public void clear() {
map.clear();
}
/**
* Invoke the given doit for every resource whose location has the given location as a
* prefix.
*/
public void matchingPrefixDo(IFileStore prefix, Doit doit) {
SortedMap matching;
IFileStore prefixParent= prefix.getParent();
if (prefixParent != null) {
//endPoint is the smallest possible path greater than the prefix that doesn't
//match the prefix
IFileStore endPoint= prefixParent.getChild(prefix.getName() + "\0"); //$NON-NLS-1$
matching= map.subMap(prefix, endPoint);
} else {
matching= map;
}
for (Iterator it= matching.values().iterator(); it.hasNext();) {
Object value= it.next();
if (value == null)
return;
if (value instanceof List) {
Iterator duplicates= ((List)value).iterator();
while (duplicates.hasNext())
doit.doit((IResource)duplicates.next());
} else {
doit.doit((IResource)value);
}
}
}
/**
* Invoke the given doit for every resource that matches the given location.
*/
public void matchingResourcesDo(IFileStore location, Doit doit) {
Object value= map.get(location);
if (value == null)
return;
if (value instanceof List) {
Iterator duplicates= ((List)value).iterator();
while (duplicates.hasNext())
doit.doit((IResource)duplicates.next());
} else {
doit.doit((IResource)value);
}
}
/**
* Calls the given doit with the project of every resource in the map whose location
* overlaps another resource in the map.
*/
public void overLappingResourcesDo(Doit doit) {
Iterator entries= map.entrySet().iterator();
IFileStore previousStore= null;
IResource previousResource= null;
while (entries.hasNext()) {
Map.Entry current= (Map.Entry)entries.next();
//value is either single resource or List of resources
IFileStore currentStore= (IFileStore)current.getKey();
IResource currentResource= null;
Object value= current.getValue();
if (value instanceof List) {
//if there are several then they're all overlapping
Iterator duplicates= ((List)value).iterator();
while (duplicates.hasNext())
doit.doit(((IResource)duplicates.next()).getProject());
} else {
//value is a single resource
currentResource= (IResource)value;
}
if (previousStore != null) {
//check for overlap with previous
//Note: previous is always shorter due to map sorting rules
if (previousStore.isParentOf(currentStore)) {
//resources will be null if they were in a list, in which case
//they've already been passed to the doit
if (previousResource != null) {
doit.doit(previousResource.getProject());
//null out previous resource so we don't call doit twice with same resource
previousResource= null;
}
if (currentResource != null)
doit.doit(currentResource.getProject());
//keep iterating with the same previous store because there may be more overlaps
continue;
}
}
previousStore= currentStore;
previousResource= currentResource;
}
}
/**
* Removes the given location from the map. Returns true if anything was actually removed,
* and false otherwise.
*/
public boolean remove(IFileStore location, IResource resource) {
Object oldValue= map.get(location);
if (oldValue == null)
return false;
if (oldValue instanceof IResource) {
if (resource.equals(oldValue)) {
map.remove(location);
return true;
}
return false;
}
ArrayList list= (ArrayList)oldValue;
boolean wasRemoved= list.remove(resource);
if (list.size() == 0)
map.remove(location);
return wasRemoved;
}
}
/**
* Doit convenience class for adding items to a list
*/
private final AddToCollectionDoit addToCollection= new AddToCollectionDoit();
/**
* The set of IProjects that have aliases.
*/
protected final Set aliasedProjects= new HashSet();
/**
* A temporary set of aliases. Used during computeAliases, but maintained as a field as an
* optimization to prevent recreating the set.
*/
protected final HashSet aliases= new HashSet();
/**
* The set of resources that have had structure changes that might invalidate the locations map
* or aliased projects set. These will be updated incrementally on the next alias request.
*/
private final Set changedLinks= new HashSet();
/**
* This flag is true when projects have been created or deleted and the location map has not
* been updated accordingly.
*/
private boolean changedProjects= false;
/**
* The Doit class used for finding aliases.
*/
private final FindAliasesDoit findAliases= new FindAliasesDoit();
/**
* This maps IFileStore ->IResource, associating a file system location with the projects and/or
* linked resources that are rooted at that location.
*/
protected final LocationMap locationsMap= new LocationMap();
/**
* The total number of resources in the workspace that are not in the default location. This
* includes all linked resources, including linked resources that don't currently have valid
* locations due to an undefined path variable. This also includes projects that are not in
* their default location. This value is used as a quick optimization, because a workspace with
* all resources in their default locations cannot have any aliases.
*/
private int nonDefaultResourceCount= 0;
/**
* The suffix object is also used only during the computeAliases method. In this case it is a
* field because it is referenced from an inner class and we want to avoid creating a pointer
* array. It is public to eliminate the need for synthetic accessor methods.
*/
public IPath suffix;
/** the workspace */
protected final Workspace workspace;
public AliasManager(Workspace workspace) {
this.workspace= workspace;
}
private void addToLocationsMap(IProject project) {
IFileStore location= ((Resource)project).getStore();
if (location != null)
locationsMap.add(location, project);
ProjectDescription description= ((Project)project).internalGetDescription();
if (description == null)
return;
if (description.getLocationURI() != null)
nonDefaultResourceCount++;
HashMap links= description.getLinks();
if (links == null)
return;
for (Iterator it= links.values().iterator(); it.hasNext();) {
LinkDescription linkDesc= (LinkDescription)it.next();
IResource link= project.findMember(linkDesc.getProjectRelativePath());
if (link != null) {
try {
URI locationURI= linkDesc.getLocationURI();
locationURI= link.getPathVariableManager().resolveURI(locationURI);
addToLocationsMap(link, EFS.getStore(locationURI));
} catch (CoreException e) {
//ignore links with invalid locations
}
}
}
}
private void addToLocationsMap(IResource link, IFileStore location) {
if (location != null && !link.isVirtual())
if (locationsMap.add(location, link))
nonDefaultResourceCount++;
}
/**
* Builds the table of aliased projects from scratch.
*/
private void buildAliasedProjectsSet() {
aliasedProjects.clear();
//if there are no resources in non-default locations then there can't be any aliased projects
if (nonDefaultResourceCount <= 0)
return;
//for every resource that overlaps another, marked its project as aliased
addToCollection.setCollection(aliasedProjects);
locationsMap.overLappingResourcesDo(addToCollection);
}
/**
* Builds the table of resource locations from scratch. Also computes an initial value for the
* linked resource counter.
*/
private void buildLocationsMap() {
locationsMap.clear();
nonDefaultResourceCount= 0;
//build table of IPath (file system location) -> IResource (project or linked resource)
IProject[] projects= workspace.getRoot().getProjects(IContainer.INCLUDE_HIDDEN);
for (int i= 0; i < projects.length; i++)
if (projects[i].isAccessible())
addToLocationsMap(projects[i]);
}
/**
* A project alias needs updating. If the project location has been deleted, then the project
* should be deleted from the workspace. This differs from the refresh local strategy, but
* operations performed from within the workspace must never leave a resource out of sync.
*
* @param project The project to check for deletion
* @param location The project location
* @return <code>true</code> if the project has been deleted, and <code>false</code> otherwise
* @exception CoreException
*/
private boolean checkDeletion(Project project, IFileStore location) throws CoreException {
if (project.exists() && !location.fetchInfo().exists()) {
//perform internal deletion of project from workspace tree because
// it is already deleted from disk and we can't acquire a different
//scheduling rule in this context (none is needed because we are
//within scope of the workspace lock)
Assert.isTrue(workspace.getWorkManager().getLock().getDepth() > 0);
project.deleteResource(false, null);
return true;
}
return false;
}
/**
* Returns all aliases of the given resource, or null if there are none.
*/
public IResource[] computeAliases(final IResource resource, IFileStore location) {
//nothing to do if we are or were in an alias-free workspace or project
if (hasNoAliases(resource))
return null;
aliases.clear();
internalComputeAliases(resource, location);
int size= aliases.size();
if (size == 0)
return null;
return (IResource[])aliases.toArray(new IResource[size]);
}
/**
* Returns all aliases of this resource, and any aliases of subtrees of this resource. Returns
* null if no aliases are found.
*/
private void computeDeepAliases(IResource resource, IFileStore location) {
//if the location is invalid then there won't be any aliases to update
if (location == null)
return;
//get the normal aliases (resources rooted in parent locations)
internalComputeAliases(resource, location);
//get all resources rooted below this resource's location
addToCollection.setCollection(aliases);
locationsMap.matchingPrefixDo(location, addToCollection);
//if this is a project, get all resources rooted below links in this project
if (resource.getType() == IResource.PROJECT) {
try {
IResource[] members= ((IProject)resource).members();
final FileSystemResourceManager localManager= workspace.getFileSystemManager();
for (int i= 0; i < members.length; i++) {
if (members[i].isLinked()) {
IFileStore linkLocation= localManager.getStore(members[i]);
if (linkLocation != null)
locationsMap.matchingPrefixDo(linkLocation, addToCollection);
}
}
} catch (CoreException e) {
//skip inaccessible projects
}
}
}
/**
* Returns the comparator to use when sorting the locations map. Comparison is based on
* segments, so that paths with the most segments in common will always be adjacent. This is
* equivalent to the natural order on the path strings, with the extra condition that the path
* separator is ordered before all other characters. (Ex: "/foo" < "/foo/zzz" < "/fooaaa").
*/
private Comparator getComparator() {
return new Comparator() {
public int compare(Object o1, Object o2) {
IFileStore store1= (IFileStore)o1;
IFileStore store2= (IFileStore)o2;
//scheme takes precedence over all else
int compare= compareStringOrNull(store1.getFileSystem().getScheme(), store2.getFileSystem().getScheme());
if (compare != 0)
return compare;
// compare based on URI path segment values
final URI uri1;
final URI uri2;
try {
uri1= store1.toURI();
uri2= store2.toURI();
} catch (Exception e) {
//protect against misbehaving 3rd party code in file system implementations
Policy.log(e);
return 1;
}
IPath path1= new Path(uri1.getPath());
IPath path2= new Path(uri2.getPath());
// compare devices
compare= compareStringOrNull(path1.getDevice(), path2.getDevice());
if (compare != 0)
return compare;
// compare segments
int segmentCount1= path1.segmentCount();
int segmentCount2= path2.segmentCount();
for (int i= 0; (i < segmentCount1) && (i < segmentCount2); i++) {
compare= path1.segment(i).compareTo(path2.segment(i));
if (compare != 0)
return compare;
}
//all segments are equal, so compare based on number of segments
compare= segmentCount1 - segmentCount2;
if (compare != 0)
return compare;
//same number of segments, so compare query
return compareStringOrNull(uri1.getQuery(), uri2.getQuery());
}
/**
* Compares two strings that are possibly null.
*/
private int compareStringOrNull(String string1, String string2) {
if (string1 == null) {
if (string2 == null)
return 0;
return 1;
}
if (string2 == null)
return -1;
return string1.compareTo(string2);
}
};
}
public void handleEvent(LifecycleEvent event) {
/*
* We can't determine the end state for most operations because they may
* fail after we receive pre-notification. In these cases, we remember
* the invalidated resources and recompute their state lazily on the
* next alias request.
*/
switch (event.kind) {
case LifecycleEvent.PRE_LINK_CHANGE:
case LifecycleEvent.PRE_LINK_DELETE:
Resource link= (Resource)event.resource;
if (link.isLinked())
removeFromLocationsMap(link, link.getStore());
//fall through
case LifecycleEvent.PRE_FILTER_ADD:
changedLinks.add(event.resource);
break;
case LifecycleEvent.PRE_FILTER_REMOVE:
changedLinks.add(event.resource);
break;
case LifecycleEvent.PRE_LINK_CREATE:
changedLinks.add(event.resource);
break;
case LifecycleEvent.PRE_LINK_COPY:
changedLinks.add(event.newResource);
break;
case LifecycleEvent.PRE_LINK_MOVE:
link= (Resource)event.resource;
if (link.isLinked())
removeFromLocationsMap(link, link.getStore());
changedLinks.add(event.newResource);
break;
}
}
/**
* Returns true if this resource is guaranteed to have no aliases, and false otherwise.
*/
private boolean hasNoAliases(final IResource resource) {
//check if we're in an aliased project or workspace before updating structure changes. In the
//deletion case, we need to know if the resource was in an aliased project *before* deletion.
IProject project= resource.getProject();
boolean noAliases= !aliasedProjects.contains(project);
//now update any structure changes and check again if an update is needed
if (hasStructureChanges()) {
updateStructureChanges();
noAliases&= nonDefaultResourceCount <= 0 || !aliasedProjects.contains(project);
}
return noAliases;
}
/**
* Returns whether there are any structure changes that we have not yet processed.
*/
private boolean hasStructureChanges() {
return changedProjects || !changedLinks.isEmpty();
}
/**
* Computes the aliases of the given resource at the given location, and adds them to the
* "aliases" collection.
*/
private void internalComputeAliases(IResource resource, IFileStore location) {
IFileStore searchLocation= location;
if (searchLocation == null)
searchLocation= ((Resource)resource).getStore();
//if the location is invalid then there won't be any aliases to update
if (searchLocation == null)
return;
suffix= Path.EMPTY;
findAliases.setSearchAlias(resource);
/*
* Walk up the location segments for this resource, looking for a
* resource with a matching location. All matches are then added to the
* "aliases" set.
*/
do {
locationsMap.matchingResourcesDo(searchLocation, findAliases);
suffix= new Path(searchLocation.getName()).append(suffix);
searchLocation= searchLocation.getParent();
} while (searchLocation != null);
}
private void removeFromLocationsMap(IResource link, IFileStore location) {
if (location != null)
if (locationsMap.remove(location, link))
nonDefaultResourceCount--;
}
public void resourceChanged(IResourceChangeEvent event) {
final IResourceDelta delta= event.getDelta();
if (delta == null)
return;
//invalidate location map if there are added or removed projects.
if (delta.getAffectedChildren(IResourceDelta.ADDED | IResourceDelta.REMOVED).length > 0)
changedProjects= true;
// invalidate location map if any project has the description changed
// or was closed/opened
IResourceDelta[] changed= delta.getAffectedChildren(IResourceDelta.CHANGED);
for (int i= 0; i < changed.length; i++) {
if ((changed[i].getFlags() & IResourceDelta.DESCRIPTION) == IResourceDelta.DESCRIPTION
|| (changed[i].getFlags() & IResourceDelta.OPEN) == IResourceDelta.OPEN) {
changedProjects= true;
break;
}
}
}
/* (non-Javadoc)
* @see IManager#shutdown(IProgressMonitor)
*/
public void shutdown(IProgressMonitor monitor) {
workspace.removeResourceChangeListener(this);
locationsMap.clear();
}
/* (non-Javadoc)
* @see IManager#startup(IProgressMonitor)
*/
public void startup(IProgressMonitor monitor) {
workspace.addLifecycleListener(this);
workspace.addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE);
buildLocationsMap();
buildAliasedProjectsSet();
}
/**
* The file underlying the given resource has changed on disk. Compute all aliases for this
* resource and update them. This method will not attempt to incur any units of work on the
* given progress monitor, but it may update the subtask to reflect what aliases are being
* updated.
*
* @param resource the resource to compute aliases for
* @param location the file system location of the resource (passed as a parameter because in
* the project deletion case the resource is no longer accessible at time of update).
* @param depth whether to search for aliases on all children of the given resource. Only depth
* ZERO and INFINITE are used.
*/
public void updateAliases(IResource resource, IFileStore location, int depth, IProgressMonitor monitor) throws CoreException {
if (hasNoAliases(resource))
return;
aliases.clear();
if (depth == IResource.DEPTH_ZERO)
internalComputeAliases(resource, location);
else
computeDeepAliases(resource, location);
if (aliases.size() == 0)
return;
FileSystemResourceManager localManager= workspace.getFileSystemManager();
HashSet aliasesCopy= (HashSet)aliases.clone();
for (Iterator it= aliasesCopy.iterator(); it.hasNext();) {
IResource alias= (IResource)it.next();
monitor.subTask(NLS.bind(Messages.links_updatingDuplicate, alias.getFullPath()));
if (alias.getType() == IResource.PROJECT) {
if (checkDeletion((Project)alias, location))
continue;
//project did not require deletion, so fall through below and refresh it
}
if (!((Resource)alias).isFiltered())
localManager.refresh(alias, IResource.DEPTH_INFINITE, false, null);
}
}
/**
* Process any structural changes that have occurred since the last alias request.
*/
private void updateStructureChanges() {
boolean hadChanges= false;
if (changedProjects) {
//if a project is added or removed, just recompute the whole world
changedProjects= false;
hadChanges= true;
buildLocationsMap();
} else {
//incrementally update location map for changed links
for (Iterator it= changedLinks.iterator(); it.hasNext();) {
IResource resource= (IResource)it.next();
hadChanges= true;
if (!resource.isAccessible())
continue;
if (resource.isLinked())
addToLocationsMap(resource, ((Resource)resource).getStore());
}
}
changedLinks.clear();
if (hadChanges)
buildAliasedProjectsSet();
changedProjects= false;
}
}