Skip to content

Commit 263930d

Browse files
committed
Regenerate Modulith metadata on select .class file create/change in the output dir
1 parent 5d40879 commit 263930d

File tree

5 files changed

+129
-55
lines changed

5 files changed

+129
-55
lines changed

Diff for: headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/BasicFileObserver.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2017, 2020 Pivotal, Inc.
2+
* Copyright (c) 2017, 2024 Pivotal, Inc.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -125,6 +125,8 @@ private static void notify(Map<String, ImmutablePair<List<PathMatcher>, Consumer
125125
.isPresent())
126126
.toArray(String[]::new)))
127127

128+
.filter(superPair -> superPair.right.length > 0)
129+
128130
// then call the accept method of each consumer with the generated array of doc URIs
129131
.forEach(superPair -> superPair.left.right.accept(superPair.right));
130132
}

Diff for: headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/FileObserver.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2017, 2020 Pivotal, Inc.
2+
* Copyright (c) 2017, 2024 Pivotal, Inc.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -44,4 +44,15 @@ default Disposable onAnyChange(List<String> globPattern, Consumer<String[]> hand
4444
};
4545
}
4646

47+
default Disposable onCreatedOrChanged(List<String> globPattern, Consumer<String[]> handler) {
48+
String[] ids = {
49+
onFilesChanged(globPattern, handler),
50+
onFilesCreated(globPattern, handler),
51+
};
52+
return () -> {
53+
for (String id : ids) {
54+
unsubscribe(id);
55+
}
56+
};
57+
}
4758
}

Diff for: headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/AppModules.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2023 VMware, Inc.
2+
* Copyright (c) 2023, 2024 VMware, Inc.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -37,7 +37,7 @@ public Optional<AppModule> getModuleNotExposingType(String targetPackage, String
3737
});
3838
}
3939

40-
private Optional<AppModule> getModuleForPackage(String pkgName) {
40+
public Optional<AppModule> getModuleForPackage(String pkgName) {
4141
return generatePackageHierarchy(pkgName)
4242
.stream()
4343
.map(p -> modules.stream().filter(m -> m.basePackage().equals(p)).findFirst())

Diff for: headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/ModulithService.java

+109-49
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2023 VMware, Inc.
2+
* Copyright (c) 2023, 2024 VMware, Inc.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -17,17 +17,21 @@
1717
import java.nio.file.Files;
1818
import java.nio.file.Path;
1919
import java.nio.file.Paths;
20+
import java.time.Duration;
2021
import java.util.ArrayList;
2122
import java.util.Arrays;
2223
import java.util.Collections;
2324
import java.util.HashSet;
2425
import java.util.List;
2526
import java.util.Map;
2627
import java.util.Objects;
28+
import java.util.Optional;
2729
import java.util.Set;
2830
import java.util.concurrent.CompletableFuture;
2931
import java.util.concurrent.ConcurrentHashMap;
30-
import java.util.concurrent.atomic.AtomicReference;
32+
import java.util.concurrent.ExecutorService;
33+
import java.util.concurrent.Executors;
34+
import java.util.concurrent.TimeUnit;
3135
import java.util.stream.Collectors;
3236
import java.util.stream.Stream;
3337

@@ -58,20 +62,29 @@
5862
import com.google.gson.JsonObject;
5963
import com.google.gson.JsonParser;
6064

65+
import reactor.core.Disposable;
66+
6167
public class ModulithService {
68+
69+
private static long regenCount = 0;
6270

71+
private static final Duration DEBOUNCE_TIME = Duration.ofMillis(500L);
72+
6373
private static final Logger log = LoggerFactory.getLogger(ModulithService.class);
6474

6575
private static final String CMD_MODULITH_REFRESH = "sts/modulith/metadata/refresh";
6676
private static final String CMD_LIST_MODULITH_PROJECTS = "sts/modulith/projects";
6777

78+
private final ExecutorService executor;
79+
6880
private SimpleLanguageServer server;
6981
private SpringSymbolIndex springIndex;
7082
private BootJavaReconcileEngine reconciler;
7183
private BootJavaConfig config;
7284

7385
private Map<URI, AppModules> cache;
7486
private Map<URI, CompletableFuture<Boolean>> metadataRequested;
87+
private Map<URI, Disposable> classFilesListeners;
7588

7689
public ModulithService(
7790
SimpleLanguageServer server,
@@ -84,24 +97,27 @@ public ModulithService(
8497
this.config = config;
8598
this.cache = new ConcurrentHashMap<>();
8699
this.metadataRequested = new ConcurrentHashMap<>();
100+
this.classFilesListeners = new ConcurrentHashMap<>();
87101
this.server = server;
88102
this.springIndex = springIndex;
89103
this.reconciler = reconciler;
104+
this.executor = Executors.newCachedThreadPool();
90105

91106
projectObserver.addListener(new ProjectObserver.Listener() {
92107

93108
@Override
94109
public void deleted(IJavaProject project) {
110+
stopListening(project);
95111
removeFromCache(project);
96112
}
97113

98114
@Override
99115
public void created(IJavaProject project) {
100116
if (isModulithDependentProject(project)) {
101117
if (anyClassFilesPresent(project)) {
102-
requestMetadata(project);
118+
requestMetadata(project, DEBOUNCE_TIME).thenAccept(res -> startListening(project));
103119
} else {
104-
waitForClassFilesCreatedInTargetFolder(project);
120+
startListening(project);
105121
}
106122
}
107123
}
@@ -110,10 +126,13 @@ public void created(IJavaProject project) {
110126
public void changed(IJavaProject project) {
111127
if (!isModulithDependentProject(project)) {
112128
removeFromCache(project);
129+
stopListening(project);
113130
} else if (anyClassFilesPresent(project)) {
114-
requestMetadata(project);
115-
} else {
116-
waitForClassFilesCreatedInTargetFolder(project);
131+
if (anyClassFilesPresent(project)) {
132+
requestMetadata(project, DEBOUNCE_TIME).thenAccept(res -> startListening(project));
133+
} else {
134+
startListening(project);
135+
}
117136
}
118137
}
119138
});
@@ -130,16 +149,62 @@ public void changed(IJavaProject project) {
130149
.collect(Collectors.toMap(p -> p.getElementName(), p -> p.getLocationUri().toASCIIString()))
131150
);
132151
});
152+
133153
}
134154

135-
private void waitForClassFilesCreatedInTargetFolder(IJavaProject project) {
136-
final AtomicReference<String> subscription = new AtomicReference<>();
137-
subscription.set(server.getWorkspaceService().getFileObserver().onFilesCreated(getNonTestClassOutputFolders(project).map(p -> p.toString() + "/**/*.class").collect(Collectors.toList()), files -> {
138-
if (subscription.get() != null) {
139-
server.getWorkspaceService().getFileObserver().unsubscribe(subscription.get());
140-
requestMetadata(project);
141-
}
142-
}));
155+
private boolean startListening(IJavaProject project) {
156+
URI uri = project.getLocationUri();
157+
if (classFilesListeners.containsKey(uri)) {
158+
return false;
159+
} else {
160+
final List<Path> outputFolders = getNonTestClassOutputFolders(project).collect(Collectors.toList());
161+
Disposable packagInfoDisposable = server.getWorkspaceService().getFileObserver().onCreatedOrChanged(outputFolders.stream().map(p -> p.toString() + "/**/package-info.class").collect(Collectors.toList()), files -> {
162+
log.info("%d MODULITH METADATA REFRESH SCHEDULED due to change/create in: file %s".formatted(++regenCount, files[0]));
163+
requestMetadata(project, DEBOUNCE_TIME);
164+
});
165+
String classFilesSubscription = server.getWorkspaceService().getFileObserver().onFilesCreated(
166+
outputFolders.stream().map(p -> p.toString() + "/**/*.class").collect(Collectors.toList()),
167+
files -> {
168+
AppModules modules = getModulesData(project);
169+
if (modules == null) {
170+
log.info("%d MODULITH METADATA REFRESH SCHEDULED due to no metadata present".formatted(++regenCount));
171+
requestMetadata(project, DEBOUNCE_TIME);
172+
} else {
173+
for (String f : files) {
174+
Path p = Path.of(URI.create(f));
175+
// Exclude 'package-info.class' files as they are handled separately
176+
if (!"package-info.class".equals(p.getFileName().toString())) {
177+
for (Path of : outputFolders) {
178+
if (p.startsWith(of) ) {
179+
Path parentFolder = of.relativize(p).getParent();
180+
String packageName = parentFolder == null ? "" : parentFolder.toString().replace(of.getFileSystem().getSeparator(), ".");
181+
Optional<AppModule> moduleOpt = modules.getModuleForPackage(packageName);
182+
if (moduleOpt.isPresent()) {
183+
log.info("%d MODULITH METADATA REFRESH SCHEDULED due to change/create in: %s for file %s".formatted(++regenCount, packageName, f));
184+
requestMetadata(project, DEBOUNCE_TIME);
185+
return;
186+
}
187+
break;
188+
}
189+
}
190+
}
191+
}
192+
}
193+
});
194+
classFilesListeners.put(uri, () -> {
195+
packagInfoDisposable.dispose();
196+
server.getWorkspaceService().getFileObserver().unsubscribe(classFilesSubscription);
197+
});
198+
return true;
199+
}
200+
}
201+
202+
private boolean stopListening(IJavaProject project) {
203+
Disposable subscription = classFilesListeners.remove(project.getLocationUri());
204+
if (subscription != null) {
205+
subscription.dispose();
206+
}
207+
return subscription != null;
143208
}
144209

145210
public AppModules getModulesData(IJavaProject project) {
@@ -156,7 +221,7 @@ private CompletableFuture<Boolean> refreshMetadata(IJavaProject project) {
156221
return CompletableFuture.completedFuture(false);
157222
}
158223
clearMetadataRequest(project);
159-
return requestMetadata(project).whenComplete((refreshed, throwable) -> {
224+
return requestMetadata(project, Duration.ZERO).whenComplete((refreshed, throwable) -> {
160225
if (throwable != null) {
161226
server.getClient().showMessage(new MessageParams(MessageType.Error, "Project '" + project.getElementName() + "' Modulith metadata refresh has failed. " + throwable.getMessage()));
162227
} else {
@@ -169,13 +234,10 @@ private CompletableFuture<Boolean> refreshMetadata(IJavaProject project) {
169234
});
170235
}
171236

172-
CompletableFuture<Boolean> requestMetadata(IJavaProject p) {
173-
URI uri = p.getLocationUri();
174-
CompletableFuture<Boolean> f = metadataRequested.get(uri);
175-
if (f == null) {
176-
f = loadModulesMetadata(p).thenApply(appModules -> updateAppModulesCache(p, appModules));
177-
metadataRequested.put(uri, f);
178-
}
237+
CompletableFuture<Boolean> requestMetadata(IJavaProject p, Duration delay) {
238+
clearMetadataRequest(p);
239+
CompletableFuture<Boolean> f = loadModulesMetadata(p, delay).thenApply(appModules -> updateAppModulesCache(p, appModules));
240+
metadataRequested.put(p.getLocationUri(), f);
179241
return f;
180242
}
181243

@@ -229,9 +291,9 @@ private void validate(IJavaProject project) {
229291
}
230292
}
231293

232-
private CompletableFuture<AppModules> loadModulesMetadata(IJavaProject project) {
294+
private CompletableFuture<AppModules> loadModulesMetadata(IJavaProject project, Duration delay) {
233295
log.info("Loading Modulith metadata for project '" + project.getElementName() + "'...");
234-
return findRootPackages(project).thenComposeAsync(packages -> {
296+
return findRootPackages(project, delay).thenComposeAsync(packages -> {
235297
if (!packages.isEmpty()) {
236298
try {
237299
String javaCmd = ProcessHandle.current().info().command().orElseThrow();
@@ -244,7 +306,7 @@ private CompletableFuture<AppModules> loadModulesMetadata(IJavaProject project)
244306
}).collect(Collectors.joining(System.getProperty("path.separator")));
245307
List<AppModule> allAppModules = new ArrayList<>();
246308
CompletableFuture<?>[] aggregateFuture = packages.stream()
247-
.map(pkg -> computeAppModules(project.getElementName(), javaCmd, classpathStr, pkg)
309+
.map(pkg -> CompletableFuture.supplyAsync(() -> computeAppModules(project.getElementName(), javaCmd, classpathStr, pkg), executor)
248310
.thenAccept(allAppModules::addAll))
249311
.toArray(CompletableFuture[]::new);
250312
return CompletableFuture.allOf(aggregateFuture).thenApply(r -> new AppModules(allAppModules));
@@ -253,10 +315,10 @@ private CompletableFuture<AppModules> loadModulesMetadata(IJavaProject project)
253315
}
254316
}
255317
return CompletableFuture.completedFuture(null);
256-
});
318+
}, executor);
257319
}
258320

259-
private CompletableFuture<List<AppModule>> computeAppModules(String projectName, String javaCmd,
321+
private List<AppModule> computeAppModules(String projectName, String javaCmd,
260322
String cp, String pkg) {
261323
try {
262324
File outputFile = File.createTempFile(projectName + "-" + pkg, "json");
@@ -275,31 +337,29 @@ private CompletableFuture<List<AppModule>> computeAppModules(String projectName,
275337
builder.append(line);
276338
builder.append(System.getProperty("line.separator"));
277339
}
278-
return process.onExit().thenApply(p -> {
279-
if (p.exitValue() == 0) {
280-
try {
281-
log.info("Updating Modulith metadata for project '" + projectName + "'");
282-
JsonObject json = JsonParser.parseReader(new FileReader(outputFile)).getAsJsonObject();
283-
log.info("Modulith metadata: " + new GsonBuilder().setPrettyPrinting().create().toJson(json));
284-
return loadAppModules(json);
285-
} catch (Exception e) {
286-
log.error("", e);
287-
}
288-
} else {
289-
log.error("Failed to generate modulith metadata for project '" + projectName + "'. Modulith Exporter process exited with code " + process.exitValue() + "\n" + builder.toString());
290-
}
291-
return Collections.emptyList();
292-
});
293-
} catch (IOException e) {
340+
int exitValue = process.waitFor();
341+
if (exitValue == 0) {
342+
log.info("Updating Modulith metadata for project '" + projectName + "'");
343+
JsonObject json = JsonParser.parseReader(new FileReader(outputFile)).getAsJsonObject();
344+
log.info("Modulith metadata: " + new GsonBuilder().setPrettyPrinting().create().toJson(json));
345+
return loadAppModules(json);
346+
} else {
347+
log.error("Failed to generate modulith metadata for project '" + projectName + "'. Modulith Exporter process exited with code " + process.exitValue() + "\n" + builder.toString());
348+
}
349+
} catch (IOException | InterruptedException e) {
294350
log.error("", e);
295351
}
296-
return CompletableFuture.completedFuture(Collections.emptyList());
352+
return Collections.emptyList();
297353
}
298354

299-
private CompletableFuture<Set<String>> findRootPackages(IJavaProject project) {
300-
BeansParams params = new BeansParams();
301-
params.setProjectName(project.getElementName());
302-
return springIndex.beans(params).thenApply(beansOfProject -> {
355+
private CompletableFuture<Set<String>> findRootPackages(IJavaProject project, Duration delay) {
356+
return CompletableFuture.supplyAsync(() -> {
357+
BeansParams params = new BeansParams();
358+
params.setProjectName(project.getElementName());
359+
return params;
360+
}, CompletableFuture.delayedExecutor(delay.toSeconds(), TimeUnit.SECONDS, executor))
361+
.thenComposeAsync(params -> springIndex.beans(params), executor)
362+
.thenApply(beansOfProject -> {
303363
HashSet<String> packages = new HashSet<>();
304364
if (beansOfProject != null) {
305365
for (Bean bean : beansOfProject) {

Diff for: headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/modulith/ModulithServiceTest.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2023 VMware, Inc.
2+
* Copyright (c) 2023, 2024 VMware, Inc.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -13,6 +13,7 @@
1313
import static org.junit.jupiter.api.Assertions.assertEquals;
1414
import static org.junit.jupiter.api.Assertions.assertTrue;
1515

16+
import java.time.Duration;
1617
import java.util.List;
1718
import java.util.concurrent.CompletableFuture;
1819
import java.util.concurrent.TimeUnit;
@@ -70,7 +71,7 @@ public void tearDown() {
7071

7172
@Test
7273
void sanityTest() throws Exception {
73-
assertTrue(modulithService.requestMetadata(jp).get());
74+
assertTrue(modulithService.requestMetadata(jp, Duration.ZERO).get());
7475
List<AppModule> modules = modulithService.getModulesData(jp).modules;
7576
assertEquals(2, modules.size());
7677
AppModule orderModule = modules.get(0);

0 commit comments

Comments
 (0)