Added: maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/AbstractInvokerMojo.java URL: http://svn.apache.org/viewvc/maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/AbstractInvokerMojo.java?rev=1795243&view=auto ============================================================================== --- maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/AbstractInvokerMojo.java (added) +++ maven/plugins/trunk/maven-invoker-plugin/src/main/java/org/apache/maven/plugins/invoker/AbstractInvokerMojo.java Mon May 15 21:10:27 2017 @@ -0,0 +1,2553 @@ +package org.apache.maven.plugins.invoker; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.TreeSet; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.model.Model; +import org.apache.maven.model.Profile; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecution; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.invoker.model.BuildJob; +import org.apache.maven.plugins.invoker.model.io.xpp3.BuildJobXpp3Writer; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.apache.maven.settings.Settings; +import org.apache.maven.settings.SettingsUtils; +import org.apache.maven.settings.TrackableBase; +import org.apache.maven.settings.building.DefaultSettingsBuildingRequest; +import org.apache.maven.settings.building.SettingsBuilder; +import org.apache.maven.settings.building.SettingsBuildingException; +import org.apache.maven.settings.building.SettingsBuildingRequest; +import org.apache.maven.settings.io.xpp3.SettingsXpp3Writer; +import org.apache.maven.shared.invoker.CommandLineConfigurationException; +import org.apache.maven.shared.invoker.DefaultInvocationRequest; +import org.apache.maven.shared.invoker.InvocationRequest; +import org.apache.maven.shared.invoker.InvocationResult; +import org.apache.maven.shared.invoker.Invoker; +import org.apache.maven.shared.invoker.MavenCommandLineBuilder; +import org.apache.maven.shared.invoker.MavenInvocationException; +import org.apache.maven.shared.scriptinterpreter.RunErrorException; +import org.apache.maven.shared.scriptinterpreter.RunFailureException; +import org.apache.maven.shared.scriptinterpreter.ScriptRunner; +import org.apache.maven.shared.utils.logging.MessageBuilder; +import org.codehaus.plexus.interpolation.InterpolationException; +import org.codehaus.plexus.interpolation.Interpolator; +import org.codehaus.plexus.interpolation.MapBasedValueSource; +import org.codehaus.plexus.interpolation.RegexBasedInterpolator; +import org.codehaus.plexus.util.DirectoryScanner; +import org.codehaus.plexus.util.FileUtils; +import org.codehaus.plexus.util.IOUtil; +import org.codehaus.plexus.util.InterpolationFilterReader; +import org.codehaus.plexus.util.ReaderFactory; +import org.codehaus.plexus.util.ReflectionUtils; +import org.codehaus.plexus.util.StringUtils; +import org.codehaus.plexus.util.WriterFactory; +import org.codehaus.plexus.util.cli.CommandLineException; +import org.codehaus.plexus.util.cli.CommandLineUtils; +import org.codehaus.plexus.util.cli.Commandline; +import org.codehaus.plexus.util.cli.StreamConsumer; +import static org.apache.maven.shared.utils.logging.MessageUtils.buffer; + +/** + * Provides common code for mojos invoking sub builds. + * + * @author Stephen Connolly + * @since 15-Aug-2009 09:09:29 + */ +public abstract class AbstractInvokerMojo + extends AbstractMojo +{ + /** + * The zero-based column index where to print the invoker result. + */ + private static final int RESULT_COLUMN = 60; + + /** + * Flag used to suppress certain invocations. This is useful in tailoring the build using profiles. + * + * @since 1.1 + */ + @Parameter( property = "invoker.skip", defaultValue = "false" ) + private boolean skipInvocation; + + /** + * Flag used to suppress the summary output notifying of successes and failures. If set to <code>true</code>, the + * only indication of the build's success or failure will be the effect it has on the main build (if it fails, the + * main build should fail as well). If {@link #streamLogs} is enabled, the sub-build summary will also provide an + * indication. + */ + @Parameter( defaultValue = "false" ) + protected boolean suppressSummaries; + + /** + * Flag used to determine whether the build logs should be output to the normal mojo log. + */ + @Parameter( property = "invoker.streamLogs", defaultValue = "false" ) + private boolean streamLogs; + + /** + * The local repository for caching artifacts. It is strongly recommended to specify a path to an isolated + * repository like <code>${project.build.directory}/it-repo</code>. Otherwise, your ordinary local repository will + * be used, potentially soiling it with broken artifacts. + */ + @Parameter( property = "invoker.localRepositoryPath", defaultValue = "${settings.localRepository}" ) + private File localRepositoryPath; + + /** + * Directory to search for integration tests. + */ + @Parameter( property = "invoker.projectsDirectory", defaultValue = "${basedir}/src/it/" ) + private File projectsDirectory; + + /** + * Base directory where all build reports are written to. Every execution of an integration test will produce an XML + * file which contains the information about success or failure of that particular build job. The format of the + * resulting XML file is documented in the given <a href="./build-job.html">build-job</a> reference. + * + * @since 1.4 + */ + @Parameter( property = "invoker.reportsDirectory", defaultValue = "${project.build.directory}/invoker-reports" ) + private File reportsDirectory; + + /** + * A flag to disable the generation of build reports. + * + * @since 1.4 + */ + @Parameter( property = "invoker.disableReports", defaultValue = "false" ) + private boolean disableReports; + + /** + * Directory to which projects should be cloned prior to execution. If not specified, each integration test will be + * run in the directory in which the corresponding IT POM was found. In this case, you most likely want to configure + * your SCM to ignore <code>target</code> and <code>build.log</code> in the test's base directory. + * + * @since 1.1 + */ + @Parameter + private File cloneProjectsTo; + + // CHECKSTYLE_OFF: LineLength + /** + * Some files are normally excluded when copying the IT projects from the directory specified by the parameter + * projectsDirectory to the directory given by cloneProjectsTo (e.g. <code>.svn</code>, <code>CVS</code>, + * <code>*~</code>, etc: see <a href= + * "https://codehaus-plexus.github.io/plexus-utils/apidocs/org/codehaus/plexus/util/AbstractScanner.html#DEFAULTEXCLUDES"> + * reference</a> for full list). Setting this parameter to <code>true</code> will cause all files to be copied to + * the <code>cloneProjectsTo</code> directory. + * + * @since 1.2 + */ + @Parameter( defaultValue = "false" ) + private boolean cloneAllFiles; + // CHECKSTYLE_ON: LineLength + + /** + * Ensure the {@link #cloneProjectsTo} directory is not polluted with files from earlier invoker runs. + * + * @since 1.6 + */ + @Parameter( defaultValue = "false" ) + private boolean cloneClean; + + /** + * A single POM to build, skipping any scanning parameters and behavior. + */ + @Parameter( property = "invoker.pom" ) + private File pom; + + /** + * Include patterns for searching the integration test directory for projects. This parameter is meant to be set + * from the POM. If this parameter is not set, the plugin will search for all <code>pom.xml</code> files one + * directory below {@link #projectsDirectory} (i.e. <code>*/pom.xml</code>).<br> + * <br> + * Starting with version 1.3, mere directories can also be matched by these patterns. For example, the include + * pattern <code>*</code> will run Maven builds on all immediate sub directories of {@link #projectsDirectory}, + * regardless if they contain a <code>pom.xml</code>. This allows to perform builds that need/should not depend on + * the existence of a POM. + */ + @Parameter + private List<String> pomIncludes = Collections.singletonList( "*/pom.xml" ); + + /** + * Exclude patterns for searching the integration test directory. This parameter is meant to be set from the POM. By + * default, no POM files are excluded. For the convenience of using an include pattern like <code>*</code>, the + * custom settings file specified by the parameter {@link #settingsFile} will always be excluded automatically. + */ + @Parameter + private List<String> pomExcludes = Collections.emptyList(); + + /** + * Include patterns for searching the projects directory for projects that need to be run before the other projects. + * This parameter allows to declare projects that perform setup tasks like installing utility artifacts into the + * local repository. Projects matched by these patterns are implicitly excluded from the scan for ordinary projects. + * Also, the exclusions defined by the parameter {@link #pomExcludes} apply to the setup projects, too. Default + * value is: <code>setup*/pom.xml</code>. + * + * @since 1.3 + */ + @Parameter + private List<String> setupIncludes = Collections.singletonList( "setup*/pom.xml" ); + + /** + * The list of goals to execute on each project. Default value is: <code>package</code>. + */ + @Parameter + private List<String> goals = Collections.singletonList( "package" ); + + /** + */ + @Component + private Invoker invoker; + + @Component + private SettingsBuilder settingsBuilder; + + /** + * Relative path of a selector script to run prior in order to decide if the build should be executed. This script + * may be written with either BeanShell or Groovy. If the file extension is omitted (e.g. <code>selector</code>), + * the plugin searches for the file by trying out the well-known extensions <code>.bsh</code> and + * <code>.groovy</code>. If this script exists for a particular project but returns any non-null value different + * from <code>true</code>, the corresponding build is flagged as skipped. In this case, none of the pre-build hook + * script, Maven nor the post-build hook script will be invoked. If this script throws an exception, the + * corresponding build is flagged as in error, and none of the pre-build hook script, Maven not the post-build hook + * script will be invoked. + * + * @since 1.5 + */ + @Parameter( property = "invoker.selectorScript", defaultValue = "selector" ) + private String selectorScript; + + /** + * Relative path of a pre-build hook script to run prior to executing the build. This script may be written with + * either BeanShell or Groovy (since 1.3). If the file extension is omitted (e.g. <code>prebuild</code>), the plugin + * searches for the file by trying out the well-known extensions <code>.bsh</code> and <code>.groovy</code>. If this + * script exists for a particular project but returns any non-null value different from <code>true</code> or throws + * an exception, the corresponding build is flagged as a failure. In this case, neither Maven nor the post-build + * hook script will be invoked. + */ + @Parameter( property = "invoker.preBuildHookScript", defaultValue = "prebuild" ) + private String preBuildHookScript; + + /** + * Relative path of a cleanup/verification hook script to run after executing the build. This script may be written + * with either BeanShell or Groovy (since 1.3). If the file extension is omitted (e.g. <code>verify</code>), the + * plugin searches for the file by trying out the well-known extensions <code>.bsh</code> and <code>.groovy</code>. + * If this script exists for a particular project but returns any non-null value different from <code>true</code> or + * throws an exception, the corresponding build is flagged as a failure. + */ + @Parameter( property = "invoker.postBuildHookScript", defaultValue = "postbuild" ) + private String postBuildHookScript; + + /** + * Location of a properties file that defines CLI properties for the test. + */ + @Parameter( property = "invoker.testPropertiesFile", defaultValue = "test.properties" ) + private String testPropertiesFile; + + /** + * Common set of properties to pass in on each project's command line, via -D parameters. + * + * @since 1.1 + */ + @Parameter + private Map<String, String> properties; + + /** + * Whether to show errors in the build output. + */ + @Parameter( property = "invoker.showErrors", defaultValue = "false" ) + private boolean showErrors; + + /** + * Whether to show debug statements in the build output. + */ + @Parameter( property = "invoker.debug", defaultValue = "false" ) + private boolean debug; + + /** + * Suppress logging to the <code>build.log</code> file. + */ + @Parameter( property = "invoker.noLog", defaultValue = "false" ) + private boolean noLog; + + /** + * List of profile identifiers to explicitly trigger in the build. + * + * @since 1.1 + */ + @Parameter + private List<String> profiles; + + /** + * A list of additional properties which will be used to filter tokens in POMs and goal files. + * + * @since 1.3 + */ + @Parameter + private Map<String, String> filterProperties; + + /** + * The Maven Project Object + * + * @since 1.1 + */ + @Parameter( defaultValue = "${project}", readonly = true, required = true ) + private MavenProject project; + + @Parameter( defaultValue = "${mojoExecution}", readonly = true, required = true ) + private MojoExecution mojoExecution; + + /** + * A comma separated list of projectname patterns to run. Specify this parameter to run individual tests by file + * name, overriding the {@link #setupIncludes}, {@link #pomIncludes} and {@link #pomExcludes} parameters. Each + * pattern you specify here will be used to create an include/exclude pattern formatted like + * <code>${projectsDirectory}/<i>pattern</i></code>. To exclude a test, prefix the pattern with a '<code>!</code>'. + * So you can just type <nobr><code>-Dinvoker.test=SimpleTest,Comp*Test,!Compare*</code></nobr> to run builds in + * <code>${projectsDirectory}/SimpleTest</code> and <code>${projectsDirectory}/ComplexTest</code>, but not + * <code>${projectsDirectory}/CompareTest</code> + * + * @since 1.1 (exclusion since 1.8) + */ + @Parameter( property = "invoker.test" ) + private String invokerTest; + + /** + * Path to an alternate <code>settings.xml</code> to use for Maven invocation with all ITs. Note that the + * <code><localRepository></code> element of this settings file is always ignored, i.e. the path given by the + * parameter {@link #localRepositoryPath} is dominant. + * + * @since 1.2 + */ + @Parameter( property = "invoker.settingsFile" ) + private File settingsFile; + + /** + * The <code>MAVEN_OPTS</code> environment variable to use when invoking Maven. This value can be overridden for + * individual integration tests by using {@link #invokerPropertiesFile}. + * + * @since 1.2 + */ + @Parameter( property = "invoker.mavenOpts" ) + private String mavenOpts; + + /** + * The home directory of the Maven installation to use for the forked builds. Defaults to the current Maven + * installation. + * + * @since 1.3 + */ + @Parameter( property = "invoker.mavenHome" ) + private File mavenHome; + + /** + * mavenExecutable can either be a file relative to <code>${maven.home}/bin/</code> or an absolute file. + * + * @since 1.8 + * @see Invoker#setMavenExecutable(File) + */ + @Parameter( property = "invoker.mavenExecutable" ) + private String mavenExecutable; + + /** + * The <code>JAVA_HOME</code> environment variable to use for forked Maven invocations. Defaults to the current Java + * home directory. + * + * @since 1.3 + */ + @Parameter( property = "invoker.javaHome" ) + private File javaHome; + + /** + * The file encoding for the pre-/post-build scripts and the list files for goals and profiles. + * + * @since 1.2 + */ + @Parameter( property = "encoding", defaultValue = "${project.build.sourceEncoding}" ) + private String encoding; + + /** + * The current user system settings for use in Maven. + * + * @since 1.2 + */ + @Parameter( defaultValue = "${settings}", readonly = true, required = true ) + private Settings settings; + + /** + * A flag whether the test class path of the project under test should be included in the class path of the + * pre-/post-build scripts. If set to <code>false</code>, the class path of script interpreter consists only of the + * <a href="dependencies.html">runtime dependencies</a> of the Maven Invoker Plugin. If set the <code>true</code>, + * the project's test class path will be prepended to the interpreter class path. Among others, this feature allows + * the scripts to access utility classes from the test sources of your project. + * + * @since 1.2 + */ + @Parameter( property = "invoker.addTestClassPath", defaultValue = "false" ) + private boolean addTestClassPath; + + /** + * The test class path of the project under test. + */ + @Parameter( defaultValue = "${project.testClasspathElements}", readonly = true ) + private List<String> testClassPath; + + /** + * The name of an optional project-specific file that contains properties used to specify settings for an individual + * Maven invocation. Any property present in the file will override the corresponding setting from the plugin + * configuration. The values of the properties are filtered and may use expressions like + * <code>${project.version}</code> to reference project properties or values from the parameter + * {@link #filterProperties}. The snippet below describes the supported properties: + * <p/> + * + * <pre> + * # A comma or space separated list of goals/phases to execute, may + * # specify an empty list to execute the default goal of the IT project. + * # Environment variables used by maven plugins can be added here + * invoker.goals = clean install -Dplugin.variable=value + * + * # Or you can give things like this if you need. + * invoker.goals = -T2 clean verify + * + * # Optionally, a list of goals to run during further invocations of Maven + * invoker.goals.2 = ${project.groupId}:${project.artifactId}:${project.version}:run + * + * # A comma or space separated list of profiles to activate + * invoker.profiles = its,jdk15 + * + * # The path to an alternative POM or base directory to invoke Maven on, defaults to the + * # project that was originally specified in the plugin configuration + * # Since plugin version 1.4 + * invoker.project = sub-module + * + * # The value for the environment variable MAVEN_OPTS + * invoker.mavenOpts = -Dfile.encoding=UTF-16 -Xms32m -Xmx256m + * + * # Possible values are "fail-fast" (default), "fail-at-end" and "fail-never" + * invoker.failureBehavior = fail-never + * + * # The expected result of the build, possible values are "success" (default) and "failure" + * invoker.buildResult = failure + * + * # A boolean value controlling the aggregator mode of Maven, defaults to "false" + * invoker.nonRecursive = true + * + * # A boolean value controlling the network behavior of Maven, defaults to "false" + * # Since plugin version 1.4 + * invoker.offline = true + * + * # The path to the properties file from which to load system properties, defaults to the + * # filename given by the plugin parameter testPropertiesFile + * # Since plugin version 1.4 + * invoker.systemPropertiesFile = test.properties + * + * # An optional human friendly name for this build job to be included in the build reports. + * # Since plugin version 1.4 + * invoker.name = Test Build 01 + * + * # An optional description for this build job to be included in the build reports. + * # Since plugin version 1.4 + * invoker.description = Checks the support for build reports. + * + * # A comma separated list of JRE versions on which this build job should be run. + * # Since plugin version 1.4 + * invoker.java.version = 1.4+, !1.4.1, 1.7- + * + * # A comma separated list of OS families on which this build job should be run. + * # Since plugin version 1.4 + * invoker.os.family = !windows, unix, mac + * + * # A comma separated list of Maven versions on which this build should be run. + * # Since plugin version 1.5 + * invoker.maven.version = 2.0.10+, !2.1.0, !2.2.0 + * + * # For java.version, maven.version and os.family it is possible to define multiple selectors. + * # If one of the indexed selectors matches, the test is executed. + * # With the invoker.x.y equivalents you can specify global matchers. + * selector.1.java.version = 1.8+ + * selector.1.maven.version = 3.2.5+ + * selector.1.os.family = !windows + * selector.2.maven.version = 3.0+ + * selector.3.java.version = 9+ + * + * # A boolean value controlling the debug logging level of Maven, , defaults to "false" + * # Since plugin version 1.8 + * invoker.debug = true + * </pre> + * + * @since 1.2 + */ + @Parameter( property = "invoker.invokerPropertiesFile", defaultValue = "invoker.properties" ) + private String invokerPropertiesFile; + + /** + * flag to enable show mvn version used for running its (cli option : -V,--show-version ) + * + * @since 1.4 + */ + @Parameter( property = "invoker.showVersion", defaultValue = "false" ) + private boolean showVersion; + + /** + * number of threads for running tests in parallel. This will be the number of maven forked process in parallel. + * + * @since 1.6 + */ + @Parameter( property = "invoker.parallelThreads", defaultValue = "1" ) + private int parallelThreads; + + /** + * @since 1.6 + */ + @Parameter( property = "plugin.artifacts", required = true, readonly = true ) + private List<Artifact> pluginArtifacts; + + /** + * If enable and if you have a settings file configured for the execution, it will be merged with your user + * settings. + * + * @since 1.6 + */ + @Parameter( property = "invoker.mergeUserSettings", defaultValue = "false" ) + private boolean mergeUserSettings; + + /** + * Additional environment variables to set on the command line. + * + * @since 1.8 + */ + @Parameter + private Map<String, String> environmentVariables; + + /** + * Additional variables for use in the hook scripts. + * + * @since 1.9 + */ + @Parameter + private Map<String, String> scriptVariables; + + /** + * The scripter runner that is responsible to execute hook scripts. + */ + private ScriptRunner scriptRunner; + + /** + * A string used to prefix the file name of the filtered POMs in case the POMs couldn't be filtered in-place (i.e. + * the projects were not cloned to a temporary directory), can be <code>null</code>. This will be set to + * <code>null</code> if the POMs have already been filtered during cloning. + */ + private String filteredPomPrefix = "interpolated-"; + + /** + * The format for elapsed build time. + */ + private final DecimalFormat secFormat = new DecimalFormat( "(0.0 s)", new DecimalFormatSymbols( Locale.ENGLISH ) ); + + /** + * The version of Maven which is used to run the builds + */ + private String actualMavenVersion; + + /** + * Invokes Maven on the configured test projects. + * + * @throws org.apache.maven.plugin.MojoExecutionException If the goal encountered severe errors. + * @throws org.apache.maven.plugin.MojoFailureException If any of the Maven builds failed. + */ + public void execute() + throws MojoExecutionException, MojoFailureException + { + if ( skipInvocation ) + { + getLog().info( "Skipping invocation per configuration." + + " If this is incorrect, ensure the skipInvocation parameter is not set to true." ); + return; + } + + if ( StringUtils.isEmpty( encoding ) ) + { + getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING + + ", i.e. build is platform dependent!" ); + } + + // done it here to prevent issues with concurrent access in case of parallel run + if ( !disableReports ) + { + setupReportsFolder(); + } + + BuildJob[] buildJobs; + if ( pom == null ) + { + try + { + buildJobs = getBuildJobs(); + } + catch ( final IOException e ) + { + throw new MojoExecutionException( "Error retrieving POM list from includes, " + + "excludes, and projects directory. Reason: " + e.getMessage(), e ); + } + } + else + { + try + { + projectsDirectory = pom.getCanonicalFile().getParentFile(); + } + catch ( IOException e ) + { + throw new MojoExecutionException( "Failed to discover projectsDirectory from " + + "pom File parameter. Reason: " + e.getMessage(), e ); + } + + buildJobs = new BuildJob[] { new BuildJob( pom.getName(), BuildJob.Type.NORMAL ) }; + } + + if ( ( buildJobs == null ) || ( buildJobs.length < 1 ) ) + { + doFailIfNoProjects(); + + getLog().info( "No projects were selected for execution." ); + return; + } + + handleScriptRunnerWithScriptClassPath(); + + Collection<String> collectedProjects = new LinkedHashSet<String>(); + for ( BuildJob buildJob : buildJobs ) + { + collectProjects( projectsDirectory, buildJob.getProject(), collectedProjects, true ); + } + + File projectsDir = projectsDirectory; + + if ( cloneProjectsTo != null ) + { + cloneProjects( collectedProjects ); + projectsDir = cloneProjectsTo; + } + else + { + getLog().warn( "Filtering of parent/child POMs is not supported without cloning the projects" ); + } + + // First run setup jobs. + BuildJob[] setupBuildJobs = null; + try + { + setupBuildJobs = getSetupBuildJobsFromFolders(); + } + catch ( IOException e ) + { + getLog().error( "Failure during scanning of folders.", e ); + // TODO: Check shouldn't we fail in case of problems? + } + + if ( setupBuildJobs != null ) + { + // Run setup jobs in single thread + // mode. + // + // Some Idea about ordering? + getLog().info( "Running Setup Jobs" ); + runBuilds( projectsDir, setupBuildJobs, 1 ); + } + + // Afterwards run all other jobs. + BuildJob[] nonSetupBuildJobs = getNonSetupJobs( buildJobs ); + // We will run the non setup jobs with the configured + // parallelThreads number. + runBuilds( projectsDir, nonSetupBuildJobs, parallelThreads ); + + writeSummaryFile( nonSetupBuildJobs ); + + processResults( new InvokerSession( nonSetupBuildJobs ) ); + + } + + /** + * This will create the necessary folders for the reports. + * + * @throws MojoExecutionException in case of failure during creation of the reports folder. + */ + private void setupReportsFolder() + throws MojoExecutionException + { + // If it exists from previous run... + if ( reportsDirectory.exists() ) + { + try + { + FileUtils.deleteDirectory( reportsDirectory ); + } + catch ( IOException e ) + { + throw new MojoExecutionException( "Failure while trying to delete " + + reportsDirectory.getAbsolutePath(), e ); + } + } + if ( !reportsDirectory.mkdirs() ) + { + throw new MojoExecutionException( "Failure while creating the " + reportsDirectory.getAbsolutePath() ); + } + } + + private BuildJob[] getNonSetupJobs( BuildJob[] buildJobs ) + { + List<BuildJob> result = new LinkedList<BuildJob>(); + for ( int i = 0; i < buildJobs.length; i++ ) + { + if ( !buildJobs[i].getType().equals( BuildJob.Type.SETUP ) ) + { + result.add( buildJobs[i] ); + } + } + BuildJob[] buildNonSetupJobs = result.toArray( new BuildJob[result.size()] ); + return buildNonSetupJobs; + } + + private void handleScriptRunnerWithScriptClassPath() + { + final List<String> scriptClassPath; + if ( addTestClassPath ) + { + scriptClassPath = new ArrayList<String>( testClassPath ); + for ( Artifact pluginArtifact : pluginArtifacts ) + { + scriptClassPath.remove( pluginArtifact.getFile().getAbsolutePath() ); + } + } + else + { + scriptClassPath = null; + } + scriptRunner = new ScriptRunner( getLog() ); + scriptRunner.setScriptEncoding( encoding ); + scriptRunner.setGlobalVariable( "localRepositoryPath", localRepositoryPath ); + if ( scriptVariables != null ) + { + for ( Entry<String, String> entry : scriptVariables.entrySet() ) + { + scriptRunner.setGlobalVariable( entry.getKey(), entry.getValue() ); + } + } + scriptRunner.setClassPath( scriptClassPath ); + } + + private void writeSummaryFile( BuildJob[] buildJobs ) + throws MojoExecutionException + { + + File summaryReportFile = new File( reportsDirectory, "invoker-summary.txt" ); + + try + { + Writer writer = new BufferedWriter( new FileWriter( summaryReportFile ) ); + + for ( int i = 0; i < buildJobs.length; i++ ) + { + BuildJob buildJob = buildJobs[i]; + if ( !buildJob.getResult().equals( BuildJob.Result.SUCCESS ) ) + { + writer.append( buildJob.getResult() ); + writer.append( " [" ); + writer.append( buildJob.getProject() ); + writer.append( "] " ); + if ( buildJob.getFailureMessage() != null ) + { + writer.append( " " ); + writer.append( buildJob.getFailureMessage() ); + } + writer.append( "\n" ); + } + } + + writer.close(); + } + catch ( IOException e ) + { + throw new MojoExecutionException( "Failed to write summary report " + summaryReportFile, e ); + } + } + + protected void doFailIfNoProjects() + throws MojoFailureException + { + // should only be used during run and verify + } + + /** + * Processes the results of invoking the build jobs. + * + * @param invokerSession The session with the build jobs, must not be <code>null</code>. + * @throws MojoFailureException If the mojo had failed as a result of invoking the build jobs. + * @since 1.4 + */ + abstract void processResults( InvokerSession invokerSession ) + throws MojoFailureException; + + /** + * Creates a new reader for the specified file, using the plugin's {@link #encoding} parameter. + * + * @param file The file to create a reader for, must not be <code>null</code>. + * @return The reader for the file, never <code>null</code>. + * @throws java.io.IOException If the specified file was not found or the configured encoding is not supported. + */ + private Reader newReader( File file ) + throws IOException + { + if ( StringUtils.isNotEmpty( encoding ) ) + { + return ReaderFactory.newReader( file, encoding ); + } + else + { + return ReaderFactory.newPlatformReader( file ); + } + } + + /** + * Collects all projects locally reachable from the specified project. The method will as such try to read the POM + * and recursively follow its parent/module elements. + * + * @param projectsDir The base directory of all projects, must not be <code>null</code>. + * @param projectPath The relative path of the current project, can denote either the POM or its base directory, + * must not be <code>null</code>. + * @param projectPaths The set of already collected projects to add new projects to, must not be <code>null</code>. + * This set will hold the relative paths to either a POM file or a project base directory. + * @param included A flag indicating whether the specified project has been explicitly included via the parameter + * {@link #pomIncludes}. Such projects will always be added to the result set even if there is no + * corresponding POM. + * @throws org.apache.maven.plugin.MojoExecutionException If the project tree could not be traversed. + */ + private void collectProjects( File projectsDir, String projectPath, Collection<String> projectPaths, + boolean included ) + throws MojoExecutionException + { + projectPath = projectPath.replace( '\\', '/' ); + File pomFile = new File( projectsDir, projectPath ); + if ( pomFile.isDirectory() ) + { + pomFile = new File( pomFile, "pom.xml" ); + if ( !pomFile.exists() ) + { + if ( included ) + { + projectPaths.add( projectPath ); + } + return; + } + if ( !projectPath.endsWith( "/" ) ) + { + projectPath += '/'; + } + projectPath += "pom.xml"; + } + else if ( !pomFile.isFile() ) + { + return; + } + if ( !projectPaths.add( projectPath ) ) + { + return; + } + getLog().debug( "Collecting parent/child projects of " + projectPath ); + + Model model = PomUtils.loadPom( pomFile ); + + try + { + String projectsRoot = projectsDir.getCanonicalPath(); + String projectDir = pomFile.getParent(); + + String parentPath = "../pom.xml"; + if ( model.getParent() != null && StringUtils.isNotEmpty( model.getParent().getRelativePath() ) ) + { + parentPath = model.getParent().getRelativePath(); + } + String parent = relativizePath( new File( projectDir, parentPath ), projectsRoot ); + if ( parent != null ) + { + collectProjects( projectsDir, parent, projectPaths, false ); + } + + Collection<String> modulePaths = new LinkedHashSet<String>(); + + modulePaths.addAll( model.getModules() ); + + for ( Profile profile : model.getProfiles() ) + { + modulePaths.addAll( profile.getModules() ); + } + + for ( String modulePath : modulePaths ) + { + String module = relativizePath( new File( projectDir, modulePath ), projectsRoot ); + if ( module != null ) + { + collectProjects( projectsDir, module, projectPaths, false ); + } + } + } + catch ( IOException e ) + { + throw new MojoExecutionException( "Failed to analyze POM: " + pomFile, e ); + } + } + + /** + * Copies the specified projects to the directory given by {@link #cloneProjectsTo}. A project may either be denoted + * by a path to a POM file or merely by a path to a base directory. During cloning, the POM files will be filtered. + * + * @param projectPaths The paths to the projects to clone, relative to the projects directory, must not be + * <code>null</code> nor contain <code>null</code> elements. + * @throws org.apache.maven.plugin.MojoExecutionException If the the projects could not be copied/filtered. + */ + private void cloneProjects( Collection<String> projectPaths ) + throws MojoExecutionException + { + if ( !cloneProjectsTo.mkdirs() && cloneClean ) + { + try + { + FileUtils.cleanDirectory( cloneProjectsTo ); + } + catch ( IOException e ) + { + throw new MojoExecutionException( "Could not clean the cloneProjectsTo directory. Reason: " + + e.getMessage(), e ); + } + } + + // determine project directories to clone + Collection<String> dirs = new LinkedHashSet<String>(); + for ( String projectPath : projectPaths ) + { + if ( !new File( projectsDirectory, projectPath ).isDirectory() ) + { + projectPath = getParentPath( projectPath ); + } + dirs.add( projectPath ); + } + + boolean filter; + + // clone project directories + try + { + filter = !cloneProjectsTo.getCanonicalFile().equals( projectsDirectory.getCanonicalFile() ); + + List<String> clonedSubpaths = new ArrayList<String>(); + + for ( String subpath : dirs ) + { + // skip this project if its parent directory is also scheduled for cloning + if ( !".".equals( subpath ) && dirs.contains( getParentPath( subpath ) ) ) + { + continue; + } + + // avoid copying subdirs that are already cloned. + if ( !alreadyCloned( subpath, clonedSubpaths ) ) + { + // avoid creating new files that point to dir/. + if ( ".".equals( subpath ) ) + { + String cloneSubdir = relativizePath( cloneProjectsTo, projectsDirectory.getCanonicalPath() ); + + // avoid infinite recursion if the cloneTo path is a subdirectory. + if ( cloneSubdir != null ) + { + File temp = File.createTempFile( "pre-invocation-clone.", "" ); + temp.delete(); + temp.mkdirs(); + + copyDirectoryStructure( projectsDirectory, temp ); + + FileUtils.deleteDirectory( new File( temp, cloneSubdir ) ); + + copyDirectoryStructure( temp, cloneProjectsTo ); + } + else + { + copyDirectoryStructure( projectsDirectory, cloneProjectsTo ); + } + } + else + { + File srcDir = new File( projectsDirectory, subpath ); + File dstDir = new File( cloneProjectsTo, subpath ); + copyDirectoryStructure( srcDir, dstDir ); + } + + clonedSubpaths.add( subpath ); + } + } + } + catch ( IOException e ) + { + throw new MojoExecutionException( "Failed to clone projects from: " + projectsDirectory + " to: " + + cloneProjectsTo + ". Reason: " + e.getMessage(), e ); + } + + // filter cloned POMs + if ( filter ) + { + for ( String projectPath : projectPaths ) + { + File pomFile = new File( cloneProjectsTo, projectPath ); + if ( pomFile.isFile() ) + { + buildInterpolatedFile( pomFile, pomFile ); + } + + // MINVOKER-186 + // The following is a temporary solution to support Maven 3.3.1 (.mvn/extensions.xml) filtering + // Will be replaced by MINVOKER-117 with general filtering mechanism + File baseDir = pomFile.getParentFile(); + File mvnDir = new File( baseDir, ".mvn" ); + if ( mvnDir.isDirectory() ) + { + File extensionsFile = new File( mvnDir, "extensions.xml" ); + if ( extensionsFile.isFile() ) + { + buildInterpolatedFile( extensionsFile, extensionsFile ); + } + } + // END MINVOKER-186 + } + filteredPomPrefix = null; + } + } + + /** + * Gets the parent path of the specified relative path. + * + * @param path The relative path whose parent should be retrieved, must not be <code>null</code>. + * @return The parent path or "." if the specified path has no parent, never <code>null</code>. + */ + private String getParentPath( String path ) + { + int lastSep = Math.max( path.lastIndexOf( '/' ), path.lastIndexOf( '\\' ) ); + return ( lastSep < 0 ) ? "." : path.substring( 0, lastSep ); + } + + /** + * Copied a directory structure with default exclusions (.svn, CVS, etc) + * + * @param sourceDir The source directory to copy, must not be <code>null</code>. + * @param destDir The target directory to copy to, must not be <code>null</code>. + * @throws java.io.IOException If the directory structure could not be copied. + */ + private void copyDirectoryStructure( File sourceDir, File destDir ) + throws IOException + { + DirectoryScanner scanner = new DirectoryScanner(); + scanner.setBasedir( sourceDir ); + if ( !cloneAllFiles ) + { + scanner.addDefaultExcludes(); + } + scanner.scan(); + + /* + * NOTE: Make sure the destination directory is always there (even if empty) to support POM-less ITs. + */ + destDir.mkdirs(); + // Create all the directories, including any symlinks present in source + FileUtils.mkDirs( sourceDir, scanner.getIncludedDirectories(), destDir ); + + for ( String includedFile : scanner.getIncludedFiles() ) + { + File sourceFile = new File( sourceDir, includedFile ); + File destFile = new File( destDir, includedFile ); + FileUtils.copyFile( sourceFile, destFile ); + + // ensure clone project must be writable for additional changes + destFile.setWritable( true ); + } + } + + /** + * Determines whether the specified sub path has already been cloned, i.e. whether one of its ancestor directories + * was already cloned. + * + * @param subpath The sub path to check, must not be <code>null</code>. + * @param clonedSubpaths The list of already cloned paths, must not be <code>null</code> nor contain + * <code>null</code> elements. + * @return <code>true</code> if the specified path has already been cloned, <code>false</code> otherwise. + */ + static boolean alreadyCloned( String subpath, List<String> clonedSubpaths ) + { + for ( String path : clonedSubpaths ) + { + if ( ".".equals( path ) || subpath.equals( path ) || subpath.startsWith( path + File.separator ) ) + { + return true; + } + } + + return false; + } + + /** + * Runs the specified build jobs. + * + * @param projectsDir The base directory of all projects, must not be <code>null</code>. + * @param buildJobs The build jobs to run must not be <code>null</code> nor contain <code>null</code> elements. + * @throws org.apache.maven.plugin.MojoExecutionException If any build could not be launched. + */ + private void runBuilds( final File projectsDir, BuildJob[] buildJobs, int runWithParallelThreads ) + throws MojoExecutionException + { + if ( !localRepositoryPath.exists() ) + { + localRepositoryPath.mkdirs(); + } + + // ----------------------------------------------- + // interpolate settings file + // ----------------------------------------------- + + File interpolatedSettingsFile = interpolateSettings(); + + final File mergedSettingsFile = mergeSettings( interpolatedSettingsFile ); + + if ( mavenHome != null ) + { + actualMavenVersion = SelectorUtils.getMavenVersion( mavenHome ); + } + else + { + actualMavenVersion = SelectorUtils.getMavenVersion(); + } + scriptRunner.setGlobalVariable( "mavenVersion", actualMavenVersion ); + + final CharSequence actualJreVersion; + // @todo if ( javaVersions ) ... to be picked up from toolchains + if ( javaHome != null ) + { + actualJreVersion = resolveExternalJreVersion(); + } + else + { + actualJreVersion = SelectorUtils.getJreVersion(); + } + + try + { + if ( runWithParallelThreads > 1 ) + { + getLog().info( "use parallelThreads " + runWithParallelThreads ); + + ExecutorService executorService = Executors.newFixedThreadPool( runWithParallelThreads ); + for ( final BuildJob job : buildJobs ) + { + executorService.execute( new Runnable() + { + public void run() + { + try + { + runBuild( projectsDir, job, mergedSettingsFile, javaHome, actualJreVersion ); + } + catch ( MojoExecutionException e ) + { + throw new RuntimeException( e.getMessage(), e ); + } + } + } ); + } + + try + { + executorService.shutdown(); + // TODO add a configurable time out + executorService.awaitTermination( Long.MAX_VALUE, TimeUnit.MILLISECONDS ); + } + catch ( InterruptedException e ) + { + throw new MojoExecutionException( e.getMessage(), e ); + } + } + else + { + for ( BuildJob job : buildJobs ) + { + runBuild( projectsDir, job, mergedSettingsFile, javaHome, actualJreVersion ); + } + } + } + finally + { + if ( interpolatedSettingsFile != null && cloneProjectsTo == null ) + { + interpolatedSettingsFile.delete(); + } + if ( mergedSettingsFile != null && mergedSettingsFile.exists() ) + { + mergedSettingsFile.delete(); + } + } + } + + /** + * Interpolate settings.xml file. + * + * @return The interpolated settings.xml file. + * @throws MojoExecutionException in case of a problem. + */ + private File interpolateSettings() + throws MojoExecutionException + { + File interpolatedSettingsFile = null; + if ( settingsFile != null ) + { + if ( cloneProjectsTo != null ) + { + interpolatedSettingsFile = new File( cloneProjectsTo, "interpolated-" + settingsFile.getName() ); + } + else + { + interpolatedSettingsFile = + new File( settingsFile.getParentFile(), "interpolated-" + settingsFile.getName() ); + } + buildInterpolatedFile( settingsFile, interpolatedSettingsFile ); + } + return interpolatedSettingsFile; + } + + /** + * Merge the settings file + * + * @param interpolatedSettingsFile The interpolated settings file. + * @return The merged settings file. + * @throws MojoExecutionException Fail the build in case the merged settings file can't be created. + */ + private File mergeSettings( File interpolatedSettingsFile ) + throws MojoExecutionException + { + File mergedSettingsFile; + Settings mergedSettings = this.settings; + if ( mergeUserSettings ) + { + if ( interpolatedSettingsFile != null ) + { + // Have to merge the specified settings file (dominant) and the one of the invoking Maven process + try + { + SettingsBuildingRequest request = new DefaultSettingsBuildingRequest(); + request.setGlobalSettingsFile( interpolatedSettingsFile ); + + Settings dominantSettings = settingsBuilder.build( request ).getEffectiveSettings(); + Settings recessiveSettings = cloneSettings(); + SettingsUtils.merge( dominantSettings, recessiveSettings, TrackableBase.USER_LEVEL ); + + mergedSettings = dominantSettings; + getLog().debug( "Merged specified settings file with settings of invoking process" ); + } + catch ( SettingsBuildingException e ) + { + throw new MojoExecutionException( "Could not read specified settings file", e ); + } + } + } + + if ( this.settingsFile != null && !mergeUserSettings ) + { + mergedSettingsFile = interpolatedSettingsFile; + } + else + { + try + { + mergedSettingsFile = writeMergedSettingsFile( mergedSettings ); + } + catch ( IOException e ) + { + throw new MojoExecutionException( "Could not create temporary file for invoker settings.xml", e ); + } + } + return mergedSettingsFile; + } + + private File writeMergedSettingsFile( Settings mergedSettings ) + throws IOException + { + File mergedSettingsFile; + mergedSettingsFile = File.createTempFile( "invoker-settings", ".xml" ); + + SettingsXpp3Writer settingsWriter = new SettingsXpp3Writer(); + + FileWriter fileWriter = null; + try + { + fileWriter = new FileWriter( mergedSettingsFile ); + settingsWriter.write( fileWriter, mergedSettings ); + fileWriter.close(); + fileWriter = null; + } + finally + { + IOUtil.close( fileWriter ); + } + + if ( getLog().isDebugEnabled() ) + { + getLog().debug( "Created temporary file for invoker settings.xml: " + + mergedSettingsFile.getAbsolutePath() ); + } + return mergedSettingsFile; + } + + private Settings cloneSettings() + { + Settings recessiveSettings = SettingsUtils.copySettings( this.settings ); + + // MINVOKER-133: reset sourceLevelSet + resetSourceLevelSet( recessiveSettings ); + for ( org.apache.maven.settings.Mirror mirror : recessiveSettings.getMirrors() ) + { + resetSourceLevelSet( mirror ); + } + for ( org.apache.maven.settings.Server server : recessiveSettings.getServers() ) + { + resetSourceLevelSet( server ); + } + for ( org.apache.maven.settings.Proxy proxy : recessiveSettings.getProxies() ) + { + resetSourceLevelSet( proxy ); + } + for ( org.apache.maven.settings.Profile profile : recessiveSettings.getProfiles() ) + { + resetSourceLevelSet( profile ); + } + + return recessiveSettings; + } + + private void resetSourceLevelSet( org.apache.maven.settings.TrackableBase trackable ) + { + try + { + ReflectionUtils.setVariableValueInObject( trackable, "sourceLevelSet", Boolean.FALSE ); + getLog().debug( "sourceLevelSet: " + + ReflectionUtils.getValueIncludingSuperclasses( "sourceLevelSet", trackable ) ); + } + catch ( IllegalAccessException e ) + { + // noop + } + } + + private CharSequence resolveExternalJreVersion() + { + Artifact pluginArtifact = mojoExecution.getMojoDescriptor().getPluginDescriptor().getPluginArtifact(); + pluginArtifact.getFile(); + + Commandline commandLine = new Commandline(); + commandLine.setExecutable( new File( javaHome, "bin/java" ).getAbsolutePath() ); + commandLine.createArg().setValue( "-cp" ); + commandLine.createArg().setFile( pluginArtifact.getFile() ); + commandLine.createArg().setValue( SystemPropertyPrinter.class.getName() ); + commandLine.createArg().setValue( "java.version" ); + + final StringBuilder actualJreVersion = new StringBuilder(); + StreamConsumer consumer = new StreamConsumer() + { + public void consumeLine( String line ) + { + actualJreVersion.append( line ); + } + }; + try + { + CommandLineUtils.executeCommandLine( commandLine, consumer, null ); + } + catch ( CommandLineException e ) + { + getLog().warn( e.getMessage() ); + } + return actualJreVersion; + } + + /** + * Interpolate the pom file. + * + * @param pomFile The pom file. + * @param basedir The base directory. + * @return interpolated pom file location in case we have interpolated the pom file otherwise the original pom file + * will be returned. + * @throws MojoExecutionException + */ + private File interpolatePomFile( File pomFile, File basedir ) + throws MojoExecutionException + { + File interpolatedPomFile = null; + if ( pomFile != null ) + { + if ( StringUtils.isNotEmpty( filteredPomPrefix ) ) + { + interpolatedPomFile = new File( basedir, filteredPomPrefix + pomFile.getName() ); + buildInterpolatedFile( pomFile, interpolatedPomFile ); + } + else + { + interpolatedPomFile = pomFile; + } + } + return interpolatedPomFile; + } + + /** + * Runs the specified project. + * + * @param projectsDir The base directory of all projects, must not be <code>null</code>. + * @param buildJob The build job to run, must not be <code>null</code>. + * @param settingsFile The (already interpolated) user settings file for the build, may be <code>null</code> to use + * the current user settings. + * @throws org.apache.maven.plugin.MojoExecutionException If the project could not be launched. + */ + private void runBuild( File projectsDir, BuildJob buildJob, File settingsFile, File actualJavaHome, + CharSequence actualJreVersion ) + throws MojoExecutionException + { + // FIXME: Think about the following code part -- START + File pomFile = new File( projectsDir, buildJob.getProject() ); + File basedir; + if ( pomFile.isDirectory() ) + { + basedir = pomFile; + pomFile = new File( basedir, "pom.xml" ); + if ( !pomFile.exists() ) + { + pomFile = null; + } + else + { + buildJob.setProject( buildJob.getProject() + File.separator + "pom.xml" ); + } + } + else + { + basedir = pomFile.getParentFile(); + } + + File interpolatedPomFile = interpolatePomFile( pomFile, basedir ); + // FIXME: Think about the following code part -- ^^^^^^^ END + + getLog().info( buffer().a( "Building: " ).strong( buildJob.getProject() ).toString() ); + + InvokerProperties invokerProperties = getInvokerProperties( basedir ); + + // let's set what details we can + buildJob.setName( invokerProperties.getJobName() ); + buildJob.setDescription( invokerProperties.getJobDescription() ); + + try + { + int selection = getSelection( invokerProperties, actualJreVersion ); + if ( selection == 0 ) + { + long milliseconds = System.currentTimeMillis(); + boolean executed; + try + { + // CHECKSTYLE_OFF: LineLength + executed = + runBuild( basedir, interpolatedPomFile, settingsFile, actualJavaHome, invokerProperties ); + // CHECKSTYLE_ON: LineLength + } + finally + { + milliseconds = System.currentTimeMillis() - milliseconds; + buildJob.setTime( milliseconds / 1000.0 ); + } + + if ( executed ) + { + buildJob.setResult( BuildJob.Result.SUCCESS ); + + if ( !suppressSummaries ) + { + getLog().info( pad( buildJob ).success( "SUCCESS" ).a( ' ' ) + + formatTime( buildJob.getTime() ) ); + } + } + else + { + buildJob.setResult( BuildJob.Result.SKIPPED ); + + if ( !suppressSummaries ) + { + getLog().info( pad( buildJob ).warning( "SKIPPED" ).a( ' ' ) + + formatTime( buildJob.getTime() ) ); + } + } + } + else + { + buildJob.setResult( BuildJob.Result.SKIPPED ); + + StringBuilder message = new StringBuilder(); + if ( selection == Selector.SELECTOR_MULTI ) + { + message.append( "non-matching selectors" ); + } + else + { + if ( ( selection & Selector.SELECTOR_MAVENVERSION ) != 0 ) + { + message.append( "Maven version" ); + } + if ( ( selection & Selector.SELECTOR_JREVERSION ) != 0 ) + { + if ( message.length() > 0 ) + { + message.append( ", " ); + } + message.append( "JRE version" ); + } + if ( ( selection & Selector.SELECTOR_OSFAMILY ) != 0 ) + { + if ( message.length() > 0 ) + { + message.append( ", " ); + } + message.append( "OS" ); + } + } + + if ( !suppressSummaries ) + { + getLog().info( pad( buildJob ).warning( "SKIPPED" ) + " due to " + message.toString() ); + } + + // Abuse failureMessage, the field in the report which should contain the reason for skipping + // Consider skipCode + I18N + buildJob.setFailureMessage( "Skipped due to " + message.toString() ); + } + } + catch ( RunErrorException e ) + { + buildJob.setResult( BuildJob.Result.ERROR ); + buildJob.setFailureMessage( e.getMessage() ); + + if ( !suppressSummaries ) + { + getLog().info( pad( buildJob ).failure( "ERROR" ).a( ' ' ) + formatTime( buildJob.getTime() ) ); + getLog().info( " " + e.getMessage() ); + } + } + catch ( RunFailureException e ) + { + buildJob.setResult( e.getType() ); + buildJob.setFailureMessage( e.getMessage() ); + + if ( !suppressSummaries ) + { + getLog().info( pad( buildJob ).failure( "FAILED" ).a( ' ' ) + formatTime( buildJob.getTime() ) ); + getLog().info( " " + e.getMessage() ); + } + } + finally + { + deleteInterpolatedPomFile( interpolatedPomFile ); + writeBuildReport( buildJob ); + } + } + + private MessageBuilder pad( BuildJob buildJob ) + { + MessageBuilder buffer = buffer( 128 ); + + buffer.a( " " ); + buffer.a( buildJob.getProject() ); + buffer.a( ' ' ); + + int l = 11 + buildJob.getProject().length(); + + if ( l < RESULT_COLUMN ) + { + for ( int i = RESULT_COLUMN - l; i > 0; i-- ) + { + buffer.a( '.' ); + } + } + + return buffer.a( ' ' ); + } + + /** + * Delete the interpolated pom file if it has been created before. + * + * @param interpolatedPomFile The interpolated pom file. + */ + private void deleteInterpolatedPomFile( File interpolatedPomFile ) + { + if ( interpolatedPomFile != null && StringUtils.isNotEmpty( filteredPomPrefix ) ) + { + interpolatedPomFile.delete(); + } + } + + /** + * Determines whether selector conditions of the specified invoker properties match the current environment. + * + * @param invokerProperties The invoker properties to check, must not be <code>null</code>. + * @return <code>0</code> if the job corresponding to the properties should be run, otherwise a bitwise value + * representing the reason why it should be skipped. + */ + private int getSelection( InvokerProperties invokerProperties, CharSequence actualJreVersion ) + { + return new Selector( actualMavenVersion, actualJreVersion.toString() ).getSelection( invokerProperties ); + } + + /** + * Writes the XML report for the specified build job unless report generation has been disabled. + * + * @param buildJob The build job whose report should be written, must not be <code>null</code>. + * @throws org.apache.maven.plugin.MojoExecutionException If the report could not be written. + */ + private void writeBuildReport( BuildJob buildJob ) + throws MojoExecutionException + { + if ( disableReports ) + { + return; + } + + String safeFileName = buildJob.getProject().replace( '/', '_' ).replace( '\\', '_' ).replace( ' ', '_' ); + if ( safeFileName.endsWith( "_pom.xml" ) ) + { + safeFileName = safeFileName.substring( 0, safeFileName.length() - "_pom.xml".length() ); + } + + File reportFile = new File( reportsDirectory, "BUILD-" + safeFileName + ".xml" ); + try + { + FileOutputStream fos = new FileOutputStream( reportFile ); + try + { + Writer osw = new OutputStreamWriter( fos, buildJob.getModelEncoding() ); + BuildJobXpp3Writer writer = new BuildJobXpp3Writer(); + writer.write( osw, buildJob ); + osw.close(); + } + finally + { + fos.close(); + } + } + catch ( IOException e ) + { + throw new MojoExecutionException( "Failed to write build report " + reportFile, e ); + } + } + + /** + * Formats the specified build duration time. + * + * @param seconds The duration of the build. + * @return The formatted time, never <code>null</code>. + */ + private String formatTime( double seconds ) + { + return secFormat.format( seconds ); + } + + /** + * Runs the specified project. + * + * @param basedir The base directory of the project, must not be <code>null</code>. + * @param pomFile The (already interpolated) POM file, may be <code>null</code> for a POM-less Maven invocation. + * @param settingsFile The (already interpolated) user settings file for the build, may be <code>null</code>. Will + * be merged with the settings file of the invoking Maven process. + * @param invokerProperties The properties to use. + * @return <code>true</code> if the project was launched or <code>false</code> if the selector script indicated that + * the project should be skipped. + * @throws org.apache.maven.plugin.MojoExecutionException If the project could not be launched. + * @throws org.apache.maven.shared.scriptinterpreter.RunFailureException If either a hook script or the build itself + * failed. + */ + private boolean runBuild( File basedir, File pomFile, File settingsFile, File actualJavaHome, + InvokerProperties invokerProperties ) + throws MojoExecutionException, RunFailureException + { + if ( getLog().isDebugEnabled() && !invokerProperties.getProperties().isEmpty() ) + { + Properties props = invokerProperties.getProperties(); + getLog().debug( "Using invoker properties:" ); + for ( String key : new TreeSet<String>( props.stringPropertyNames() ) ) + { + String value = props.getProperty( key ); + getLog().debug( " " + key + " = " + value ); + } + } + + List<String> goals = getGoals( basedir ); + + List<String> profiles = getProfiles( basedir ); + + Map<String, Object> context = new LinkedHashMap<String, Object>(); + + FileLogger logger = setupBuildLogFile( basedir ); + try + { + try + { + scriptRunner.run( "selector script", basedir, selectorScript, context, logger, BuildJob.Result.SKIPPED, + false ); + } + catch ( RunErrorException e ) + { + throw e; + } + catch ( RunFailureException e ) + { + return false; + } + + scriptRunner.run( "pre-build script", basedir, preBuildHookScript, context, logger, + BuildJob.Result.FAILURE_PRE_HOOK, false ); + + final InvocationRequest request = new DefaultInvocationRequest(); + + request.setLocalRepositoryDirectory( localRepositoryPath ); + + request.setBatchMode( true ); + + request.setShowErrors( showErrors ); + + request.setDebug( debug ); + + request.setShowVersion( showVersion ); + + setupLoggerForBuildJob( logger, request ); + + if ( mavenHome != null ) + { + invoker.setMavenHome( mavenHome ); + // FIXME: Should we really take care of M2_HOME? + request.addShellEnvironment( "M2_HOME", mavenHome.getAbsolutePath() ); + } + + if ( mavenExecutable != null ) + { + invoker.setMavenExecutable( new File( mavenExecutable ) ); + } + + if ( actualJavaHome != null ) + { + request.setJavaHome( actualJavaHome ); + } + + if ( environmentVariables != null ) + { + for ( Map.Entry<String, String> variable : environmentVariables.entrySet() ) + { + request.addShellEnvironment( variable.getKey(), variable.getValue() ); + } + } + + for ( int invocationIndex = 1;; invocationIndex++ ) + { + if ( invocationIndex > 1 && !invokerProperties.isInvocationDefined( invocationIndex ) ) + { + break; + } + + request.setBaseDirectory( basedir ); + + request.setPomFile( pomFile ); + + request.setGoals( goals ); + + request.setProfiles( profiles ); + + request.setMavenOpts( mavenOpts ); + + request.setOffline( false ); + + request.setUserSettingsFile( settingsFile ); + + Properties systemProperties = + getSystemProperties( basedir, invokerProperties.getSystemPropertiesFile( invocationIndex ) ); + request.setProperties( systemProperties ); + + invokerProperties.configureInvocation( request, invocationIndex ); + + if ( getLog().isDebugEnabled() ) + { + try + { + getLog().debug( "Using MAVEN_OPTS: " + request.getMavenOpts() ); + getLog().debug( "Executing: " + new MavenCommandLineBuilder().build( request ) ); + } + catch ( CommandLineConfigurationException e ) + { + getLog().debug( "Failed to display command line: " + e.getMessage() ); + } + } + + InvocationResult result; + + try + { + result = invoker.execute( request ); + } + catch ( final MavenInvocationException e ) + { + getLog().debug( "Error invoking Maven: " + e.getMessage(), e ); + throw new RunFailureException( "Maven invocation failed. " + e.getMessage(), + BuildJob.Result.FAILURE_BUILD ); + } + + verify( result, invocationIndex, invokerProperties, logger ); + } + + scriptRunner.run( "post-build script", basedir, postBuildHookScript, context, logger, + BuildJob.Result.FAILURE_POST_HOOK, true ); + } + catch ( IOException e ) + { + throw new MojoExecutionException( e.getMessage(), e ); + } + finally + { + if ( logger != null ) + { + logger.close(); + } + } + return true; + } + + private void setupLoggerForBuildJob( FileLogger logger, final InvocationRequest request ) + { + if ( logger != null ) + { + request.setErrorHandler( logger ); + + request.setOutputHandler( logger ); + } + } + + /** + * Initializes the build logger for the specified project. This will write the logging information into + * {@code build.log}. + * + * @param basedir The base directory of the project, must not be <code>null</code>. + * @return The build logger or <code>null</code> if logging has been disabled. + * @throws org.apache.maven.plugin.MojoExecutionException If the log file could not be created. + */ + private FileLogger setupBuildLogFile( File basedir ) + throws MojoExecutionException + { + FileLogger logger = null; + + if ( !noLog ) + { + File outputLog = new File( basedir, "build.log" ); + try + { + if ( streamLogs ) + { + logger = new FileLogger( outputLog, getLog() ); + } + else + { + logger = new FileLogger( outputLog ); + } + + getLog().debug( "Build log initialized in: " + outputLog ); + } + catch ( IOException e ) + { + throw new MojoExecutionException( "Error initializing build logfile in: " + outputLog, e ); + } + } + + return logger; + } + + /** + * Gets the system properties to use for the specified project. + * + * @param basedir The base directory of the project, must not be <code>null</code>. + * @param filename The filename to the properties file to load, may be <code>null</code> to use the default path + * given by {@link #testPropertiesFile}. + * @return The system properties to use, may be empty but never <code>null</code>. + * @throws org.apache.maven.plugin.MojoExecutionException If the properties file exists but could not be read. + */ + private Properties getSystemProperties( final File basedir, final String filename ) + throws MojoExecutionException + { + Properties collectedTestProperties = new Properties(); + + if ( properties != null ) + { + // MINVOKER-118: property can have empty value, which is not accepted by collectedTestProperties + for ( Map.Entry<String, String> entry : properties.entrySet() ) + { + if ( entry.getValue() != null ) + { + collectedTestProperties.put( entry.getKey(), entry.getValue() ); + } + } + } + + File propertiesFile = null; + if ( filename != null ) + { + propertiesFile = new File( basedir, filename ); + } + else if ( testPropertiesFile != null ) + { + propertiesFile = new File( basedir, testPropertiesFile ); + } + + if ( propertiesFile != null && propertiesFile.isFile() ) + { + InputStream fin = null; + try + { + fin = new FileInputStream( propertiesFile ); + + Properties loadedProperties = new Properties(); + loadedProperties.load( fin ); + fin.close(); + fin = null; + collectedTestProperties.putAll( loadedProperties ); + } + catch ( IOException e ) + { + throw new MojoExecutionException( "Error reading system properties from " + propertiesFile ); + } + finally + { + IOUtil.close( fin ); + } + } + + return collectedTestProperties; + } + + /** + * Verifies the invocation result. + * + * @param result The invocation result to check, must not be <code>null</code>. + * @param invocationIndex The index of the invocation for which to check the exit code, must not be negative. + * @param invokerProperties The invoker properties used to check the exit code, must not be <code>null</code>. + * @param logger The build logger, may be <code>null</code> if logging is disabled. + * @throws org.apache.maven.shared.scriptinterpreter.RunFailureException If the invocation result indicates a build + * failure. + */ + private void verify( InvocationResult result, int invocationIndex, InvokerProperties invokerProperties, + FileLogger logger ) + throws RunFailureException + { + if ( result.getExecutionException() != null ) + { + throw new RunFailureException( "The Maven invocation failed. " + + result.getExecutionException().getMessage(), BuildJob.Result.ERROR ); + } + else if ( !invokerProperties.isExpectedResult( result.getExitCode(), invocationIndex ) ) + { + StringBuilder buffer = new StringBuilder( 256 ); + buffer.append( "The build exited with code " ).append( result.getExitCode() ).append( ". " ); + if ( logger != null ) + { + buffer.append( "See " ); + buffer.append( logger.getOutputFile().getAbsolutePath() ); + buffer.append( " for details." ); + } + else + { + buffer.append( "See console output for details." ); + } + throw new RunFailureException( buffer.toString(), BuildJob.Result.FAILURE_BUILD ); + } + } + + /** + * Gets the goal list for the specified project. + * + * @param basedir The base directory of the project, must not be <code>null</code>. + * @return The list of goals to run when building the project, may be empty but never <code>null</code>. + * @throws org.apache.maven.plugin.MojoExecutionException If the profile file could not be read. + */ + List<String> getGoals( final File basedir ) + throws MojoExecutionException + { + try + { + // FIXME: Currently we have null for goalsFile which has been removed. + // This might mean we can remove getGoals() at all ? Check this. + return getTokens( basedir, null, goals ); + } + catch ( IOException e ) + { + throw new MojoExecutionException( "error reading goals", e ); + } + } + + /** + * Gets the profile list for the specified project. + * + * @param basedir The base directory of the project, must not be <code>null</code>. + * @return The list of profiles to activate when building the project, may be empty but never <code>null</code>. + * @throws org.apache.maven.plugin.MojoExecutionException If the profile file could not be read. + */ + List<String> getProfiles( File basedir ) + throws MojoExecutionException + { + try + { + return getTokens( basedir, null, profiles ); + } + catch ( IOException e ) + { + throw new MojoExecutionException( "error reading profiles", e ); + } + } + + private List<String> calculateExcludes() + throws IOException + { + List<String> excludes = + ( pomExcludes != null ) ? new ArrayList<String>( pomExcludes ) : new ArrayList<String>(); + if ( this.settingsFile != null ) + { + String exclude = relativizePath( this.settingsFile, projectsDirectory.getCanonicalPath() ); + if ( exclude != null ) + { + excludes.add( exclude.replace( '\\', '/' ) ); + getLog().debug( "Automatically excluded " + exclude + " from project scanning" ); + } + } + return excludes; + + } + + /** + * @return The list of setupUp jobs. + * @throws IOException + * @see {@link #setupIncludes} + */ + private BuildJob[] getSetupBuildJobsFromFolders() + throws IOException + { + List<String> excludes = calculateExcludes(); + + BuildJob[] setupPoms = scanProjectsDirectory( setupIncludes, excludes, BuildJob.Type.SETUP ); + if ( getLog().isDebugEnabled() ) + { + getLog().debug( "Setup projects: " + Arrays.asList( setupPoms ) ); + } + + return setupPoms; + } + + /** + * Gets the build jobs that should be processed. Note that the order of the returned build jobs is significant. + * + * @return The build jobs to process, may be empty but never <code>null</code>. + * @throws java.io.IOException If the projects directory could not be scanned. + */ + BuildJob[] getBuildJobs() + throws IOException + { + BuildJob[] buildJobs; + + if ( invokerTest == null ) + { + List<String> excludes = calculateExcludes(); + + BuildJob[] setupPoms = scanProjectsDirectory( setupIncludes, excludes, BuildJob.Type.SETUP ); + if ( getLog().isDebugEnabled() ) + { + getLog().debug( "Setup projects: " + Arrays.asList( setupPoms ) ); + } + + BuildJob[] normalPoms = scanProjectsDirectory( pomIncludes, excludes, BuildJob.Type.NORMAL ); + + Map<String, BuildJob> uniquePoms = new LinkedHashMap<String, BuildJob>(); + for ( BuildJob setupPom : setupPoms ) + { + uniquePoms.put( setupPom.getProject(), setupPom ); + } + for ( BuildJob normalPom : normalPoms ) + { + if ( !uniquePoms.containsKey( normalPom.getProject() ) ) + { + uniquePoms.put( normalPom.getProject(), normalPom ); + } + } + + buildJobs = uniquePoms.values().toArray( new BuildJob[uniquePoms.size()] ); + } + else + { + String[] testRegexes = StringUtils.split( invokerTest, "," ); + List<String> includes = new ArrayList<String>( testRegexes.length ); + List<String> excludes = new ArrayList<String>(); + + for ( String regex : testRegexes ) + { + // user just use -Dinvoker.test=MWAR191,MNG111 to use a directory thats the end is not pom.xml + if ( regex.startsWith( "!" ) ) + { + excludes.add( regex.substring( 1 ) ); + } + else + { + includes.add( regex ); + } + } + + // it would be nice if we could figure out what types these are... but perhaps + // not necessary for the -Dinvoker.test=xxx t + buildJobs = scanProjectsDirectory( includes, excludes, BuildJob.Type.DIRECT ); + } + + relativizeProjectPaths( buildJobs ); + + return buildJobs; + } + + /** + * Scans the projects directory for projects to build. Both (POM) files and mere directories will be matched by the + * scanner patterns. If the patterns match a directory which contains a file named "pom.xml", the results will + * include the path to this file rather than the directory path in order to avoid duplicate invocations of the same + * project. + * + * @param includes The include patterns for the scanner, may be <code>null</code>. + * @param excludes The exclude patterns for the scanner, may be <code>null</code> to exclude nothing. + * @param type The type to assign to the resulting build jobs, must not be <code>null</code>. + * @return The build jobs matching the patterns, never <code>null</code>. + * @throws java.io.IOException If the project directory could not be scanned. + */ + private BuildJob[] scanProjectsDirectory( List<String> includes, List<String> excludes, String type ) + throws IOException + { + if ( !projectsDirectory.isDirectory() ) + { + return new BuildJob[0]; + } + + DirectoryScanner scanner = new DirectoryScanner(); + scanner.setBasedir( projectsDirectory.getCanonicalFile() ); + scanner.setFollowSymlinks( false ); + if ( includes != null ) + { + scanner.setIncludes( includes.toArray( new String[includes.size()] ) ); + } + if ( excludes != null ) + { + scanner.setExcludes( excludes.toArray( new String[excludes.size()] ) ); + } + scanner.addDefaultExcludes(); + scanner.scan(); + + Map<String, BuildJob> matches = new LinkedHashMap<String, BuildJob>(); + + for ( String includedFile : scanner.getIncludedFiles() ) + { + matches.put( includedFile, new BuildJob( includedFile, type ) ); + } + + for ( String includedDir : scanner.getIncludedDirectories() ) + { + String includedFile = includedDir + File.separatorChar + "pom.xml"; + if ( new File( scanner.getBasedir(), includedFile ).isFile() ) + { + matches.put( includedFile, new BuildJob( includedFile, type ) ); + } + else + { + matches.put( includedDir, new BuildJob( includedDir, type ) ); + } + } + + return matches.values().toArray( new BuildJob[matches.size()] ); + } + + /** + * Relativizes the project paths of the specified build jobs against the directory specified by + * {@link #projectsDirectory} (if possible). If a project path does not denote a sub path of the projects directory, + * it is returned as is. + * + * @param buildJobs The build jobs whose project paths should be relativized, must not be <code>null</code> nor + * contain <code>null</code> elements. + * @throws java.io.IOException If any path could not be relativized. + */
[... 317 lines stripped ...]