From 263930d7b5115aa9cc42cd31110ebdf8a3e86c7f Mon Sep 17 00:00:00 2001 From: aboyko Date: Thu, 25 Jan 2024 19:01:46 -0500 Subject: [PATCH] Regenerate Modulith metadata on select .class file create/change in the output dir --- .../commons/util/BasicFileObserver.java | 4 +- .../ide/vscode/commons/util/FileObserver.java | 13 +- .../ide/vscode/boot/modulith/AppModules.java | 4 +- .../vscode/boot/modulith/ModulithService.java | 158 ++++++++++++------ .../boot/modulith/ModulithServiceTest.java | 5 +- 5 files changed, 129 insertions(+), 55 deletions(-) diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/BasicFileObserver.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/BasicFileObserver.java index 1c4b1072a..33cefbf82 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/BasicFileObserver.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/BasicFileObserver.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2020 Pivotal, Inc. + * Copyright (c) 2017, 2024 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 @@ -125,6 +125,8 @@ public class BasicFileObserver implements FileObserver { .isPresent()) .toArray(String[]::new))) + .filter(superPair -> superPair.right.length > 0) + // then call the accept method of each consumer with the generated array of doc URIs .forEach(superPair -> superPair.left.right.accept(superPair.right)); } diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/FileObserver.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/FileObserver.java index 5d0c484be..06627f713 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/FileObserver.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/FileObserver.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2020 Pivotal, Inc. + * Copyright (c) 2017, 2024 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 @@ -44,4 +44,15 @@ public interface FileObserver { }; } + default Disposable onCreatedOrChanged(List globPattern, Consumer handler) { + String[] ids = { + onFilesChanged(globPattern, handler), + onFilesCreated(globPattern, handler), + }; + return () -> { + for (String id : ids) { + unsubscribe(id); + } + }; + } } 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 9939a99eb..9c1474f8b 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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 VMware, Inc. + * Copyright (c) 2023, 2024 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 @@ -37,7 +37,7 @@ public final class AppModules { }); } - private Optional getModuleForPackage(String pkgName) { + public Optional getModuleForPackage(String pkgName) { return generatePackageHierarchy(pkgName) .stream() .map(p -> modules.stream().filter(m -> m.basePackage().equals(p)).findFirst()) 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 7c1b37fce..091367b75 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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 VMware, Inc. + * Copyright (c) 2023, 2024 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 @@ -17,6 +17,7 @@ import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -24,10 +25,13 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +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.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -58,13 +62,21 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import reactor.core.Disposable; + public class ModulithService { + + private static long regenCount = 0; + private static final Duration DEBOUNCE_TIME = Duration.ofMillis(500L); + private static final Logger log = LoggerFactory.getLogger(ModulithService.class); private static final String CMD_MODULITH_REFRESH = "sts/modulith/metadata/refresh"; private static final String CMD_LIST_MODULITH_PROJECTS = "sts/modulith/projects"; + private final ExecutorService executor; + private SimpleLanguageServer server; private SpringSymbolIndex springIndex; private BootJavaReconcileEngine reconciler; @@ -72,6 +84,7 @@ public class ModulithService { private Map cache; private Map> metadataRequested; + private Map classFilesListeners; public ModulithService( SimpleLanguageServer server, @@ -84,14 +97,17 @@ public class ModulithService { this.config = config; this.cache = new ConcurrentHashMap<>(); this.metadataRequested = new ConcurrentHashMap<>(); + this.classFilesListeners = new ConcurrentHashMap<>(); this.server = server; this.springIndex = springIndex; this.reconciler = reconciler; + this.executor = Executors.newCachedThreadPool(); projectObserver.addListener(new ProjectObserver.Listener() { @Override public void deleted(IJavaProject project) { + stopListening(project); removeFromCache(project); } @@ -99,9 +115,9 @@ public class ModulithService { public void created(IJavaProject project) { if (isModulithDependentProject(project)) { if (anyClassFilesPresent(project)) { - requestMetadata(project); + requestMetadata(project, DEBOUNCE_TIME).thenAccept(res -> startListening(project)); } else { - waitForClassFilesCreatedInTargetFolder(project); + startListening(project); } } } @@ -110,10 +126,13 @@ public class ModulithService { public void changed(IJavaProject project) { if (!isModulithDependentProject(project)) { removeFromCache(project); + stopListening(project); } else if (anyClassFilesPresent(project)) { - requestMetadata(project); - } else { - waitForClassFilesCreatedInTargetFolder(project); + if (anyClassFilesPresent(project)) { + requestMetadata(project, DEBOUNCE_TIME).thenAccept(res -> startListening(project)); + } else { + startListening(project); + } } } }); @@ -130,16 +149,62 @@ public class ModulithService { .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); - } - })); + private boolean startListening(IJavaProject project) { + URI uri = project.getLocationUri(); + if (classFilesListeners.containsKey(uri)) { + return false; + } else { + final List outputFolders = getNonTestClassOutputFolders(project).collect(Collectors.toList()); + Disposable packagInfoDisposable = server.getWorkspaceService().getFileObserver().onCreatedOrChanged(outputFolders.stream().map(p -> p.toString() + "/**/package-info.class").collect(Collectors.toList()), files -> { + log.info("%d MODULITH METADATA REFRESH SCHEDULED due to change/create in: file %s".formatted(++regenCount, files[0])); + requestMetadata(project, DEBOUNCE_TIME); + }); + String classFilesSubscription = server.getWorkspaceService().getFileObserver().onFilesCreated( + outputFolders.stream().map(p -> p.toString() + "/**/*.class").collect(Collectors.toList()), + files -> { + AppModules modules = getModulesData(project); + if (modules == null) { + log.info("%d MODULITH METADATA REFRESH SCHEDULED due to no metadata present".formatted(++regenCount)); + requestMetadata(project, DEBOUNCE_TIME); + } else { + for (String f : files) { + Path p = Path.of(URI.create(f)); + // Exclude 'package-info.class' files as they are handled separately + if (!"package-info.class".equals(p.getFileName().toString())) { + for (Path of : outputFolders) { + if (p.startsWith(of) ) { + Path parentFolder = of.relativize(p).getParent(); + String packageName = parentFolder == null ? "" : parentFolder.toString().replace(of.getFileSystem().getSeparator(), "."); + Optional moduleOpt = modules.getModuleForPackage(packageName); + if (moduleOpt.isPresent()) { + log.info("%d MODULITH METADATA REFRESH SCHEDULED due to change/create in: %s for file %s".formatted(++regenCount, packageName, f)); + requestMetadata(project, DEBOUNCE_TIME); + return; + } + break; + } + } + } + } + } + }); + classFilesListeners.put(uri, () -> { + packagInfoDisposable.dispose(); + server.getWorkspaceService().getFileObserver().unsubscribe(classFilesSubscription); + }); + return true; + } + } + + private boolean stopListening(IJavaProject project) { + Disposable subscription = classFilesListeners.remove(project.getLocationUri()); + if (subscription != null) { + subscription.dispose(); + } + return subscription != null; } public AppModules getModulesData(IJavaProject project) { @@ -156,7 +221,7 @@ public class ModulithService { return CompletableFuture.completedFuture(false); } clearMetadataRequest(project); - return requestMetadata(project).whenComplete((refreshed, throwable) -> { + return requestMetadata(project, Duration.ZERO).whenComplete((refreshed, throwable) -> { if (throwable != null) { server.getClient().showMessage(new MessageParams(MessageType.Error, "Project '" + project.getElementName() + "' Modulith metadata refresh has failed. " + throwable.getMessage())); } else { @@ -169,13 +234,10 @@ public class ModulithService { }); } - 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); - } + CompletableFuture requestMetadata(IJavaProject p, Duration delay) { + clearMetadataRequest(p); + CompletableFuture f = loadModulesMetadata(p, delay).thenApply(appModules -> updateAppModulesCache(p, appModules)); + metadataRequested.put(p.getLocationUri(), f); return f; } @@ -229,9 +291,9 @@ public class ModulithService { } } - private CompletableFuture loadModulesMetadata(IJavaProject project) { + private CompletableFuture loadModulesMetadata(IJavaProject project, Duration delay) { log.info("Loading Modulith metadata for project '" + project.getElementName() + "'..."); - return findRootPackages(project).thenComposeAsync(packages -> { + return findRootPackages(project, delay).thenComposeAsync(packages -> { if (!packages.isEmpty()) { try { String javaCmd = ProcessHandle.current().info().command().orElseThrow(); @@ -244,7 +306,7 @@ public class ModulithService { }).collect(Collectors.joining(System.getProperty("path.separator"))); List allAppModules = new ArrayList<>(); CompletableFuture[] aggregateFuture = packages.stream() - .map(pkg -> computeAppModules(project.getElementName(), javaCmd, classpathStr, pkg) + .map(pkg -> CompletableFuture.supplyAsync(() -> computeAppModules(project.getElementName(), javaCmd, classpathStr, pkg), executor) .thenAccept(allAppModules::addAll)) .toArray(CompletableFuture[]::new); return CompletableFuture.allOf(aggregateFuture).thenApply(r -> new AppModules(allAppModules)); @@ -253,10 +315,10 @@ public class ModulithService { } } return CompletableFuture.completedFuture(null); - }); + }, executor); } - private CompletableFuture> computeAppModules(String projectName, String javaCmd, + private List computeAppModules(String projectName, String javaCmd, String cp, String pkg) { try { File outputFile = File.createTempFile(projectName + "-" + pkg, "json"); @@ -275,31 +337,29 @@ public class ModulithService { builder.append(line); builder.append(System.getProperty("line.separator")); } - return process.onExit().thenApply(p -> { - if (p.exitValue() == 0) { - try { - log.info("Updating Modulith metadata for project '" + projectName + "'"); - JsonObject json = JsonParser.parseReader(new FileReader(outputFile)).getAsJsonObject(); - log.info("Modulith metadata: " + new GsonBuilder().setPrettyPrinting().create().toJson(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() + "\n" + builder.toString()); - } - return Collections.emptyList(); - }); - } catch (IOException e) { + int exitValue = process.waitFor(); + if (exitValue == 0) { + log.info("Updating Modulith metadata for project '" + projectName + "'"); + JsonObject json = JsonParser.parseReader(new FileReader(outputFile)).getAsJsonObject(); + log.info("Modulith metadata: " + new GsonBuilder().setPrettyPrinting().create().toJson(json)); + return loadAppModules(json); + } else { + log.error("Failed to generate modulith metadata for project '" + projectName + "'. Modulith Exporter process exited with code " + process.exitValue() + "\n" + builder.toString()); + } + } catch (IOException | InterruptedException e) { log.error("", e); } - return CompletableFuture.completedFuture(Collections.emptyList()); + return Collections.emptyList(); } - private CompletableFuture> findRootPackages(IJavaProject project) { - BeansParams params = new BeansParams(); - params.setProjectName(project.getElementName()); - return springIndex.beans(params).thenApply(beansOfProject -> { + private CompletableFuture> findRootPackages(IJavaProject project, Duration delay) { + return CompletableFuture.supplyAsync(() -> { + BeansParams params = new BeansParams(); + params.setProjectName(project.getElementName()); + return params; + }, CompletableFuture.delayedExecutor(delay.toSeconds(), TimeUnit.SECONDS, executor)) + .thenComposeAsync(params -> springIndex.beans(params), executor) + .thenApply(beansOfProject -> { HashSet packages = new HashSet<>(); if (beansOfProject != null) { for (Bean bean : beansOfProject) { 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 2ff2ca93e..b83ecb2f9 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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 VMware, Inc. + * Copyright (c) 2023, 2024 VMware, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -13,6 +13,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.time.Duration; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -70,7 +71,7 @@ public class ModulithServiceTest { @Test void sanityTest() throws Exception { - assertTrue(modulithService.requestMetadata(jp).get()); + assertTrue(modulithService.requestMetadata(jp, Duration.ZERO).get()); List modules = modulithService.getModulesData(jp).modules; assertEquals(2, modules.size()); AppModule orderModule = modules.get(0);