blob: 25a3edf5d5c5fc9f6693e88f2d4bbd2c32ab0127 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2015 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* 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
* Sergey Prigogin (Google) - [338010] Resource.createLink() does not preserve symbolic links
* Lars Vogel <Lars.Vogel@vogella.com> - Bug 473427
*******************************************************************************/
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.*;
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<IResource> collection;
@Override
public void doit(IResource resource) {
collection.add(resource);
}
public void setCollection(Collection<IResource> collection) {
this.collection = collection;
}
}
interface Doit {
void doit(IResource resource);
}
class FindAliasesDoit implements Doit {
private int aliasType;
private IPath searchPath;
@Override
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-&gt;IResource, such that multiple resources
* mapped from the same location are tolerated.
*/
class LocationMap {
/**
* Map of FileStore-&gt;IResource OR FileStore-&gt;ArrayList of (IResource)
*/
private final SortedMap<IFileStore, Object> 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<Object> newValue = new ArrayList<>(2);
newValue.add(oldValue);
newValue.add(resource);
map.put(location, newValue);
return true;
}
@SuppressWarnings("unchecked")
ArrayList<IResource> list = (ArrayList<IResource>) 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<IFileStore, Object> 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 (Object value : matching.values()) {
if (value == null)
return;
if (value instanceof List) {
@SuppressWarnings("unchecked")
Iterator<IResource> duplicates = ((List<IResource>) value).iterator();
while (duplicates.hasNext())
doit.doit(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) {
@SuppressWarnings("unchecked")
Iterator<IResource> duplicates = ((List<IResource>) value).iterator();
while (duplicates.hasNext())
doit.doit(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<Map.Entry<IFileStore, Object>> entries = map.entrySet().iterator();
IFileStore previousStore = null;
IResource previousResource = null;
while (entries.hasNext()) {
Map.Entry<IFileStore, Object> current = entries.next();
//value is either single resource or List of resources
IFileStore currentStore = current.getKey();
IResource currentResource = null;
Object value = current.getValue();
if (value instanceof List) {
//if there are several then they're all overlapping
@SuppressWarnings("unchecked")
Iterator<IResource> duplicates = ((List<IResource>) value).iterator();
while (duplicates.hasNext())
doit.doit(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;
}
@SuppressWarnings("unchecked")
ArrayList<IResource> list = (ArrayList<IResource>) oldValue;
boolean wasRemoved = list.remove(resource);
if (list.isEmpty())
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<IResource> 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<IResource> 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<IResource> 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 -&gt; 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<IPath, LinkDescription> links = description.getLinks();
if (links == null)
return;
for (LinkDescription linkDesc : links.values()) {
IResource link = project.findMember(linkDesc.getProjectRelativePath());
if (link != null) {
try {
URI locationURI = linkDesc.getLocationURI();
locationURI = FileUtil.canonicalURI(locationURI);
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 (IProject project : projects)
if (project.isAccessible())
addToLocationsMap(project);
}
/**
* 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 aliases.toArray(new IResource[size]);
}
/**
* Returns all resources pointing to the given location, or an empty array if there are none.
*/
public IResource[] findResources(IFileStore location) {
final ArrayList<IResource> resources = new ArrayList<>();
locationsMap.matchingResourcesDo(location, resource -> resources.add(resource));
return resources.toArray(new IResource[0]);
}
/**
* 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 (IResource member : members) {
if (member.isLinked()) {
IFileStore linkLocation = localManager.getStore(member);
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" &lt; "/foo/zzz" &lt; "/fooaaa").
*/
Comparator<IFileStore> getComparator() {
return new Comparator<IFileStore>() {
@Override
public int compare(IFileStore store1, IFileStore store2) {
//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;
}
// compare hosts
compare = compareStringOrNull(uri1.getHost(), uri2.getHost());
if (compare != 0)
return compare;
// compare user infos
compare = compareStringOrNull(uri1.getUserInfo(), uri2.getUserInfo());
if (compare != 0)
return compare;
// compare ports
int port1 = uri1.getPort();
int port2 = uri2.getPort();
if (port1 != port2)
return port1 - port2;
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);
}
};
}
@Override
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--;
}
@Override
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, IContainer.INCLUDE_HIDDEN).length > 0)
changedProjects = true;
// invalidate location map if any project has the description changed
// or was closed/opened
IResourceDelta[] changed = delta.getAffectedChildren(IResourceDelta.CHANGED, IContainer.INCLUDE_HIDDEN);
for (IResourceDelta element : changed) {
if ((element.getFlags() & IResourceDelta.DESCRIPTION) == IResourceDelta.DESCRIPTION || (element.getFlags() & IResourceDelta.OPEN) == IResourceDelta.OPEN) {
changedProjects = true;
break;
}
}
}
@Override
public void shutdown(IProgressMonitor monitor) {
workspace.removeResourceChangeListener(this);
locationsMap.clear();
}
@Override
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.
*/
@SuppressWarnings({"unchecked"})
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.isEmpty())
return;
FileSystemResourceManager localManager = workspace.getFileSystemManager();
HashSet<IResource> aliasesCopy = (HashSet<IResource>) aliases.clone();
for (IResource alias : aliasesCopy) {
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 (IResource resource : changedLinks) {
hadChanges = true;
if (!resource.isAccessible())
continue;
if (resource.isLinked())
addToLocationsMap(resource, ((Resource) resource).getStore());
}
}
changedLinks.clear();
if (hadChanges)
buildAliasedProjectsSet();
changedProjects = false;
}
}