Modulith integration improved with manual refresh

This commit is contained in:
aboyko
2023-07-11 19:48:05 -04:00
parent 7dfbad709f
commit 6496537e61
14 changed files with 462 additions and 141 deletions

View File

@@ -256,6 +256,10 @@
class="org.springframework.tooling.boot.ls.commands.RewriteRefactoringsHandler$UpgradeBootVersion"
commandId="org.springframework.tooling.boot.ls.rewrite.boot-upgrade">
</handler>
<handler
class="org.springframework.tooling.boot.ls.commands.RefreshModulithMetadata"
commandId="org.springframework.tooling.boot.ls.modulith.metadata.refresh">
</handler>
</extension>
<extension
point="org.eclipse.ui.commands">
@@ -282,6 +286,11 @@
id="org.springframework.tooling.boot.ls.rewrite.boot-upgrade"
name="Upgrade Spring Boot Version...">
</command>
<command
description="Refresh project&apos;s Modulith metadata and re-validate the project"
id="org.springframework.tooling.boot.ls.modulith.metadata.refresh"
name="Refresh Modulith Metadata">
</command>
</extension>
<extension
@@ -644,6 +653,80 @@
name="org.springframework.tooling.boot.ls.rewrite"
visible="true">
</separator>
<command
commandId="org.springframework.tooling.boot.ls.modulith.metadata.refresh"
id="org.springframework.tooling.boot.ls.modulith.metadata.refresh"
label="Refresh Modulith Metadata"
style="push">
<visibleWhen
checkEnabled="false">
<and>
<count
value="1">
</count>
<iterate>
<or>
<adapt
type="org.eclipse.core.resources.IFile">
<and>
<test
args="spring-modulith-core"
forcePluginActivation="true"
property="org.springframework.tooling.boot.isProjectWithDependencyResource">
</test>
<or>
<and>
<test
property="org.eclipse.core.resources.path"
value="/*/pom.xml">
</test>
<test
property="org.eclipse.core.resources.projectNature"
value="org.eclipse.m2e.core.maven2Nature">
</test>
</and>
<and>
<test
property="org.eclipse.core.resources.path"
value="/*/build.gradle">
</test>
<test
property="org.eclipse.core.resources.projectNature"
value="org.eclipse.buildship.core.gradleprojectnature">
</test>
</and>
</or>
</and>
</adapt>
<adapt
type="org.eclipse.core.resources.IProject">
<and>
<test
args="spring-modulith-core"
forcePluginActivation="true"
property="org.springframework.tooling.boot.isProjectWithDependencyResource">
</test>
<test
property="org.eclipse.core.resources.projectNature"
value="org.eclipse.jdt.core.javanature">
</test>
<or>
<test
property="org.eclipse.core.resources.projectNature"
value="org.eclipse.m2e.core.maven2Nature">
</test>
<test
property="org.eclipse.core.resources.projectNature"
value="org.eclipse.buildship.core.gradleprojectnature">
</test>
</or>
</and>
</adapt>
</or>
</iterate>
</and>
</visibleWhen>
</command>
</menuContribution>
</extension>
<extension
@@ -659,7 +742,7 @@
class="org.springframework.tooling.boot.ls.BootProjectTester"
id="org.springframework.tooling.boot"
namespace="org.springframework.tooling.boot"
properties="isBootResource"
properties="isBootResource,isProjectWithDependencyResource"
type="java.lang.Object">
</propertyTester>
</extension>

View File

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

View File

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

View File

@@ -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<BootJavaProjectReconcilerScheduler> projectReconcileScheduler, BootJavaReconcileEngine reconciler,
BootJavaConfig config) {
return new ModulithService(server, projectFinder, projectObserver, springIndex, reconciler, projectReconcileScheduler, config);
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<ExecutionContext> getMarkerVisitor(MarkerVisitorContext context) {
ModulithService modulithService = context.appContext().getBean(ModulithService.class);
CompletableFuture<AppModules> 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<ExecutionContext>() {

View File

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

View File

@@ -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<String> 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<IJavaProject, CompletableFuture<AppModules>> cache;
private SpringMetamodelIndex springIndex;
private SimpleLanguageServer server;
private Optional<BootJavaProjectReconcilerScheduler> 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<URI, AppModules> cache;
private Map<URI, CompletableFuture<Boolean>> metadataRequested;
public ModulithService(
SimpleLanguageServer server,
JavaProjectFinder projectFinder,
ProjectObserver projectObserver,
SpringSymbolIndex springIndex,
BootJavaReconcileEngine reconciler,
Optional<BootJavaProjectReconcilerScheduler> 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<AppModules> 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<String> 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<Boolean> 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<Boolean> requestMetadata(IJavaProject p) {
URI uri = p.getLocationUri();
CompletableFuture<Boolean> 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<Boolean> 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<AppModules> loadModulesMetadata(IJavaProject project) {
Version v = SpringProjectUtil.getDependencyVersion(project, "spring-modulith-core");
if (v != null) {
Set<String> 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<AppModule> 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<AppModule> 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<AppModules> getModulesData(IJavaProject project) {
synchronized(project) {
CompletableFuture<AppModules> modules = cache.get(project);
if (modules == null) {
modules = loadModulesMetadata(project);
cache.put(project, modules);
}
return modules;
}
}
private Set<String> findRootPackages(IJavaProject project) {
HashSet<String> 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<Optional<List<AppModule>>> computeAppModules(String projectName, String javaCmd,
private CompletableFuture<List<AppModule>> 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<Set<String>> findRootPackages(IJavaProject project) {
BeansParams params = new BeansParams();
params.setProjectName(project.getElementName());
return springIndex.beans(params).thenApply(beansOfProject -> {
HashSet<String> 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<AppModule> 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<Path> 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();
}
}
}

View File

@@ -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"
}
]

View File

@@ -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<AppModule> modules = modulithService.getModulesData(jp).get().modules;
assertTrue(modulithService.requestMetadata(jp).get());
List<AppModule> modules = modulithService.getModulesData(jp).modules;
assertEquals(2, modules.size());
AppModule orderModule = modules.get(0);
assertEquals("order", orderModule.name());

View File

@@ -47,7 +47,7 @@
<dependency>
<groupId>io.spring.initializr</groupId>
<artifactId>initializr-generator-spring</artifactId>
<version>0.8.0.BUILD-SNAPSHOT</version>
<version>0.8.0.RELEASE</version>
</dependency>
<dependency>

View File

@@ -164,6 +164,21 @@ export function activate(context: VSCode.ExtensionContext): Thenable<ExtensionAP
VSCode.commands.registerCommand('vscode-spring-boot.ls.stop', () => 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;
});
}

View File

@@ -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",