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
/******************************************************************************* * 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.

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.

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.

/** * 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 locations, hashes and projects. Necessary because source lookup queries and java model changes are processed on different threads.
@TODOconsider using ConcurrentMaps instead of explicit locking.
/** * 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.
/** * 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.
/** * 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.
/** * 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 null if there is no such workspace project.
/** * 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 null if there is no such project classpath entry or if the classpath entry does not have associated sources.
/** * 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)); } } }