Skip to content

Commit

Permalink
Enable to exec:java runnables and not only mains with loosely coupled…
Browse files Browse the repository at this point in the history
… injections
  • Loading branch information
rmannibucau committed Jan 26, 2024
1 parent c8d3fe0 commit ead991b
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 29 deletions.
18 changes: 18 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,24 @@
</pluginManagement>

<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<executions>
<execution>
<id>default-testCompile</id>
<goals>
<goal>testCompile</goal>
</goals>
<phase>test-compile</phase>
<configuration>
<parameters>true</parameters>
</configuration>
</execution>
</executions>
</plugin>

<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>animal-sniffer-maven-plugin</artifactId>
Expand Down
162 changes: 146 additions & 16 deletions src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ForkJoinPool;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand All @@ -29,14 +34,19 @@
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.collection.CollectRequest;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.graph.DependencyFilter;
import org.eclipse.aether.resolution.ArtifactResult;
import org.eclipse.aether.resolution.DependencyRequest;
import org.eclipse.aether.resolution.DependencyResolutionException;
import org.eclipse.aether.resolution.DependencyResult;
import org.eclipse.aether.resolution.VersionRangeRequest;
import org.eclipse.aether.resolution.VersionRangeResolutionException;
import org.eclipse.aether.resolution.VersionRangeResult;
import org.eclipse.aether.util.filter.DependencyFilterUtils;

/**
Expand All @@ -59,6 +69,20 @@ public class ExecJavaMojo extends AbstractExecMojo {
* With Java 9 and above you can prefix it with the modulename, e.g. <code>com.greetings/com.greetings.Main</code>
* Without modulename the classpath will be used, with modulename a new modulelayer will be created.
*
* Note that you can also provide a {@link Runnable} fully qualified name.
* The runnable can get constructor injections either by type if you have maven in your classpath (can be provided)
* or by name for loose coupling.
* Current support loose injections are:
* <ul>
* <li><code>systemProperties</code>: <code>Properties</code>, session system properties</li>
* <li><code>systemPropertiesUpdater</code>: <code>BiConsumer&lt;String, String&gt;</code>, session system properties update callback (pass the key/value to update, null value means removal of the key)</li>
* <li><code>userProperties</code>: <code>Properties</code>, session user properties</li>
* <li><code>userPropertiesUpdater</code>: <code>BiConsumer&lt;String, String&gt;</code>, session user properties update callback (pass the key/value to update, null value means removal of the key)</li>
* <li><code>projectProperties</code>: <code>Properties</code>, project properties</li>
* <li><code>projectPropertiesUpdater</code>: <code>BiConsumer&lt;String, String&gt;</code>, project properties update callback (pass the key/value to update, null value means removal of the key)</li>
* <li><code>highestVersionResolver</code>: <code>Function&lt;String, String&gt;</code>, passing a <code>groupId:artifactId</code> you get the latest resolved version from the project repositories</li>
* </ul>
*
* @since 1.0
*/
@Parameter(required = true, property = "exec.mainClass")
Expand Down Expand Up @@ -97,8 +121,8 @@ public class ExecJavaMojo extends AbstractExecMojo {
* Indicates if mojo should be kept running after the mainclass terminates. Use full for server like apps with
* daemon threads.
*
* @deprecated since 1.1-alpha-1
* @since 1.0
* @deprecated since 1.1-alpha-1
*/
@Parameter(property = "exec.keepAlive", defaultValue = "false")
@Deprecated
Expand Down Expand Up @@ -217,7 +241,7 @@ public class ExecJavaMojo extends AbstractExecMojo {
* Execute goal.
*
* @throws MojoExecutionException execution of the main class or one of the threads it generated failed.
* @throws MojoFailureException something bad happened...
* @throws MojoFailureException something bad happened...
*/
public void execute() throws MojoExecutionException, MojoFailureException {
if (isSkip()) {
Expand Down Expand Up @@ -254,7 +278,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
// See https://bugs.openjdk.org/browse/JDK-8199704 for details about how users might be able to
// block
// System::exit in post-removal JDKs (still undecided at the time of writing this comment).
Thread bootstrapThread = new Thread(
Thread bootstrapThread = new Thread( // TODO: drop this useless thread 99% of the time
threadGroup,
() -> {
int sepIndex = mainClass.indexOf('/');
Expand All @@ -269,18 +293,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
SecurityManager originalSecurityManager = System.getSecurityManager();

try {
Class<?> bootClass =
Thread.currentThread().getContextClassLoader().loadClass(bootClassName);

MethodHandles.Lookup lookup = MethodHandles.lookup();

MethodHandle mainHandle =
lookup.findStatic(bootClass, "main", MethodType.methodType(void.class, String[].class));

if (blockSystemExit) {
System.setSecurityManager(new SystemExitManager(originalSecurityManager));
}
mainHandle.invoke(arguments);
doExec(bootClassName, originalSecurityManager);
} catch (IllegalAccessException | NoSuchMethodException | NoSuchMethodError e) { // just pass it on
Thread.currentThread()
.getThreadGroup()
Expand Down Expand Up @@ -309,7 +322,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
}
},
mainClass + ".main()");
URLClassLoader classLoader = getClassLoader();
URLClassLoader classLoader = getClassLoader(); // TODO: enable to cache accross executions
bootstrapThread.setContextClassLoader(classLoader);
setSystemProperties();

Expand Down Expand Up @@ -358,6 +371,123 @@ public void execute() throws MojoExecutionException, MojoFailureException {
registerSourceRoots();
}

private void doExec(final String bootClassName, final SecurityManager originalSecurityManager) throws Throwable {
Class<?> bootClass = Thread.currentThread().getContextClassLoader().loadClass(bootClassName);
MethodHandles.Lookup lookup = MethodHandles.lookup();
try {
doMain(
originalSecurityManager,
lookup.findStatic(bootClass, "main", MethodType.methodType(void.class, String[].class)));
} catch (final NoSuchMethodException nsme) {
if (Runnable.class.isAssignableFrom(bootClass)) {
doRun(bootClass);
} else {
throw nsme;
}
}
}

private void doMain(final SecurityManager originalSecurityManager, final MethodHandle mainHandle) throws Throwable {
if (blockSystemExit && originalSecurityManager == null) {
System.setSecurityManager(new SystemExitManager(originalSecurityManager));
}
mainHandle.invoke(arguments);
}

private void doRun(final Class<?> bootClass)
throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
final Class<? extends Runnable> runnableClass = bootClass.asSubclass(Runnable.class);
final Constructor<? extends Runnable> constructor = Stream.of(runnableClass.getDeclaredConstructors())
.map(i -> (Constructor<? extends Runnable>) i)
.filter(i -> Modifier.isPublic(i.getModifiers()))
.max(Comparator.<Constructor<? extends Runnable>, Integer>comparing(Constructor::getParameterCount))
.orElseThrow(() -> new IllegalArgumentException("No public constructor found for " + bootClass));
if (getLog().isDebugEnabled()) {
getLog().debug("Using constructor " + constructor);
}

Runnable runnable;
try { // todo: enhance that but since injection API is being defined at mvn4 level it is
// good enough
final Object[] args = Stream.of(constructor.getParameters())
.map(param -> {
try {
return lookupParam(param);
} catch (final ComponentLookupException e) {
getLog().error(e.getMessage(), e);
throw new IllegalStateException(e);
}
})
.toArray(Object[]::new);
constructor.setAccessible(true);
runnable = constructor.newInstance(args);
} catch (final RuntimeException re) {
if (getLog().isDebugEnabled()) {
getLog().debug(
"Can't inject " + runnableClass + "': " + re.getMessage() + ", will ignore injections",
re);
}
final Constructor<? extends Runnable> declaredConstructor = runnableClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
runnable = declaredConstructor.newInstance();
}
runnable.run();
}

private Object lookupParam(final java.lang.reflect.Parameter param) throws ComponentLookupException {
switch (param.getName()) {
// loose coupled to maven (wrapped with standard jvm types to not require it)
case "systemProperties": // Properties
return getSession().getSystemProperties();
case "systemPropertiesUpdater": // BiConsumer<String, String>
return propertiesUpdater(getSession().getSystemProperties());
case "userProperties": // Properties
return getSession().getUserProperties();
case "userPropertiesUpdater": // BiConsumer<String, String>
return propertiesUpdater(getSession().getUserProperties());
case "projectProperties": // Properties
return project.getProperties();
case "projectPropertiesUpdater": // BiConsumer<String, String>
return propertiesUpdater(project.getProperties());
case "highestVersionResolver": // Function<String, String>
return (Function<String, String>) ga -> {
final int sep = ga.indexOf(':');
if (sep < 0) {
throw new IllegalArgumentException("Invalid groupId:artifactId argument: '" + ga + "'");
}

final org.eclipse.aether.artifact.Artifact artifact = new DefaultArtifact(ga + ":[0,)");
final VersionRangeRequest rangeRequest = new VersionRangeRequest();
rangeRequest.setArtifact(artifact);
try {
rangeRequest.setRepositories(project.getRemotePluginRepositories());
final VersionRangeResult rangeResult = repositorySystem.resolveVersionRange(
getSession().getRepositorySession(), rangeRequest);
return String.valueOf(rangeResult.getHighestVersion());
} catch (final VersionRangeResolutionException e) {
throw new IllegalStateException(e);
}
};
// standard bindings
case "session": // MavenSession
return getSession();
case "container": // PlexusContainer
return getSession().getContainer();
default: // Any
return getSession().getContainer().lookup(param.getType());
}
}

private BiConsumer<String, String> propertiesUpdater(final Properties props) {
return (k, v) -> {
if (v == null) {
props.remove(k);
} else {
props.setProperty(k, v);
}
};
}

/**
* To avoid the exec:java to consider common pool threads leaked, let's pre-create them.
*/
Expand Down
58 changes: 45 additions & 13 deletions src/test/java/org/codehaus/mojo/exec/ExecJavaMojoTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.Properties;

import org.apache.maven.execution.MavenSession;
import org.apache.maven.monitor.logging.DefaultLog;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.testing.AbstractMojoTestCase;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuilder;
Expand All @@ -47,8 +49,6 @@ public class ExecJavaMojoTest extends AbstractMojoTestCase {
@Mock
private MavenSession session;

private static final File LOCAL_REPO = new File("src/test/repository");

private static final int JAVA_VERSION_MAJOR =
Integer.parseInt(System.getProperty("java.version").replaceFirst("[.].*", ""));

Expand All @@ -58,6 +58,25 @@ public class ExecJavaMojoTest extends AbstractMojoTestCase {
* pom, "java" ); System.out.println(output); assertEquals( -1, output.trim().indexOf( "ERROR" ) ); }
*/

/**
* Check that a simple execution with no arguments and no system properties produces the expected result.<br>
* We load the config from a pom file and fill up the MavenProject property ourselves
*
* @throws Exception if any exception occurs
*/
public void testRunnable() throws Exception {
File pom = new File(getBasedir(), "src/test/projects/project19/pom.xml");
ExecJavaMojo mojo = (ExecJavaMojo) lookupMojo("java", pom);
setUpProject(pom, mojo);
mojo.setLog(new DefaultLog(new ConsoleLogger(Logger.LEVEL_ERROR, "exec:java")));
doExecute(mojo, null, null);
assertEquals(
"junit: true",
((MavenSession) getVariableValueFromObject(mojo, "session"))
.getSystemProperties()
.getProperty("hello.runnable.output"));
}

/**
* Check that a simple execution with no arguments and no system properties produces the expected result.<br>
* We load the config from a pom file and fill up the MavenProject property ourselves
Expand Down Expand Up @@ -295,18 +314,12 @@ private String execute(File pom, String goal) throws Exception {
private String execute(File pom, String goal, ByteArrayOutputStream stringOutputStream, OutputStream stderr)
throws Exception {

ExecJavaMojo mojo;
mojo = (ExecJavaMojo) lookupMojo(goal, pom);
ExecJavaMojo mojo = (ExecJavaMojo) lookupMojo(goal, pom);

setUpProject(pom, mojo);

MavenProject project = (MavenProject) getVariableValueFromObject(mojo, "project");

// why isn't this set up by the harness based on the default-value? TODO get to bottom of this!
setVariableValueToObject(mojo, "includeProjectDependencies", Boolean.TRUE);
setVariableValueToObject(mojo, "cleanupDaemonThreads", Boolean.TRUE);
setVariableValueToObject(mojo, "classpathScope", "compile");

assertNotNull(mojo);
assertNotNull(project);

Expand All @@ -318,22 +331,41 @@ private String execute(File pom, String goal, ByteArrayOutputStream stringOutput
// ensure we don't log unnecessary stuff which would interfere with assessing success of tests
mojo.setLog(new DefaultLog(new ConsoleLogger(Logger.LEVEL_ERROR, "exec:java")));

doExecute(mojo, out, err);

return stringOutputStream.toString();
}

private void doExecute(final ExecJavaMojo mojo, final PrintStream out, final PrintStream err)
throws MojoExecutionException, MojoFailureException, InterruptedException {
try {
mojo.execute();
} finally {
// see testUncooperativeThread() for explaination
Thread.sleep(300); // time seems about right
System.setOut(out);
System.setErr(err);
if (out != null) {
System.setOut(out);
}
if (err != null) {
System.setErr(err);
}
}

return stringOutputStream.toString();
}

private void setUpProject(File pomFile, AbstractMojo mojo) throws Exception {
super.setUp();

// why isn't this set up by the harness based on the default-value? TODO get to bottom of this!
setVariableValueToObject(mojo, "includeProjectDependencies", Boolean.TRUE);
setVariableValueToObject(mojo, "cleanupDaemonThreads", Boolean.TRUE);
setVariableValueToObject(mojo, "classpathScope", "compile");

Properties systemProps = new Properties();
systemProps.setProperty("test.version", "junit");

MockitoAnnotations.initMocks(this);
setVariableValueToObject(mojo, "session", session);
when(session.getSystemProperties()).thenReturn(systemProps);

ProjectBuildingRequest buildingRequest = mock(ProjectBuildingRequest.class);
when(session.getProjectBuildingRequest()).thenReturn(buildingRequest);
Expand Down
Loading

0 comments on commit ead991b

Please sign in to comment.