Re-work project reconcile for optimization purposes

This commit is contained in:
aboyko
2023-01-25 16:10:39 -05:00
parent 72ce3e03b2
commit df5b6c2cf7
12 changed files with 378 additions and 175 deletions

View File

@@ -10,16 +10,9 @@
*******************************************************************************/ *******************************************************************************/
package org.springframework.ide.vscode.boot.app; package org.springframework.ide.vscode.boot.app;
import java.net.URI; import java.util.ArrayList;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.List; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean; 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.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.ide.vscode.boot.common.IJavaProjectReconcileEngine; 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.factories.SpringFactoriesLanguageServerComponents;
import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents;
import org.springframework.ide.vscode.boot.java.links.JavaElementLocationProvider; 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.properties.BootPropertiesLanguageServerComponents;
import org.springframework.ide.vscode.boot.validation.BootVersionValidationEngine; import org.springframework.ide.vscode.boot.validation.BootVersionValidationEngine;
import org.springframework.ide.vscode.boot.xml.SpringXMLLanguageServerComponents; 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.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.completion.CompositeCompletionEngine; 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.ICompletionEngine;
import org.springframework.ide.vscode.commons.languageserver.completion.VscodeCompletionEngineAdapter; 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.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.config.LanguageServerProperties;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; 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.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.HoverHandler;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; 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.stereotype.Component;
import org.springframework.util.Assert; 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 @Component
public class BootLanguageServerInitializer implements InitializingBean { public class BootLanguageServerInitializer implements InitializingBean {
private static final long DEBOUNCE_PERIOD_PROJECT_RECONCILE = 500;
private static final List<String> FILES_TO_WATCH_GLOB = List.of("**/*.java");
@Autowired SimpleLanguageServer server; @Autowired SimpleLanguageServer server;
@Autowired BootLanguageServerParams params; @Autowired BootLanguageServerParams params;
@Autowired SourceLinks sourceLinks; @Autowired SourceLinks sourceLinks;
@@ -93,9 +77,6 @@ public class BootLanguageServerInitializer implements InitializingBean {
private CompositeLanguageServerComponents components; private CompositeLanguageServerComponents components;
private VscodeCompletionEngineAdapter completionEngineAdapter; private VscodeCompletionEngineAdapter completionEngineAdapter;
private List<IJavaProjectReconcileEngine> projectReconcilers;
private Scheduler projectReconcileScheduler = Schedulers.newBoundedElastic(1, Integer.MAX_VALUE, "Project-Reconciler", 10);
private Map<URI, Disposable> projectReconcileRequests = new ConcurrentHashMap<>();
private static final Logger log = LoggerFactory.getLogger(BootLanguageServerInitializer.class); private static final Logger log = LoggerFactory.getLogger(BootLanguageServerInitializer.class);
@@ -104,7 +85,6 @@ public class BootLanguageServerInitializer implements InitializingBean {
@Override @Override
public void deleted(IJavaProject project) { public void deleted(IJavaProject project) {
doNotValidateProject(project, true);
} }
@Override @Override
@@ -126,8 +106,6 @@ public class BootLanguageServerInitializer implements InitializingBean {
s.validateWith(doc.getId(), reconciler); 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 //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 // some server intialization code. Migrate that code and get rid of the ComposableLanguageServer class
CompositeLanguageServerComponents.Builder builder = new CompositeLanguageServerComponents.Builder(); CompositeLanguageServerComponents.Builder builder = new CompositeLanguageServerComponents.Builder();
builder.add(new BootPropertiesLanguageServerComponents(server, params, javaElementLocationProvider, parser, yamlStructureProvider, yamlAssistContextProvider, sourceLinks)); List<LanguageServerComponents> componentsList = List.of(
BootJavaLanguageServerComponents bootJavaLanguageServerComponent = new BootJavaLanguageServerComponents(appContext); new BootPropertiesLanguageServerComponents(server, params, javaElementLocationProvider, parser, yamlStructureProvider, yamlAssistContextProvider, sourceLinks),
builder.add(bootJavaLanguageServerComponent); new BootJavaLanguageServerComponents(appContext),
builder.add(new SpringXMLLanguageServerComponents(server, springIndexer, params, config)); new SpringXMLLanguageServerComponents(server, springIndexer, params, config),
builder.add(new SpringFactoriesLanguageServerComponents(projectFinder, springIndexer, config)); new SpringFactoriesLanguageServerComponents(projectFinder, springIndexer, config)
components = builder.build(server);
projectReconcilers = List.of(
(IJavaProjectReconcileEngine) bootJavaLanguageServerComponent.getReconcileEngine().get(),
new BootVersionValidationEngine(server, config)
); );
SimpleTextDocumentService documents = server.getTextDocumentService(); List<ProjectReconcileScheduler> reconcileSchedulers = new ArrayList<>(componentsList.size() + 1);
for (LanguageServerComponents c : componentsList) {
builder.add(c);
components.getReconcileEngine().ifPresent(reconcileEngine -> { if (!configProps.isReconcileOnlyOpenedDocs()) {
documents.onDidChangeContent(params -> { c.getReconcileEngine()
TextDocument doc = params.getDocument(); .filter(IJavaProjectReconcileEngine.class::isInstance)
server.validateWith(doc.getId(), reconcileEngine); .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());
// 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()) { if (!completionEngines.isEmpty()) {
CompositeCompletionEngine compositeCompletionEngine = new CompositeCompletionEngine(); CompositeCompletionEngine compositeCompletionEngine = new CompositeCompletionEngine();
@@ -175,6 +166,7 @@ public class BootLanguageServerInitializer implements InitializingBean {
components.getDocumentSymbolProvider().ifPresent(documents::onDocumentSymbol); components.getDocumentSymbolProvider().ifPresent(documents::onDocumentSymbol);
if (recipesRepo != null) { if (recipesRepo != null) {
recipesRepo.onRecipesLoaded(v -> { recipesRepo.onRecipesLoaded(v -> {
// Recipes will start loading only after config has been received. Therefore safe to start listening to config changes now // 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(); reconcile();
}); });
} else { } else {
// Reconcile would occur as listeners will be receiving events
startListeningToPerformReconcile(); startListeningToPerformReconcile();
reconcile();
} }
server.onShutdown(() -> {
for (IJavaProject p : projectFinder.all()) {
doNotValidateProject(p, false);
}
});
} }
private void startListeningToPerformReconcile() { private void startListeningToPerformReconcile() {
components.getReconcileEngine().ifPresent(reconcileEngine -> {
server.getTextDocumentService().onDidChangeContent(params -> {
TextDocument doc = params.getDocument();
server.validateWith(doc.getId(), reconcileEngine);
});
});
config.addListener(evt -> reconcile()); config.addListener(evt -> reconcile());
params.projectObserver.addListener(reconcileDocumentsForProjectChange(server, components, params.projectFinder)); params.projectObserver.addListener(reconcileDocumentsForProjectChange(server, components, params.projectFinder));
server.getWorkspaceService().getFileObserver().onFilesChanged(FILES_TO_WATCH_GLOB, this::handleFiles); // // TODO: index update even happens on every file save. Very expensive to blindly reconcile all projects.
server.getWorkspaceService().getFileObserver().onFilesCreated(FILES_TO_WATCH_GLOB, this::handleFiles); // // 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() { private void reconcile() {
components.getReconcileEngine().ifPresent(reconciler -> { 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()) { for (TextDocument doc : server.getTextDocumentService().getAll()) {
server.validateWith(doc.getId(), reconciler); 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);
}
});
}
}
});
}
} }

View File

@@ -10,16 +10,14 @@
*******************************************************************************/ *******************************************************************************/
package org.springframework.ide.vscode.boot.common; 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.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
public interface IJavaProjectReconcileEngine { public interface IJavaProjectReconcileEngine {
void reconcile(IJavaProject project, Function<TextDocument, IProblemCollector> problemCollectorFactory); void reconcile(IJavaProject project);
void clear(IJavaProject project); void clear(IJavaProject project);
ProjectReconcileScheduler getScheduler();
} }

View File

@@ -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<URI, Disposable> 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;
}
}

View File

@@ -191,8 +191,10 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
RewriteCompilationUnitCache orCompilationUnitCache = appContext.getBean(RewriteCompilationUnitCache.class); RewriteCompilationUnitCache orCompilationUnitCache = appContext.getBean(RewriteCompilationUnitCache.class);
RewriteRecipeRepository recipeRepo = appContext.getBean(RewriteRecipeRepository.class);
RewriteReconciler rewriteJavaReconciler = new RewriteReconciler( RewriteReconciler rewriteJavaReconciler = new RewriteReconciler(
appContext.getBean(RewriteRecipeRepository.class), recipeRepo,
orCompilationUnitCache, orCompilationUnitCache,
server.getQuickfixRegistry(), server.getQuickfixRegistry(),
config config
@@ -201,7 +203,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
reconcileEngine = new BootJavaReconcileEngine(projectFinder, new JavaReconciler[] { reconcileEngine = new BootJavaReconcileEngine(projectFinder, new JavaReconciler[] {
jdtReconciler, jdtReconciler,
rewriteJavaReconciler rewriteJavaReconciler
}, documents); }, server, config, projectObserver, recipeRepo);
codeActionProvider = new BootJavaCodeActionProvider( codeActionProvider = new BootJavaCodeActionProvider(
projectFinder, projectFinder,

View File

@@ -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<String> 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);
}
});
}
}
}
}

View File

@@ -18,22 +18,26 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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.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.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.IClasspathUtil;
import org.springframework.ide.vscode.commons.java.IJavaProject; 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.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.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine; 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.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.IDocument;
import org.springframework.ide.vscode.commons.util.text.LanguageId; import org.springframework.ide.vscode.commons.util.text.LanguageId;
import org.springframework.ide.vscode.commons.util.text.LazyTextDocument; 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 static final Logger log = LoggerFactory.getLogger(BootJavaReconcileEngine.class);
private final SimpleTextDocumentService documents;
private final JavaProjectFinder projectFinder; 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) { public BootJavaReconcileEngine(JavaProjectFinder projectFinder, JavaReconciler[] javaReconcilers,
this.documents = documents; SimpleLanguageServer server, BootJavaConfig config, ProjectObserver projectObserver,
RewriteRecipeRepository recipeRepo) {
this.projectFinder = projectFinder; this.projectFinder = projectFinder;
this.javaReconcilers = javaReconcilers; this.javaReconcilers = javaReconcilers;
this.server = server;
this.projectReconeilerScheduler = new BootJavaProjectReconcilerScheduler(
this,
server.getWorkspaceService().getFileObserver(),
projectObserver,
config,
recipeRepo,
server.getTextDocumentService(),
projectFinder
);
} }
@Override @Override
@@ -113,7 +129,7 @@ public class BootJavaReconcileEngine implements IReconcileEngine, IJavaProjectRe
} }
@Override @Override
public void reconcile(IJavaProject project, Function<TextDocument, IProblemCollector> problemCollectorFactory) { public void reconcile(IJavaProject project) {
Stream<Path> files = IClasspathUtil.getProjectJavaSourceFolders(project.getClasspath()).flatMap(folder -> { Stream<Path> files = IClasspathUtil.getProjectJavaSourceFolders(project.getClasspath()).flatMap(folder -> {
try { try {
return Files.walk(folder.toPath()).filter(Files::isRegularFile); 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")) .filter(f -> f.getFileName().toString().endsWith(".java"))
.map(f -> new TextDocumentIdentifier(f.toUri().toASCIIString())); .map(f -> new TextDocumentIdentifier(f.toUri().toASCIIString()));
List<TextDocument> docs = docIds.filter(docId -> documents.getLatestSnapshot(docId.getUri()) == null) List<TextDocument> docs = docIds.filter(docId -> server.getTextDocumentService().getLatestSnapshot(docId.getUri()) == null)
.map(docId -> new LazyTextDocument(docId.getUri(), LanguageId.JAVA)).collect(Collectors.toList()); .map(docId -> new LazyTextDocument(docId.getUri(), LanguageId.JAVA)).collect(Collectors.toList());
Map<IDocument, IProblemCollector> problemCollectors = docs.stream() Map<IDocument, IProblemCollector> 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()); problemCollectors.values().forEach(c -> c.beginCollecting());
for (JavaReconciler jr : javaReconcilers) { for (JavaReconciler jr : javaReconcilers) {
try { try {
Map<IDocument, Collection<ReconcileProblem>> problems = jr.reconcile(project, docs, Map<IDocument, Collection<ReconcileProblem>> problems = jr.reconcile(project, docs);
problemCollectorFactory);
problems.entrySet().forEach(e -> { problems.entrySet().forEach(e -> {
IProblemCollector collector = problemCollectors.get(e.getKey()); IProblemCollector collector = problemCollectors.get(e.getKey());
e.getValue().forEach(p -> collector.accept(p)); e.getValue().forEach(p -> collector.accept(p));
@@ -161,8 +176,14 @@ public class BootJavaReconcileEngine implements IReconcileEngine, IJavaProjectRe
} }
}) })
.filter(f -> f.getFileName().toString().endsWith(".java")) .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;
}
} }

View File

@@ -1,5 +1,5 @@
/******************************************************************************* /*******************************************************************************
* Copyright (c) 2022 VMware, Inc. * Copyright (c) 2022, 2023 VMware, Inc.
* All rights reserved. This program and the accompanying materials * All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0 * are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at * 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.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Function;
import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
@@ -25,7 +24,6 @@ public interface JavaReconciler {
void reconcile(IJavaProject project, IDocument doc, IProblemCollector problemCollector); void reconcile(IJavaProject project, IDocument doc, IProblemCollector problemCollector);
Map<IDocument, Collection<ReconcileProblem>> reconcile(IJavaProject project, List<TextDocument> docs, Map<IDocument, Collection<ReconcileProblem>> reconcile(IJavaProject project, List<TextDocument> docs);
Function<TextDocument, IProblemCollector> problemCollectorFactory);
} }

View File

@@ -15,7 +15,6 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Function;
import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.Annotation;
@@ -160,8 +159,7 @@ public class JdtReconciler implements JavaReconciler {
} }
@Override @Override
public Map<IDocument, Collection<ReconcileProblem>> reconcile(IJavaProject project, List<TextDocument> docs, public Map<IDocument, Collection<ReconcileProblem>> reconcile(IJavaProject project, List<TextDocument> docs) {
Function<TextDocument, IProblemCollector> problemCollectorFactory) {
if (config.isRewriteReconcileEnabled()) { if (config.isRewriteReconcileEnabled()) {
// long start = System.currentTimeMillis(); // long start = System.currentTimeMillis();

View File

@@ -22,7 +22,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.openrewrite.ExecutionContext; import org.openrewrite.ExecutionContext;
@@ -139,8 +138,7 @@ public class RewriteReconciler implements JavaReconciler {
} }
@Override @Override
public Map<IDocument, Collection<ReconcileProblem>> reconcile(IJavaProject project, List<TextDocument> docs, public Map<IDocument, Collection<ReconcileProblem>> reconcile(IJavaProject project, List<TextDocument> docs) {
Function<TextDocument, IProblemCollector> problemCollectorFactory) {
if (!config.isRewriteReconcileEnabled()) { if (!config.isRewriteReconcileEnabled()) {
return Collections.emptyMap(); return Collections.emptyMap();
@@ -162,9 +160,9 @@ public class RewriteReconciler implements JavaReconciler {
} }
JavaParser javaParser = ORAstUtils.createJavaParser(project); JavaParser javaParser = ORAstUtils.createJavaParser(project);
javaParser.setSourceSet(MavenProjectParser.MAIN); javaParser.setSourceSet(MavenProjectParser.MAIN);
allProblems.putAll(doReconcile(project, mainSources, problemCollectorFactory, javaParser)); allProblems.putAll(doReconcile(project, mainSources, javaParser));
javaParser.setSourceSet(MavenProjectParser.TEST); javaParser.setSourceSet(MavenProjectParser.TEST);
allProblems.putAll(doReconcile(project, testSources, problemCollectorFactory, javaParser)); allProblems.putAll(doReconcile(project, testSources, javaParser));
long end = System.currentTimeMillis(); long end = System.currentTimeMillis();
log.info("reconciling project (OpenRewrite): " + project.getElementName() + " - " + docs.size() + " done in " + (end - start) + "ms"); 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; private static final int BATCH = 50;
// Parse in batches and share the parser // Parse in batches and share the parser
private Map<IDocument, Collection<ReconcileProblem>> doReconcile(IJavaProject project, List<TextDocument> docs, private Map<IDocument, Collection<ReconcileProblem>> doReconcile(IJavaProject project, List<TextDocument> docs, JavaParser javaParser) {
Function<TextDocument, IProblemCollector> problemCollectorFactory, JavaParser javaParser) {
Map<IDocument, Collection<ReconcileProblem>> allProblems = new HashMap<>(); Map<IDocument, Collection<ReconcileProblem>> allProblems = new HashMap<>();
if (javaParser != null && config.isRewriteReconcileEnabled()) { if (javaParser != null && config.isRewriteReconcileEnabled()) {
try { try {

View File

@@ -99,8 +99,8 @@ public class SpringBootUpgrade {
if (version.getMajor() == targetVersion.getMajor() && version.getMinor() == targetVersion.getMinor()) { if (version.getMajor() == targetVersion.getMajor() && version.getMinor() == targetVersion.getMinor()) {
// patch version upgrade - treat as pom versions only upgrade // 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 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)); recipe.doNext(new UpgradeParentVersion("org.springframework.boot", "spring-boot-starter-parent", version.getMajor() + "." + version.getMinor() + ".x", null, null));
} else { } else {
createRecipeIdsChain(version.getMajor(), version.getMinor(), targetVersion.getMajor(), targetVersion.getMinor()).stream() createRecipeIdsChain(version.getMajor(), version.getMinor(), targetVersion.getMajor(), targetVersion.getMinor()).stream()
.map(recipeRepo::getRecipe) .map(recipeRepo::getRecipe)

View File

@@ -11,13 +11,13 @@
package org.springframework.ide.vscode.boot.validation; package org.springframework.ide.vscode.boot.validation;
import java.util.Collections; import java.util.Collections;
import java.util.function.Function;
import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.app.BootJavaConfig; import org.springframework.ide.vscode.boot.app.BootJavaConfig;
import org.springframework.ide.vscode.boot.common.IJavaProjectReconcileEngine; 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;
import org.springframework.ide.vscode.boot.validation.generations.ProjectVersionDiagnosticProvider.DiagnosticResult; import org.springframework.ide.vscode.boot.validation.generations.ProjectVersionDiagnosticProvider.DiagnosticResult;
import org.springframework.ide.vscode.boot.validation.generations.SpringIoProjectsProvider; 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.VersionValidators;
import org.springframework.ide.vscode.boot.validation.generations.preferences.VersionValidationPreferences; import org.springframework.ide.vscode.boot.validation.generations.preferences.VersionValidationPreferences;
import org.springframework.ide.vscode.commons.java.IJavaProject; 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.languageserver.util.SimpleLanguageServer;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
public class BootVersionValidationEngine implements IJavaProjectReconcileEngine { public class BootVersionValidationEngine implements IJavaProjectReconcileEngine {
@@ -36,13 +36,41 @@ public class BootVersionValidationEngine implements IJavaProjectReconcileEngine
private SimpleLanguageServer server; private SimpleLanguageServer server;
private BootJavaConfig config; 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.server = server;
this.config = config; 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<TextDocument, IProblemCollector> problemCollectorFactory) { public void reconcile(IJavaProject project) {
if (config.isBootVersionValidationEnabled()) { if (config.isBootVersionValidationEnabled()) {
log.debug("validating Spring Boot version on project: " + project.getElementName()); log.debug("validating Spring Boot version on project: " + project.getElementName());
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
@@ -86,4 +114,9 @@ public class BootVersionValidationEngine implements IJavaProjectReconcileEngine
Collections.emptyList()); Collections.emptyList());
} }
} }
@Override
public ProjectReconcileScheduler getScheduler() {
return projectReconcileScheduler;
}
} }

View File

@@ -77,6 +77,7 @@ public class ValueSpelExpressionValidationTest {
@Autowired private CompilationUnitCache compilationUnitCache; @Autowired private CompilationUnitCache compilationUnitCache;
@Autowired private SimpleLanguageServer server; @Autowired private SimpleLanguageServer server;
@Autowired private BootJavaConfig config; @Autowired private BootJavaConfig config;
@Autowired private ProjectObserver projectObserver;
private File directory; private File directory;
private String docUri; private String docUri;
@@ -149,7 +150,7 @@ public class ValueSpelExpressionValidationTest {
problemCollector = new TestProblemCollector(); problemCollector = new TestProblemCollector();
reconcileEngine = new BootJavaReconcileEngine(projectFinder, new JavaReconciler[] { reconcileEngine = new BootJavaReconcileEngine(projectFinder, new JavaReconciler[] {
new JdtReconciler(compilationUnitCache, config) new JdtReconciler(compilationUnitCache, config)
}, server.getTextDocumentService()); }, server, config, projectObserver, null);
} }
@AfterEach @AfterEach