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