org.codehaus.mojo.exec.ExecJavaMojo Maven / Gradle / Ivy
package org.codehaus.mojo.exec;
/*
* Copyright 2005-2006 The Codehaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.x
*/
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectBuilder;
import org.apache.maven.project.artifact.MavenMetadataSource;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Exclusion;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
import org.apache.maven.artifact.resolver.filter.ExcludesArtifactFilter;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.VersionRange;
import java.io.File;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
/**
* Executes the supplied java class in the current VM with the enclosing project's
* dependencies as classpath.
*
* @author Kaare Nilsen, David Smiley
* @goal java
* @requiresDependencyResolution test
* @execute phase="validate"
* @since 1.0
*/
public class ExecJavaMojo
extends AbstractExecMojo
{
/**
* @component
*/
private ArtifactResolver artifactResolver;
/**
* @component
*/
private ArtifactFactory artifactFactory;
/**
* @component
*/
private ArtifactMetadataSource metadataSource;
/**
* @parameter expression="${localRepository}"
* @required
* @readonly
* @since 1.0
*/
private ArtifactRepository localRepository;
/**
* @parameter expression="${project.remoteArtifactRepositories}"
* @required
* @readonly
* @since 1.1-beta-1
*/
private List remoteRepositories;
/**
* @component
* @since 1.0
*/
private MavenProjectBuilder projectBuilder;
/**
* @parameter expression="${plugin.artifacts}"
* @readonly
* @since 1.1-beta-1
*/
private List pluginDependencies;
/**
* The main class to execute.
*
* @parameter expression="${exec.mainClass}"
* @required
* @since 1.0
*/
private String mainClass;
/**
* The class arguments.
*
* @parameter expression="${exec.arguments}"
* @since 1.0
*/
private String[] arguments;
/**
* A list of system properties to be passed. Note: as the execution is not forked, some system properties
* required by the JVM cannot be passed here. Use MAVEN_OPTS or the exec:exec instead. See the user guide for
* more information.
*
* @parameter
* @since 1.0
*/
private Property[] systemProperties;
/**
* Indicates if mojo should be kept running after the mainclass terminates.
* Usefull for serverlike apps with deamonthreads.
*
* @parameter expression="${exec.keepAlive}" default-value="false"
* @deprecated since 1.1-alpha-1
* @since 1.0
*/
private boolean keepAlive;
/**
* Indicates if the project dependencies should be used when executing
* the main class.
*
* @parameter expression="${exec.includeProjectDependencies}" default-value="true"
* @since 1.1-beta-1
*/
private boolean includeProjectDependencies;
/**
* Indicates if this plugin's dependencies should be used when executing
* the main class.
*
* This is useful when project dependencies are not appropriate. Using only
* the plugin dependencies can be particularly useful when the project is
* not a java project. For example a mvn project using the csharp plugins
* only expects to see dotnet libraries as dependencies.
*
* @parameter expression="${exec.includePluginDependencies}" default-value="false"
* @since 1.1-beta-1
*/
private boolean includePluginDependencies;
/**
* If provided the ExecutableDependency identifies which of the plugin dependencies
* contains the executable class. This will have the affect of only including
* plugin dependencies required by the identified ExecutableDependency.
*
* If includeProjectDependencies is set to true
, all of the project dependencies
* will be included on the executable's classpath. Whether a particular project
* dependency is a dependency of the identified ExecutableDependency will be
* irrelevant to its inclusion in the classpath.
*
* @parameter
* @optional
* @since 1.1-beta-1
*/
private ExecutableDependency executableDependency;
/**
* Wether to interrupt/join and possibly stop the daemon threads upon quitting.
If this is false
,
* maven does nothing about the daemon threads. When maven has no more work to do, the VM will normally terminate
* any remaining daemon threads.
*
* In certain cases (in particular if maven is embedded),
* you might need to keep this enabled to make sure threads are properly cleaned up to ensure they don't interfere
* with subsequent activity.
* In that case, see {@link #daemonThreadJoinTimeout} and
* {@link #stopUnresponsiveDaemonThreads} for further tuning.
*
* @parameter expression="${exec.cleanupDaemonThreads} default-value="true"
* @since 1.1-beta-1
*/
private boolean cleanupDaemonThreads;
/**
* This defines the number of milliseconds to wait for daemon threads to quit following their interruption.
* This is only taken into account if {@link #cleanupDaemonThreads} is true
.
* A value <=0 means to not timeout (i.e. wait indefinitely for threads to finish). Following a timeout, a
* warning will be logged.
* Note: properly coded threads should terminate upon interruption but some threads may prove
* problematic: as the VM does interrupt daemon threads, some code may not have been written to handle
* interruption properly. For example java.util.Timer is known to not handle interruptions in JDK <= 1.6.
* So it is not possible for us to infinitely wait by default otherwise maven could hang. A sensible default
* value has been chosen, but this default value may change in the future based on user feedback.
* @parameter expression="${exec.daemonThreadJoinTimeout}" default-value="15000"
* @since 1.1-beta-1
*/
private long daemonThreadJoinTimeout;
/**
* Wether to call {@link Thread#stop()} following a timing out of waiting for an interrupted thread to finish.
* This is only taken into account if {@link #cleanupDaemonThreads} is true
* and the {@link #daemonThreadJoinTimeout} threshold has been reached for an uncooperative thread.
* If this is false
, or if {@link Thread#stop()} fails to get the thread to stop, then
* a warning is logged and Maven will continue on while the affected threads (and related objects in memory)
* linger on. Consider setting this to true
if you are invoking problematic code that you can't fix.
* An example is {@link java.util.Timer} which doesn't respond to interruption. To have Timer
* fixed, vote for this bug.
* @parameter expression="${exec.stopUnresponsiveDaemonThreads} default-value="false"
* @since 1.1-beta-1
*/
private boolean stopUnresponsiveDaemonThreads;
/**
* Deprecated this is not needed anymore.
*
* @parameter expression="${exec.killAfter}" default-value="-1"
* @deprecated since 1.1-alpha-1
* @since 1.0
*/
private long killAfter;
private Properties originalSystemProperties;
/**
* Execute goal.
* @throws MojoExecutionException execution of the main class or one of the threads it generated failed.
* @throws MojoFailureException something bad happened...
*/
public void execute()
throws MojoExecutionException, MojoFailureException
{
if ( killAfter != -1 )
{
getLog().warn( "Warning: killAfter is now deprecated. Do you need it ? Please comment on MEXEC-6." );
}
if ( null == arguments )
{
arguments = new String[0];
}
if ( getLog().isDebugEnabled() )
{
StringBuffer msg = new StringBuffer( "Invoking : " );
msg.append( mainClass );
msg.append( ".main(" );
for ( int i = 0; i < arguments.length; i++ )
{
if ( i > 0 )
{
msg.append( ", " );
}
msg.append( arguments[i] );
}
msg.append( ")" );
getLog().debug( msg );
}
IsolatedThreadGroup threadGroup = new IsolatedThreadGroup( mainClass /*name*/ );
Thread bootstrapThread = new Thread( threadGroup, new Runnable()
{
public void run()
{
try
{
Method main = Thread.currentThread().getContextClassLoader().loadClass( mainClass )
.getMethod( "main", new Class[]{ String[].class } );
if ( ! main.isAccessible() )
{
getLog().debug( "Setting accessibility to true in order to invoke main()." );
main.setAccessible( true );
}
main.invoke( main, new Object[]{arguments} );
}
catch ( NoSuchMethodException e )
{ // just pass it on
Thread.currentThread().getThreadGroup().uncaughtException( Thread.currentThread(),
new Exception(
"The specified mainClass doesn't contain a main method with appropriate signature.", e
)
);
}
catch ( Exception e )
{ // just pass it on
Thread.currentThread().getThreadGroup().uncaughtException( Thread.currentThread(), e );
}
}
}, mainClass + ".main()" );
bootstrapThread.setContextClassLoader( getClassLoader() );
setSystemProperties();
bootstrapThread.start();
joinNonDaemonThreads( threadGroup );
// It's plausible that spontaneously a non-daemon thread might be created as we try and shut down,
// but it's too late since the termination condition (only daemon threads) has been triggered.
if ( keepAlive )
{
getLog().warn(
"Warning: keepAlive is now deprecated and obsolete. Do you need it? Please comment on MEXEC-6." );
waitFor( 0 );
}
if ( cleanupDaemonThreads )
{
terminateThreads( threadGroup );
try
{
threadGroup.destroy();
}
catch ( IllegalThreadStateException e )
{
getLog().warn( "Couldn't destroy threadgroup " + threadGroup, e );
}
}
if ( originalSystemProperties != null )
{
System.setProperties( originalSystemProperties );
}
synchronized ( threadGroup )
{
if ( threadGroup.uncaughtException != null )
{
throw new MojoExecutionException( "An exception occured while executing the Java class. "
+ threadGroup.uncaughtException.getMessage(),
threadGroup.uncaughtException );
}
}
registerSourceRoots();
}
/**
* a ThreadGroup to isolate execution and collect exceptions.
*/
class IsolatedThreadGroup extends ThreadGroup
{
Throwable uncaughtException; // synchronize access to this
public IsolatedThreadGroup( String name )
{
super( name );
}
public void uncaughtException( Thread thread, Throwable throwable )
{
if ( throwable instanceof ThreadDeath )
{
return; //harmless
}
boolean doLog = false;
synchronized ( this )
{
if ( uncaughtException == null ) // only remember the first one
{
uncaughtException = throwable; // will be reported eventually
}
else
{
doLog = true;
}
}
if ( doLog )
{
getLog().warn( "an additional exception was thrown", throwable );
}
}
}
private void joinNonDaemonThreads( ThreadGroup threadGroup )
{
boolean foundNonDaemon;
do
{
foundNonDaemon = false;
Collection threads = getActiveThreads( threadGroup );
for ( Iterator iter = threads.iterator(); iter.hasNext(); )
{
Thread thread = (Thread) iter.next();
if ( thread.isDaemon() )
{
continue;
}
foundNonDaemon = true; //try again; maybe more threads were created while we were busy
joinThread( thread, 0 );
}
} while ( foundNonDaemon );
}
private void joinThread( Thread thread, long timeoutMsecs )
{
try
{
getLog().debug( "joining on thread " + thread );
thread.join( timeoutMsecs );
}
catch ( InterruptedException e )
{
Thread.currentThread().interrupt(); // good practice if don't throw
getLog().warn( "interrupted while joining against thread " + thread, e ); // not expected!
}
if ( thread.isAlive() ) //generally abnormal
{
getLog().warn( "thread " + thread + " was interrupted but is still alive after waiting at least "
+ timeoutMsecs + "msecs" );
}
}
private void terminateThreads( ThreadGroup threadGroup )
{
long startTime = System.currentTimeMillis();
Set uncooperativeThreads = new HashSet(); // these were not responsive to interruption
for ( Collection threads = getActiveThreads( threadGroup ); !threads.isEmpty();
threads = getActiveThreads( threadGroup ), threads.removeAll( uncooperativeThreads ) )
{
// Interrupt all threads we know about as of this instant (harmless if spuriously went dead (! isAlive())
// or if something else interrupted it ( isInterrupted() ).
for ( Iterator iter = threads.iterator(); iter.hasNext(); )
{
Thread thread = (Thread) iter.next();
getLog().debug( "interrupting thread " + thread );
thread.interrupt();
}
// Now join with a timeout and call stop() (assuming flags are set right)
for ( Iterator iter = threads.iterator(); iter.hasNext(); )
{
Thread thread = (Thread) iter.next();
if ( ! thread.isAlive() )
{
continue; //and, presumably it won't show up in getActiveThreads() next iteration
}
if ( daemonThreadJoinTimeout <= 0 )
{
joinThread( thread, 0 ); //waits until not alive; no timeout
continue;
}
long timeout = daemonThreadJoinTimeout
- ( System.currentTimeMillis() - startTime );
if ( timeout > 0 )
{
joinThread( thread, timeout );
}
if ( ! thread.isAlive() )
{
continue;
}
uncooperativeThreads.add( thread ); // ensure we don't process again
if ( stopUnresponsiveDaemonThreads )
{
getLog().warn( "thread " + thread + " will be Thread.stop()'ed" );
thread.stop();
}
else
{
getLog().warn( "thread " + thread + " will linger despite being asked to die via interruption" );
}
}
}
if ( ! uncooperativeThreads.isEmpty() )
{
getLog().warn( "NOTE: " + uncooperativeThreads.size() + " thread(s) did not finish despite being asked to "
+ " via interruption. This is not a problem with exec:java, it is a problem with the running code."
+ " Although not serious, it should be remedied." );
}
else
{
int activeCount = threadGroup.activeCount();
if ( activeCount != 0 )
{
// TODO this may be nothing; continue on anyway; perhaps don't even log in future
Thread[] threadsArray = new Thread[1];
threadGroup.enumerate( threadsArray );
getLog().debug( "strange; " + activeCount
+ " thread(s) still active in the group " + threadGroup + " such as " + threadsArray[0] );
}
}
}
private Collection getActiveThreads( ThreadGroup threadGroup )
{
Thread[] threads = new Thread[ threadGroup.activeCount() ];
int numThreads = threadGroup.enumerate( threads );
Collection result = new ArrayList( numThreads );
for ( int i = 0; i < threads.length && threads[i] != null; i++ )
{
result.add( threads[i] );
}
return result; //note: result should be modifiable
}
/**
* Pass any given system properties to the java system properties.
*/
private void setSystemProperties()
{
if ( systemProperties != null )
{
originalSystemProperties = System.getProperties();
for ( int i = 0; i < systemProperties.length; i++ )
{
Property systemProperty = systemProperties[i];
String value = systemProperty.getValue();
System.setProperty( systemProperty.getKey(), value == null ? "" : value );
}
}
}
/**
* Set up a classloader for the execution of the main class.
*
* @return the classloader
* @throws MojoExecutionException if a problem happens
*/
private ClassLoader getClassLoader()
throws MojoExecutionException
{
List classpathURLs = new ArrayList();
this.addRelevantPluginDependenciesToClasspath( classpathURLs );
this.addRelevantProjectDependenciesToClasspath( classpathURLs );
return new URLClassLoader( ( URL[] ) classpathURLs.toArray( new URL[ classpathURLs.size() ] ) );
}
/**
* Add any relevant project dependencies to the classpath.
* Indirectly takes includePluginDependencies and ExecutableDependency into consideration.
*
* @param path classpath of {@link java.net.URL} objects
* @throws MojoExecutionException if a problem happens
*/
private void addRelevantPluginDependenciesToClasspath( List path )
throws MojoExecutionException
{
if ( hasCommandlineArgs() )
{
arguments = parseCommandlineArgs();
}
try
{
Iterator iter = this.determineRelevantPluginDependencies().iterator();
while ( iter.hasNext() )
{
Artifact classPathElement = (Artifact) iter.next();
getLog().debug(
"Adding plugin dependency artifact: " + classPathElement.getArtifactId() + " to classpath" );
path.add( classPathElement.getFile().toURL() );
}
}
catch ( MalformedURLException e )
{
throw new MojoExecutionException( "Error during setting up classpath", e );
}
}
/**
* Add any relevant project dependencies to the classpath.
* Takes includeProjectDependencies into consideration.
*
* @param path classpath of {@link java.net.URL} objects
* @throws MojoExecutionException if a problem happens
*/
private void addRelevantProjectDependenciesToClasspath( List path )
throws MojoExecutionException
{
if ( this.includeProjectDependencies )
{
try
{
getLog().debug( "Project Dependencies will be included." );
List artifacts = new ArrayList();
List theClasspathFiles = new ArrayList();
collectProjectArtifactsAndClasspath( artifacts, theClasspathFiles );
for ( Iterator it = theClasspathFiles.iterator(); it.hasNext(); )
{
URL url = ( (File) it.next() ).toURL();
getLog().debug( "Adding to classpath : " + url );
path.add( url );
}
Iterator iter = artifacts.iterator();
while ( iter.hasNext() )
{
Artifact classPathElement = (Artifact) iter.next();
getLog().debug(
"Adding project dependency artifact: " + classPathElement.getArtifactId() + " to classpath" );
path.add( classPathElement.getFile().toURL() );
}
}
catch ( MalformedURLException e )
{
throw new MojoExecutionException( "Error during setting up classpath", e );
}
}
else
{
getLog().debug( "Project Dependencies will be excluded." );
}
}
/**
* Determine all plugin dependencies relevant to the executable.
* Takes includePlugins, and the executableDependency into consideration.
*
* @return a set of Artifact objects.
* (Empty set is returned if there are no relevant plugin dependencies.)
* @throws MojoExecutionException if a problem happens resolving the plufin dependencies
*/
private Set determineRelevantPluginDependencies()
throws MojoExecutionException
{
Set relevantDependencies;
if ( this.includePluginDependencies )
{
if ( this.executableDependency == null )
{
getLog().debug( "All Plugin Dependencies will be included." );
relevantDependencies = new HashSet( this.pluginDependencies );
}
else
{
getLog().debug( "Selected plugin Dependencies will be included." );
Artifact executableArtifact = this.findExecutableArtifact();
Artifact executablePomArtifact = this.getExecutablePomArtifact( executableArtifact );
relevantDependencies = this.resolveExecutableDependencies( executablePomArtifact );
}
}
else
{
relevantDependencies = Collections.EMPTY_SET;
getLog().debug( "Plugin Dependencies will be excluded." );
}
return relevantDependencies;
}
/**
* Get the artifact which refers to the POM of the executable artifact.
*
* @param executableArtifact this artifact refers to the actual assembly.
* @return an artifact which refers to the POM of the executable artifact.
*/
private Artifact getExecutablePomArtifact( Artifact executableArtifact )
{
return this.artifactFactory.createBuildArtifact( executableArtifact.getGroupId(),
executableArtifact.getArtifactId(),
executableArtifact.getVersion(), "pom" );
}
/**
* Examine the plugin dependencies to find the executable artifact.
*
* @return an artifact which refers to the actual executable tool (not a POM)
* @throws MojoExecutionException if no executable artifact was found
*/
private Artifact findExecutableArtifact()
throws MojoExecutionException
{
//ILimitedArtifactIdentifier execToolAssembly = this.getExecutableToolAssembly();
Artifact executableTool = null;
for ( Iterator iter = this.pluginDependencies.iterator(); iter.hasNext(); )
{
Artifact pluginDep = (Artifact) iter.next();
if ( this.executableDependency.matches( pluginDep ) )
{
executableTool = pluginDep;
break;
}
}
if ( executableTool == null )
{
throw new MojoExecutionException(
"No dependency of the plugin matches the specified executableDependency."
+ " Specified executableToolAssembly is: " + executableDependency.toString() );
}
return executableTool;
}
/**
* Resolve the executable dependencies for the specified project
* @param executablePomArtifact the project's POM
* @return a set of Artifacts
* @throws MojoExecutionException if a failure happens
*/
private Set resolveExecutableDependencies( Artifact executablePomArtifact )
throws MojoExecutionException
{
Set executableDependencies;
try
{
MavenProject executableProject = this.projectBuilder.buildFromRepository( executablePomArtifact,
this.remoteRepositories,
this.localRepository );
//get all of the dependencies for the executable project
List dependencies = executableProject.getDependencies();
//make Artifacts of all the dependencies
Set dependencyArtifacts =
MavenMetadataSource.createArtifacts( this.artifactFactory, dependencies, null, null, null );
//not forgetting the Artifact of the project itself
dependencyArtifacts.add( executableProject.getArtifact() );
//resolve all dependencies transitively to obtain a comprehensive list of assemblies
ArtifactResolutionResult result = artifactResolver.resolveTransitively( dependencyArtifacts,
executablePomArtifact,
Collections.EMPTY_MAP,
this.localRepository,
this.remoteRepositories,
metadataSource, null,
Collections.EMPTY_LIST );
executableDependencies = result.getArtifacts();
}
catch ( Exception ex )
{
throw new MojoExecutionException(
"Encountered problems resolving dependencies of the executable " + "in preparation for its execution.",
ex );
}
return executableDependencies;
}
/**
* Stop program execution for nn millis.
*
* @param millis the number of millis-seconds to wait for,
* 0
stops program forever.
*/
private void waitFor( long millis )
{
Object lock = new Object();
synchronized ( lock )
{
try
{
lock.wait( millis );
}
catch ( InterruptedException e )
{
Thread.currentThread().interrupt(); // good practice if don't throw
getLog().warn( "Spuriously interrupted while waiting for " + millis + "ms", e );
}
}
}
}