diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java index 36bb9b711..5bd4f7e67 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java @@ -10,16 +10,9 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.app; -import java.net.URI; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Duration; +import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import org.eclipse.lsp4j.TextDocumentIdentifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; @@ -27,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; import org.springframework.ide.vscode.boot.common.IJavaProjectReconcileEngine; +import org.springframework.ide.vscode.boot.common.ProjectReconcileScheduler; import org.springframework.ide.vscode.boot.factories.SpringFactoriesLanguageServerComponents; import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; import org.springframework.ide.vscode.boot.java.links.JavaElementLocationProvider; @@ -39,16 +33,15 @@ import org.springframework.ide.vscode.boot.metadata.ProjectBasedPropertyIndexPro import org.springframework.ide.vscode.boot.properties.BootPropertiesLanguageServerComponents; import org.springframework.ide.vscode.boot.validation.BootVersionValidationEngine; import org.springframework.ide.vscode.boot.xml.SpringXMLLanguageServerComponents; -import org.springframework.ide.vscode.commons.java.IClasspathUtil; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.completion.CompositeCompletionEngine; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionEngine; import org.springframework.ide.vscode.commons.languageserver.completion.VscodeCompletionEngineAdapter; import org.springframework.ide.vscode.commons.languageserver.composable.CompositeLanguageServerComponents; +import org.springframework.ide.vscode.commons.languageserver.composable.LanguageServerComponents; import org.springframework.ide.vscode.commons.languageserver.config.LanguageServerProperties; 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.reconcile.IReconcileEngine; import org.springframework.ide.vscode.commons.languageserver.util.HoverHandler; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; @@ -59,18 +52,9 @@ import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureProvid import org.springframework.stereotype.Component; import org.springframework.util.Assert; -import reactor.core.Disposable; -import reactor.core.publisher.Mono; -import reactor.core.scheduler.Scheduler; -import reactor.core.scheduler.Schedulers; - @Component public class BootLanguageServerInitializer implements InitializingBean { - private static final long DEBOUNCE_PERIOD_PROJECT_RECONCILE = 500; - - private static final List FILES_TO_WATCH_GLOB = List.of("**/*.java"); - @Autowired SimpleLanguageServer server; @Autowired BootLanguageServerParams params; @Autowired SourceLinks sourceLinks; @@ -93,9 +77,6 @@ public class BootLanguageServerInitializer implements InitializingBean { private CompositeLanguageServerComponents components; private VscodeCompletionEngineAdapter completionEngineAdapter; - private List projectReconcilers; - private Scheduler projectReconcileScheduler = Schedulers.newBoundedElastic(1, Integer.MAX_VALUE, "Project-Reconciler", 10); - private Map projectReconcileRequests = new ConcurrentHashMap<>(); private static final Logger log = LoggerFactory.getLogger(BootLanguageServerInitializer.class); @@ -104,7 +85,6 @@ public class BootLanguageServerInitializer implements InitializingBean { @Override public void deleted(IJavaProject project) { - doNotValidateProject(project, true); } @Override @@ -126,8 +106,6 @@ public class BootLanguageServerInitializer implements InitializingBean { s.validateWith(doc.getId(), reconciler); } } - validateProject(project, reconciler); - }); } }; @@ -138,26 +116,39 @@ public class BootLanguageServerInitializer implements InitializingBean { //TODO: CompositeLanguageServerComponents object instance serves no purpose anymore. The constructor really just contains // some server intialization code. Migrate that code and get rid of the ComposableLanguageServer class CompositeLanguageServerComponents.Builder builder = new CompositeLanguageServerComponents.Builder(); - builder.add(new BootPropertiesLanguageServerComponents(server, params, javaElementLocationProvider, parser, yamlStructureProvider, yamlAssistContextProvider, sourceLinks)); - BootJavaLanguageServerComponents bootJavaLanguageServerComponent = new BootJavaLanguageServerComponents(appContext); - builder.add(bootJavaLanguageServerComponent); - builder.add(new SpringXMLLanguageServerComponents(server, springIndexer, params, config)); - builder.add(new SpringFactoriesLanguageServerComponents(projectFinder, springIndexer, config)); - components = builder.build(server); - - projectReconcilers = List.of( - (IJavaProjectReconcileEngine) bootJavaLanguageServerComponent.getReconcileEngine().get(), - new BootVersionValidationEngine(server, config) + List componentsList = List.of( + new BootPropertiesLanguageServerComponents(server, params, javaElementLocationProvider, parser, yamlStructureProvider, yamlAssistContextProvider, sourceLinks), + new BootJavaLanguageServerComponents(appContext), + new SpringXMLLanguageServerComponents(server, springIndexer, params, config), + new SpringFactoriesLanguageServerComponents(projectFinder, springIndexer, config) ); - SimpleTextDocumentService documents = server.getTextDocumentService(); + List reconcileSchedulers = new ArrayList<>(componentsList.size() + 1); + for (LanguageServerComponents c : componentsList) { + builder.add(c); + + if (!configProps.isReconcileOnlyOpenedDocs()) { + c.getReconcileEngine() + .filter(IJavaProjectReconcileEngine.class::isInstance) + .map(IJavaProjectReconcileEngine.class::cast) + .map(r -> r.getScheduler()) + .ifPresent(reconcileSchedulers::add); + } + } + + // Version reconciler is for Maven/Gradle build files which are not part of + // server components because they come from docs we don't support at the moment + reconcileSchedulers.add(new BootVersionValidationEngine(server, config, params.projectObserver, projectFinder).getScheduler()); - components.getReconcileEngine().ifPresent(reconcileEngine -> { - documents.onDidChangeContent(params -> { - TextDocument doc = params.getDocument(); - server.validateWith(doc.getId(), reconcileEngine); - }); - }); + // Kick off project reconcile schedulers + for (ProjectReconcileScheduler scheduler : reconcileSchedulers) { + scheduler.start(); + server.onShutdown(() -> scheduler.stop()); + } + + components = builder.build(server); + + SimpleTextDocumentService documents = server.getTextDocumentService(); if (!completionEngines.isEmpty()) { CompositeCompletionEngine compositeCompletionEngine = new CompositeCompletionEngine(); @@ -175,6 +166,7 @@ public class BootLanguageServerInitializer implements InitializingBean { components.getDocumentSymbolProvider().ifPresent(documents::onDocumentSymbol); + if (recipesRepo != null) { recipesRepo.onRecipesLoaded(v -> { // Recipes will start loading only after config has been received. Therefore safe to start listening to config changes now @@ -183,37 +175,34 @@ public class BootLanguageServerInitializer implements InitializingBean { reconcile(); }); } else { + // Reconcile would occur as listeners will be receiving events startListeningToPerformReconcile(); - reconcile(); } - - server.onShutdown(() -> { - for (IJavaProject p : projectFinder.all()) { - doNotValidateProject(p, false); - } - }); + + } private void startListeningToPerformReconcile() { + components.getReconcileEngine().ifPresent(reconcileEngine -> { + server.getTextDocumentService().onDidChangeContent(params -> { + TextDocument doc = params.getDocument(); + server.validateWith(doc.getId(), reconcileEngine); + }); + }); config.addListener(evt -> reconcile()); - params.projectObserver.addListener(reconcileDocumentsForProjectChange(server, components, params.projectFinder)); - server.getWorkspaceService().getFileObserver().onFilesChanged(FILES_TO_WATCH_GLOB, this::handleFiles); - server.getWorkspaceService().getFileObserver().onFilesCreated(FILES_TO_WATCH_GLOB, this::handleFiles); - - // TODO: index update even happens on every file save. Very expensive to blindly reconcile all projects. - // Need to figure out a check if spring index has any changes -// springIndexer.onUpdate(v -> reconcile()); +// // TODO: index update even happens on every file save. Very expensive to blindly reconcile all projects. +// // Need to figure out a check if spring index has any changes +//// springIndexer.onUpdate(v -> reconcile()); } private void reconcile() { components.getReconcileEngine().ifPresent(reconciler -> { - log.info("A configuration changed, triggering reconcile on all open documents"); + log.info("Triggering reconcile on all open documents"); for (TextDocument doc : server.getTextDocumentService().getAll()) { server.validateWith(doc.getId(), reconciler); } - params.projectFinder.all().forEach(p -> validateProject(p, reconciler)); }); } @@ -228,79 +217,4 @@ public class BootLanguageServerInitializer implements InitializingBean { } } - private void validateProject(IJavaProject project, IReconcileEngine reconcileEngine) { - if (configProps.isReconcileOnlyOpenedDocs()) { - return; - } - - URI uri = project.getLocationUri(); - - Disposable previousRequest = projectReconcileRequests.put(uri, Mono.delay(Duration.ofMillis(DEBOUNCE_PERIOD_PROJECT_RECONCILE)) - .publishOn(projectReconcileScheduler) - .doOnSuccess(l -> { - if (projectReconcileRequests.remove(uri) != null) { - projectFinder.find(new TextDocumentIdentifier(uri.toASCIIString())).ifPresent(p -> { - for (IJavaProjectReconcileEngine projectReconciler : projectReconcilers) { - projectReconciler.clear(project); - projectReconciler.reconcile(p, doc -> server.createProblemCollector(doc)); - } - }); - } - }) - .subscribe()); - // Dispose previous request to debounce project reconcile requests. - if (previousRequest != null) { - previousRequest.dispose(); - } - } - - private void doNotValidateProject(IJavaProject project, boolean asyncClear) { - if (configProps.isReconcileOnlyOpenedDocs()) { - return; - } - - URI uri = project.getLocationUri(); - Disposable request = projectReconcileRequests.remove(uri); - if (request != null) { - request.dispose(); - } - - /* - * TODO: Look at LanguageServerHarness to fix the deadlock that occurs every 2 second time maven build is ran - * If #clear(IJavaProject) is synchronous then the locked LanguageServerHarness instance is attempted to call publishDiagnostic() - * which is caused by the #clear(...) call. In the LS reality this will never happen as #publishDiagnsotics() is always a future - */ - if (asyncClear) { - Mono.fromFuture(CompletableFuture.runAsync(() -> projectReconcilers.forEach(projectReconciler -> projectReconciler.clear(project)))) - .publishOn(projectReconcileScheduler); - } else { - for (IJavaProjectReconcileEngine projectReconciler : projectReconcilers) { - projectReconciler.clear(project); - } - } - } - - private void handleFiles(String[] files) { - if (configProps.isReconcileOnlyOpenedDocs()) { - return; - } - - components.getReconcileEngine().ifPresent(reconcileEngine -> { - for (String f : files) { - URI uri = URI.create(f); - TextDocumentIdentifier docId = new TextDocumentIdentifier(uri.toASCIIString()); - TextDocument doc = server.getTextDocumentService().getLatestSnapshot(docId.getUri()); - if (doc == null) { - projectFinder.find(docId).ifPresent(project -> { - Path p = Paths.get(uri); - if (IClasspathUtil.getSourceFolders(project.getClasspath()).filter(folder -> p.startsWith(folder.toPath())).findFirst().isPresent()) { - validateProject(project, reconcileEngine); - } - }); - } - } - }); - - } - } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/common/IJavaProjectReconcileEngine.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/common/IJavaProjectReconcileEngine.java index 1737362f7..45173ac51 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/common/IJavaProjectReconcileEngine.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/common/IJavaProjectReconcileEngine.java @@ -10,16 +10,14 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.common; -import java.util.function.Function; - import org.springframework.ide.vscode.commons.java.IJavaProject; -import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; -import org.springframework.ide.vscode.commons.util.text.TextDocument; public interface IJavaProjectReconcileEngine { - void reconcile(IJavaProject project, Function problemCollectorFactory); + void reconcile(IJavaProject project); void clear(IJavaProject project); - + + ProjectReconcileScheduler getScheduler(); + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/common/ProjectReconcileScheduler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/common/ProjectReconcileScheduler.java new file mode 100644 index 000000000..fafe58e32 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/common/ProjectReconcileScheduler.java @@ -0,0 +1,127 @@ +/******************************************************************************* + * Copyright (c) 2023 VMware, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.common; + +import java.net.URI; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; + +import reactor.core.Disposable; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +public abstract class ProjectReconcileScheduler { + + private IJavaProjectReconcileEngine reconciler; + private JavaProjectFinder projectFinder; + private long debounce; + + private Scheduler projectReconcileScheduler; + private Map projectReconcileRequests = new ConcurrentHashMap<>(); + private boolean started; + + public ProjectReconcileScheduler(IJavaProjectReconcileEngine reconciler, JavaProjectFinder projectFinder, long debounce, int numberOFthreads) { + this.reconciler = reconciler; + this.debounce = debounce; + this.projectFinder = projectFinder; + + projectReconcileScheduler = Schedulers.newBoundedElastic(numberOFthreads, Integer.MAX_VALUE, "Project-Reconciler", 10); + } + + public ProjectReconcileScheduler(IJavaProjectReconcileEngine reconciler, JavaProjectFinder projectFinder) { + this(reconciler, projectFinder, 500, 1); + } + + public final synchronized void start() { + if (!started) { + init(); + started = true; + } + } + + public final synchronized void stop() { + if (started) { + dispose(); + started = false; + } + } + + protected void init() { + + } + + protected void dispose() { + for (IJavaProject p : projectFinder.all()) { + unscheduleValidation(p); + reconciler.clear(p); + } + } + + protected final void scheduleValidationForAllProjects() { + for (IJavaProject p : projectFinder.all()) { + scheduleValidation(p); + } + } + + protected final void scheduleValidation(IJavaProject project) { + URI uri = project.getLocationUri(); + + Disposable previousRequest = projectReconcileRequests.put(uri, Mono.delay(Duration.ofMillis(debounce)) + .publishOn(projectReconcileScheduler) + .doOnSuccess(l -> { + if (projectReconcileRequests.remove(uri) != null) { + projectFinder.find(new TextDocumentIdentifier(uri.toASCIIString())).ifPresent(p -> { + reconciler.clear(project); + reconciler.reconcile(p); + }); + } + }) + .subscribe()); + // Dispose previous request to debounce project reconcile requests. + if (previousRequest != null) { + previousRequest.dispose(); + } + } + + protected final void unscheduleValidation(IJavaProject project) { + URI uri = project.getLocationUri(); + Disposable request = projectReconcileRequests.remove(uri); + if (request != null) { + request.dispose(); + } + } + + protected final void clear(IJavaProject project, boolean async) { + /* + * TODO: Look at LanguageServerHarness to fix the deadlock that occurs every 2 second time maven build is ran + * If #clear(IJavaProject) is synchronous then the locked LanguageServerHarness instance is attempted to call publishDiagnostic() + * which is caused by the #clear(...) call. In the LS reality this will never happen as #publishDiagnsotics() is always a future + */ + if (async) { + Mono.fromRunnable(() -> reconciler.clear(project)) + .publishOn(projectReconcileScheduler) + .subscribe(); + } else { + reconciler.clear(project); + } + } + + public JavaProjectFinder getProjectFinder() { + return projectFinder; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java index 2269b89ba..09ab24c80 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java @@ -191,8 +191,10 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent RewriteCompilationUnitCache orCompilationUnitCache = appContext.getBean(RewriteCompilationUnitCache.class); + RewriteRecipeRepository recipeRepo = appContext.getBean(RewriteRecipeRepository.class); + RewriteReconciler rewriteJavaReconciler = new RewriteReconciler( - appContext.getBean(RewriteRecipeRepository.class), + recipeRepo, orCompilationUnitCache, server.getQuickfixRegistry(), config @@ -201,7 +203,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent reconcileEngine = new BootJavaReconcileEngine(projectFinder, new JavaReconciler[] { jdtReconciler, rewriteJavaReconciler - }, documents); + }, server, config, projectObserver, recipeRepo); codeActionProvider = new BootJavaCodeActionProvider( projectFinder, diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaProjectReconcilerScheduler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaProjectReconcilerScheduler.java new file mode 100644 index 000000000..2ac756840 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaProjectReconcilerScheduler.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * Copyright (c) 2023 VMware, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.handlers; + +import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.springframework.ide.vscode.boot.app.BootJavaConfig; +import org.springframework.ide.vscode.boot.common.IJavaProjectReconcileEngine; +import org.springframework.ide.vscode.boot.common.ProjectReconcileScheduler; +import org.springframework.ide.vscode.boot.java.rewrite.RewriteRecipeRepository; +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.SimpleTextDocumentService; +import org.springframework.ide.vscode.commons.util.FileObserver; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +public class BootJavaProjectReconcilerScheduler extends ProjectReconcileScheduler { + + private static final List FILES_TO_WATCH_GLOB = List.of("**/*.java"); + + private ProjectObserver projectObserver; + private BootJavaConfig config; + private RewriteRecipeRepository recipesRepo; + private FileObserver fileObserver; + private SimpleTextDocumentService documents; + + public BootJavaProjectReconcilerScheduler(IJavaProjectReconcileEngine reconciler, FileObserver fileObserver, ProjectObserver projectObserver, + BootJavaConfig config, RewriteRecipeRepository recipesRepo, SimpleTextDocumentService documents, JavaProjectFinder projectFinder) { + super(reconciler, projectFinder); + this.fileObserver = fileObserver; + this.projectObserver = projectObserver; + this.config = config; + this.recipesRepo = recipesRepo; + this.documents = documents; + } + + @Override + protected void init() { + super.init(); + if (recipesRepo != null) { + recipesRepo.onRecipesLoaded(v -> { + // Recipes will start loading only after config has been received. Therefore safe to start listening to config changes now + // and launch initial project reconcile since both config and recipes are present + startListeningToPerformReconcile(); + scheduleValidationForAllProjects(); + }); + } else { + startListeningToPerformReconcile(); + scheduleValidationForAllProjects(); + } + } + + private void startListeningToPerformReconcile() { + config.addListener(evt -> scheduleValidationForAllProjects()); + + projectObserver.addListener(new ProjectObserver.Listener() { + + @Override + public void deleted(IJavaProject project) { + unscheduleValidation(project); + clear(project, true); + } + + @Override + public void created(IJavaProject project) { + scheduleValidation(project); + } + + @Override + public void changed(IJavaProject project) { + scheduleValidation(project); + } + }); + + fileObserver.onFilesChanged(FILES_TO_WATCH_GLOB, this::handleFiles); + fileObserver.onFilesCreated(FILES_TO_WATCH_GLOB, this::handleFiles); + + // TODO: index update even happens on every file save. Very expensive to blindly reconcile all projects. + // Need to figure out a check if spring index has any changes +// springIndexer.onUpdate(v -> reconcile()); + } + + private void handleFiles(String[] files) { + for (String f : files) { + URI uri = URI.create(f); + TextDocumentIdentifier docId = new TextDocumentIdentifier(uri.toASCIIString()); + TextDocument doc = documents.getLatestSnapshot(docId.getUri()); + if (doc == null) { + getProjectFinder().find(docId).ifPresent(project -> { + Path p = Paths.get(uri); + if (IClasspathUtil.getSourceFolders(project.getClasspath()) + .filter(folder -> p.startsWith(folder.toPath())).findFirst().isPresent()) { + scheduleValidation(project); + } + }); + } + } + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaReconcileEngine.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaReconcileEngine.java index b14cb921d..0fd137800 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaReconcileEngine.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaReconcileEngine.java @@ -18,22 +18,26 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.app.BootJavaConfig; import org.springframework.ide.vscode.boot.common.IJavaProjectReconcileEngine; +import org.springframework.ide.vscode.boot.common.ProjectReconcileScheduler; import org.springframework.ide.vscode.boot.java.reconcilers.JavaReconciler; +import org.springframework.ide.vscode.boot.java.rewrite.RewriteRecipeRepository; 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.reconcile.IProblemCollector; import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine; import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; -import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; +import org.springframework.ide.vscode.commons.util.UriUtil; import org.springframework.ide.vscode.commons.util.text.IDocument; import org.springframework.ide.vscode.commons.util.text.LanguageId; import org.springframework.ide.vscode.commons.util.text.LazyTextDocument; @@ -46,14 +50,26 @@ public class BootJavaReconcileEngine implements IReconcileEngine, IJavaProjectRe private static final Logger log = LoggerFactory.getLogger(BootJavaReconcileEngine.class); - private final SimpleTextDocumentService documents; private final JavaProjectFinder projectFinder; - private JavaReconciler[] javaReconcilers; + private final JavaReconciler[] javaReconcilers; + private final BootJavaProjectReconcilerScheduler projectReconeilerScheduler; + private final SimpleLanguageServer server; - public BootJavaReconcileEngine(JavaProjectFinder projectFinder, JavaReconciler[] javaReconcilers, SimpleTextDocumentService documents) { - this.documents = documents; + public BootJavaReconcileEngine(JavaProjectFinder projectFinder, JavaReconciler[] javaReconcilers, + SimpleLanguageServer server, BootJavaConfig config, ProjectObserver projectObserver, + RewriteRecipeRepository recipeRepo) { this.projectFinder = projectFinder; this.javaReconcilers = javaReconcilers; + this.server = server; + this.projectReconeilerScheduler = new BootJavaProjectReconcilerScheduler( + this, + server.getWorkspaceService().getFileObserver(), + projectObserver, + config, + recipeRepo, + server.getTextDocumentService(), + projectFinder + ); } @Override @@ -113,7 +129,7 @@ public class BootJavaReconcileEngine implements IReconcileEngine, IJavaProjectRe } @Override - public void reconcile(IJavaProject project, Function problemCollectorFactory) { + public void reconcile(IJavaProject project) { Stream files = IClasspathUtil.getProjectJavaSourceFolders(project.getClasspath()).flatMap(folder -> { try { return Files.walk(folder.toPath()).filter(Files::isRegularFile); @@ -126,18 +142,17 @@ public class BootJavaReconcileEngine implements IReconcileEngine, IJavaProjectRe .filter(f -> f.getFileName().toString().endsWith(".java")) .map(f -> new TextDocumentIdentifier(f.toUri().toASCIIString())); - List docs = docIds.filter(docId -> documents.getLatestSnapshot(docId.getUri()) == null) + List docs = docIds.filter(docId -> server.getTextDocumentService().getLatestSnapshot(docId.getUri()) == null) .map(docId -> new LazyTextDocument(docId.getUri(), LanguageId.JAVA)).collect(Collectors.toList()); Map problemCollectors = docs.stream() - .collect(Collectors.toMap(d -> d, d -> problemCollectorFactory.apply(d))); + .collect(Collectors.toMap(d -> d, d -> server.createProblemCollector(d))); problemCollectors.values().forEach(c -> c.beginCollecting()); for (JavaReconciler jr : javaReconcilers) { try { - Map> problems = jr.reconcile(project, docs, - problemCollectorFactory); + Map> problems = jr.reconcile(project, docs); problems.entrySet().forEach(e -> { IProblemCollector collector = problemCollectors.get(e.getKey()); e.getValue().forEach(p -> collector.accept(p)); @@ -161,8 +176,14 @@ public class BootJavaReconcileEngine implements IReconcileEngine, IJavaProjectRe } }) .filter(f -> f.getFileName().toString().endsWith(".java")) - .forEach(p -> documents.publishDiagnostics(new TextDocumentIdentifier(p.toUri().toASCIIString()), Collections.emptyList())); + .filter(f -> server.getTextDocumentService().getLatestSnapshot(UriUtil.toUri(f.toFile()).toASCIIString()) == null) + .forEach(p -> server.getTextDocumentService().publishDiagnostics(new TextDocumentIdentifier(p.toUri().toASCIIString()), Collections.emptyList())); } + @Override + public ProjectReconcileScheduler getScheduler() { + return projectReconeilerScheduler; + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JavaReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JavaReconciler.java index e69f1988a..1b4f33faf 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JavaReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JavaReconciler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2022 VMware, Inc. + * Copyright (c) 2022, 2023 VMware, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -13,7 +13,6 @@ package org.springframework.ide.vscode.boot.java.reconcilers; import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.function.Function; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; @@ -25,7 +24,6 @@ public interface JavaReconciler { void reconcile(IJavaProject project, IDocument doc, IProblemCollector problemCollector); - Map> reconcile(IJavaProject project, List docs, - Function problemCollectorFactory); + Map> reconcile(IJavaProject project, List docs); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JdtReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JdtReconciler.java index 2a41df595..cda52bdb4 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JdtReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JdtReconciler.java @@ -15,7 +15,6 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.function.Function; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.Annotation; @@ -160,8 +159,7 @@ public class JdtReconciler implements JavaReconciler { } @Override - public Map> reconcile(IJavaProject project, List docs, - Function problemCollectorFactory) { + public Map> reconcile(IJavaProject project, List docs) { if (config.isRewriteReconcileEnabled()) { // long start = System.currentTimeMillis(); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteReconciler.java index bf3e052a2..d2bef41c9 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteReconciler.java @@ -22,7 +22,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.function.Consumer; -import java.util.function.Function; import java.util.stream.Collectors; import org.openrewrite.ExecutionContext; @@ -139,8 +138,7 @@ public class RewriteReconciler implements JavaReconciler { } @Override - public Map> reconcile(IJavaProject project, List docs, - Function problemCollectorFactory) { + public Map> reconcile(IJavaProject project, List docs) { if (!config.isRewriteReconcileEnabled()) { return Collections.emptyMap(); @@ -162,9 +160,9 @@ public class RewriteReconciler implements JavaReconciler { } JavaParser javaParser = ORAstUtils.createJavaParser(project); javaParser.setSourceSet(MavenProjectParser.MAIN); - allProblems.putAll(doReconcile(project, mainSources, problemCollectorFactory, javaParser)); + allProblems.putAll(doReconcile(project, mainSources, javaParser)); javaParser.setSourceSet(MavenProjectParser.TEST); - allProblems.putAll(doReconcile(project, testSources, problemCollectorFactory, javaParser)); + allProblems.putAll(doReconcile(project, testSources, javaParser)); long end = System.currentTimeMillis(); log.info("reconciling project (OpenRewrite): " + project.getElementName() + " - " + docs.size() + " done in " + (end - start) + "ms"); @@ -244,8 +242,7 @@ public class RewriteReconciler implements JavaReconciler { private static final int BATCH = 50; // Parse in batches and share the parser - private Map> doReconcile(IJavaProject project, List docs, - Function problemCollectorFactory, JavaParser javaParser) { + private Map> doReconcile(IJavaProject project, List docs, JavaParser javaParser) { Map> allProblems = new HashMap<>(); if (javaParser != null && config.isRewriteReconcileEnabled()) { try { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/SpringBootUpgrade.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/SpringBootUpgrade.java index f7fbbb2d3..7a341a25d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/SpringBootUpgrade.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/SpringBootUpgrade.java @@ -99,8 +99,8 @@ public class SpringBootUpgrade { if (version.getMajor() == targetVersion.getMajor() && version.getMinor() == targetVersion.getMinor()) { // patch version upgrade - treat as pom versions only upgrade - recipe.doNext(new UpgradeDependencyVersion("org.springframework.boot", "*", version.getMajor() + "." + version.getMinor() + ".x", null, null)); - recipe.doNext(new UpgradeParentVersion("org.springframework.boot", "spring-boot-starter-parent", version.getMajor() + "." + version.getMinor() + ".x", null)); + recipe.doNext(new UpgradeDependencyVersion("org.springframework.boot", "*", version.getMajor() + "." + version.getMinor() + ".x", null, null, null)); + recipe.doNext(new UpgradeParentVersion("org.springframework.boot", "spring-boot-starter-parent", version.getMajor() + "." + version.getMinor() + ".x", null, null)); } else { createRecipeIdsChain(version.getMajor(), version.getMinor(), targetVersion.getMajor(), targetVersion.getMinor()).stream() .map(recipeRepo::getRecipe) diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/BootVersionValidationEngine.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/BootVersionValidationEngine.java index db6d8e7bc..509ff2dab 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/BootVersionValidationEngine.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/BootVersionValidationEngine.java @@ -11,13 +11,13 @@ package org.springframework.ide.vscode.boot.validation; import java.util.Collections; -import java.util.function.Function; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.app.BootJavaConfig; import org.springframework.ide.vscode.boot.common.IJavaProjectReconcileEngine; +import org.springframework.ide.vscode.boot.common.ProjectReconcileScheduler; import org.springframework.ide.vscode.boot.validation.generations.ProjectVersionDiagnosticProvider; import org.springframework.ide.vscode.boot.validation.generations.ProjectVersionDiagnosticProvider.DiagnosticResult; import org.springframework.ide.vscode.boot.validation.generations.SpringIoProjectsProvider; @@ -26,9 +26,9 @@ import org.springframework.ide.vscode.boot.validation.generations.SpringProjects import org.springframework.ide.vscode.boot.validation.generations.VersionValidators; import org.springframework.ide.vscode.boot.validation.generations.preferences.VersionValidationPreferences; import org.springframework.ide.vscode.commons.java.IJavaProject; -import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; +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.util.text.TextDocument; public class BootVersionValidationEngine implements IJavaProjectReconcileEngine { @@ -36,13 +36,41 @@ public class BootVersionValidationEngine implements IJavaProjectReconcileEngine private SimpleLanguageServer server; private BootJavaConfig config; + private ProjectReconcileScheduler projectReconcileScheduler; - public BootVersionValidationEngine(SimpleLanguageServer server, BootJavaConfig config) { + public BootVersionValidationEngine(SimpleLanguageServer server, BootJavaConfig config, ProjectObserver projectObserver, JavaProjectFinder projectFinder) { this.server = server; this.config = config; + this.projectReconcileScheduler = new ProjectReconcileScheduler(this, projectFinder) { + + @Override + protected void init() { + super.init(); + config.addListener(evt -> scheduleValidationForAllProjects()); + projectObserver.addListener(new ProjectObserver.Listener() { + + @Override + public void deleted(IJavaProject project) { + unscheduleValidation(project); + clear(project, true); + } + + @Override + public void created(IJavaProject project) { + scheduleValidation(project); + } + + @Override + public void changed(IJavaProject project) { + scheduleValidation(project); + } + }); + } + + }; } - public void reconcile(IJavaProject project, Function problemCollectorFactory) { + public void reconcile(IJavaProject project) { if (config.isBootVersionValidationEnabled()) { log.debug("validating Spring Boot version on project: " + project.getElementName()); long start = System.currentTimeMillis(); @@ -86,4 +114,9 @@ public class BootVersionValidationEngine implements IJavaProjectReconcileEngine Collections.emptyList()); } } + + @Override + public ProjectReconcileScheduler getScheduler() { + return projectReconcileScheduler; + } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueSpelExpressionValidationTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueSpelExpressionValidationTest.java index b7de53ec4..419fbbe62 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueSpelExpressionValidationTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueSpelExpressionValidationTest.java @@ -77,6 +77,7 @@ public class ValueSpelExpressionValidationTest { @Autowired private CompilationUnitCache compilationUnitCache; @Autowired private SimpleLanguageServer server; @Autowired private BootJavaConfig config; + @Autowired private ProjectObserver projectObserver; private File directory; private String docUri; @@ -149,7 +150,7 @@ public class ValueSpelExpressionValidationTest { problemCollector = new TestProblemCollector(); reconcileEngine = new BootJavaReconcileEngine(projectFinder, new JavaReconciler[] { new JdtReconciler(compilationUnitCache, config) - }, server.getTextDocumentService()); + }, server, config, projectObserver, null); } @AfterEach