diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/plugin.xml b/eclipse-language-servers/org.springframework.tooling.boot.ls/plugin.xml
index c9453ba38..87ff4611d 100644
--- a/eclipse-language-servers/org.springframework.tooling.boot.ls/plugin.xml
+++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/plugin.xml
@@ -256,6 +256,10 @@
class="org.springframework.tooling.boot.ls.commands.RewriteRefactoringsHandler$UpgradeBootVersion"
commandId="org.springframework.tooling.boot.ls.rewrite.boot-upgrade">
+
+
@@ -282,6 +286,11 @@
id="org.springframework.tooling.boot.ls.rewrite.boot-upgrade"
name="Upgrade Spring Boot Version...">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/BootProjectTester.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/BootProjectTester.java
index ed0d2c82d..cb6ecd003 100644
--- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/BootProjectTester.java
+++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/BootProjectTester.java
@@ -39,7 +39,23 @@ public class BootProjectTester extends PropertyTester {
if (project != null) {
IJavaProject jp = JavaCore.create(project);
if (jp != null) {
- return isBootProject(project);
+ return isProjectWithDependency(jp, "spring-boot");
+ }
+ }
+ }
+ } else if ("isProjectWithDependencyResource".equals(property)) {
+ IResource resource = null;
+ if (receiver instanceof IAdaptable) {
+ resource = ((IAdaptable) receiver).getAdapter(IResource.class);
+ } else if (receiver instanceof IDocument) {
+ resource = LSPEclipseUtils.getFile((IDocument) receiver);
+ }
+ if (resource != null) {
+ IProject project = resource.getProject();
+ if (project != null) {
+ IJavaProject jp = JavaCore.create(project);
+ if (jp != null) {
+ return isProjectWithDependency(jp, (String) args[0]);
}
}
}
@@ -47,42 +63,38 @@ public class BootProjectTester extends PropertyTester {
return false;
}
- private static boolean isBootProject(IProject project) {
- if (project==null || ! project.isAccessible()) {
+ private static boolean isProjectWithDependency(IJavaProject jp, String dep) {
+ if (jp == null || ! jp.getProject().isAccessible()) {
return false;
}
try {
- if (project.hasNature(JavaCore.NATURE_ID)) {
- IJavaProject jp = JavaCore.create(project);
- IClasspathEntry[] classpath = jp.getResolvedClasspath(true);
- //Look for a 'spring-boot' jar or project entry
+ IClasspathEntry[] classpath = jp.getResolvedClasspath(true);
- for (IClasspathEntry e : classpath) {
- if (isBootJar(e) || isBootProject(e)) {
- return true;
- }
- }
+ for (IClasspathEntry e : classpath) {
+ if (isDependencyJar(dep, e) || isDependencyProject(dep, e)) {
+ return true;
+ }
}
} catch (Exception e) {
CorePlugin.log(e);
}
return false;
}
-
- private static boolean isBootJar(IClasspathEntry e) {
+
+ private static boolean isDependencyJar(String dep, IClasspathEntry e) {
if (e.getEntryKind()==IClasspathEntry.CPE_LIBRARY) {
IPath path = e.getPath();
String name = path.lastSegment();
- return name.endsWith(".jar") && name.startsWith("spring-boot");
+ return name.endsWith(".jar") && name.startsWith(dep);
}
return false;
}
- private static boolean isBootProject(IClasspathEntry e) {
+ private static boolean isDependencyProject(String dep, IClasspathEntry e) {
if (e.getEntryKind()==IClasspathEntry.CPE_PROJECT) {
IPath path = e.getPath();
String name = path.lastSegment();
- return name.startsWith("spring-boot");
+ return name.startsWith(dep);
}
return false;
}
diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RefreshModulithMetadata.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RefreshModulithMetadata.java
new file mode 100644
index 000000000..67d2363ab
--- /dev/null
+++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RefreshModulithMetadata.java
@@ -0,0 +1,59 @@
+/*******************************************************************************
+ * 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.tooling.boot.ls.commands;
+
+import java.util.List;
+
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.lsp4e.LanguageServers;
+import org.eclipse.lsp4e.LanguageServersRegistry;
+import org.eclipse.lsp4e.LanguageServersRegistry.LanguageServerDefinition;
+import org.eclipse.lsp4j.ExecuteCommandParams;
+import org.eclipse.ui.handlers.HandlerUtil;
+import org.springframework.tooling.boot.ls.BootLanguageServerPlugin;
+
+@SuppressWarnings("restriction")
+public class RefreshModulithMetadata extends AbstractHandler {
+
+ @Override
+ public Object execute(ExecutionEvent event) throws ExecutionException {
+ IStructuredSelection selection = HandlerUtil.getCurrentStructuredSelection(event);
+ Object o = selection.getFirstElement();
+ IProject project = null;
+ if (o instanceof IResource) {
+ project = ((IResource) o).getProject();
+ } else if (o instanceof IProject) {
+ project = (IProject) o;
+ } else if (o instanceof IAdaptable) {
+ project = ((IAdaptable) o).getAdapter(IProject.class);
+ }
+ if (project != null) {
+ LanguageServerDefinition def = LanguageServersRegistry.getInstance().getDefinition(BootLanguageServerPlugin.BOOT_LS_DEFINITION_ID);
+ Assert.isLegal(def != null, "No definition found for Boot Language Server");
+
+ final String uri = project.getLocationURI().toASCIIString();
+ ExecuteCommandParams commandParams = new ExecuteCommandParams();
+ commandParams.setCommand("sts/modulith/metadata/refresh");
+ commandParams.setArguments(List.of(uri));
+
+ LanguageServers.forProject(project).withPreferredServer(def).computeFirst(ls -> ls.getWorkspaceService().executeCommand(commandParams));
+ }
+ return null;
+ }
+
+}
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 67893ccd3..4cede7916 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
@@ -376,8 +376,11 @@ public class BootLanguageServerBootApp {
}
@Bean
- ModulithService modulithService(SimpleLanguageServer server, ProjectObserver projectObserver, SpringMetamodelIndex springIndex, JavaProjectFinder projectFinder) {
- return new ModulithService(projectObserver, server.getWorkspaceService().getFileObserver(), projectFinder, springIndex);
+ ModulithService modulithService(SimpleLanguageServer server, JavaProjectFinder projectFinder,
+ ProjectObserver projectObserver, SpringSymbolIndex springIndex,
+ Optional projectReconcileScheduler, BootJavaReconcileEngine reconciler,
+ BootJavaConfig config) {
+ return new ModulithService(server, projectFinder, projectObserver, springIndex, reconciler, projectReconcileScheduler, config);
}
}
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
index 7f2c0b61f..6305dce92 100644
--- 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
@@ -69,7 +69,7 @@ public abstract class ProjectReconcileScheduler {
}
}
- protected final void scheduleValidation(IJavaProject project) {
+ public final void scheduleValidation(IJavaProject project) {
if (!SpringProjectUtil.isSpringProject(project)) {
// Bail out if not a spring project
return;
diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Boot3JavaProblemType.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Boot3JavaProblemType.java
index e835b17b9..4610bea91 100644
--- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Boot3JavaProblemType.java
+++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Boot3JavaProblemType.java
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (c) 2020, 2022 Pivotal, Inc.
+ * Copyright (c) 2020, 2023 Pivotal, 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
@@ -27,7 +27,7 @@ public enum Boot3JavaProblemType implements ProblemType {
FACTORIES_KEY_NOT_SUPPORTED(ERROR, "Spring factories key not supported", "Spring factories key not supported"),
- MODULITH_TYPE_REF_VIOLATION(ERROR, "Restricted dependency", "Import from restricted module");
+ MODULITH_TYPE_REF_VIOLATION(ERROR, "Modulith restricted type reference", "Modulith restricted type reference");
private final ProblemSeverity defaultSeverity;
private String description;
diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/ModulithTypeReferenceViolation.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/ModulithTypeReferenceViolation.java
index 5009778ef..c32253a5a 100644
--- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/ModulithTypeReferenceViolation.java
+++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/ModulithTypeReferenceViolation.java
@@ -11,8 +11,6 @@
package org.springframework.ide.vscode.boot.java.rewrite.reconcile;
import java.util.Optional;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Tree;
@@ -44,16 +42,7 @@ public class ModulithTypeReferenceViolation implements RecipeCodeActionDescripto
@Override
public JavaVisitor getMarkerVisitor(MarkerVisitorContext context) {
ModulithService modulithService = context.appContext().getBean(ModulithService.class);
- CompletableFuture future = modulithService.getModulesData(context.project());
-
- AppModules modules = null;
- try {
- modules = future == null ? null : future.get();
- } catch (InterruptedException | ExecutionException e) {
- // ignore
- }
-
- final AppModules appModules = modules;
+ AppModules appModules = modulithService.getModulesData(context.project());
return new JavaIsoVisitor() {
diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/AppModules.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/AppModules.java
index 88af53765..9939a99eb 100644
--- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/AppModules.java
+++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/AppModules.java
@@ -12,6 +12,7 @@ package org.springframework.ide.vscode.boot.modulith;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
import java.util.Optional;
public final class AppModules {
@@ -60,4 +61,21 @@ public final class AppModules {
return packageHierarchy;
}
+ @Override
+ public int hashCode() {
+ return Objects.hash(modules);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ AppModules other = (AppModules) obj;
+ return Objects.equals(modules, other.modules);
+ }
+
}
diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/ModulithService.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/ModulithService.java
index 356b1345d..067c26cde 100644
--- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/ModulithService.java
+++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/ModulithService.java
@@ -14,10 +14,12 @@ import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.net.URI;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -25,154 +27,233 @@ import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.eclipse.lsp4j.MessageParams;
+import org.eclipse.lsp4j.MessageType;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
+import org.springframework.ide.vscode.boot.app.BootJavaConfig;
+import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
import org.springframework.ide.vscode.boot.java.Annotations;
-import org.springframework.ide.vscode.commons.java.IClasspathUtil;
+import org.springframework.ide.vscode.boot.java.Boot3JavaProblemType;
+import org.springframework.ide.vscode.boot.java.handlers.BootJavaProjectReconcilerScheduler;
+import org.springframework.ide.vscode.boot.java.handlers.BootJavaReconcileEngine;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.java.SpringProjectUtil;
import org.springframework.ide.vscode.commons.java.Version;
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.ProblemCategory.Toggle.Option;
+import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
import org.springframework.ide.vscode.commons.protocol.java.Classpath;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
-import org.springframework.ide.vscode.commons.util.FileObserver;
+import org.springframework.ide.vscode.commons.protocol.spring.BeansParams;
+import org.springframework.ide.vscode.commons.util.text.TextDocument;
+import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
+import com.tngtech.archunit.thirdparty.com.google.common.base.Objects;
public class ModulithService {
-
+
private static final Logger log = LoggerFactory.getLogger(ModulithService.class);
- private static final List FILE_PATTERNS = List.of("**/*.java");
+ private static final String CMD_MODULITH_REFRESH = "sts/modulith/metadata/refresh";
+ private static final String CMD_LIST_MODULITH_PROJECTS = "sts/modulith/projects";
- private Map> cache;
-
- private SpringMetamodelIndex springIndex;
-
+ private SimpleLanguageServer server;
+ private Optional projectReconcileScheduler;
+ private SpringSymbolIndex springIndex;
+ private BootJavaReconcileEngine reconciler;
private JavaProjectFinder projectFinder;
+ private BootJavaConfig config;
- public ModulithService(ProjectObserver projectObserver, FileObserver fileObserver, JavaProjectFinder projectFinder, SpringMetamodelIndex springIndex) {
+ private Map cache;
+ private Map> metadataRequested;
+
+ public ModulithService(
+ SimpleLanguageServer server,
+ JavaProjectFinder projectFinder,
+ ProjectObserver projectObserver,
+ SpringSymbolIndex springIndex,
+ BootJavaReconcileEngine reconciler,
+ Optional projectReconcileScheduler,
+ BootJavaConfig config
+ ) {
this.projectFinder = projectFinder;
+ this.config = config;
+ this.cache = new ConcurrentHashMap<>();
+ this.metadataRequested = new ConcurrentHashMap<>();
+ this.server = server;
+ this.projectReconcileScheduler = projectReconcileScheduler;
this.springIndex = springIndex;
- cache = new ConcurrentHashMap<>();
+ this.reconciler = reconciler;
projectObserver.addListener(new ProjectObserver.Listener() {
@Override
public void deleted(IJavaProject project) {
- invalidate(project);
+ removeFromCache(project);
}
@Override
public void created(IJavaProject project) {
- invalidate(project);
+ Version v = SpringProjectUtil.getDependencyVersion(project, "spring-modulith-core");
+ if (v != null) {
+ if (anyClassFilesPresent(project)) {
+ requestMetadata(project);
+ } else {
+ waitForClassFilesCreatedInTargetFolder(project);
+ }
+ }
}
@Override
public void changed(IJavaProject project) {
- invalidate(project);
+ Version v = SpringProjectUtil.getDependencyVersion(project, "spring-modulith-core");
+ if (v == null) {
+ removeFromCache(project);
+ } else if (anyClassFilesPresent(project)) {
+ requestMetadata(project);
+ } else {
+ waitForClassFilesCreatedInTargetFolder(project);
+ }
}
-
});
- fileObserver.onFilesCreated(FILE_PATTERNS, this::handleFilesChanged);
- fileObserver.onFilesDeleted(FILE_PATTERNS, this::handleFilesChanged);
- }
-
- private void handleFilesChanged(String[] files) {
- for (String f : files) {
- URI uri = URI.create(f);
- TextDocumentIdentifier docId = new TextDocumentIdentifier(uri.toASCIIString());
- projectFinder.find(docId).ifPresent(project -> {
- synchronized (project) {
- if (cache.containsKey(project)) {
- Path filePath = Paths.get(uri);
- if (IClasspathUtil.getProjectJavaSourceFoldersWithoutTests(project.getClasspath()).map(folder -> folder.toPath()).anyMatch(folderPath -> filePath.startsWith(folderPath))) {
- cache.remove(project);
- }
- }
- }
- });
- }
- }
-
- private void invalidate(IJavaProject project) {
- CompletableFuture future = cache.remove(project);
- if (future != null) {
- future.cancel(true);
- }
- }
+ server.onCommand(CMD_MODULITH_REFRESH, params -> {
+ String uri = ((JsonElement) params.getArguments().get(0)).getAsString();
+ return projectFinder.find(new TextDocumentIdentifier(uri)).map(this::refreshMetadata).orElse(CompletableFuture.completedFuture(false)).thenApply(String::valueOf);
+ });
+ server.onCommand(CMD_LIST_MODULITH_PROJECTS, params -> {
+ return CompletableFuture.completedFuture(projectFinder.all()
+ .stream()
+ .filter(p -> SpringProjectUtil.getDependencyVersion(p, "spring-modulith-core") != null)
+ .collect(Collectors.toMap(p -> p.getElementName(), p -> p.getLocationUri().toASCIIString()))
+ );
+ });
+ }
+
+ private void waitForClassFilesCreatedInTargetFolder(IJavaProject project) {
+ final AtomicReference subscription = new AtomicReference<>();
+ subscription.set(server.getWorkspaceService().getFileObserver().onFilesCreated(getNonTestClassOutputFolders(project).map(p -> p.toString() + "/**/*.class").collect(Collectors.toList()), files -> {
+ if (subscription.get() != null) {
+ server.getWorkspaceService().getFileObserver().unsubscribe(subscription.get());
+ requestMetadata(project);
+ }
+ }));
+ }
+
+ public AppModules getModulesData(IJavaProject project) {
+ return cache.get(project.getLocationUri());
+ }
+
+ private CompletableFuture refreshMetadata(IJavaProject project) {
+ Version v = SpringProjectUtil.getDependencyVersion(project, "spring-modulith-core");
+ if (v == null) {
+ server.getClient().showMessage(new MessageParams(MessageType.Error, "Project '" + project.getElementName() + "' does not depend on spring-modulith."));
+ return CompletableFuture.completedFuture(false);
+ }
+ if (!anyClassFilesPresent(project)) {
+ server.getClient().showMessage(new MessageParams(MessageType.Error, "Project '" + project.getElementName() + "' output folder does not contain any '.class' files. Consider re-building."));
+ return CompletableFuture.completedFuture(false);
+ }
+ clearMetadataRequest(project);
+ return requestMetadata(project).whenComplete((refreshed, throwable) -> {
+ if (throwable != null) {
+ server.getClient().showMessage(new MessageParams(MessageType.Error, "Project '" + project.getElementName() + "' Modulith metadata refresh has failed. " + throwable.getMessage()));
+ } else {
+ if (refreshed) {
+ server.getClient().showMessage(new MessageParams(MessageType.Info, "Project '" + project.getElementName() + "' Modulith metadata has been changed."));
+ } else {
+ server.getClient().showMessage(new MessageParams(MessageType.Info, "Project '" + project.getElementName() + "' Modulith metadata has been refreshed but it has not unchanged."));
+ }
+ }
+ });
+ }
+
+ CompletableFuture requestMetadata(IJavaProject p) {
+ URI uri = p.getLocationUri();
+ CompletableFuture f = metadataRequested.get(uri);
+ if (f == null) {
+ f = loadModulesMetadata(p).thenApply(appModules -> updateAppModulesCache(p, appModules));
+ metadataRequested.put(uri, f);
+ }
+ return f;
+ }
+
+ private boolean updateAppModulesCache(IJavaProject project, AppModules modules) {
+ URI uri = project.getLocationUri();
+ AppModules oldModules = modules == null ? cache.remove(uri) : cache.put(uri, modules);
+ if (!Objects.equal(modules, oldModules)) {
+ validate(project);
+ return true;
+ }
+ return false;
+ }
+
+ private AppModules removeFromCache(IJavaProject project) {
+ clearMetadataRequest(project);
+ return cache.remove(project.getLocationUri());
+ }
+
+ private void clearMetadataRequest(IJavaProject project) {
+ CompletableFuture f = metadataRequested.remove(project.getLocationUri());
+ if (f != null && !f.isDone()) {
+ f.cancel(true);
+ }
+ }
+
+ private void validate(IJavaProject project) {
+ if (server.getDiagnosticSeverityProvider().getDiagnosticSeverity(Boot3JavaProblemType.MODULITH_TYPE_REF_VIOLATION) != null
+ && config.getProblemApplicability(Boot3JavaProblemType.MODULITH_TYPE_REF_VIOLATION) != Option.OFF) {
+ for (TextDocument doc : server.getTextDocumentService().getAll()) {
+ if (projectFinder.find(doc.getId()).orElse(null) == project) {
+ server.validateWith(doc.getId(), reconciler);
+ }
+ }
+ projectReconcileScheduler.ifPresent(r -> r.scheduleValidation(project));
+ }
+ }
+
private CompletableFuture loadModulesMetadata(IJavaProject project) {
Version v = SpringProjectUtil.getDependencyVersion(project, "spring-modulith-core");
if (v != null) {
- Set packages = findRootPackages(project);
- if (!packages.isEmpty()) {
- try {
- String javaCmd = ProcessHandle.current().info().command().orElseThrow();
- String classpathStr = project.getClasspath().getClasspathEntries().stream().map(cpe -> {
- if (Classpath.ENTRY_KIND_SOURCE.equals(cpe.getKind())) {
- return cpe.getOutputFolder();
- } else {
- return cpe.getPath();
- }
- }).collect(Collectors.joining(System.getProperty("path.separator")));
- List allAppModules = new ArrayList<>();
- CompletableFuture>[] aggregateFuture = packages
- .stream()
- .map(pkg -> computeAppModules(project.getElementName(), javaCmd, classpathStr, pkg).thenAccept(oa -> oa.ifPresent(allAppModules::addAll)))
- .toArray(CompletableFuture[]::new);
- return CompletableFuture.allOf(aggregateFuture).thenApply(r -> new AppModules(allAppModules));
- } catch (Exception e) {
- log.error("", e);
+ log.info("Loading Modulith metadata for project '" + project.getElementName() + "'...");
+ return findRootPackages(project).thenComposeAsync(packages -> {
+ if (!packages.isEmpty()) {
+ try {
+ String javaCmd = ProcessHandle.current().info().command().orElseThrow();
+ String classpathStr = project.getClasspath().getClasspathEntries().stream().map(cpe -> {
+ if (Classpath.ENTRY_KIND_SOURCE.equals(cpe.getKind())) {
+ return cpe.getOutputFolder();
+ } else {
+ return cpe.getPath();
+ }
+ }).collect(Collectors.joining(System.getProperty("path.separator")));
+ List allAppModules = new ArrayList<>();
+ CompletableFuture>[] aggregateFuture = packages
+ .stream()
+ .map(pkg -> computeAppModules(project.getElementName(), javaCmd, classpathStr, pkg).thenAccept(allAppModules::addAll))
+ .toArray(CompletableFuture[]::new);
+ return CompletableFuture.allOf(aggregateFuture).thenApply(r -> new AppModules(allAppModules));
+ } catch (Exception e) {
+ log.error("", e);
+ }
}
- }
+ return CompletableFuture.completedFuture(null);
+ });
}
return CompletableFuture.completedFuture(null);
}
- public CompletableFuture getModulesData(IJavaProject project) {
- synchronized(project) {
- CompletableFuture modules = cache.get(project);
- if (modules == null) {
- modules = loadModulesMetadata(project);
- cache.put(project, modules);
- }
- return modules;
- }
- }
-
- private Set findRootPackages(IJavaProject project) {
- HashSet packages = new HashSet<>();
- for (Bean bean : springIndex.getBeansOfProject(project.getElementName())) {
- String beanType = bean.getType();
- if (beanType != null) {
- if (Arrays.stream(bean.getAnnotations()).anyMatch(Annotations.BOOT_APP::equals)) {
- packages.add(getPackageNameFromTypeFQName(beanType));
- }
- }
- }
- return packages;
- }
-
- static String getPackageNameFromTypeFQName(String fqn) {
- int idx = 0;
- for (; idx < fqn.length() - 1; idx++) {
- char c = fqn.charAt(idx);
- if (c == '.' && Character.isUpperCase(fqn.charAt(idx + 1))) {
- return fqn.substring(0, idx);
- }
- }
- return fqn;
- }
-
- private CompletableFuture>> computeAppModules(String projectName, String javaCmd,
+ private CompletableFuture> computeAppModules(String projectName, String javaCmd,
String cp, String pkg) {
try {
File outputFile = File.createTempFile(projectName + "-" + pkg, "json");
@@ -191,19 +272,49 @@ public class ModulithService {
log.info("Updating Modulith metadata for project '" + projectName + "'");
JsonObject json = JsonParser.parseReader(new FileReader(outputFile)).getAsJsonObject();
log.info("Modulith metadata: " + json);
- return Optional.ofNullable(loadAppModules(json));
+ return loadAppModules(json);
} catch (Exception e) {
log.error("", e);
}
} else {
log.error("Failed to generate modulith metadata for project '" + projectName + "'. Modulith Exporter process exited with code " + process.exitValue());
}
- return Optional.empty();
+ return Collections.emptyList();
});
} catch (IOException e) {
log.error("", e);
}
- return CompletableFuture.completedFuture(Optional.empty());
+ return CompletableFuture.completedFuture(Collections.emptyList());
+ }
+
+ private CompletableFuture> findRootPackages(IJavaProject project) {
+ BeansParams params = new BeansParams();
+ params.setProjectName(project.getElementName());
+ return springIndex.beans(params).thenApply(beansOfProject -> {
+ HashSet packages = new HashSet<>();
+ if (beansOfProject != null) {
+ for (Bean bean : beansOfProject) {
+ String beanType = bean.getType();
+ if (beanType != null) {
+ if (Arrays.stream(bean.getAnnotations()).anyMatch(Annotations.BOOT_APP::equals)) {
+ packages.add(getPackageNameFromTypeFQName(beanType));
+ }
+ }
+ }
+ }
+ return packages;
+ });
+ }
+
+ static String getPackageNameFromTypeFQName(String fqn) {
+ int idx = 0;
+ for (; idx < fqn.length() - 1; idx++) {
+ char c = fqn.charAt(idx);
+ if (c == '.' && Character.isUpperCase(fqn.charAt(idx + 1))) {
+ return fqn.substring(0, idx);
+ }
+ }
+ return fqn;
}
private static List loadAppModules(JsonObject json) {
@@ -222,4 +333,27 @@ public class ModulithService {
.collect(Collectors.toList());
return new AppModule(name, basePackage, namedInterfaces);
}
+
+ private static boolean anyClassFilesPresent(IJavaProject p) {
+ return getNonTestClassOutputFolders(p).anyMatch(path -> {
+ try {
+ return Files.exists(path) && Files.walk(path).anyMatch(f -> Files.isRegularFile(f) && f.toFile().getName().endsWith(".class"));
+ } catch (IOException e) {
+ log.error("", e);
+ return false;
+ }
+ });
+ }
+
+ private static Stream getNonTestClassOutputFolders(IJavaProject p) {
+ try {
+ return p.getClasspath().getClasspathEntries()
+ .stream()
+ .filter(cpe -> Classpath.ENTRY_KIND_SOURCE.equals(cpe.getKind()) && cpe.isJavaContent() && cpe.isOwn() && !cpe.isTest())
+ .map(cpe -> Paths.get(cpe.getOutputFolder()));
+ } catch (Exception e) {
+ log.error("", e);
+ return Stream.empty();
+ }
+ }
}
diff --git a/headless-services/spring-boot-language-server/src/main/resources/problem-types.json b/headless-services/spring-boot-language-server/src/main/resources/problem-types.json
index a3ccaf8f5..39da42893 100644
--- a/headless-services/spring-boot-language-server/src/main/resources/problem-types.json
+++ b/headless-services/spring-boot-language-server/src/main/resources/problem-types.json
@@ -111,8 +111,8 @@
},
{
"code": "MODULITH_TYPE_REF_VIOLATION",
- "label": "Import from restricted module",
- "description": "Restricted dependency",
+ "label": "Modulith restricted type reference",
+ "description": "Modulith restricted type reference",
"defaultSeverity": "ERROR"
}
]
diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/modulith/ModulithServiceTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/modulith/ModulithServiceTest.java
index 59e6e0939..2ff2ca93e 100644
--- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/modulith/ModulithServiceTest.java
+++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/modulith/ModulithServiceTest.java
@@ -11,6 +11,7 @@
package org.springframework.ide.vscode.boot.modulith;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -48,6 +49,7 @@ public class ModulithServiceTest {
@BeforeEach
public void setup() throws Exception {
+ harness.intialize(null);
// Use project harness with a customizer to land the project in the 'temp' folder rather than 'target/test-classes' because Modulith will filter everything with 'target/test-classes' out :-\
jp = projects.mavenProject("spring-modulith-example-full", p -> {});
harness.useProject(jp);
@@ -68,7 +70,8 @@ public class ModulithServiceTest {
@Test
void sanityTest() throws Exception {
- List modules = modulithService.getModulesData(jp).get().modules;
+ assertTrue(modulithService.requestMetadata(jp).get());
+ List modules = modulithService.getModulesData(jp).modules;
assertEquals(2, modules.size());
AppModule orderModule = modules.get(0);
assertEquals("order", orderModule.name());
diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/super-property-nav-sample/pom.xml b/headless-services/spring-boot-language-server/src/test/resources/test-projects/super-property-nav-sample/pom.xml
index 1f7645b0f..6d98fc9df 100644
--- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/super-property-nav-sample/pom.xml
+++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/super-property-nav-sample/pom.xml
@@ -47,7 +47,7 @@
io.spring.initializr
initializr-generator-spring
- 0.8.0.BUILD-SNAPSHOT
+ 0.8.0.RELEASE
diff --git a/vscode-extensions/vscode-spring-boot/lib/Main.ts b/vscode-extensions/vscode-spring-boot/lib/Main.ts
index 0d19300d8..90979e037 100644
--- a/vscode-extensions/vscode-spring-boot/lib/Main.ts
+++ b/vscode-extensions/vscode-spring-boot/lib/Main.ts
@@ -164,6 +164,21 @@ export function activate(context: VSCode.ExtensionContext): Thenable client.stop());
liveHoverUi.activate(client, options, context);
rewrite.activate(client, options, context);
+
+ VSCode.commands.registerCommand('vscode-spring-boot.spring.modulith.metadata.refresh', async () => {
+ const modulithProjects = await VSCode.commands.executeCommand('sts/modulith/projects');
+ const projectNames = Object.keys(modulithProjects);
+ if (projectNames.length === 0) {
+ VSCode.window.showErrorMessage('No Spring Modulith projects found');
+ } else {
+ const projectName = projectNames.length === 1 ? projectNames[0] : await VSCode.window.showQuickPick(
+ projectNames,
+ { placeHolder: "Select the target project." },
+ );
+ VSCode.commands.executeCommand('sts/modulith/metadata/refresh', modulithProjects[projectName]);
+ }
+ });
+
return new ApiManager(client).api;
});
}
diff --git a/vscode-extensions/vscode-spring-boot/package.json b/vscode-extensions/vscode-spring-boot/package.json
index 7a0148ba2..7c0840bb1 100644
--- a/vscode-extensions/vscode-spring-boot/package.json
+++ b/vscode-extensions/vscode-spring-boot/package.json
@@ -126,6 +126,11 @@
"command": "sts/common-properties/reload",
"title": "Reload Shared Properties Metadata",
"category": "Spring Boot"
+ },
+ {
+ "command": "vscode-spring-boot.spring.modulith.metadata.refresh",
+ "title": "Refresh Modulith Metadata",
+ "category": "Spring Boot"
}
],
"configuration": [
@@ -498,7 +503,7 @@
"spring-boot.ls.problem.boot3.MODULITH_TYPE_REF_VIOLATION": {
"type": "string",
"default": "ERROR",
- "description": "Restricted dependency",
+ "description": "Modulith restricted type reference",
"enum": [
"IGNORE",
"INFO",