diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ISourceLookUpProvider.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ISourceLookUpProvider.java index 12f9756ab..233d539e3 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ISourceLookUpProvider.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ISourceLookUpProvider.java @@ -18,6 +18,15 @@ public interface ISourceLookUpProvider extends IProvider { String[] getFullyQualifiedName(String uri, int[] lines, int[] columns) throws DebugException; + /** + * Given a fully qualified class name and source file path, search the associated disk source file. + * + * @param fullyQualifiedName + * the fully qualified class name (e.g. com.microsoft.java.debug.core.adapter.ISourceLookUpProvider). + * @param sourcePath + * the qualified source file path (e.g. com\microsoft\java\debug\core\adapter\ISourceLookupProvider.java). + * @return the associated source file uri. + */ String getSourceFileURI(String fullyQualifiedName, String sourcePath); String getSourceContents(String uri); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java index e6c3c65db..4bae86c58 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java @@ -11,6 +11,7 @@ package com.microsoft.java.debug.core.adapter.handler; +import java.io.File; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; @@ -123,7 +124,7 @@ private Types.Source convertDebuggerSourceToClient(Location location, IDebugAdap } catch (AbsentInformationException e) { String enclosingType = AdapterUtils.parseEnclosingType(fullyQualifiedName); sourceName = enclosingType.substring(enclosingType.lastIndexOf('.') + 1) + ".java"; - relativeSourcePath = enclosingType.replace('.', '/') + ".java"; + relativeSourcePath = enclosingType.replace('.', File.separatorChar) + ".java"; } final String finalRelativeSourcePath = relativeSourcePath; diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProvider.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProvider.java index 46ef62f26..fdd372e6f 100644 --- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProvider.java +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProvider.java @@ -20,31 +20,22 @@ import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import org.eclipse.core.resources.IResource; +import org.eclipse.debug.core.sourcelookup.ISourceContainer; import org.eclipse.jdt.core.IBuffer; import org.eclipse.jdt.core.IClassFile; import org.eclipse.jdt.core.IJavaElement; -import org.eclipse.jdt.core.IJavaProject; -import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.ITypeRoot; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.CompilationUnit; -import org.eclipse.jdt.core.search.IJavaSearchConstants; -import org.eclipse.jdt.core.search.IJavaSearchScope; -import org.eclipse.jdt.core.search.SearchEngine; -import org.eclipse.jdt.core.search.SearchMatch; -import org.eclipse.jdt.core.search.SearchParticipant; -import org.eclipse.jdt.core.search.SearchPattern; -import org.eclipse.jdt.core.search.SearchRequestor; import org.eclipse.jdt.internal.debug.core.breakpoints.ValidBreakpointLocationLocator; import com.microsoft.java.debug.core.Configuration; @@ -58,6 +49,7 @@ public class JdtSourceLookUpProvider implements ISourceLookUpProvider { private static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); private static final String JDT_SCHEME = "jdt"; private static final String PATH_SEPARATOR = "/"; + private ISourceContainer[] sourceContainers = null; private HashMap options = new HashMap(); @@ -67,6 +59,10 @@ public void initialize(IDebugSession debugSession, Map props) { throw new IllegalArgumentException("argument is null"); } options.putAll(props); + // During initialization, trigger a background job to load the source containers to improve the perf. + new Thread(() -> { + getSourceContainers(); + }).start(); } @Override @@ -164,17 +160,32 @@ public String[] getFullyQualifiedName(String uri, int[] lines, int[] columns) th @Override public String getSourceFileURI(String fullyQualifiedName, String sourcePath) { - if (fullyQualifiedName == null) { - throw new IllegalArgumentException("fullyQualifiedName is null"); + if (sourcePath == null) { + return null; } - // Jdt Search Engine doesn't support searching anonymous class or local type directly. - // But because the inner class and anonymous class have the same java file as the enclosing type, - // search their enclosing type instead. - if (fullyQualifiedName.indexOf("$") >= 0) { - return searchDeclarationFileByFqn(AdapterUtils.parseEnclosingType(fullyQualifiedName)); - } else { - return searchDeclarationFileByFqn(fullyQualifiedName); + + Object sourceElement = JdtUtils.findSourceElement(sourcePath, getSourceContainers()); + if (sourceElement instanceof IResource) { + return getFileURI((IResource) sourceElement); + } else if (sourceElement instanceof IClassFile) { + try { + IClassFile file = (IClassFile) sourceElement; + if (file.getBuffer() != null) { + return getFileURI(file); + } + } catch (JavaModelException e) { + // do nothing. + } + } + return null; + } + + private synchronized ISourceContainer[] getSourceContainers() { + if (sourceContainers == null) { + sourceContainers = JdtUtils.getSourceContainers((String) options.get(Constants.PROJECTNAME)); } + + return sourceContainers; } @Override @@ -204,50 +215,6 @@ private String getContents(IClassFile cf) { return source; } - private String searchDeclarationFileByFqn(String fullyQualifiedName) { - String projectName = (String) options.get(Constants.PROJECTNAME); - IJavaProject project = JdtUtils.getJavaProject(projectName); - IJavaSearchScope searchScope = createSearchScope(project); - SearchPattern pattern = SearchPattern.createPattern( - fullyQualifiedName, - IJavaSearchConstants.TYPE, - IJavaSearchConstants.DECLARATIONS, - SearchPattern.R_EXACT_MATCH); - - ArrayList uris = new ArrayList(); - - SearchRequestor requestor = new SearchRequestor() { - @Override - public void acceptSearchMatch(SearchMatch match) { - Object element = match.getElement(); - if (element instanceof IType) { - IType type = (IType) element; - if (type.isBinary()) { - try { - // let the search engine to ignore those class files without attached source. - if (type.getSource() != null) { - uris.add(getFileURI(type.getClassFile())); - } - } catch (JavaModelException e) { - // ignore - } - } else { - uris.add(getFileURI(type.getResource())); - } - } - } - }; - SearchEngine searchEngine = new SearchEngine(); - try { - searchEngine.search(pattern, new SearchParticipant[] { - SearchEngine.getDefaultSearchParticipant() - }, searchScope, requestor, null /* progress monitor */); - } catch (Exception e) { - logger.log(Level.SEVERE, String.format("Search engine failed: %s", e.toString()), e); - } - return uris.size() == 0 ? null : uris.get(0); - } - private static String getFileURI(IClassFile classFile) { String packageName = classFile.getParent().getElementName(); String jarName = classFile.getParent().getParent().getElementName(); @@ -288,14 +255,6 @@ private static IClassFile resolveClassFile(String uriString) { return null; } - private static IJavaSearchScope createSearchScope(IJavaProject project) { - if (project == null) { - return SearchEngine.createWorkspaceScope(); - } - return SearchEngine.createJavaSearchScope(new IJavaProject[] {project}, - IJavaSearchScope.SOURCES | IJavaSearchScope.APPLICATION_LIBRARIES | IJavaSearchScope.SYSTEM_LIBRARIES); - } - private static String readFile(String filePath, Charset cs) { StringBuilder builder = new StringBuilder(); try (BufferedReader bufferReader = @@ -314,4 +273,5 @@ private static String readFile(String filePath, Charset cs) { } return builder.toString(); } + } diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtUtils.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtUtils.java index ad3f6fc29..cb652134c 100644 --- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtUtils.java +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtUtils.java @@ -11,14 +11,33 @@ package com.microsoft.java.debug.plugin.internal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.debug.core.sourcelookup.ISourceContainer; +import org.eclipse.jdt.core.IClassFile; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IModuleDescription; +import org.eclipse.jdt.core.IPackageFragmentRoot; import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.launching.IRuntimeClasspathEntry; import org.eclipse.jdt.launching.JavaRuntime; +import org.eclipse.jdt.launching.sourcelookup.containers.JavaProjectSourceContainer; +import org.eclipse.jdt.launching.sourcelookup.containers.PackageFragmentRootSourceContainer; + +import com.sun.jdi.AbsentInformationException; +import com.sun.jdi.Location; +import com.sun.jdi.StackFrame; public class JdtUtils { @@ -78,4 +97,167 @@ public static IJavaProject getJavaProject(String projectName) { IProject project = root.getProject(projectName); return getJavaProject(project); } + + /** + * Given the project name, return the corresponding project object. + * If the project doesn't exist, return null. + */ + public static IProject getProject(String projectName) { + if (projectName == null) { + return null; + } + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + return root.getProject(projectName); + } + + /** + * Compute the possible source containers that the specified project could be associated with. + *

+ * If the project name is specified, it will put the source containers parsed from the specified project's + * classpath entries in the front of the result, then the other projects at the same workspace. + *

+ *

+ * Otherwise, just loop every projects at the current workspace and combine the parsed source containers directly. + *

+ * @param projectName + * the project name. + * @return the possible source container list. + */ + public static ISourceContainer[] getSourceContainers(String projectName) { + Set containers = new LinkedHashSet<>(); + List projects = new ArrayList<>(); + + // If the project name is specified, firstly compute the source containers from the specified project's + // classpath entries so that they can be placed in the front of the result. + IProject targetProject = JdtUtils.getProject(projectName); + if (targetProject != null) { + projects.add(targetProject); + } + + List workspaceProjects = Arrays.asList(ResourcesPlugin.getWorkspace().getRoot().getProjects()); + projects.addAll(workspaceProjects); + + Set calculated = new LinkedHashSet<>(); + + projects.stream().distinct().map(project -> JdtUtils.getJavaProject(project)) + .filter(javaProject -> javaProject != null && javaProject.exists()) + .forEach(javaProject -> { + // Add source containers associated with the project's runtime classpath entries. + containers.addAll(Arrays.asList(getSourceContainers(javaProject, calculated))); + // Add source containers associated with the project's source folders. + containers.add(new JavaProjectSourceContainer(javaProject)); + }); + + return containers.toArray(new ISourceContainer[0]); + } + + private static ISourceContainer[] getSourceContainers(IJavaProject project, Set calculated) { + if (project == null || !project.exists()) { + return new ISourceContainer[0]; + } + + try { + IRuntimeClasspathEntry[] unresolved = JavaRuntime.computeUnresolvedRuntimeClasspath(project); + List resolved = new ArrayList<>(); + for (IRuntimeClasspathEntry entry : unresolved) { + for (IRuntimeClasspathEntry resolvedEntry : JavaRuntime.resolveRuntimeClasspathEntry(entry, project)) { + if (!calculated.contains(resolvedEntry)) { + calculated.add(resolvedEntry); + resolved.add(resolvedEntry); + } + } + } + Set containers = new LinkedHashSet<>(); + containers.addAll(Arrays.asList( + JavaRuntime.getSourceContainers(resolved.toArray(new IRuntimeClasspathEntry[0])))); + + // Due to a known jdt java 9 support bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=525840, + // it would miss some JRE libraries source containers when the debugger is running on JDK9. + // As a workaround, recompute the possible source containers for JDK9 jrt-fs.jar libraries. + IRuntimeClasspathEntry jrtFs = resolved.stream().filter(entry -> { + return entry.getType() == IRuntimeClasspathEntry.ARCHIVE && entry.getPath().lastSegment().equals("jrt-fs.jar"); + }).findFirst().orElse(null); + if (jrtFs != null && project.isOpen()) { + IPackageFragmentRoot[] allRoots = project.getPackageFragmentRoots(); + for (IPackageFragmentRoot root : allRoots) { + if (root.getPath().equals(jrtFs.getPath()) && isSourceAttachmentEqual(root, jrtFs)) { + containers.add(new PackageFragmentRootSourceContainer(root)); + } + } + } + + return containers.toArray(new ISourceContainer[0]); + } catch (CoreException ex) { + // do nothing. + } + + return new ISourceContainer[0]; + } + + private static boolean isSourceAttachmentEqual(IPackageFragmentRoot root, IRuntimeClasspathEntry entry) throws JavaModelException { + IPath entryPath = entry.getSourceAttachmentPath(); + if (entryPath == null) { + return true; + } + IPath rootPath = root.getSourceAttachmentPath(); + if (rootPath == null) { + // entry has a source attachment that the package root does not + return false; + } + return rootPath.equals(entryPath); + + } + + /** + * Given a source name info, search the associated source file or class file from the source container list. + * + * @param sourcePath + * the target source name (e.g. com\microsoft\java\debug\xxx.java). + * @param containers + * the source container list. + * @return the associated source file or class file. + */ + public static Object findSourceElement(String sourcePath, ISourceContainer[] containers) { + if (containers == null) { + return null; + } + for (ISourceContainer container : containers) { + try { + Object[] objects = container.findSourceElements(sourcePath); + if (objects.length > 0 && (objects[0] instanceof IResource || objects[0] instanceof IClassFile)) { + return objects[0]; + } + } catch (CoreException e) { + // do nothing. + } + } + return null; + } + + /** + * Given a stack frame, find the target project that the associated source file belongs to. + * + * @param stackFrame + * the stack frame. + * @param containers + * the source container list. + * @return the context project. + */ + public static IProject findProject(StackFrame stackFrame, ISourceContainer[] containers) { + Location location = stackFrame.location(); + try { + Object sourceElement = findSourceElement(location.sourcePath(), containers); + if (sourceElement instanceof IResource) { + return ((IResource) sourceElement).getProject(); + } else if (sourceElement instanceof IClassFile) { + IJavaProject javaProject = ((IClassFile) sourceElement).getJavaProject(); + if (javaProject != null) { + return javaProject.getProject(); + } + } + } catch (AbsentInformationException e) { + // When the compiled .class file doesn't contain debug source information, return null. + } + return null; + } }