diff --git a/headless-services/commons/pom.xml b/headless-services/commons/pom.xml index 8fa781e3e..acd2fc217 100644 --- a/headless-services/commons/pom.xml +++ b/headless-services/commons/pom.xml @@ -106,6 +106,7 @@ 0.7.5.RELEASE 2.4 1.13 + 7.18.0-SNAPSHOT true vmware diff --git a/headless-services/spring-boot-language-server/pom.xml b/headless-services/spring-boot-language-server/pom.xml index e5625908a..b13b7399b 100644 --- a/headless-services/spring-boot-language-server/pom.xml +++ b/headless-services/spring-boot-language-server/pom.xml @@ -38,6 +38,17 @@ true + + + rewrite-snapshots + https://oss.sonatype.org/content/repositories/snapshots/ + + true + + + false + + @@ -93,6 +104,11 @@ org.eclipse.jdt.core ${jdt.core.version} + + org.openrewrite + rewrite-java + ${rewrite-version} + commons-io commons-io diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java index 211b8dc1b..c91867dcb 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java @@ -36,6 +36,7 @@ import org.springframework.ide.vscode.boot.java.links.SourceLinkFactory; import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessLiveDataProvider; import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; +import org.springframework.ide.vscode.boot.java.utils.ORCompilationUnitCache; import org.springframework.ide.vscode.boot.java.utils.SymbolCache; import org.springframework.ide.vscode.boot.java.utils.SymbolCacheOnDisc; import org.springframework.ide.vscode.boot.java.utils.SymbolCacheVoid; @@ -150,12 +151,12 @@ public class BootLanguageServerBootApp { } @ConditionalOnMissingClass("org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness") - @Bean SourceLinks sourceLinks(SimpleLanguageServer server, CompilationUnitCache cuCache, BootLanguageServerParams params) { + @Bean SourceLinks sourceLinks(SimpleLanguageServer server, ORCompilationUnitCache cuCache, BootLanguageServerParams params) { return SourceLinkFactory.createSourceLinks(server, cuCache, params.projectFinder); } - @Bean CompilationUnitCache cuCache(SimpleLanguageServer server, BootLanguageServerParams params) { - return new CompilationUnitCache(params.projectFinder, server, params.projectObserver); + @Bean ORCompilationUnitCache cuCache(SimpleLanguageServer server, BootLanguageServerParams params) { + return new ORCompilationUnitCache(params.projectFinder, server, params.projectObserver); } @Bean SpringXMLCompletionEngine xmlCompletionEngine(SimpleLanguageServer server, JavaProjectFinder projectFinder, SpringSymbolIndex symbolIndex, BootJavaConfig config) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks.java index 107cc146a..3b80f100a 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks.java @@ -17,15 +17,13 @@ import java.nio.file.Path; import java.util.Optional; import java.util.Stack; -import org.eclipse.jdt.core.dom.ASTVisitor; -import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; -import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration; -import org.eclipse.jdt.core.dom.CompilationUnit; -import org.eclipse.jdt.core.dom.EnumDeclaration; -import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.tree.J.CompilationUnit; +import org.openrewrite.java.tree.TypeUtils; +import org.openrewrite.marker.Position; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; +import org.springframework.ide.vscode.boot.java.utils.ORCompilationUnitCache; import org.springframework.ide.vscode.commons.java.IClasspath; import org.springframework.ide.vscode.commons.java.IClasspathUtil; import org.springframework.ide.vscode.commons.java.IJavaModuleData; @@ -44,11 +42,11 @@ public abstract class AbstractSourceLinks implements SourceLinks { private static final Logger log = LoggerFactory.getLogger(AbstractSourceLinks.class); - private CompilationUnitCache cuCache; + private ORCompilationUnitCache cuCache; private JavaProjectFinder projectFinder; - protected AbstractSourceLinks(CompilationUnitCache cuCache, JavaProjectFinder projectFinder) { + protected AbstractSourceLinks(ORCompilationUnitCache cuCache, JavaProjectFinder projectFinder) { this.cuCache = cuCache; this.projectFinder = projectFinder; } @@ -142,55 +140,30 @@ public abstract class AbstractSourceLinks implements SourceLinks { int lastDotIndex = fqName.lastIndexOf('.'); String packageName = fqName.substring(0, lastDotIndex); String typeName = fqName.substring(lastDotIndex + 1); - if (packageName.equals(cu.getPackage().getName().getFullyQualifiedName())) { + if (packageName.equals(TypeUtils.asFullyQualified(cu.getPackageDeclaration().getExpression().getType()).getFullyQualifiedName())) { Stack visitedType = new Stack<>(); - cu.accept(new ASTVisitor() { + new JavaIsoVisitor<>() { - private boolean visitDeclaration(AbstractTypeDeclaration node) { - visitedType.push(node.getName().getIdentifier()); + public org.openrewrite.java.tree.J.ClassDeclaration visitClassDeclaration(org.openrewrite.java.tree.J.ClassDeclaration classDecl, Object p) { + String fqName = classDecl.getType().getFullyQualifiedName(); + visitedType.push(fqName); if (values[1] < 0) { if (String.join("$", visitedType.toArray(new String[visitedType.size()])).equals(typeName)) { - values[0] = node.getName().getStartPosition(); - values[1] = node.getName().getLength(); + Position pos = classDecl.getName().getMarkers().findFirst(Position.class).orElseThrow(); + values[0] = pos.getStartPosition(); + values[1] = pos.getLength(); } } - return values[1] < 0; - } - - @Override - public boolean visit(TypeDeclaration node) { - return visitDeclaration(node); - } - - @Override - public boolean visit(AnnotationTypeDeclaration node) { - return visitDeclaration(node); - } - - @Override - public boolean visit(EnumDeclaration node) { - return visitDeclaration(node); - } - - @Override - public void endVisit(EnumDeclaration node) { - visitedType.pop(); - super.endVisit(node); - } - - @Override - public void endVisit(AnnotationTypeDeclaration node) { - visitedType.pop(); - super.endVisit(node); - } - - @Override - public void endVisit(TypeDeclaration node) { - visitedType.pop(); - super.endVisit(node); - } - - }); + if (values[1] < 0) { + return super.visitClassDeclaration(classDecl, visitedType); + } else { + return classDecl; + } + + }; + + + }.visitNonNull(cu, visitedType); } return values[1] < 0 ? null : new Region(values[0], values[1]); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AtomSourceLinks.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AtomSourceLinks.java index ea413e3c2..bdb8b2df7 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AtomSourceLinks.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AtomSourceLinks.java @@ -15,10 +15,10 @@ import java.net.URLEncoder; import java.nio.file.Path; import java.util.Optional; -import org.eclipse.jdt.core.dom.CompilationUnit; +import org.openrewrite.java.tree.J.CompilationUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; +import org.springframework.ide.vscode.boot.java.utils.ORCompilationUnitCache; import org.springframework.ide.vscode.commons.java.IJavaModuleData; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; @@ -37,7 +37,7 @@ public class AtomSourceLinks extends AbstractSourceLinks { private static Supplier LOG = Suppliers.memoize(() -> LoggerFactory.getLogger(AbstractSourceLinks.class)); - public AtomSourceLinks(CompilationUnitCache cuCache, JavaProjectFinder projectFinder) { + public AtomSourceLinks(ORCompilationUnitCache cuCache, JavaProjectFinder projectFinder) { super(cuCache, projectFinder); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinkFactory.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinkFactory.java index 25a712f41..c5d481c30 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinkFactory.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinkFactory.java @@ -13,7 +13,7 @@ package org.springframework.ide.vscode.boot.java.links; import java.nio.file.Path; import java.util.Optional; -import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; +import org.springframework.ide.vscode.boot.java.utils.ORCompilationUnitCache; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; import org.springframework.ide.vscode.commons.languageserver.util.LspClient; @@ -51,7 +51,7 @@ public final class SourceLinkFactory { * @param server the boot LS * @return appropriate source links object */ - public static SourceLinks createSourceLinks(SimpleLanguageServer server, CompilationUnitCache cuCache, JavaProjectFinder projectFinder) { + public static SourceLinks createSourceLinks(SimpleLanguageServer server, ORCompilationUnitCache cuCache, JavaProjectFinder projectFinder) { switch (LspClient.currentClient()) { case VSCODE: case THEIA: diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks.java index 4711c6e03..2ee3e156f 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks.java @@ -13,8 +13,8 @@ package org.springframework.ide.vscode.boot.java.links; import java.nio.file.Path; import java.util.Optional; -import org.eclipse.jdt.core.dom.CompilationUnit; -import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; +import org.openrewrite.java.tree.J.CompilationUnit; +import org.springframework.ide.vscode.boot.java.utils.ORCompilationUnitCache; import org.springframework.ide.vscode.commons.java.IJavaModuleData; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; @@ -28,7 +28,7 @@ import org.springframework.ide.vscode.commons.util.text.Region; */ public class VSCodeSourceLinks extends AbstractSourceLinks { - public VSCodeSourceLinks(CompilationUnitCache cuCache, JavaProjectFinder projectFinder) { + public VSCodeSourceLinks(ORCompilationUnitCache cuCache, JavaProjectFinder projectFinder) { super(cuCache, projectFinder); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ORCompilationUnitCache.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ORCompilationUnitCache.java new file mode 100644 index 000000000..28e7fbfad --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ORCompilationUnitCache.java @@ -0,0 +1,238 @@ +package org.springframework.ide.vscode.boot.java.utils; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.net.URI; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.apache.commons.io.IOUtils; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.Parser.Input; +import org.openrewrite.Result; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.UpdateSourcePositions; +import org.openrewrite.java.tree.J.CompilationUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.commons.java.IClasspath; +import org.springframework.ide.vscode.commons.java.IClasspathUtil; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +import reactor.core.Disposable; + +public class ORCompilationUnitCache implements DocumentContentProvider, Disposable { + + private static final Logger logger = LoggerFactory.getLogger(CompilationUnitCache.class); + + private static final long CU_ACCESS_EXPIRATION = 1; + private JavaProjectFinder projectFinder; + private ProjectObserver projectObserver; + + private final ProjectObserver.Listener projectListener; + private final SimpleTextDocumentService documentService; + + private final Cache uriToCu; + private final Cache> projectToDocs; + private final Cache javaParsers; + + public ORCompilationUnitCache(JavaProjectFinder projectFinder, SimpleLanguageServer server, ProjectObserver projectObserver) { + this.projectFinder = projectFinder; + this.projectObserver = projectObserver; + + // PT 154618835 - Avoid retaining the CU in the cache as it consumes memory if it hasn't been + // accessed after some time + this.uriToCu = CacheBuilder.newBuilder() + .expireAfterWrite(CU_ACCESS_EXPIRATION, TimeUnit.MINUTES) + .build(); + this.projectToDocs = CacheBuilder.newBuilder().build(); + this.javaParsers = CacheBuilder.newBuilder().build(); + + this.documentService = server == null ? null : server.getTextDocumentService(); + + // IMPORTANT ===> these notifications arrive within the lsp message loop, so reactions to them have to be fast + // and not be blocked by waiting for anything + if (this.documentService != null) { + this.documentService.onDidChangeContent(doc -> invalidateCuForJavaFile(doc.getDocument().getId().getUri())); + this.documentService.onDidClose(doc -> invalidateCuForJavaFile(doc.getId().getUri())); + } + + if (this.projectFinder != null) { + for (IJavaProject project : this.projectFinder.all()) { + logger.info("CU Cache: initial lookup env creation for project <{}>", project.getElementName()); + loadJavaParser(project); + } + } + + this.projectListener = new ProjectObserver.Listener() { + + @Override + public void deleted(IJavaProject project) { + logger.info("CU Cache: deleted project {}", project.getElementName()); + invalidateProject(project); + } + + @Override + public void created(IJavaProject project) { + logger.info("CU Cache: created project {}", project.getElementName()); + invalidateProject(project); + loadJavaParser(project); + } + + @Override + public void changed(IJavaProject project) { + logger.info("CU Cache: changed project {}", project.getElementName()); + invalidateProject(project); + // Load the new cache the value right away + loadJavaParser(project); + } + }; + + if (this.projectObserver != null) { + this.projectObserver.addListener(this.projectListener); + } + + } + + public void dispose() { + if (this.projectObserver != null) { + this.projectObserver.removeListener(this.projectListener); + } + } + + + private JavaParser loadJavaParser(IJavaProject project) { + try { + return javaParsers.get(project, () -> JavaParser.fromJavaVersion().classpath(getClasspathEntries(project)).build()); + } catch (ExecutionException e) { + logger.error("{}", e); + return null; + } + } + + private static String[] getClasspathEntries(IJavaProject project) throws Exception { + if (project == null) { + return new String[0]; + } else { + IClasspath classpath = project.getClasspath(); + Stream classpathEntries = IClasspathUtil.getAllBinaryRoots(classpath).stream(); + return classpathEntries + .filter(file -> file.exists()) + .map(file -> file.getAbsolutePath()).toArray(String[]::new); + } + } + + private void invalidateCuForJavaFile(String uriStr) { + logger.info("CU Cache: invalidate AST for {}", uriStr); + + URI uri = URI.create(uriStr); + uriToCu.invalidate(uri); + JavaParser parser = javaParsers.getIfPresent(uri); + if (parser != null) { + parser.reset(); + } + } + + private void invalidateProject(IJavaProject project) { + logger.info("CU Cache: invalidate project <{}>", project.getElementName()); + + Set docUris = projectToDocs.getIfPresent(project); + if (docUris != null) { + uriToCu.invalidateAll(docUris); + projectToDocs.invalidate(project); + } + javaParsers.invalidate(project); + } + + @Override + public String fetchContent(URI uri) throws Exception { + if (documentService != null) { + TextDocument document = documentService.getLatestSnapshot(uri.toString()); + if (document != null) { + return document.get(); + } + } + return IOUtils.toString(uri); + } + + /** + * Never research shows at the AST is thread-safe when used in read-only mode: + * https://bugs.eclipse.org/bugs/show_bug.cgi?id=58314 + * + * This means that the previous implemented synchronization around the requestor + * working on the AST is not necessary as long as the requestor operates in read-only + * mode on the AST nodes. + * + * Warning: Callers should take care to do all AST processing inside of the requestor callback and + * not pass of AST nodes to helper functions that work aynchronously or store AST nodes or ITypeBindings + * for later use. The JDT ASTs are not thread safe! + */ + public T withCompilationUnit(IJavaProject project, URI uri, Function requestor) { + logger.info("CU Cache: work item submitted for doc {}", uri.toString()); + + if (project != null) { + + CompilationUnit cu = null; + + try { + cu = uriToCu.get(uri, () -> { + JavaParser javaParser = loadJavaParser(project); + Input input = new Input(Paths.get(uri), () -> { + try { + return new ByteArrayInputStream(fetchContent(uri).getBytes()); + } catch (Exception e) { + throw new IllegalStateException("Unexpected error fetching document content"); + } + }); + + Result result = new UpdateSourcePositions().run(javaParser.parseInputs(List.of(input), null, new InMemoryExecutionContext())).get(0); + + logger.info("CU Cache: created new AST for {}", uri.toString()); + + return (CompilationUnit) (result.getAfter() == null ? result.getBefore() : result.getAfter()); + }); + + if (cu != null) { + projectToDocs.get(project, () -> new HashSet<>()).add(uri); + } + + } catch (Exception e) { + logger.error("", e); + } + + if (cu != null) { + try { + logger.info("CU Cache: start work on AST for {}", uri.toString()); + return requestor.apply(cu); + } + catch (CancellationException e) { + throw e; + } + catch (Exception e) { + logger.error("", e); + } + finally { + logger.info("CU Cache: end work on AST for {}", uri.toString()); + } + } + } + + return requestor.apply(null); + } + +}