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);
+ }
+
+}