Skip to content

Commit fdfbd3c

Browse files
committed
Materialize jdt:// sources to cache files for clients that only open file paths
Stack frames paused in classes packaged in jars or the JRT runtime are reported with a jdt://contents/... source uri, which only VS Code can render through the jdt TextDocumentContentProvider registered by the Red Hat Java extension. Any other DAP client can only open absolute file paths, so those frames are rendered without source. Materialize the source of such classes (attached source, or decompiled source when java.debug.settings.debugSupportOnDecompiledSource is on) into a cache file under {java.io.tmpdir}/java-debug-sources/{jarName}/{package}/{Type}.java and report its path instead. The decompiled line mapping APIs accept the cache path and resolve it back to the class file. When getFileURI returns null for a class file (JDTUtils.toUri() returns null for jar class files in some language server versions, e.g. the jdt.ls 1.63.0 used by the Zed extension), fall back to resolving the class file via the source containers so the source can still be materialized. Unresolvable sources fall back to SourceType.REMOTE, matching the contract documented on ISourceLookUpProvider#getSource. Refs zed-extensions/java#326
1 parent 7d62d35 commit fdfbd3c

1 file changed

Lines changed: 158 additions & 2 deletions

File tree

‎com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProvider.java‎

Lines changed: 158 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
import java.net.URI;
1717
import java.net.URISyntaxException;
1818
import java.nio.charset.Charset;
19+
import java.nio.charset.StandardCharsets;
1920
import java.nio.file.Files;
21+
import java.nio.file.Path;
2022
import java.nio.file.Paths;
2123
import java.util.ArrayList;
2224
import java.util.Arrays;
@@ -84,6 +86,8 @@
8486
import com.microsoft.java.debug.core.adapter.Constants;
8587
import com.microsoft.java.debug.core.adapter.IDebugAdapterContext;
8688
import com.microsoft.java.debug.core.adapter.ISourceLookUpProvider;
89+
import com.microsoft.java.debug.core.adapter.Source;
90+
import com.microsoft.java.debug.core.adapter.SourceType;
8791
import com.microsoft.java.debug.core.protocol.Types.BreakpointLocation;
8892
import com.microsoft.java.debug.core.protocol.Types.SourceBreakpoint;
8993

@@ -406,6 +410,158 @@ public String getSourceFileURI(String fullyQualifiedName, String sourcePath) {
406410
return null;
407411
}
408412

413+
@Override
414+
public Source getSource(String fullyQualifiedName, String sourcePath) {
415+
String uri = getSourceFileURI(fullyQualifiedName, sourcePath);
416+
if (uri == null || uri.startsWith("file:")) {
417+
if (uri == null && sourcePath != null) {
418+
// Since #624, getFileURI delegates to JDTUtils.toUri(), which may return null
419+
// for class files (depending on the language server version). Fall back to
420+
// resolving the class file directly so its source can still be materialized.
421+
Object sourceElement = JdtUtils.findSourceElement(sourcePath, getSourceContainers());
422+
if (sourceElement instanceof IClassFile) {
423+
String cachedFileUri = materializeClassFile((IClassFile) sourceElement);
424+
if (cachedFileUri != null) {
425+
return new Source(cachedFileUri, SourceType.LOCAL);
426+
}
427+
}
428+
}
429+
return new Source(uri, SourceType.LOCAL);
430+
}
431+
432+
// The jdt:// uri (for classes packaged in a jar or the JRT runtime, with or without
433+
// source attachment) can only be rendered by clients that implement a jdt content
434+
// provider (e.g. VS Code with the Red Hat Java extension). Clients that can only open
435+
// absolute file paths (e.g. Zed) cannot display it. Materialize the source contents
436+
// (attached source, or decompiled source) into a local cache file and hand out its
437+
// path instead, so that every DAP client can display the source of library classes.
438+
String cachedFileUri = materializeSourceToCache(uri);
439+
if (cachedFileUri != null) {
440+
return new Source(cachedFileUri, SourceType.LOCAL);
441+
}
442+
443+
// Fall back to the jdt:// uri marked as remote source, so that clients able to fetch
444+
// contents through the DAP 'source' request can still display it.
445+
return new Source(uri, SourceType.REMOTE);
446+
}
447+
448+
private static final String SOURCE_CACHE_DIR_NAME = "java-debug-sources";
449+
450+
/**
451+
* Maps the file uri of a materialized source cache file back to the handle identifier of
452+
* the underlying class file, so that the decompiled line mapping APIs keep working when
453+
* clients report the cache path.
454+
*/
455+
private final Map<String, String> cachedFileToHandleId = new HashMap<>();
456+
457+
/**
458+
* For non-file (jdt://) source uris, cache the resolved contents (attached source or
459+
* decompiled source) into a local file and return the file uri of the cached file.
460+
* Returns null when the contents cannot be resolved.
461+
*/
462+
private String materializeSourceToCache(String uri) {
463+
IClassFile classFile = resolveClassFile(uri);
464+
if (classFile == null) {
465+
return null;
466+
}
467+
468+
return materializeClassFile(classFile);
469+
}
470+
471+
/**
472+
* Materializes the source of a class file (attached source, or decompiled source when
473+
* debugging on decompiled source is enabled) under
474+
* {java.io.tmpdir}/java-debug-sources/{jarName}/{package}/{Type}.java and returns the
475+
* file uri of the cache file. Returns null when the contents cannot be resolved.
476+
*/
477+
private String materializeClassFile(IClassFile classFile) {
478+
String contents = null;
479+
try {
480+
if (classFile.getSourceRange() != null) {
481+
// Source attachment (e.g. JDK src.zip, maven *-sources.jar) is available.
482+
// Its line numbers match the line table of the compiled class.
483+
contents = getContents(classFile);
484+
} else if (DebugSettings.getCurrent().debugSupportOnDecompiledSource == Switch.ON) {
485+
// No source attachment. When debugging on decompiled source is enabled,
486+
// decompile it with the same decompiler that the VS Code client uses, so that
487+
// clients without a jdt content provider can still display the source.
488+
ContentProviderManager contentProvider = JavaLanguageServerPlugin.getContentProviderManager();
489+
String decompiled = contentProvider.getSource(classFile, new NullProgressMonitor());
490+
if (decompiled != null && !decompiled.isBlank()) {
491+
contents = decompiled;
492+
}
493+
}
494+
} catch (JavaModelException e) {
495+
logger.log(Level.WARNING, String.format("Failed to load the source contents of %s: %s", classFile.getElementName(), e.toString()), e);
496+
} catch (Exception e) {
497+
logger.log(Level.WARNING, String.format("Failed to decompile %s: %s", classFile.getElementName(), e.toString()), e);
498+
}
499+
500+
if (contents == null || contents.isEmpty()) {
501+
return null;
502+
}
503+
504+
try {
505+
String jarName = sanitizeFileName(classFile.getParent().getParent().getElementName());
506+
String packageName = classFile.getParent().getElementName().replace('.', '/');
507+
String typeName = sanitizeFileName(classFile.getElementName().replaceFirst("\\.class$", ".java"));
508+
Path cachedPath = Paths.get(System.getProperty("java.io.tmpdir", "."), SOURCE_CACHE_DIR_NAME, jarName, packageName, typeName);
509+
if (!Files.exists(cachedPath)) {
510+
Files.createDirectories(cachedPath.getParent());
511+
Files.write(cachedPath, contents.getBytes(StandardCharsets.UTF_8));
512+
}
513+
String cachedUri = cachedPath.toUri().toString();
514+
cachedFileToHandleId.put(cachedUri, classFile.getHandleIdentifier());
515+
return cachedUri;
516+
} catch (IOException e) {
517+
logger.log(Level.WARNING, String.format("Failed to cache the source of %s: %s", classFile.getElementName(), e.toString()), e);
518+
return null;
519+
}
520+
}
521+
522+
/**
523+
* Resolves the class file for a source uri that is either a jdt:// uri or the file uri of
524+
* a materialized cache file. Returns null when it cannot be resolved.
525+
*/
526+
private IClassFile resolveClassFileForUri(String uri) {
527+
if (uri == null) {
528+
return null;
529+
}
530+
if (uri.startsWith(JDT_SCHEME)) {
531+
return resolveClassFile(uri);
532+
}
533+
String handleId = cachedFileToHandleId.get(uri);
534+
if (handleId == null) {
535+
// The cache files are reported back by clients as plain file paths or file uris.
536+
try {
537+
String fileName = Paths.get(uri).getFileName().toString();
538+
for (Map.Entry<String, String> entry : cachedFileToHandleId.entrySet()) {
539+
if (isSameURI(entry.getKey(), uri) || entry.getKey().endsWith("/" + fileName)) {
540+
handleId = entry.getValue();
541+
break;
542+
}
543+
}
544+
} catch (Exception e) {
545+
// not a valid file path, ignore.
546+
}
547+
}
548+
if (handleId != null) {
549+
IJavaElement element = JavaCore.create(handleId);
550+
if (element != null) {
551+
return (IClassFile) element.getAncestor(IJavaElement.CLASS_FILE);
552+
}
553+
}
554+
return null;
555+
}
556+
557+
/**
558+
* For the class names loaded from the jrt runtime, the element name may contain
559+
* characters (e.g. '!') that are invalid in file names.
560+
*/
561+
private static String sanitizeFileName(String name) {
562+
return name.replaceAll("[\\\\/:*?\"<>|!]", "_");
563+
}
564+
409565
@Override
410566
public String getJavaRuntimeVersion(String projectName) {
411567
IJavaProject project = JdtUtils.getJavaProject(projectName);
@@ -605,7 +761,7 @@ private boolean isSameURI(String uri1, String uri2) {
605761
}
606762

607763
public int[] getOriginalLineMappings(String uri) {
608-
IClassFile classFile = resolveClassFile(uri);
764+
IClassFile classFile = resolveClassFileForUri(uri);
609765
try {
610766
if (classFile == null) {
611767
return null;
@@ -636,7 +792,7 @@ public int[] getOriginalLineMappings(String uri) {
636792
}
637793

638794
public int[] getDecompiledLineMappings(String uri) {
639-
IClassFile classFile = resolveClassFile(uri);
795+
IClassFile classFile = resolveClassFileForUri(uri);
640796
try {
641797
if (classFile != null && classFile.getSourceRange() == null) {
642798
ContentProviderManager contentProvider = JavaLanguageServerPlugin.getContentProviderManager();

0 commit comments

Comments
 (0)