blob: 792d543aa2a8e35bbf1d95d372d80251c73892f2 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012-2016 Igor Fedorenko
*
* 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:
* Igor Fedorenko - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.internal.launching.sourcelookup.advanced;
import static org.eclipse.jdt.core.IJavaElementDelta.F_ADDED_TO_CLASSPATH;
import static org.eclipse.jdt.core.IJavaElementDelta.F_CLASSPATH_CHANGED;
import static org.eclipse.jdt.core.IJavaElementDelta.F_CLOSED;
import static org.eclipse.jdt.core.IJavaElementDelta.F_OPENED;
import static org.eclipse.jdt.core.IJavaElementDelta.F_REMOVED_FROM_CLASSPATH;
import static org.eclipse.jdt.core.IJavaElementDelta.F_RESOLVED_CLASSPATH_CHANGED;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.debug.core.sourcelookup.ISourceContainer;
import org.eclipse.jdt.core.ElementChangedEvent;
import org.eclipse.jdt.core.IElementChangedListener;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaElementDelta;
import org.eclipse.jdt.core.IJavaModel;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.launching.sourcelookup.advanced.FileHashing.Hasher;
import org.eclipse.jdt.launching.sourcelookup.advanced.IWorkspaceProjectDescriber;
import org.eclipse.jdt.launching.sourcelookup.advanced.IWorkspaceProjectDescriber.IJavaProjectSourceDescription;
import org.eclipse.jdt.launching.sourcelookup.containers.PackageFragmentRootSourceContainer;
/**
* Workspace project source container factory.
*
* <p>
* The factory creates both project and project classpath entry containers. Both projects and project classpath entries can be identified by their
* filesystem location and, if the location is a file, by the file SHA1 checksum.
*
* <p>
* The factory maintains up-to-date registry of workspace projects and their classpath entries and can be used to create source containers fast enough
* to be used from UI thread.
*/
public class WorkspaceProjectSourceContainers {
private final IElementChangedListener changeListener = new IElementChangedListener() {
@Override
public void elementChanged(ElementChangedEvent event) {
try {
final Set<IJavaProject> remove = new HashSet<>();
final Set<IJavaProject> add = new HashSet<>();
processDelta(event.getDelta(), remove, add);
if (!remove.isEmpty() || !add.isEmpty()) {
AdvancedSourceLookupSupport.schedule((m) -> updateProjects(remove, add, m));
}
}
catch (CoreException e) {
// maybe do something about it?
}
}
private void processDelta(final IJavaElementDelta delta, Set<IJavaProject> remove, Set<IJavaProject> add) throws CoreException {
// TODO review, this looks too complicated to add/remove java projects
final IJavaElement element = delta.getElement();
final int kind = delta.getKind();
switch (element.getElementType()) {
case IJavaElement.JAVA_MODEL:
processChangedChildren(delta, remove, add);
break;
case IJavaElement.JAVA_PROJECT:
switch (kind) {
case IJavaElementDelta.REMOVED:
remove.add((IJavaProject) element);
break;
case IJavaElementDelta.ADDED:
add.add((IJavaProject) element);
break;
case IJavaElementDelta.CHANGED:
if ((delta.getFlags() & F_CLOSED) != 0) {
remove.add((IJavaProject) element);
} else if ((delta.getFlags() & F_OPENED) != 0) {
add.add((IJavaProject) element);
} else if ((delta.getFlags() & (F_CLASSPATH_CHANGED | F_RESOLVED_CLASSPATH_CHANGED)) != 0) {
remove.add((IJavaProject) element);
add.add((IJavaProject) element);
}
break;
}
processChangedChildren(delta, remove, add);
break;
case IJavaElement.PACKAGE_FRAGMENT_ROOT:
if ((delta.getFlags() & (F_ADDED_TO_CLASSPATH | F_REMOVED_FROM_CLASSPATH)) != 0) {
remove.add(element.getJavaProject());
add.add(element.getJavaProject());
}
break;
}
}
private void processChangedChildren(IJavaElementDelta delta, Set<IJavaProject> remove, Set<IJavaProject> add) throws CoreException {
for (IJavaElementDelta childDelta : delta.getAffectedChildren()) {
processDelta(childDelta, remove, add);
}
}
};
private static class JavaProjectDescriptionBuilder implements IJavaProjectSourceDescription {
final Set<File> locations = new HashSet<>();
final List<Supplier<ISourceContainer>> factories = new ArrayList<>();
final Map<File, IPackageFragmentRoot> dependencyLocations = new HashMap<>();
@Override
public void addLocation(File location) {
locations.add(location);
}
@Override
public void addSourceContainerFactory(Supplier<ISourceContainer> factory) {
factories.add(factory);
}
@Override
public void addDependencies(Map<File, IPackageFragmentRoot> dependencies) {
// TODO decide what happens if the same location is associated with multiple package fragment roots
this.dependencyLocations.putAll(dependencies);
}
}
private static class JavaProjectDescription {
final Set<File> classesLocations;
final Set<Object> classesLocationsHashes;
final List<Supplier<ISourceContainer>> sourceContainerFactories;
final Map<File, IPackageFragmentRoot> dependencies;
final Map<Object, IPackageFragmentRoot> dependencyHashes;
public JavaProjectDescription(Set<File> locations, Set<Object> hashes, List<Supplier<ISourceContainer>> factories, Map<File, IPackageFragmentRoot> dependencies, Map<Object, IPackageFragmentRoot> dependencyHashes) {
this.classesLocations = Collections.unmodifiableSet(locations);
this.classesLocationsHashes = Collections.unmodifiableSet(hashes);
this.sourceContainerFactories = Collections.unmodifiableList(factories);
this.dependencies = Collections.unmodifiableMap(dependencies);
this.dependencyHashes = Collections.unmodifiableMap(dependencyHashes);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof JavaProjectDescription)) {
return false;
}
JavaProjectDescription other = (JavaProjectDescription) obj;
return classesLocations.equals(other.classesLocations);
}
@Override
public int hashCode() {
return classesLocations.hashCode();
}
}
/**
* Guards concurrent access to {@link #locations}, {@link #hashes} and {@link #projects}. Necessary because source lookup queries and java model
* changes are processed on different threads.
*
* @TODO consider using ConcurrentMaps instead of explicit locking.
*/
private final Object lock = new Object() {
};
/**
* Maps project classes location to project description.
*/
private final Map<File, JavaProjectDescription> locations = new HashMap<>();
/**
* Maps project dependency hash to project descriptions. Hash-based source lookup is useful when runtime uses copies of jars used by the
* workspace.
*/
private final Map<Object, Collection<JavaProjectDescription>> hashes = new HashMap<>();
/**
* Maps java project to project description.
*/
private final Map<IJavaProject, JavaProjectDescription> projects = new HashMap<>();
/**
* Creates and returns new source containers for the workspace project identified by the given location. Returns {@code null} if there is no such
* workspace project.
*/
public ISourceContainer createProjectContainer(File projectLocation) {
Hasher hasher = FileHashing.hasher(); // use long-lived hasher
JavaProjectDescription description = getProjectByLocation(projectLocation);
if (description == null) {
Collection<JavaProjectDescription> desciptions = getProjectsByHash(projectLocation, hasher);
if (!desciptions.isEmpty()) {
// it is possible, but unlikely, to have multiple binary projects for the same jar
description = desciptions.iterator().next();
}
}
if (description == null) {
return null;
}
List<ISourceContainer> containers = new ArrayList<>();
for (Supplier<ISourceContainer> factory : description.sourceContainerFactories) {
containers.add(factory.get());
}
return CompositeSourceContainer.compose(containers);
}
private JavaProjectDescription getProjectByLocation(File projectLocation) {
synchronized (lock) {
return locations.get(projectLocation);
}
}
private Collection<JavaProjectDescription> getProjectsByHash(File projectLocation, FileHashing.Hasher hasher) {
Collection<JavaProjectDescription> projects;
synchronized (lock) {
projects = hashes.get(hasher.hash(projectLocation));
return projects != null ? new HashSet<>(projects) : Collections.emptySet();
}
}
/**
* Creates and returns new source container for the workspace project classpath entry identified by the given project and entry locations. Returns
* {@code null} if there is no such project classpath entry or if the classpath entry does not have associated sources.
*/
public ISourceContainer createClasspathEntryContainer(File projectLocation, File entryLocation) {
Hasher hasher = FileHashing.hasher(); // use long-lived hasher
JavaProjectDescription projectByLocation = getProjectByLocation(projectLocation);
IPackageFragmentRoot dependency = getProjectDependency(projectByLocation, entryLocation, hasher);
if (dependency == null && projectByLocation == null) {
for (JavaProjectDescription projectByHash : getProjectsByHash(projectLocation, hasher)) {
dependency = getProjectDependency(projectByHash, entryLocation, hasher);
if (dependency != null) {
break;
}
}
}
try {
if (dependency == null || (dependency.getKind() == IPackageFragmentRoot.K_BINARY && dependency.getSourceAttachmentPath() == null)) {
return null;
}
} catch (JavaModelException e) {
return null;
}
return new PackageFragmentRootSourceContainer(dependency);
}
private IPackageFragmentRoot getProjectDependency(JavaProjectDescription project, File entryLocation, FileHashing.Hasher hasher) {
if (project == null) {
return null;
}
IPackageFragmentRoot dependency = project.dependencies.get(entryLocation);
if (dependency == null) {
dependency = project.dependencyHashes.get(hasher.hash(entryLocation));
}
return dependency;
}
public void initialize(IProgressMonitor monitor) throws CoreException {
// note that initialization and java element change events are processed by the same background job
// this guarantees the events aren't lost when they are delivered while the initialization is running
JavaCore.addElementChangedListener(changeListener);
final IJavaModel javaModel = JavaCore.create(ResourcesPlugin.getWorkspace().getRoot());
final IJavaProject[] javaProjects = javaModel.getJavaProjects();
SubMonitor progress = SubMonitor.convert(monitor, javaProjects.length);
// TODO this can take significant time for large workspaces, consider running on multiple threads
// NB: can't persist state across restarts because java element change events are not delivered when this plugin isn't active
Hasher hasher = FileHashing.newHasher(); // short-lived hasher for bulk workspace indexing
List<IWorkspaceProjectDescriber> describers = getJavaProjectDescribers();
for (IJavaProject project : javaProjects) {
addJavaProject(project, describers, hasher, progress.split(1));
}
}
public void close() {
JavaCore.removeElementChangedListener(changeListener);
synchronized (lock) {
this.locations.clear();
this.hashes.clear();
this.projects.clear();
}
}
private void addJavaProject(IJavaProject project, List<IWorkspaceProjectDescriber> describers, FileHashing.Hasher hasher, IProgressMonitor monitor) throws CoreException {
if (project == null) {
throw new IllegalArgumentException();
}
JavaProjectDescriptionBuilder builder = new JavaProjectDescriptionBuilder();
for (IWorkspaceProjectDescriber describer : describers) {
describer.describeProject(project, builder);
}
Set<File> locations = builder.locations;
List<Supplier<ISourceContainer>> factories = builder.factories;
Map<File, IPackageFragmentRoot> dependencies = builder.dependencyLocations;
// make binary project support little easier to implement
locations.forEach(location -> dependencies.remove(location));
Set<Object> hashes = new HashSet<>();
locations.forEach(location -> {
Object hash = hasher.hash(location);
if (hash != null) {
hashes.add(hash);
}
});
Map<Object, IPackageFragmentRoot> dependencyHashes = new HashMap<>();
dependencies.forEach((location, packageFragmentRoot) -> dependencyHashes.put(hasher.hash(location), packageFragmentRoot));
JavaProjectDescription info = new JavaProjectDescription(locations, hashes, factories, dependencies, dependencyHashes);
synchronized (this.lock) {
for (File location : locations) {
this.locations.put(location, info);
}
for (Object hash : hashes) {
Collection<JavaProjectDescription> hashProjects = this.hashes.get(hash);
if (hashProjects == null) {
hashProjects = new HashSet<>();
this.hashes.put(hash, hashProjects);
}
hashProjects.add(info);
}
this.projects.put(project, info);
}
SubMonitor.done(monitor);
}
protected List<IWorkspaceProjectDescriber> getJavaProjectDescribers() {
List<IWorkspaceProjectDescriber> result = new ArrayList<>();
IExtensionRegistry registry = Platform.getExtensionRegistry();
IConfigurationElement[] elements = registry.getConfigurationElementsFor(AdvancedSourceLookupSupport.ID_workspaceProjectDescribers);
for (IConfigurationElement element : elements) {
if ("describer".equals(element.getName())) { //$NON-NLS-1$
try {
result.add((IWorkspaceProjectDescriber) element.createExecutableExtension("class")); //$NON-NLS-1$
}
catch (CoreException e) {
}
}
}
result.add(new DefaultProjectDescriber());
return result;
}
private void removeJavaProject(IJavaProject project) {
if (project == null) {
throw new IllegalArgumentException();
}
synchronized (lock) {
JavaProjectDescription description = projects.remove(project);
if (description != null) {
for (File location : description.classesLocations) {
locations.remove(location);
}
for (Object hash : description.classesLocationsHashes) {
Collection<JavaProjectDescription> hashProjects = hashes.get(hash);
if (hashProjects != null) {
hashProjects.remove(description);
if (hashProjects.isEmpty()) {
hashes.remove(hash);
}
}
}
}
}
}
void updateProjects(final Set<IJavaProject> remove, final Set<IJavaProject> add, IProgressMonitor monitor) throws CoreException {
SubMonitor progress = SubMonitor.convert(monitor, 1 + add.size());
progress.split(1);
for (IJavaProject project : remove) {
removeJavaProject(project);
}
List<IWorkspaceProjectDescriber> describers = getJavaProjectDescribers();
Hasher hasher = FileHashing.newHasher();
for (IJavaProject project : add) {
addJavaProject(project, describers, hasher, progress.split(1));
}
}
}