4
4
import java .lang .invoke .MethodHandle ;
5
5
import java .lang .invoke .MethodHandles ;
6
6
import java .lang .invoke .MethodType ;
7
+ import java .lang .reflect .Constructor ;
7
8
import java .lang .reflect .InvocationTargetException ;
9
+ import java .lang .reflect .Modifier ;
8
10
import java .net .URLClassLoader ;
9
11
import java .nio .file .Path ;
10
12
import java .nio .file .Paths ;
11
13
import java .util .ArrayList ;
12
14
import java .util .Collection ;
13
15
import java .util .Collections ;
16
+ import java .util .Comparator ;
14
17
import java .util .HashSet ;
15
18
import java .util .List ;
16
19
import java .util .Properties ;
17
20
import java .util .Set ;
18
21
import java .util .concurrent .CountDownLatch ;
19
22
import java .util .concurrent .ExecutorService ;
20
23
import java .util .concurrent .ForkJoinPool ;
24
+ import java .util .function .BiConsumer ;
25
+ import java .util .function .Function ;
21
26
import java .util .stream .Collectors ;
22
27
import java .util .stream .Stream ;
23
28
29
34
import org .apache .maven .plugins .annotations .Mojo ;
30
35
import org .apache .maven .plugins .annotations .Parameter ;
31
36
import org .apache .maven .plugins .annotations .ResolutionScope ;
37
+ import org .codehaus .plexus .PlexusContainer ;
38
+ import org .codehaus .plexus .component .repository .exception .ComponentLookupException ;
32
39
import org .eclipse .aether .RepositorySystem ;
40
+ import org .eclipse .aether .artifact .DefaultArtifact ;
33
41
import org .eclipse .aether .collection .CollectRequest ;
34
42
import org .eclipse .aether .graph .Dependency ;
35
43
import org .eclipse .aether .graph .DependencyFilter ;
36
44
import org .eclipse .aether .resolution .ArtifactResult ;
37
45
import org .eclipse .aether .resolution .DependencyRequest ;
38
46
import org .eclipse .aether .resolution .DependencyResolutionException ;
39
47
import org .eclipse .aether .resolution .DependencyResult ;
48
+ import org .eclipse .aether .resolution .VersionRangeRequest ;
49
+ import org .eclipse .aether .resolution .VersionRangeResolutionException ;
50
+ import org .eclipse .aether .resolution .VersionRangeResult ;
40
51
import org .eclipse .aether .util .filter .DependencyFilterUtils ;
41
52
53
+ import static java .util .stream .Collectors .toList ;
54
+
42
55
/**
43
56
* Executes the supplied java class in the current VM with the enclosing project's dependencies as classpath.
44
57
*
@@ -58,6 +71,20 @@ public class ExecJavaMojo extends AbstractExecMojo {
58
71
* The main class to execute.<br>
59
72
* With Java 9 and above you can prefix it with the modulename, e.g. <code>com.greetings/com.greetings.Main</code>
60
73
* Without modulename the classpath will be used, with modulename a new modulelayer will be created.
74
+ * <p>
75
+ * Note that you can also provide a {@link Runnable} fully qualified name.
76
+ * The runnable can get constructor injections either by type if you have maven in your classpath (can be provided)
77
+ * or by name (ensure to enable {@code -parameters} Java compiler option) for loose coupling.
78
+ * Current support loose injections are:
79
+ * <ul>
80
+ * <li><code>systemProperties</code>: <code>Properties</code>, session system properties</li>
81
+ * <li><code>systemPropertiesUpdater</code>: <code>BiConsumer<String, String></code>, session system properties update callback (pass the key/value to update, null value means removal of the key)</li>
82
+ * <li><code>userProperties</code>: <code>Properties</code>, session user properties</li>
83
+ * <li><code>userPropertiesUpdater</code>: <code>BiConsumer<String, String></code>, session user properties update callback (pass the key/value to update, null value means removal of the key)</li>
84
+ * <li><code>projectProperties</code>: <code>Properties</code>, project properties</li>
85
+ * <li><code>projectPropertiesUpdater</code>: <code>BiConsumer<String, String></code>, project properties update callback (pass the key/value to update, null value means removal of the key)</li>
86
+ * <li><code>highestVersionResolver</code>: <code>Function<String, String></code>, passing a <code>groupId:artifactId</code> you get the latest resolved version from the project repositories</li>
87
+ * </ul>
61
88
*
62
89
* @since 1.0
63
90
*/
@@ -196,10 +223,11 @@ public class ExecJavaMojo extends AbstractExecMojo {
196
223
197
224
/**
198
225
* Whether to try and prohibit the called Java program from terminating the JVM (and with it the whole Maven build)
199
- * by calling {@link System#exit(int)}. When active, a special security manager will intercept those calls. In case
200
- * of an exit code 0 (OK), it will simply log the fact that {@link System#exit(int)} was called. Otherwise, it will
201
- * throw a {@link SystemExitException}, failing the Maven goal as if the called Java code itself had exited with an
202
- * exception. This way, the error is propagated without terminating the whole Maven JVM. In previous versions, users
226
+ * by calling {@link System#exit(int)}. When active, loaded classes will replace this call by a custom callback.
227
+ * In case of an exit code 0 (OK), it will simply log the fact that {@link System#exit(int)} was called.
228
+ * Otherwise, it will throw a {@link SystemExitException}, failing the Maven goal as if the called Java code itself
229
+ * had exited with an exception.
230
+ * This way, the error is propagated without terminating the whole Maven JVM. In previous versions, users
203
231
* had to use the {@code exec} instead of the {@code java} goal in such cases, which now with this option is no
204
232
* longer necessary.
205
233
*
@@ -208,6 +236,9 @@ public class ExecJavaMojo extends AbstractExecMojo {
208
236
@ Parameter (property = "exec.blockSystemExit" , defaultValue = "false" )
209
237
private boolean blockSystemExit ;
210
238
239
+ @ Component // todo: for maven4 move to Lookup instead
240
+ private PlexusContainer container ;
241
+
211
242
/**
212
243
* Execute goal.
213
244
*
@@ -249,7 +280,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
249
280
// See https://bugs.openjdk.org/browse/JDK-8199704 for details about how users might be able to
250
281
// block
251
282
// System::exit in post-removal JDKs (still undecided at the time of writing this comment).
252
- Thread bootstrapThread = new Thread (
283
+ Thread bootstrapThread = new Thread ( // TODO: drop this useless thread 99% of the time
253
284
threadGroup ,
254
285
() -> {
255
286
int sepIndex = mainClass .indexOf ('/' );
@@ -262,15 +293,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
262
293
}
263
294
264
295
try {
265
- Class <?> bootClass =
266
- Thread .currentThread ().getContextClassLoader ().loadClass (bootClassName );
267
-
268
- MethodHandles .Lookup lookup = MethodHandles .lookup ();
269
-
270
- MethodHandle mainHandle =
271
- lookup .findStatic (bootClass , "main" , MethodType .methodType (void .class , String [].class ));
272
-
273
- mainHandle .invoke (arguments );
296
+ doExec (bootClassName );
274
297
} catch (IllegalAccessException | NoSuchMethodException | NoSuchMethodError e ) { // just pass it on
275
298
Thread .currentThread ()
276
299
.getThreadGroup ()
@@ -295,7 +318,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
295
318
}
296
319
},
297
320
mainClass + ".main()" );
298
- URLClassLoader classLoader = getClassLoader ();
321
+ URLClassLoader classLoader = getClassLoader (); // TODO: enable to cache accross executions
299
322
bootstrapThread .setContextClassLoader (classLoader );
300
323
setSystemProperties ();
301
324
@@ -315,7 +338,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
315
338
316
339
try {
317
340
threadGroup .destroy ();
318
- } catch (RuntimeException /* missing method in future java version */ e ) {
341
+ } catch (RuntimeException | Error /* missing method in future java version */ e ) {
319
342
getLog ().warn ("Couldn't destroy threadgroup " + threadGroup , e );
320
343
}
321
344
}
@@ -344,6 +367,160 @@ public void execute() throws MojoExecutionException, MojoFailureException {
344
367
registerSourceRoots ();
345
368
}
346
369
370
+ private void doExec (final String bootClassName ) throws Throwable {
371
+ Class <?> bootClass = Thread .currentThread ().getContextClassLoader ().loadClass (bootClassName );
372
+ MethodHandles .Lookup lookup = MethodHandles .lookup ();
373
+ try {
374
+ doMain (lookup .findStatic (bootClass , "main" , MethodType .methodType (void .class , String [].class )));
375
+ } catch (final NoSuchMethodException nsme ) {
376
+ if (Runnable .class .isAssignableFrom (bootClass )) {
377
+ doRun (bootClass );
378
+ } else {
379
+ throw nsme ;
380
+ }
381
+ }
382
+ }
383
+
384
+ private void doMain (final MethodHandle mainHandle ) throws Throwable {
385
+ mainHandle .invoke (arguments );
386
+ }
387
+
388
+ private void doRun (final Class <?> bootClass )
389
+ throws InstantiationException , IllegalAccessException , InvocationTargetException , NoSuchMethodException {
390
+ final Class <? extends Runnable > runnableClass = bootClass .asSubclass (Runnable .class );
391
+ final Constructor <? extends Runnable > constructor = Stream .of (runnableClass .getDeclaredConstructors ())
392
+ .map (i -> (Constructor <? extends Runnable >) i )
393
+ .filter (i -> Modifier .isPublic (i .getModifiers ()))
394
+ .max (Comparator .<Constructor <? extends Runnable >, Integer >comparing (Constructor ::getParameterCount ))
395
+ .orElseThrow (() -> new IllegalArgumentException ("No public constructor found for " + bootClass ));
396
+ if (getLog ().isDebugEnabled ()) {
397
+ getLog ().debug ("Using constructor " + constructor );
398
+ }
399
+
400
+ Runnable runnable ;
401
+ try { // todo: enhance that but since injection API is being defined at mvn4 level it is
402
+ // good enough
403
+ final Object [] args = Stream .of (constructor .getParameters ())
404
+ .map (param -> {
405
+ try {
406
+ return lookupParam (param );
407
+ } catch (final ComponentLookupException e ) {
408
+ getLog ().error (e .getMessage (), e );
409
+ throw new IllegalStateException (e );
410
+ }
411
+ })
412
+ .toArray (Object []::new );
413
+ constructor .setAccessible (true );
414
+ runnable = constructor .newInstance (args );
415
+ } catch (final RuntimeException re ) {
416
+ if (getLog ().isDebugEnabled ()) {
417
+ getLog ().debug (
418
+ "Can't inject " + runnableClass + "': " + re .getMessage () + ", will ignore injections" ,
419
+ re );
420
+ }
421
+ final Constructor <? extends Runnable > declaredConstructor = runnableClass .getDeclaredConstructor ();
422
+ declaredConstructor .setAccessible (true );
423
+ runnable = declaredConstructor .newInstance ();
424
+ }
425
+ runnable .run ();
426
+ }
427
+
428
+ private Object lookupParam (final java .lang .reflect .Parameter param ) throws ComponentLookupException {
429
+ final String name = param .getName ();
430
+ switch (name ) {
431
+ // loose coupled to maven (wrapped with standard jvm types to not require it)
432
+ case "systemProperties" : // Properties
433
+ return getSession ().getSystemProperties ();
434
+ case "systemPropertiesUpdater" : // BiConsumer<String, String>
435
+ return propertiesUpdater (getSession ().getSystemProperties ());
436
+ case "userProperties" : // Properties
437
+ return getSession ().getUserProperties ();
438
+ case "userPropertiesUpdater" : // BiConsumer<String, String>
439
+ return propertiesUpdater (getSession ().getUserProperties ());
440
+ case "projectProperties" : // Properties
441
+ return project .getProperties ();
442
+ case "projectPropertiesUpdater" : // BiConsumer<String, String>
443
+ return propertiesUpdater (project .getProperties ());
444
+ case "highestVersionResolver" : // Function<String, String>
445
+ return resolveVersion (VersionRangeResult ::getHighestVersion );
446
+ // standard bindings
447
+ case "session" : // MavenSession
448
+ return getSession ();
449
+ case "container" : // PlexusContainer
450
+ return container ;
451
+ default : // Any
452
+ return lookup (param , name );
453
+ }
454
+ }
455
+
456
+ private Object lookup (final java .lang .reflect .Parameter param , final String name ) throws ComponentLookupException {
457
+ // try injecting a real instance but loose coupled - will use reflection
458
+ if (param .getType () == Object .class && name .contains ("_" )) {
459
+ final ClassLoader loader = Thread .currentThread ().getContextClassLoader ();
460
+
461
+ try {
462
+ final int hintIdx = name .indexOf ("__hint_" );
463
+ if (hintIdx > 0 ) {
464
+ final String hint = name .substring (hintIdx + "__hint_" .length ());
465
+ final String typeName = name .substring (0 , hintIdx ).replace ('_' , '.' );
466
+ return container .lookup (loader .loadClass (typeName ), hint );
467
+ }
468
+
469
+ final String typeName = name .replace ('_' , '.' );
470
+ return container .lookup (loader .loadClass (typeName ));
471
+ } catch (final ClassNotFoundException cnfe ) {
472
+ if (getLog ().isDebugEnabled ()) {
473
+ getLog ().debug ("Can't load param (" + name + "): " + cnfe .getMessage (), cnfe );
474
+ }
475
+ // let's try to lookup object, unlikely but not impossible
476
+ }
477
+ }
478
+
479
+ // just lookup by type
480
+ return container .lookup (param .getType ());
481
+ }
482
+
483
+ private Function <String , String > resolveVersion (final Function <VersionRangeResult , Object > fn ) {
484
+ return ga -> {
485
+ final int sep = ga .indexOf (':' );
486
+ if (sep < 0 ) {
487
+ throw new IllegalArgumentException ("Invalid groupId:artifactId argument: '" + ga + "'" );
488
+ }
489
+
490
+ final org .eclipse .aether .artifact .Artifact artifact = new DefaultArtifact (ga + ":[0,)" );
491
+ final VersionRangeRequest rangeRequest = new VersionRangeRequest ();
492
+ rangeRequest .setArtifact (artifact );
493
+ try {
494
+ if (includePluginDependencies && includeProjectDependencies ) {
495
+ rangeRequest .setRepositories (Stream .concat (
496
+ project .getRemoteProjectRepositories ().stream (),
497
+ project .getRemotePluginRepositories ().stream ())
498
+ .distinct ()
499
+ .collect (toList ()));
500
+ } else if (includePluginDependencies ) {
501
+ rangeRequest .setRepositories (project .getRemotePluginRepositories ());
502
+ } else if (includeProjectDependencies ) {
503
+ rangeRequest .setRepositories (project .getRemoteProjectRepositories ());
504
+ }
505
+ final VersionRangeResult rangeResult =
506
+ repositorySystem .resolveVersionRange (getSession ().getRepositorySession (), rangeRequest );
507
+ return String .valueOf (fn .apply (rangeResult ));
508
+ } catch (final VersionRangeResolutionException e ) {
509
+ throw new IllegalStateException (e );
510
+ }
511
+ };
512
+ }
513
+
514
+ private BiConsumer <String , String > propertiesUpdater (final Properties props ) {
515
+ return (k , v ) -> {
516
+ if (v == null ) {
517
+ props .remove (k );
518
+ } else {
519
+ props .setProperty (k , v );
520
+ }
521
+ };
522
+ }
523
+
347
524
/**
348
525
* To avoid the exec:java to consider common pool threads leaked, let's pre-create them.
349
526
*/
0 commit comments