1
1
/*******************************************************************************
2
- * Copyright (c) 2023 VMware, Inc.
2
+ * Copyright (c) 2023, 2024 VMware, Inc.
3
3
* All rights reserved. This program and the accompanying materials
4
4
* are made available under the terms of the Eclipse Public License v1.0
5
5
* which accompanies this distribution, and is available at
17
17
import java .nio .file .Files ;
18
18
import java .nio .file .Path ;
19
19
import java .nio .file .Paths ;
20
+ import java .time .Duration ;
20
21
import java .util .ArrayList ;
21
22
import java .util .Arrays ;
22
23
import java .util .Collections ;
23
24
import java .util .HashSet ;
24
25
import java .util .List ;
25
26
import java .util .Map ;
26
27
import java .util .Objects ;
28
+ import java .util .Optional ;
27
29
import java .util .Set ;
28
30
import java .util .concurrent .CompletableFuture ;
29
31
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 ;
31
35
import java .util .stream .Collectors ;
32
36
import java .util .stream .Stream ;
33
37
58
62
import com .google .gson .JsonObject ;
59
63
import com .google .gson .JsonParser ;
60
64
65
+ import reactor .core .Disposable ;
66
+
61
67
public class ModulithService {
68
+
69
+ private static long regenCount = 0 ;
62
70
71
+ private static final Duration DEBOUNCE_TIME = Duration .ofMillis (500L );
72
+
63
73
private static final Logger log = LoggerFactory .getLogger (ModulithService .class );
64
74
65
75
private static final String CMD_MODULITH_REFRESH = "sts/modulith/metadata/refresh" ;
66
76
private static final String CMD_LIST_MODULITH_PROJECTS = "sts/modulith/projects" ;
67
77
78
+ private final ExecutorService executor ;
79
+
68
80
private SimpleLanguageServer server ;
69
81
private SpringSymbolIndex springIndex ;
70
82
private BootJavaReconcileEngine reconciler ;
71
83
private BootJavaConfig config ;
72
84
73
85
private Map <URI , AppModules > cache ;
74
86
private Map <URI , CompletableFuture <Boolean >> metadataRequested ;
87
+ private Map <URI , Disposable > classFilesListeners ;
75
88
76
89
public ModulithService (
77
90
SimpleLanguageServer server ,
@@ -84,24 +97,27 @@ public ModulithService(
84
97
this .config = config ;
85
98
this .cache = new ConcurrentHashMap <>();
86
99
this .metadataRequested = new ConcurrentHashMap <>();
100
+ this .classFilesListeners = new ConcurrentHashMap <>();
87
101
this .server = server ;
88
102
this .springIndex = springIndex ;
89
103
this .reconciler = reconciler ;
104
+ this .executor = Executors .newCachedThreadPool ();
90
105
91
106
projectObserver .addListener (new ProjectObserver .Listener () {
92
107
93
108
@ Override
94
109
public void deleted (IJavaProject project ) {
110
+ stopListening (project );
95
111
removeFromCache (project );
96
112
}
97
113
98
114
@ Override
99
115
public void created (IJavaProject project ) {
100
116
if (isModulithDependentProject (project )) {
101
117
if (anyClassFilesPresent (project )) {
102
- requestMetadata (project );
118
+ requestMetadata (project , DEBOUNCE_TIME ). thenAccept ( res -> startListening ( project ) );
103
119
} else {
104
- waitForClassFilesCreatedInTargetFolder (project );
120
+ startListening (project );
105
121
}
106
122
}
107
123
}
@@ -110,10 +126,13 @@ public void created(IJavaProject project) {
110
126
public void changed (IJavaProject project ) {
111
127
if (!isModulithDependentProject (project )) {
112
128
removeFromCache (project );
129
+ stopListening (project );
113
130
} 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
+ }
117
136
}
118
137
}
119
138
});
@@ -130,16 +149,62 @@ public void changed(IJavaProject project) {
130
149
.collect (Collectors .toMap (p -> p .getElementName (), p -> p .getLocationUri ().toASCIIString ()))
131
150
);
132
151
});
152
+
133
153
}
134
154
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 ;
143
208
}
144
209
145
210
public AppModules getModulesData (IJavaProject project ) {
@@ -156,7 +221,7 @@ private CompletableFuture<Boolean> refreshMetadata(IJavaProject project) {
156
221
return CompletableFuture .completedFuture (false );
157
222
}
158
223
clearMetadataRequest (project );
159
- return requestMetadata (project ).whenComplete ((refreshed , throwable ) -> {
224
+ return requestMetadata (project , Duration . ZERO ).whenComplete ((refreshed , throwable ) -> {
160
225
if (throwable != null ) {
161
226
server .getClient ().showMessage (new MessageParams (MessageType .Error , "Project '" + project .getElementName () + "' Modulith metadata refresh has failed. " + throwable .getMessage ()));
162
227
} else {
@@ -169,13 +234,10 @@ private CompletableFuture<Boolean> refreshMetadata(IJavaProject project) {
169
234
});
170
235
}
171
236
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 );
179
241
return f ;
180
242
}
181
243
@@ -229,9 +291,9 @@ private void validate(IJavaProject project) {
229
291
}
230
292
}
231
293
232
- private CompletableFuture <AppModules > loadModulesMetadata (IJavaProject project ) {
294
+ private CompletableFuture <AppModules > loadModulesMetadata (IJavaProject project , Duration delay ) {
233
295
log .info ("Loading Modulith metadata for project '" + project .getElementName () + "'..." );
234
- return findRootPackages (project ).thenComposeAsync (packages -> {
296
+ return findRootPackages (project , delay ).thenComposeAsync (packages -> {
235
297
if (!packages .isEmpty ()) {
236
298
try {
237
299
String javaCmd = ProcessHandle .current ().info ().command ().orElseThrow ();
@@ -244,7 +306,7 @@ private CompletableFuture<AppModules> loadModulesMetadata(IJavaProject project)
244
306
}).collect (Collectors .joining (System .getProperty ("path.separator" )));
245
307
List <AppModule > allAppModules = new ArrayList <>();
246
308
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 )
248
310
.thenAccept (allAppModules ::addAll ))
249
311
.toArray (CompletableFuture []::new );
250
312
return CompletableFuture .allOf (aggregateFuture ).thenApply (r -> new AppModules (allAppModules ));
@@ -253,10 +315,10 @@ private CompletableFuture<AppModules> loadModulesMetadata(IJavaProject project)
253
315
}
254
316
}
255
317
return CompletableFuture .completedFuture (null );
256
- });
318
+ }, executor );
257
319
}
258
320
259
- private CompletableFuture < List <AppModule > > computeAppModules (String projectName , String javaCmd ,
321
+ private List <AppModule > computeAppModules (String projectName , String javaCmd ,
260
322
String cp , String pkg ) {
261
323
try {
262
324
File outputFile = File .createTempFile (projectName + "-" + pkg , "json" );
@@ -275,31 +337,29 @@ private CompletableFuture<List<AppModule>> computeAppModules(String projectName,
275
337
builder .append (line );
276
338
builder .append (System .getProperty ("line.separator" ));
277
339
}
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 ) {
294
350
log .error ("" , e );
295
351
}
296
- return CompletableFuture . completedFuture ( Collections .emptyList () );
352
+ return Collections .emptyList ();
297
353
}
298
354
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 -> {
303
363
HashSet <String > packages = new HashSet <>();
304
364
if (beansOfProject != null ) {
305
365
for (Bean bean : beansOfProject ) {
0 commit comments