View Javadoc
1   package org.apache.maven.plugins.invoker;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.BufferedReader;
23  import java.io.BufferedWriter;
24  import java.io.File;
25  import java.io.FileInputStream;
26  import java.io.FileOutputStream;
27  import java.io.FileWriter;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.io.OutputStreamWriter;
31  import java.io.Reader;
32  import java.io.Writer;
33  import java.text.DecimalFormat;
34  import java.text.DecimalFormatSymbols;
35  import java.util.ArrayList;
36  import java.util.Arrays;
37  import java.util.Collection;
38  import java.util.Collections;
39  import java.util.HashMap;
40  import java.util.LinkedHashMap;
41  import java.util.LinkedHashSet;
42  import java.util.LinkedList;
43  import java.util.List;
44  import java.util.Locale;
45  import java.util.Map;
46  import java.util.Map.Entry;
47  import java.util.Properties;
48  import java.util.StringTokenizer;
49  import java.util.TreeSet;
50  import java.util.concurrent.ExecutorService;
51  import java.util.concurrent.Executors;
52  import java.util.concurrent.TimeUnit;
53  import org.apache.maven.artifact.Artifact;
54  import org.apache.maven.model.Model;
55  import org.apache.maven.model.Profile;
56  import org.apache.maven.plugin.AbstractMojo;
57  import org.apache.maven.plugin.MojoExecution;
58  import org.apache.maven.plugin.MojoExecutionException;
59  import org.apache.maven.plugin.MojoFailureException;
60  import org.apache.maven.plugins.invoker.model.BuildJob;
61  import org.apache.maven.plugins.invoker.model.io.xpp3.BuildJobXpp3Writer;
62  import org.apache.maven.plugins.annotations.Component;
63  import org.apache.maven.plugins.annotations.Parameter;
64  import org.apache.maven.project.MavenProject;
65  import org.apache.maven.settings.Settings;
66  import org.apache.maven.settings.SettingsUtils;
67  import org.apache.maven.settings.TrackableBase;
68  import org.apache.maven.settings.building.DefaultSettingsBuildingRequest;
69  import org.apache.maven.settings.building.SettingsBuilder;
70  import org.apache.maven.settings.building.SettingsBuildingException;
71  import org.apache.maven.settings.building.SettingsBuildingRequest;
72  import org.apache.maven.settings.io.xpp3.SettingsXpp3Writer;
73  import org.apache.maven.shared.invoker.CommandLineConfigurationException;
74  import org.apache.maven.shared.invoker.DefaultInvocationRequest;
75  import org.apache.maven.shared.invoker.InvocationRequest;
76  import org.apache.maven.shared.invoker.InvocationResult;
77  import org.apache.maven.shared.invoker.Invoker;
78  import org.apache.maven.shared.invoker.MavenCommandLineBuilder;
79  import org.apache.maven.shared.invoker.MavenInvocationException;
80  import org.apache.maven.shared.scriptinterpreter.RunErrorException;
81  import org.apache.maven.shared.scriptinterpreter.RunFailureException;
82  import org.apache.maven.shared.scriptinterpreter.ScriptRunner;
83  import org.apache.maven.shared.utils.logging.MessageBuilder;
84  import org.codehaus.plexus.interpolation.InterpolationException;
85  import org.codehaus.plexus.interpolation.Interpolator;
86  import org.codehaus.plexus.interpolation.MapBasedValueSource;
87  import org.codehaus.plexus.interpolation.RegexBasedInterpolator;
88  import org.codehaus.plexus.util.DirectoryScanner;
89  import org.codehaus.plexus.util.FileUtils;
90  import org.codehaus.plexus.util.IOUtil;
91  import org.codehaus.plexus.util.InterpolationFilterReader;
92  import org.codehaus.plexus.util.ReaderFactory;
93  import org.codehaus.plexus.util.ReflectionUtils;
94  import org.codehaus.plexus.util.StringUtils;
95  import org.codehaus.plexus.util.WriterFactory;
96  import org.codehaus.plexus.util.cli.CommandLineException;
97  import org.codehaus.plexus.util.cli.CommandLineUtils;
98  import org.codehaus.plexus.util.cli.Commandline;
99  import org.codehaus.plexus.util.cli.StreamConsumer;
100 import static org.apache.maven.shared.utils.logging.MessageUtils.buffer;
101 
102 /**
103  * Provides common code for mojos invoking sub builds.
104  *
105  * @author Stephen Connolly
106  * @since 15-Aug-2009 09:09:29
107  */
108 public abstract class AbstractInvokerMojo
109     extends AbstractMojo
110 {
111     /**
112      * The zero-based column index where to print the invoker result.
113      */
114     private static final int RESULT_COLUMN = 60;
115 
116     /**
117      * Flag used to suppress certain invocations. This is useful in tailoring the build using profiles.
118      *
119      * @since 1.1
120      */
121     @Parameter( property = "invoker.skip", defaultValue = "false" )
122     private boolean skipInvocation;
123 
124     /**
125      * Flag used to suppress the summary output notifying of successes and failures. If set to <code>true</code>, the
126      * only indication of the build's success or failure will be the effect it has on the main build (if it fails, the
127      * main build should fail as well). If {@link #streamLogs} is enabled, the sub-build summary will also provide an
128      * indication.
129      */
130     @Parameter( defaultValue = "false" )
131     protected boolean suppressSummaries;
132 
133     /**
134      * Flag used to determine whether the build logs should be output to the normal mojo log.
135      */
136     @Parameter( property = "invoker.streamLogs", defaultValue = "false" )
137     private boolean streamLogs;
138 
139     /**
140      * The local repository for caching artifacts. It is strongly recommended to specify a path to an isolated
141      * repository like <code>${project.build.directory}/it-repo</code>. Otherwise, your ordinary local repository will
142      * be used, potentially soiling it with broken artifacts.
143      */
144     @Parameter( property = "invoker.localRepositoryPath", defaultValue = "${settings.localRepository}" )
145     private File localRepositoryPath;
146 
147     /**
148      * Directory to search for integration tests.
149      */
150     @Parameter( property = "invoker.projectsDirectory", defaultValue = "${basedir}/src/it/" )
151     private File projectsDirectory;
152 
153     /**
154      * Base directory where all build reports are written to. Every execution of an integration test will produce an XML
155      * file which contains the information about success or failure of that particular build job. The format of the
156      * resulting XML file is documented in the given <a href="./build-job.html">build-job</a> reference.
157      *
158      * @since 1.4
159      */
160     @Parameter( property = "invoker.reportsDirectory", defaultValue = "${project.build.directory}/invoker-reports" )
161     private File reportsDirectory;
162 
163     /**
164      * A flag to disable the generation of build reports.
165      *
166      * @since 1.4
167      */
168     @Parameter( property = "invoker.disableReports", defaultValue = "false" )
169     private boolean disableReports;
170 
171     /**
172      * Directory to which projects should be cloned prior to execution. If set to {@code null}, each integration test 
173      * will be run in the directory in which the corresponding IT POM was found. In this case, you most likely want to
174      * configure your SCM to ignore <code>target</code> and <code>build.log</code> in the test's base directory.
175      *
176      * @since 1.1
177      */
178     @Parameter( defaultValue = "${project.build.directory}/its" )
179     private File cloneProjectsTo;
180 
181     // CHECKSTYLE_OFF: LineLength
182     /**
183      * Some files are normally excluded when copying the IT projects from the directory specified by the parameter
184      * projectsDirectory to the directory given by cloneProjectsTo (e.g. <code>.svn</code>, <code>CVS</code>,
185      * <code>*~</code>, etc: see <a href=
186      * "https://codehaus-plexus.github.io/plexus-utils/apidocs/org/codehaus/plexus/util/AbstractScanner.html#DEFAULTEXCLUDES">
187      * reference</a> for full list). Setting this parameter to <code>true</code> will cause all files to be copied to
188      * the <code>cloneProjectsTo</code> directory.
189      *
190      * @since 1.2
191      */
192     @Parameter( defaultValue = "false" )
193     private boolean cloneAllFiles;
194     // CHECKSTYLE_ON: LineLength
195 
196     /**
197      * Ensure the {@link #cloneProjectsTo} directory is not polluted with files from earlier invoker runs.
198      *
199      * @since 1.6
200      */
201     @Parameter( defaultValue = "true" )
202     private boolean cloneClean;
203 
204     /**
205      * A single POM to build, skipping any scanning parameters and behavior.
206      */
207     @Parameter( property = "invoker.pom" )
208     private File pom;
209 
210     /**
211      * Include patterns for searching the integration test directory for projects. This parameter is meant to be set
212      * from the POM. If this parameter is not set, the plugin will search for all <code>pom.xml</code> files one
213      * directory below {@link #projectsDirectory} (i.e. <code>*&#47;pom.xml</code>).<br>
214      * <br>
215      * Starting with version 1.3, mere directories can also be matched by these patterns. For example, the include
216      * pattern <code>*</code> will run Maven builds on all immediate sub directories of {@link #projectsDirectory},
217      * regardless if they contain a <code>pom.xml</code>. This allows to perform builds that need/should not depend on
218      * the existence of a POM.
219      */
220     @Parameter
221     private List<String> pomIncludes = Collections.singletonList( "*/pom.xml" );
222 
223     /**
224      * Exclude patterns for searching the integration test directory. This parameter is meant to be set from the POM. By
225      * default, no POM files are excluded. For the convenience of using an include pattern like <code>*</code>, the
226      * custom settings file specified by the parameter {@link #settingsFile} will always be excluded automatically.
227      */
228     @Parameter
229     private List<String> pomExcludes = Collections.emptyList();
230 
231     /**
232      * Include patterns for searching the projects directory for projects that need to be run before the other projects.
233      * This parameter allows to declare projects that perform setup tasks like installing utility artifacts into the
234      * local repository. Projects matched by these patterns are implicitly excluded from the scan for ordinary projects.
235      * Also, the exclusions defined by the parameter {@link #pomExcludes} apply to the setup projects, too. Default
236      * value is: <code>setup*&#47;pom.xml</code>.
237      *
238      * @since 1.3
239      */
240     @Parameter
241     private List<String> setupIncludes = Collections.singletonList( "setup*/pom.xml" );
242 
243     /**
244      * The list of goals to execute on each project. Default value is: <code>package</code>.
245      */
246     @Parameter
247     private List<String> goals = Collections.singletonList( "package" );
248 
249     /**
250      */
251     @Component
252     private Invoker invoker;
253 
254     @Component
255     private SettingsBuilder settingsBuilder;
256 
257     /**
258      * Relative path of a selector script to run prior in order to decide if the build should be executed. This script
259      * may be written with either BeanShell or Groovy. If the file extension is omitted (e.g. <code>selector</code>),
260      * the plugin searches for the file by trying out the well-known extensions <code>.bsh</code> and
261      * <code>.groovy</code>. If this script exists for a particular project but returns any non-null value different
262      * from <code>true</code>, the corresponding build is flagged as skipped. In this case, none of the pre-build hook
263      * script, Maven nor the post-build hook script will be invoked. If this script throws an exception, the
264      * corresponding build is flagged as in error, and none of the pre-build hook script, Maven not the post-build hook
265      * script will be invoked.
266      *
267      * @since 1.5
268      */
269     @Parameter( property = "invoker.selectorScript", defaultValue = "selector" )
270     private String selectorScript;
271 
272     /**
273      * Relative path of a pre-build hook script to run prior to executing the build. This script may be written with
274      * either BeanShell or Groovy (since 1.3). If the file extension is omitted (e.g. <code>prebuild</code>), the plugin
275      * searches for the file by trying out the well-known extensions <code>.bsh</code> and <code>.groovy</code>. If this
276      * script exists for a particular project but returns any non-null value different from <code>true</code> or throws
277      * an exception, the corresponding build is flagged as a failure. In this case, neither Maven nor the post-build
278      * hook script will be invoked.
279      */
280     @Parameter( property = "invoker.preBuildHookScript", defaultValue = "prebuild" )
281     private String preBuildHookScript;
282 
283     /**
284      * Relative path of a cleanup/verification hook script to run after executing the build. This script may be written
285      * with either BeanShell or Groovy (since 1.3). If the file extension is omitted (e.g. <code>verify</code>), the
286      * plugin searches for the file by trying out the well-known extensions <code>.bsh</code> and <code>.groovy</code>.
287      * If this script exists for a particular project but returns any non-null value different from <code>true</code> or
288      * throws an exception, the corresponding build is flagged as a failure.
289      */
290     @Parameter( property = "invoker.postBuildHookScript", defaultValue = "postbuild" )
291     private String postBuildHookScript;
292 
293     /**
294      * Location of a properties file that defines CLI properties for the test.
295      */
296     @Parameter( property = "invoker.testPropertiesFile", defaultValue = "test.properties" )
297     private String testPropertiesFile;
298 
299     /**
300      * Common set of properties to pass in on each project's command line, via -D parameters.
301      *
302      * @since 1.1
303      */
304     @Parameter
305     private Map<String, String> properties;
306 
307     /**
308      * Whether to show errors in the build output.
309      */
310     @Parameter( property = "invoker.showErrors", defaultValue = "false" )
311     private boolean showErrors;
312 
313     /**
314      * Whether to show debug statements in the build output.
315      */
316     @Parameter( property = "invoker.debug", defaultValue = "false" )
317     private boolean debug;
318 
319     /**
320      * Suppress logging to the <code>build.log</code> file.
321      */
322     @Parameter( property = "invoker.noLog", defaultValue = "false" )
323     private boolean noLog;
324 
325     /**
326      * List of profile identifiers to explicitly trigger in the build.
327      *
328      * @since 1.1
329      */
330     @Parameter
331     private List<String> profiles;
332 
333     /**
334      * A list of additional properties which will be used to filter tokens in POMs and goal files.
335      *
336      * @since 1.3
337      */
338     @Parameter
339     private Map<String, String> filterProperties;
340 
341     /**
342      * The Maven Project Object
343      *
344      * @since 1.1
345      */
346     @Parameter( defaultValue = "${project}", readonly = true, required = true )
347     private MavenProject project;
348 
349     @Parameter( defaultValue = "${mojoExecution}", readonly = true, required = true )
350     private MojoExecution mojoExecution;
351 
352     /**
353      * A comma separated list of projectname patterns to run. Specify this parameter to run individual tests by file
354      * name, overriding the {@link #setupIncludes}, {@link #pomIncludes} and {@link #pomExcludes} parameters. Each
355      * pattern you specify here will be used to create an include/exclude pattern formatted like
356      * <code>${projectsDirectory}/<i>pattern</i></code>. To exclude a test, prefix the pattern with a '<code>!</code>'.
357      * So you can just type <nobr><code>-Dinvoker.test=SimpleTest,Comp*Test,!Compare*</code></nobr> to run builds in
358      * <code>${projectsDirectory}/SimpleTest</code> and <code>${projectsDirectory}/ComplexTest</code>, but not
359      * <code>${projectsDirectory}/CompareTest</code>
360      *
361      * @since 1.1 (exclusion since 1.8)
362      */
363     @Parameter( property = "invoker.test" )
364     private String invokerTest;
365 
366     /**
367      * Path to an alternate <code>settings.xml</code> to use for Maven invocation with all ITs. Note that the
368      * <code>&lt;localRepository&gt;</code> element of this settings file is always ignored, i.e. the path given by the
369      * parameter {@link #localRepositoryPath} is dominant.
370      *
371      * @since 1.2
372      */
373     @Parameter( property = "invoker.settingsFile" )
374     private File settingsFile;
375 
376     /**
377      * The <code>MAVEN_OPTS</code> environment variable to use when invoking Maven. This value can be overridden for
378      * individual integration tests by using {@link #invokerPropertiesFile}.
379      *
380      * @since 1.2
381      */
382     @Parameter( property = "invoker.mavenOpts" )
383     private String mavenOpts;
384 
385     /**
386      * The home directory of the Maven installation to use for the forked builds. Defaults to the current Maven
387      * installation.
388      *
389      * @since 1.3
390      */
391     @Parameter( property = "invoker.mavenHome" )
392     private File mavenHome;
393 
394     /**
395      * mavenExecutable can either be a file relative to <code>${maven.home}/bin/</code> or an absolute file.
396      *
397      * @since 1.8
398      * @see Invoker#setMavenExecutable(File)
399      */
400     @Parameter( property = "invoker.mavenExecutable" )
401     private String mavenExecutable;
402 
403     /**
404      * The <code>JAVA_HOME</code> environment variable to use for forked Maven invocations. Defaults to the current Java
405      * home directory.
406      *
407      * @since 1.3
408      */
409     @Parameter( property = "invoker.javaHome" )
410     private File javaHome;
411 
412     /**
413      * The file encoding for the pre-/post-build scripts and the list files for goals and profiles.
414      *
415      * @since 1.2
416      */
417     @Parameter( property = "encoding", defaultValue = "${project.build.sourceEncoding}" )
418     private String encoding;
419 
420     /**
421      * The current user system settings for use in Maven.
422      *
423      * @since 1.2
424      */
425     @Parameter( defaultValue = "${settings}", readonly = true, required = true )
426     private Settings settings;
427 
428     /**
429      * A flag whether the test class path of the project under test should be included in the class path of the
430      * pre-/post-build scripts. If set to <code>false</code>, the class path of script interpreter consists only of the
431      * <a href="dependencies.html">runtime dependencies</a> of the Maven Invoker Plugin. If set the <code>true</code>,
432      * the project's test class path will be prepended to the interpreter class path. Among others, this feature allows
433      * the scripts to access utility classes from the test sources of your project.
434      *
435      * @since 1.2
436      */
437     @Parameter( property = "invoker.addTestClassPath", defaultValue = "false" )
438     private boolean addTestClassPath;
439 
440     /**
441      * The test class path of the project under test.
442      */
443     @Parameter( defaultValue = "${project.testClasspathElements}", readonly = true )
444     private List<String> testClassPath;
445 
446     /**
447      * The name of an optional project-specific file that contains properties used to specify settings for an individual
448      * Maven invocation. Any property present in the file will override the corresponding setting from the plugin
449      * configuration. The values of the properties are filtered and may use expressions like
450      * <code>${project.version}</code> to reference project properties or values from the parameter
451      * {@link #filterProperties}. The snippet below describes the supported properties:
452      * <p/>
453      *
454      * <pre>
455      * # A comma or space separated list of goals/phases to execute, may
456      * # specify an empty list to execute the default goal of the IT project.
457      * # Environment variables used by maven plugins can be added here
458      * invoker.goals = clean install -Dplugin.variable=value
459      *
460      * # Or you can give things like this if you need.
461      * invoker.goals = -T2 clean verify
462      *
463      * # Optionally, a list of goals to run during further invocations of Maven
464      * invoker.goals.2 = ${project.groupId}:${project.artifactId}:${project.version}:run
465      *
466      * # A comma or space separated list of profiles to activate
467      * invoker.profiles = its,jdk15
468      *
469      * # The path to an alternative POM or base directory to invoke Maven on, defaults to the
470      * # project that was originally specified in the plugin configuration
471      * # Since plugin version 1.4
472      * invoker.project = sub-module
473      *
474      * # The value for the environment variable MAVEN_OPTS
475      * invoker.mavenOpts = -Dfile.encoding=UTF-16 -Xms32m -Xmx256m
476      *
477      * # Possible values are &quot;fail-fast&quot; (default), &quot;fail-at-end&quot; and &quot;fail-never&quot;
478      * invoker.failureBehavior = fail-never
479      *
480      * # The expected result of the build, possible values are &quot;success&quot; (default) and &quot;failure&quot;
481      * invoker.buildResult = failure
482      *
483      * # A boolean value controlling the aggregator mode of Maven, defaults to &quot;false&quot;
484      * invoker.nonRecursive = true
485      *
486      * # A boolean value controlling the network behavior of Maven, defaults to &quot;false&quot;
487      * # Since plugin version 1.4
488      * invoker.offline = true
489      *
490      * # The path to the properties file from which to load system properties, defaults to the
491      * # filename given by the plugin parameter testPropertiesFile
492      * # Since plugin version 1.4
493      * invoker.systemPropertiesFile = test.properties
494      *
495      * # An optional human friendly name for this build job to be included in the build reports.
496      * # Since plugin version 1.4
497      * invoker.name = Test Build 01
498      *
499      * # An optional description for this build job to be included in the build reports.
500      * # Since plugin version 1.4
501      * invoker.description = Checks the support for build reports.
502      *
503      * # A comma separated list of JRE versions on which this build job should be run.
504      * # Since plugin version 1.4
505      * invoker.java.version = 1.4+, !1.4.1, 1.7-
506      * 
507      * # A comma separated list of OS families on which this build job should be run.
508      * # Since plugin version 1.4
509      * invoker.os.family = !windows, unix, mac
510      *
511      * # A comma separated list of Maven versions on which this build should be run.
512      * # Since plugin version 1.5
513      * invoker.maven.version = 2.0.10+, !2.1.0, !2.2.0
514      * 
515      * # For java.version, maven.version and os.family it is possible to define multiple selectors.
516      * # If one of the indexed selectors matches, the test is executed.
517      * # With the invoker.x.y equivalents you can specify global matchers.  
518      * selector.1.java.version = 1.8+
519      * selector.1.maven.version = 3.2.5+
520      * selector.1.os.family = !windows
521      * selector.2.maven.version = 3.0+
522      * selector.3.java.version = 9+
523      * 
524      * # A boolean value controlling the debug logging level of Maven, , defaults to &quot;false&quot;
525      * # Since plugin version 1.8
526      * invoker.debug = true
527      * 
528      * # Path to an alternate <code>settings.xml</code> to use for Maven invocation with this IT.
529      * # Since plugin version 3.0.1
530      * invoker.settingsFile = ../
531      * </pre>
532      *
533      * @since 1.2
534      */
535     @Parameter( property = "invoker.invokerPropertiesFile", defaultValue = "invoker.properties" )
536     private String invokerPropertiesFile;
537 
538     /**
539      * flag to enable show mvn version used for running its (cli option : -V,--show-version )
540      *
541      * @since 1.4
542      */
543     @Parameter( property = "invoker.showVersion", defaultValue = "false" )
544     private boolean showVersion;
545 
546     /**
547      * number of threads for running tests in parallel. This will be the number of maven forked process in parallel.
548      *
549      * @since 1.6
550      */
551     @Parameter( property = "invoker.parallelThreads", defaultValue = "1" )
552     private int parallelThreads;
553 
554     /**
555      * @since 1.6
556      */
557     @Parameter( property = "plugin.artifacts", required = true, readonly = true )
558     private List<Artifact> pluginArtifacts;
559 
560     /**
561      * If enable and if you have a settings file configured for the execution, it will be merged with your user
562      * settings.
563      *
564      * @since 1.6
565      */
566     @Parameter( property = "invoker.mergeUserSettings", defaultValue = "false" )
567     private boolean mergeUserSettings;
568 
569     /**
570      * Additional environment variables to set on the command line.
571      *
572      * @since 1.8
573      */
574     @Parameter
575     private Map<String, String> environmentVariables;
576 
577     /**
578      * Additional variables for use in the hook scripts.
579      *
580      * @since 1.9
581      */
582     @Parameter
583     private Map<String, String> scriptVariables;
584 
585     /**
586      * The scripter runner that is responsible to execute hook scripts.
587      */
588     private ScriptRunner scriptRunner;
589 
590     /**
591      * A string used to prefix the file name of the filtered POMs in case the POMs couldn't be filtered in-place (i.e.
592      * the projects were not cloned to a temporary directory), can be <code>null</code>. This will be set to
593      * <code>null</code> if the POMs have already been filtered during cloning.
594      */
595     private String filteredPomPrefix = "interpolated-";
596 
597     /**
598      * The format for elapsed build time.
599      */
600     private final DecimalFormat secFormat = new DecimalFormat( "(0.0 s)", new DecimalFormatSymbols( Locale.ENGLISH ) );
601 
602     /**
603      * The version of Maven which is used to run the builds
604      */
605     private String actualMavenVersion;
606 
607     /**
608      * Invokes Maven on the configured test projects.
609      *
610      * @throws org.apache.maven.plugin.MojoExecutionException If the goal encountered severe errors.
611      * @throws org.apache.maven.plugin.MojoFailureException If any of the Maven builds failed.
612      */
613     public void execute()
614         throws MojoExecutionException, MojoFailureException
615     {
616         if ( skipInvocation )
617         {
618             getLog().info( "Skipping invocation per configuration."
619                 + " If this is incorrect, ensure the skipInvocation parameter is not set to true." );
620             return;
621         }
622 
623         if ( StringUtils.isEmpty( encoding ) )
624         {
625             getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
626                 + ", i.e. build is platform dependent!" );
627         }
628 
629         // done it here to prevent issues with concurrent access in case of parallel run
630         if ( !disableReports )
631         {
632             setupReportsFolder();
633         }
634 
635         BuildJob[] buildJobs;
636         if ( pom == null )
637         {
638             try
639             {
640                 buildJobs = getBuildJobs();
641             }
642             catch ( final IOException e )
643             {
644                 throw new MojoExecutionException( "Error retrieving POM list from includes, "
645                     + "excludes, and projects directory. Reason: " + e.getMessage(), e );
646             }
647         }
648         else
649         {
650             try
651             {
652                 projectsDirectory = pom.getCanonicalFile().getParentFile();
653             }
654             catch ( IOException e )
655             {
656                 throw new MojoExecutionException( "Failed to discover projectsDirectory from "
657                     + "pom File parameter. Reason: " + e.getMessage(), e );
658             }
659 
660             buildJobs = new BuildJob[] { new BuildJob( pom.getName(), BuildJob.Type.NORMAL ) };
661         }
662 
663         if ( ( buildJobs == null ) || ( buildJobs.length < 1 ) )
664         {
665             doFailIfNoProjects();
666 
667             getLog().info( "No projects were selected for execution." );
668             return;
669         }
670 
671         handleScriptRunnerWithScriptClassPath();
672 
673         Collection<String> collectedProjects = new LinkedHashSet<String>();
674         for ( BuildJob buildJob : buildJobs )
675         {
676             collectProjects( projectsDirectory, buildJob.getProject(), collectedProjects, true );
677         }
678 
679         File projectsDir = projectsDirectory;
680 
681         if ( cloneProjectsTo != null )
682         {
683             cloneProjects( collectedProjects );
684             projectsDir = cloneProjectsTo;
685         }
686         else
687         {
688             getLog().warn( "Filtering of parent/child POMs is not supported without cloning the projects" );
689         }
690 
691         // First run setup jobs.
692         BuildJob[] setupBuildJobs = null;
693         try
694         {
695             setupBuildJobs = getSetupBuildJobsFromFolders();
696         }
697         catch ( IOException e )
698         {
699             getLog().error( "Failure during scanning of folders.", e );
700             // TODO: Check shouldn't we fail in case of problems?
701         }
702 
703         if ( setupBuildJobs != null )
704         {
705             // Run setup jobs in single thread
706             // mode.
707             //
708             // Some Idea about ordering?
709             getLog().info( "Running Setup Jobs" );
710             runBuilds( projectsDir, setupBuildJobs, 1 );
711         }
712 
713         // Afterwards run all other jobs.
714         BuildJob[] nonSetupBuildJobs = getNonSetupJobs( buildJobs );
715         // We will run the non setup jobs with the configured
716         // parallelThreads number.
717         runBuilds( projectsDir, nonSetupBuildJobs, parallelThreads );
718 
719         writeSummaryFile( nonSetupBuildJobs );
720 
721         processResults( new InvokerSession( nonSetupBuildJobs ) );
722 
723     }
724 
725     /**
726      * This will create the necessary folders for the reports.
727      * 
728      * @throws MojoExecutionException in case of failure during creation of the reports folder.
729      */
730     private void setupReportsFolder()
731         throws MojoExecutionException
732     {
733         // If it exists from previous run...
734         if ( reportsDirectory.exists() )
735         {
736             try
737             {
738                 FileUtils.deleteDirectory( reportsDirectory );
739             }
740             catch ( IOException e )
741             {
742                 throw new MojoExecutionException( "Failure while trying to delete "
743                     + reportsDirectory.getAbsolutePath(), e );
744             }
745         }
746         if ( !reportsDirectory.mkdirs() )
747         {
748             throw new MojoExecutionException( "Failure while creating the " + reportsDirectory.getAbsolutePath() );
749         }
750     }
751 
752     private BuildJob[] getNonSetupJobs( BuildJob[] buildJobs )
753     {
754         List<BuildJob> result = new LinkedList<BuildJob>();
755         for ( int i = 0; i < buildJobs.length; i++ )
756         {
757             if ( !buildJobs[i].getType().equals( BuildJob.Type.SETUP ) )
758             {
759                 result.add( buildJobs[i] );
760             }
761         }
762         BuildJob[] buildNonSetupJobs = result.toArray( new BuildJob[result.size()] );
763         return buildNonSetupJobs;
764     }
765 
766     private void handleScriptRunnerWithScriptClassPath()
767     {
768         final List<String> scriptClassPath;
769         if ( addTestClassPath )
770         {
771             scriptClassPath = new ArrayList<String>( testClassPath );
772             for ( Artifact pluginArtifact : pluginArtifacts )
773             {
774                 scriptClassPath.remove( pluginArtifact.getFile().getAbsolutePath() );
775             }
776         }
777         else
778         {
779             scriptClassPath = null;
780         }
781         scriptRunner = new ScriptRunner( getLog() );
782         scriptRunner.setScriptEncoding( encoding );
783         scriptRunner.setGlobalVariable( "localRepositoryPath", localRepositoryPath );
784         if ( scriptVariables != null )
785         {
786             for ( Entry<String, String> entry : scriptVariables.entrySet() )
787             {
788                 scriptRunner.setGlobalVariable( entry.getKey(), entry.getValue() );
789             }
790         }
791         scriptRunner.setClassPath( scriptClassPath );
792     }
793 
794     private void writeSummaryFile( BuildJob[] buildJobs )
795         throws MojoExecutionException
796     {
797 
798         File summaryReportFile = new File( reportsDirectory, "invoker-summary.txt" );
799 
800         try
801         {
802             Writer writer = new BufferedWriter( new FileWriter( summaryReportFile ) );
803 
804             for ( int i = 0; i < buildJobs.length; i++ )
805             {
806                 BuildJob buildJob = buildJobs[i];
807                 if ( !buildJob.getResult().equals( BuildJob.Result.SUCCESS ) )
808                 {
809                     writer.append( buildJob.getResult() );
810                     writer.append( " [" );
811                     writer.append( buildJob.getProject() );
812                     writer.append( "] " );
813                     if ( buildJob.getFailureMessage() != null )
814                     {
815                         writer.append( " " );
816                         writer.append( buildJob.getFailureMessage() );
817                     }
818                     writer.append( "\n" );
819                 }
820             }
821 
822             writer.close();
823         }
824         catch ( IOException e )
825         {
826             throw new MojoExecutionException( "Failed to write summary report " + summaryReportFile, e );
827         }
828     }
829 
830     protected void doFailIfNoProjects()
831         throws MojoFailureException
832     {
833         // should only be used during run and verify
834     }
835 
836     /**
837      * Processes the results of invoking the build jobs.
838      *
839      * @param invokerSession The session with the build jobs, must not be <code>null</code>.
840      * @throws MojoFailureException If the mojo had failed as a result of invoking the build jobs.
841      * @since 1.4
842      */
843     abstract void processResults( InvokerSession invokerSession )
844         throws MojoFailureException;
845 
846     /**
847      * Creates a new reader for the specified file, using the plugin's {@link #encoding} parameter.
848      *
849      * @param file The file to create a reader for, must not be <code>null</code>.
850      * @return The reader for the file, never <code>null</code>.
851      * @throws java.io.IOException If the specified file was not found or the configured encoding is not supported.
852      */
853     private Reader newReader( File file )
854         throws IOException
855     {
856         if ( StringUtils.isNotEmpty( encoding ) )
857         {
858             return ReaderFactory.newReader( file, encoding );
859         }
860         else
861         {
862             return ReaderFactory.newPlatformReader( file );
863         }
864     }
865 
866     /**
867      * Collects all projects locally reachable from the specified project. The method will as such try to read the POM
868      * and recursively follow its parent/module elements.
869      *
870      * @param projectsDir The base directory of all projects, must not be <code>null</code>.
871      * @param projectPath The relative path of the current project, can denote either the POM or its base directory,
872      *            must not be <code>null</code>.
873      * @param projectPaths The set of already collected projects to add new projects to, must not be <code>null</code>.
874      *            This set will hold the relative paths to either a POM file or a project base directory.
875      * @param included A flag indicating whether the specified project has been explicitly included via the parameter
876      *            {@link #pomIncludes}. Such projects will always be added to the result set even if there is no
877      *            corresponding POM.
878      * @throws org.apache.maven.plugin.MojoExecutionException If the project tree could not be traversed.
879      */
880     private void collectProjects( File projectsDir, String projectPath, Collection<String> projectPaths,
881                                   boolean included )
882         throws MojoExecutionException
883     {
884         projectPath = projectPath.replace( '\\', '/' );
885         File pomFile = new File( projectsDir, projectPath );
886         if ( pomFile.isDirectory() )
887         {
888             pomFile = new File( pomFile, "pom.xml" );
889             if ( !pomFile.exists() )
890             {
891                 if ( included )
892                 {
893                     projectPaths.add( projectPath );
894                 }
895                 return;
896             }
897             if ( !projectPath.endsWith( "/" ) )
898             {
899                 projectPath += '/';
900             }
901             projectPath += "pom.xml";
902         }
903         else if ( !pomFile.isFile() )
904         {
905             return;
906         }
907         if ( !projectPaths.add( projectPath ) )
908         {
909             return;
910         }
911         getLog().debug( "Collecting parent/child projects of " + projectPath );
912 
913         Model model = PomUtils.loadPom( pomFile );
914 
915         try
916         {
917             String projectsRoot = projectsDir.getCanonicalPath();
918             String projectDir = pomFile.getParent();
919 
920             String parentPath = "../pom.xml";
921             if ( model.getParent() != null && StringUtils.isNotEmpty( model.getParent().getRelativePath() ) )
922             {
923                 parentPath = model.getParent().getRelativePath();
924             }
925             String parent = relativizePath( new File( projectDir, parentPath ), projectsRoot );
926             if ( parent != null )
927             {
928                 collectProjects( projectsDir, parent, projectPaths, false );
929             }
930 
931             Collection<String> modulePaths = new LinkedHashSet<String>();
932 
933             modulePaths.addAll( model.getModules() );
934 
935             for ( Profile profile : model.getProfiles() )
936             {
937                 modulePaths.addAll( profile.getModules() );
938             }
939 
940             for ( String modulePath : modulePaths )
941             {
942                 String module = relativizePath( new File( projectDir, modulePath ), projectsRoot );
943                 if ( module != null )
944                 {
945                     collectProjects( projectsDir, module, projectPaths, false );
946                 }
947             }
948         }
949         catch ( IOException e )
950         {
951             throw new MojoExecutionException( "Failed to analyze POM: " + pomFile, e );
952         }
953     }
954 
955     /**
956      * Copies the specified projects to the directory given by {@link #cloneProjectsTo}. A project may either be denoted
957      * by a path to a POM file or merely by a path to a base directory. During cloning, the POM files will be filtered.
958      *
959      * @param projectPaths The paths to the projects to clone, relative to the projects directory, must not be
960      *            <code>null</code> nor contain <code>null</code> elements.
961      * @throws org.apache.maven.plugin.MojoExecutionException If the the projects could not be copied/filtered.
962      */
963     private void cloneProjects( Collection<String> projectPaths )
964         throws MojoExecutionException
965     {
966         if ( !cloneProjectsTo.mkdirs() && cloneClean )
967         {
968             try
969             {
970                 FileUtils.cleanDirectory( cloneProjectsTo );
971             }
972             catch ( IOException e )
973             {
974                 throw new MojoExecutionException( "Could not clean the cloneProjectsTo directory. Reason: "
975                     + e.getMessage(), e );
976             }
977         }
978 
979         // determine project directories to clone
980         Collection<String> dirs = new LinkedHashSet<String>();
981         for ( String projectPath : projectPaths )
982         {
983             if ( !new File( projectsDirectory, projectPath ).isDirectory() )
984             {
985                 projectPath = getParentPath( projectPath );
986             }
987             dirs.add( projectPath );
988         }
989 
990         boolean filter;
991 
992         // clone project directories
993         try
994         {
995             filter = !cloneProjectsTo.getCanonicalFile().equals( projectsDirectory.getCanonicalFile() );
996 
997             List<String> clonedSubpaths = new ArrayList<String>();
998 
999             for ( String subpath : dirs )
1000             {
1001                 // skip this project if its parent directory is also scheduled for cloning
1002                 if ( !".".equals( subpath ) && dirs.contains( getParentPath( subpath ) ) )
1003                 {
1004                     continue;
1005                 }
1006 
1007                 // avoid copying subdirs that are already cloned.
1008                 if ( !alreadyCloned( subpath, clonedSubpaths ) )
1009                 {
1010                     // avoid creating new files that point to dir/.
1011                     if ( ".".equals( subpath ) )
1012                     {
1013                         String cloneSubdir = relativizePath( cloneProjectsTo, projectsDirectory.getCanonicalPath() );
1014 
1015                         // avoid infinite recursion if the cloneTo path is a subdirectory.
1016                         if ( cloneSubdir != null )
1017                         {
1018                             File temp = File.createTempFile( "pre-invocation-clone.", "" );
1019                             temp.delete();
1020                             temp.mkdirs();
1021 
1022                             copyDirectoryStructure( projectsDirectory, temp );
1023 
1024                             FileUtils.deleteDirectory( new File( temp, cloneSubdir ) );
1025 
1026                             copyDirectoryStructure( temp, cloneProjectsTo );
1027                         }
1028                         else
1029                         {
1030                             copyDirectoryStructure( projectsDirectory, cloneProjectsTo );
1031                         }
1032                     }
1033                     else
1034                     {
1035                         File srcDir = new File( projectsDirectory, subpath );
1036                         File dstDir = new File( cloneProjectsTo, subpath );
1037                         copyDirectoryStructure( srcDir, dstDir );
1038                     }
1039 
1040                     clonedSubpaths.add( subpath );
1041                 }
1042             }
1043         }
1044         catch ( IOException e )
1045         {
1046             throw new MojoExecutionException( "Failed to clone projects from: " + projectsDirectory + " to: "
1047                 + cloneProjectsTo + ". Reason: " + e.getMessage(), e );
1048         }
1049 
1050         // filter cloned POMs
1051         if ( filter )
1052         {
1053             for ( String projectPath : projectPaths )
1054             {
1055                 File pomFile = new File( cloneProjectsTo, projectPath );
1056                 if ( pomFile.isFile() )
1057                 {
1058                     buildInterpolatedFile( pomFile, pomFile );
1059                 }
1060 
1061                 // MINVOKER-186
1062                 // The following is a temporary solution to support Maven 3.3.1 (.mvn/extensions.xml) filtering
1063                 // Will be replaced by MINVOKER-117 with general filtering mechanism
1064                 File baseDir = pomFile.getParentFile();
1065                 File mvnDir = new File( baseDir, ".mvn" );
1066                 if ( mvnDir.isDirectory() )
1067                 {
1068                     File extensionsFile = new File( mvnDir, "extensions.xml" );
1069                     if ( extensionsFile.isFile() )
1070                     {
1071                         buildInterpolatedFile( extensionsFile, extensionsFile );
1072                     }
1073                 }
1074                 // END MINVOKER-186
1075             }
1076             filteredPomPrefix = null;
1077         }
1078     }
1079 
1080     /**
1081      * Gets the parent path of the specified relative path.
1082      *
1083      * @param path The relative path whose parent should be retrieved, must not be <code>null</code>.
1084      * @return The parent path or "." if the specified path has no parent, never <code>null</code>.
1085      */
1086     private String getParentPath( String path )
1087     {
1088         int lastSep = Math.max( path.lastIndexOf( '/' ), path.lastIndexOf( '\\' ) );
1089         return ( lastSep < 0 ) ? "." : path.substring( 0, lastSep );
1090     }
1091 
1092     /**
1093      * Copied a directory structure with default exclusions (.svn, CVS, etc)
1094      *
1095      * @param sourceDir The source directory to copy, must not be <code>null</code>.
1096      * @param destDir The target directory to copy to, must not be <code>null</code>.
1097      * @throws java.io.IOException If the directory structure could not be copied.
1098      */
1099     private void copyDirectoryStructure( File sourceDir, File destDir )
1100         throws IOException
1101     {
1102         DirectoryScanner scanner = new DirectoryScanner();
1103         scanner.setBasedir( sourceDir );
1104         if ( !cloneAllFiles )
1105         {
1106             scanner.addDefaultExcludes();
1107         }
1108         scanner.scan();
1109 
1110         /*
1111          * NOTE: Make sure the destination directory is always there (even if empty) to support POM-less ITs.
1112          */
1113         destDir.mkdirs();
1114         // Create all the directories, including any symlinks present in source
1115         FileUtils.mkDirs( sourceDir, scanner.getIncludedDirectories(), destDir );
1116 
1117         for ( String includedFile : scanner.getIncludedFiles() )
1118         {
1119             File sourceFile = new File( sourceDir, includedFile );
1120             File destFile = new File( destDir, includedFile );
1121             FileUtils.copyFile( sourceFile, destFile );
1122 
1123             // ensure clone project must be writable for additional changes
1124             destFile.setWritable( true );
1125         }
1126     }
1127 
1128     /**
1129      * Determines whether the specified sub path has already been cloned, i.e. whether one of its ancestor directories
1130      * was already cloned.
1131      *
1132      * @param subpath The sub path to check, must not be <code>null</code>.
1133      * @param clonedSubpaths The list of already cloned paths, must not be <code>null</code> nor contain
1134      *            <code>null</code> elements.
1135      * @return <code>true</code> if the specified path has already been cloned, <code>false</code> otherwise.
1136      */
1137     static boolean alreadyCloned( String subpath, List<String> clonedSubpaths )
1138     {
1139         for ( String path : clonedSubpaths )
1140         {
1141             if ( ".".equals( path ) || subpath.equals( path ) || subpath.startsWith( path + File.separator ) )
1142             {
1143                 return true;
1144             }
1145         }
1146 
1147         return false;
1148     }
1149 
1150     /**
1151      * Runs the specified build jobs.
1152      *
1153      * @param projectsDir The base directory of all projects, must not be <code>null</code>.
1154      * @param buildJobs The build jobs to run must not be <code>null</code> nor contain <code>null</code> elements.
1155      * @throws org.apache.maven.plugin.MojoExecutionException If any build could not be launched.
1156      */
1157     private void runBuilds( final File projectsDir, BuildJob[] buildJobs, int runWithParallelThreads )
1158         throws MojoExecutionException
1159     {
1160         if ( !localRepositoryPath.exists() )
1161         {
1162             localRepositoryPath.mkdirs();
1163         }
1164 
1165         // -----------------------------------------------
1166         // interpolate settings file
1167         // -----------------------------------------------
1168 
1169         File interpolatedSettingsFile = interpolateSettings( settingsFile );
1170 
1171         final File mergedSettingsFile = mergeSettings( interpolatedSettingsFile );
1172 
1173         if ( mavenHome != null )
1174         {
1175             actualMavenVersion = SelectorUtils.getMavenVersion( mavenHome );
1176         }
1177         else
1178         {
1179             actualMavenVersion = SelectorUtils.getMavenVersion();
1180         }
1181         scriptRunner.setGlobalVariable( "mavenVersion", actualMavenVersion );
1182 
1183         final CharSequence actualJreVersion;
1184         // @todo if ( javaVersions ) ... to be picked up from toolchains
1185         if ( javaHome != null )
1186         {
1187             actualJreVersion = resolveExternalJreVersion();
1188         }
1189         else
1190         {
1191             actualJreVersion = SelectorUtils.getJreVersion();
1192         }
1193 
1194         try
1195         {
1196             if ( runWithParallelThreads > 1 )
1197             {
1198                 getLog().info( "use parallelThreads " + runWithParallelThreads );
1199 
1200                 ExecutorService executorService = Executors.newFixedThreadPool( runWithParallelThreads );
1201                 for ( final BuildJob job : buildJobs )
1202                 {
1203                     executorService.execute( new Runnable()
1204                     {
1205                         public void run()
1206                         {
1207                             try
1208                             {
1209                                 runBuild( projectsDir, job, mergedSettingsFile, javaHome, actualJreVersion );
1210                             }
1211                             catch ( MojoExecutionException e )
1212                             {
1213                                 throw new RuntimeException( e.getMessage(), e );
1214                             }
1215                         }
1216                     } );
1217                 }
1218 
1219                 try
1220                 {
1221                     executorService.shutdown();
1222                     // TODO add a configurable time out
1223                     executorService.awaitTermination( Long.MAX_VALUE, TimeUnit.MILLISECONDS );
1224                 }
1225                 catch ( InterruptedException e )
1226                 {
1227                     throw new MojoExecutionException( e.getMessage(), e );
1228                 }
1229             }
1230             else
1231             {
1232                 for ( BuildJob job : buildJobs )
1233                 {
1234                     runBuild( projectsDir, job, mergedSettingsFile, javaHome, actualJreVersion );
1235                 }
1236             }
1237         }
1238         finally
1239         {
1240             if ( interpolatedSettingsFile != null && cloneProjectsTo == null )
1241             {
1242                 interpolatedSettingsFile.delete();
1243             }
1244             if ( mergedSettingsFile != null && mergedSettingsFile.exists() )
1245             {
1246                 mergedSettingsFile.delete();
1247             }
1248         }
1249     }
1250 
1251     /**
1252      * Interpolate settings.xml file.
1253      * @param settingsFile a settings file
1254      * 
1255      * @return The interpolated settings.xml file.
1256      * @throws MojoExecutionException in case of a problem.
1257      */
1258     private File interpolateSettings( File settingsFile )
1259         throws MojoExecutionException
1260     {
1261         File interpolatedSettingsFile = null;
1262         if ( settingsFile != null )
1263         {
1264             if ( cloneProjectsTo != null )
1265             {
1266                 interpolatedSettingsFile = new File( cloneProjectsTo, "interpolated-" + settingsFile.getName() );
1267             }
1268             else
1269             {
1270                 interpolatedSettingsFile =
1271                     new File( settingsFile.getParentFile(), "interpolated-" + settingsFile.getName() );
1272             }
1273             buildInterpolatedFile( settingsFile, interpolatedSettingsFile );
1274         }
1275         return interpolatedSettingsFile;
1276     }
1277 
1278     /**
1279      * Merge the settings file
1280      * 
1281      * @param interpolatedSettingsFile The interpolated settings file.
1282      * @return The merged settings file.
1283      * @throws MojoExecutionException Fail the build in case the merged settings file can't be created.
1284      */
1285     private File mergeSettings( File interpolatedSettingsFile )
1286         throws MojoExecutionException
1287     {
1288         File mergedSettingsFile;
1289         Settings mergedSettings = this.settings;
1290         if ( mergeUserSettings )
1291         {
1292             if ( interpolatedSettingsFile != null )
1293             {
1294                 // Have to merge the specified settings file (dominant) and the one of the invoking Maven process
1295                 try
1296                 {
1297                     SettingsBuildingRequest request = new DefaultSettingsBuildingRequest();
1298                     request.setGlobalSettingsFile( interpolatedSettingsFile );
1299 
1300                     Settings dominantSettings = settingsBuilder.build( request ).getEffectiveSettings();
1301                     Settings recessiveSettings = cloneSettings();
1302                     SettingsUtils.merge( dominantSettings, recessiveSettings, TrackableBase.USER_LEVEL );
1303 
1304                     mergedSettings = dominantSettings;
1305                     getLog().debug( "Merged specified settings file with settings of invoking process" );
1306                 }
1307                 catch ( SettingsBuildingException e )
1308                 {
1309                     throw new MojoExecutionException( "Could not read specified settings file", e );
1310                 }
1311             }
1312         }
1313 
1314         if ( this.settingsFile != null && !mergeUserSettings )
1315         {
1316             mergedSettingsFile = interpolatedSettingsFile;
1317         }
1318         else
1319         {
1320             try
1321             {
1322                 mergedSettingsFile = writeMergedSettingsFile( mergedSettings );
1323             }
1324             catch ( IOException e )
1325             {
1326                 throw new MojoExecutionException( "Could not create temporary file for invoker settings.xml", e );
1327             }
1328         }
1329         return mergedSettingsFile;
1330     }
1331 
1332     private File writeMergedSettingsFile( Settings mergedSettings )
1333         throws IOException
1334     {
1335         File mergedSettingsFile;
1336         mergedSettingsFile = File.createTempFile( "invoker-settings", ".xml" );
1337 
1338         SettingsXpp3Writer settingsWriter = new SettingsXpp3Writer();
1339 
1340         FileWriter fileWriter = null;
1341         try
1342         {
1343             fileWriter = new FileWriter( mergedSettingsFile );
1344             settingsWriter.write( fileWriter, mergedSettings );
1345             fileWriter.close();
1346             fileWriter = null;
1347         }
1348         finally
1349         {
1350             IOUtil.close( fileWriter );
1351         }
1352 
1353         if ( getLog().isDebugEnabled() )
1354         {
1355             getLog().debug( "Created temporary file for invoker settings.xml: "
1356                 + mergedSettingsFile.getAbsolutePath() );
1357         }
1358         return mergedSettingsFile;
1359     }
1360 
1361     private Settings cloneSettings()
1362     {
1363         Settings recessiveSettings = SettingsUtils.copySettings( this.settings );
1364 
1365         // MINVOKER-133: reset sourceLevelSet
1366         resetSourceLevelSet( recessiveSettings );
1367         for ( org.apache.maven.settings.Mirror mirror : recessiveSettings.getMirrors() )
1368         {
1369             resetSourceLevelSet( mirror );
1370         }
1371         for ( org.apache.maven.settings.Server server : recessiveSettings.getServers() )
1372         {
1373             resetSourceLevelSet( server );
1374         }
1375         for ( org.apache.maven.settings.Proxy proxy : recessiveSettings.getProxies() )
1376         {
1377             resetSourceLevelSet( proxy );
1378         }
1379         for ( org.apache.maven.settings.Profile profile : recessiveSettings.getProfiles() )
1380         {
1381             resetSourceLevelSet( profile );
1382         }
1383 
1384         return recessiveSettings;
1385     }
1386 
1387     private void resetSourceLevelSet( org.apache.maven.settings.TrackableBase trackable )
1388     {
1389         try
1390         {
1391             ReflectionUtils.setVariableValueInObject( trackable, "sourceLevelSet", Boolean.FALSE );
1392             getLog().debug( "sourceLevelSet: "
1393                 + ReflectionUtils.getValueIncludingSuperclasses( "sourceLevelSet", trackable ) );
1394         }
1395         catch ( IllegalAccessException e )
1396         {
1397             // noop
1398         }
1399     }
1400 
1401     private CharSequence resolveExternalJreVersion()
1402     {
1403         Artifact pluginArtifact = mojoExecution.getMojoDescriptor().getPluginDescriptor().getPluginArtifact();
1404         pluginArtifact.getFile();
1405 
1406         Commandline commandLine = new Commandline();
1407         commandLine.setExecutable( new File( javaHome, "bin/java" ).getAbsolutePath() );
1408         commandLine.createArg().setValue( "-cp" );
1409         commandLine.createArg().setFile( pluginArtifact.getFile() );
1410         commandLine.createArg().setValue( SystemPropertyPrinter.class.getName() );
1411         commandLine.createArg().setValue( "java.version" );
1412 
1413         final StringBuilder actualJreVersion = new StringBuilder();
1414         StreamConsumer consumer = new StreamConsumer()
1415         {
1416             public void consumeLine( String line )
1417             {
1418                 actualJreVersion.append( line );
1419             }
1420         };
1421         try
1422         {
1423             CommandLineUtils.executeCommandLine( commandLine, consumer, null );
1424         }
1425         catch ( CommandLineException e )
1426         {
1427             getLog().warn( e.getMessage() );
1428         }
1429         return actualJreVersion;
1430     }
1431 
1432     /**
1433      * Interpolate the pom file.
1434      * 
1435      * @param pomFile The pom file.
1436      * @param basedir The base directory.
1437      * @return interpolated pom file location in case we have interpolated the pom file otherwise the original pom file
1438      *         will be returned.
1439      * @throws MojoExecutionException
1440      */
1441     private File interpolatePomFile( File pomFile, File basedir )
1442         throws MojoExecutionException
1443     {
1444         File interpolatedPomFile = null;
1445         if ( pomFile != null )
1446         {
1447             if ( StringUtils.isNotEmpty( filteredPomPrefix ) )
1448             {
1449                 interpolatedPomFile = new File( basedir, filteredPomPrefix + pomFile.getName() );
1450                 buildInterpolatedFile( pomFile, interpolatedPomFile );
1451             }
1452             else
1453             {
1454                 interpolatedPomFile = pomFile;
1455             }
1456         }
1457         return interpolatedPomFile;
1458     }
1459 
1460     /**
1461      * Runs the specified project.
1462      *
1463      * @param projectsDir The base directory of all projects, must not be <code>null</code>.
1464      * @param buildJob The build job to run, must not be <code>null</code>.
1465      * @param settingsFile The (already interpolated) user settings file for the build, may be <code>null</code> to use
1466      *            the current user settings.
1467      * @throws org.apache.maven.plugin.MojoExecutionException If the project could not be launched.
1468      */
1469     private void runBuild( File projectsDir, BuildJob buildJob, File settingsFile, File actualJavaHome,
1470                            CharSequence actualJreVersion )
1471         throws MojoExecutionException
1472     {
1473         // FIXME: Think about the following code part -- START
1474         File pomFile = new File( projectsDir, buildJob.getProject() );
1475         File basedir;
1476         if ( pomFile.isDirectory() )
1477         {
1478             basedir = pomFile;
1479             pomFile = new File( basedir, "pom.xml" );
1480             if ( !pomFile.exists() )
1481             {
1482                 pomFile = null;
1483             }
1484             else
1485             {
1486                 buildJob.setProject( buildJob.getProject() + File.separator + "pom.xml" );
1487             }
1488         }
1489         else
1490         {
1491             basedir = pomFile.getParentFile();
1492         }
1493 
1494         File interpolatedPomFile = interpolatePomFile( pomFile, basedir );
1495         // FIXME: Think about the following code part -- ^^^^^^^ END
1496 
1497         getLog().info( buffer().a( "Building: " ).strong( buildJob.getProject() ).toString() );
1498 
1499         InvokerProperties invokerProperties = getInvokerProperties( basedir );
1500 
1501         // let's set what details we can
1502         buildJob.setName( invokerProperties.getJobName() );
1503         buildJob.setDescription( invokerProperties.getJobDescription() );
1504 
1505         try
1506         {
1507             int selection = getSelection( invokerProperties, actualJreVersion );
1508             if ( selection == 0 )
1509             {
1510                 long milliseconds = System.currentTimeMillis();
1511                 boolean executed;
1512                 try
1513                 {
1514                     // CHECKSTYLE_OFF: LineLength
1515                     executed =
1516                         runBuild( basedir, interpolatedPomFile, settingsFile, actualJavaHome, invokerProperties );
1517                     // CHECKSTYLE_ON: LineLength
1518                 }
1519                 finally
1520                 {
1521                     milliseconds = System.currentTimeMillis() - milliseconds;
1522                     buildJob.setTime( milliseconds / 1000.0 );
1523                 }
1524 
1525                 if ( executed )
1526                 {
1527                     buildJob.setResult( BuildJob.Result.SUCCESS );
1528 
1529                     if ( !suppressSummaries )
1530                     {
1531                         getLog().info( pad( buildJob ).success( "SUCCESS" ).a( ' ' )
1532                             + formatTime( buildJob.getTime() ) );
1533                     }
1534                 }
1535                 else
1536                 {
1537                     buildJob.setResult( BuildJob.Result.SKIPPED );
1538 
1539                     if ( !suppressSummaries )
1540                     {
1541                         getLog().info( pad( buildJob ).warning( "SKIPPED" ).a( ' ' )
1542                             + formatTime( buildJob.getTime() ) );
1543                     }
1544                 }
1545             }
1546             else
1547             {
1548                 buildJob.setResult( BuildJob.Result.SKIPPED );
1549 
1550                 StringBuilder message = new StringBuilder();
1551                 if ( selection == Selector.SELECTOR_MULTI )
1552                 {
1553                     message.append( "non-matching selectors" );
1554                 }
1555                 else
1556                 {
1557                     if ( ( selection & Selector.SELECTOR_MAVENVERSION ) != 0 )
1558                     {
1559                         message.append( "Maven version" );
1560                     }
1561                     if ( ( selection & Selector.SELECTOR_JREVERSION ) != 0 )
1562                     {
1563                         if ( message.length() > 0 )
1564                         {
1565                             message.append( ", " );
1566                         }
1567                         message.append( "JRE version" );
1568                     }
1569                     if ( ( selection & Selector.SELECTOR_OSFAMILY ) != 0 )
1570                     {
1571                         if ( message.length() > 0 )
1572                         {
1573                             message.append( ", " );
1574                         }
1575                         message.append( "OS" );
1576                     }
1577                 }
1578 
1579                 if ( !suppressSummaries )
1580                 {
1581                     getLog().info( pad( buildJob ).warning( "SKIPPED" ) + " due to " + message.toString() );
1582                 }
1583 
1584                 // Abuse failureMessage, the field in the report which should contain the reason for skipping
1585                 // Consider skipCode + I18N
1586                 buildJob.setFailureMessage( "Skipped due to " + message.toString() );
1587             }
1588         }
1589         catch ( RunErrorException e )
1590         {
1591             buildJob.setResult( BuildJob.Result.ERROR );
1592             buildJob.setFailureMessage( e.getMessage() );
1593 
1594             if ( !suppressSummaries )
1595             {
1596                 getLog().info( pad( buildJob ).failure( "ERROR" ).a( ' ' ) + formatTime( buildJob.getTime() ) );
1597                 getLog().info( "  " + e.getMessage() );
1598             }
1599         }
1600         catch ( RunFailureException e )
1601         {
1602             buildJob.setResult( e.getType() );
1603             buildJob.setFailureMessage( e.getMessage() );
1604 
1605             if ( !suppressSummaries )
1606             {
1607                 getLog().info( pad( buildJob ).failure( "FAILED" ).a( ' ' ) + formatTime( buildJob.getTime() ) );
1608                 getLog().info( "  " + e.getMessage() );
1609             }
1610         }
1611         finally
1612         {
1613             deleteInterpolatedPomFile( interpolatedPomFile );
1614             writeBuildReport( buildJob );
1615         }
1616     }
1617 
1618     private MessageBuilder pad( BuildJob buildJob )
1619     {
1620         MessageBuilder buffer = buffer( 128 );
1621 
1622         buffer.a( "          " );
1623         buffer.a( buildJob.getProject() );
1624 
1625         int l = 10 + buildJob.getProject().length();
1626 
1627         if ( l < RESULT_COLUMN )
1628         {
1629             buffer.a( ' ' );
1630             l++;
1631 
1632             if ( l < RESULT_COLUMN )
1633             {
1634                 for ( int i = RESULT_COLUMN - l; i > 0; i-- )
1635                 {
1636                     buffer.a( '.' );
1637                 }
1638             }
1639         }
1640 
1641         return buffer.a( ' ' );
1642     }
1643 
1644     /**
1645      * Delete the interpolated pom file if it has been created before.
1646      * 
1647      * @param interpolatedPomFile The interpolated pom file.
1648      */
1649     private void deleteInterpolatedPomFile( File interpolatedPomFile )
1650     {
1651         if ( interpolatedPomFile != null && StringUtils.isNotEmpty( filteredPomPrefix ) )
1652         {
1653             interpolatedPomFile.delete();
1654         }
1655     }
1656 
1657     /**
1658      * Determines whether selector conditions of the specified invoker properties match the current environment.
1659      *
1660      * @param invokerProperties The invoker properties to check, must not be <code>null</code>.
1661      * @return <code>0</code> if the job corresponding to the properties should be run, otherwise a bitwise value
1662      *         representing the reason why it should be skipped.
1663      */
1664     private int getSelection( InvokerProperties invokerProperties, CharSequence actualJreVersion )
1665     {
1666         return new Selector( actualMavenVersion, actualJreVersion.toString() ).getSelection( invokerProperties );
1667     }
1668 
1669     /**
1670      * Writes the XML report for the specified build job unless report generation has been disabled.
1671      *
1672      * @param buildJob The build job whose report should be written, must not be <code>null</code>.
1673      * @throws org.apache.maven.plugin.MojoExecutionException If the report could not be written.
1674      */
1675     private void writeBuildReport( BuildJob buildJob )
1676         throws MojoExecutionException
1677     {
1678         if ( disableReports )
1679         {
1680             return;
1681         }
1682 
1683         String safeFileName = buildJob.getProject().replace( '/', '_' ).replace( '\\', '_' ).replace( ' ', '_' );
1684         if ( safeFileName.endsWith( "_pom.xml" ) )
1685         {
1686             safeFileName = safeFileName.substring( 0, safeFileName.length() - "_pom.xml".length() );
1687         }
1688 
1689         File reportFile = new File( reportsDirectory, "BUILD-" + safeFileName + ".xml" );
1690         try
1691         {
1692             FileOutputStream fos = new FileOutputStream( reportFile );
1693             try
1694             {
1695                 Writer osw = new OutputStreamWriter( fos, buildJob.getModelEncoding() );
1696                 BuildJobXpp3Writer writer = new BuildJobXpp3Writer();
1697                 writer.write( osw, buildJob );
1698                 osw.close();
1699             }
1700             finally
1701             {
1702                 fos.close();
1703             }
1704         }
1705         catch ( IOException e )
1706         {
1707             throw new MojoExecutionException( "Failed to write build report " + reportFile, e );
1708         }
1709     }
1710 
1711     /**
1712      * Formats the specified build duration time.
1713      *
1714      * @param seconds The duration of the build.
1715      * @return The formatted time, never <code>null</code>.
1716      */
1717     private String formatTime( double seconds )
1718     {
1719         return secFormat.format( seconds );
1720     }
1721 
1722     /**
1723      * Runs the specified project.
1724      *
1725      * @param basedir The base directory of the project, must not be <code>null</code>.
1726      * @param pomFile The (already interpolated) POM file, may be <code>null</code> for a POM-less Maven invocation.
1727      * @param settingsFile The (already interpolated) user settings file for the build, may be <code>null</code>. Will
1728      *            be merged with the settings file of the invoking Maven process.
1729      * @param invokerProperties The properties to use.
1730      * @return <code>true</code> if the project was launched or <code>false</code> if the selector script indicated that
1731      *         the project should be skipped.
1732      * @throws org.apache.maven.plugin.MojoExecutionException If the project could not be launched.
1733      * @throws org.apache.maven.shared.scriptinterpreter.RunFailureException If either a hook script or the build itself
1734      *             failed.
1735      */
1736     private boolean runBuild( File basedir, File pomFile, File settingsFile, File actualJavaHome,
1737                               InvokerProperties invokerProperties )
1738         throws MojoExecutionException, RunFailureException
1739     {
1740         if ( getLog().isDebugEnabled() && !invokerProperties.getProperties().isEmpty() )
1741         {
1742             Properties props = invokerProperties.getProperties();
1743             getLog().debug( "Using invoker properties:" );
1744             for ( String key : new TreeSet<String>( props.stringPropertyNames() ) )
1745             {
1746                 String value = props.getProperty( key );
1747                 getLog().debug( "  " + key + " = " + value );
1748             }
1749         }
1750 
1751         List<String> goals = getGoals( basedir );
1752 
1753         List<String> profiles = getProfiles( basedir );
1754 
1755         Map<String, Object> context = new LinkedHashMap<String, Object>();
1756 
1757         FileLogger logger = setupBuildLogFile( basedir );
1758         boolean selectorResult = true;
1759         try
1760         {
1761             try
1762             {
1763                 scriptRunner.run( "selector script", basedir, selectorScript, context, logger, BuildJob.Result.SKIPPED,
1764                                   false );
1765             }
1766             catch ( RunErrorException e )
1767             {
1768                 selectorResult = false;
1769                 throw e;
1770             }
1771             catch ( RunFailureException e )
1772             {
1773                 selectorResult = false;
1774                 return false;
1775             }
1776 
1777             scriptRunner.run( "pre-build script", basedir, preBuildHookScript, context, logger,
1778                               BuildJob.Result.FAILURE_PRE_HOOK, false );
1779 
1780             final InvocationRequest request = new DefaultInvocationRequest();
1781 
1782             request.setLocalRepositoryDirectory( localRepositoryPath );
1783 
1784             request.setBatchMode( true );
1785 
1786             request.setShowErrors( showErrors );
1787 
1788             request.setDebug( debug );
1789 
1790             request.setShowVersion( showVersion );
1791 
1792             setupLoggerForBuildJob( logger, request );
1793 
1794             if ( mavenHome != null )
1795             {
1796                 invoker.setMavenHome( mavenHome );
1797                 // FIXME: Should we really take care of M2_HOME?
1798                 request.addShellEnvironment( "M2_HOME", mavenHome.getAbsolutePath() );
1799             }
1800 
1801             if ( mavenExecutable != null )
1802             {
1803                 invoker.setMavenExecutable( new File( mavenExecutable ) );
1804             }
1805 
1806             if ( actualJavaHome != null )
1807             {
1808                 request.setJavaHome( actualJavaHome );
1809             }
1810 
1811             if ( environmentVariables != null )
1812             {
1813                 for ( Map.Entry<String, String> variable : environmentVariables.entrySet() )
1814                 {
1815                     request.addShellEnvironment( variable.getKey(), variable.getValue() );
1816                 }
1817             }
1818 
1819             for ( int invocationIndex = 1;; invocationIndex++ )
1820             {
1821                 if ( invocationIndex > 1 && !invokerProperties.isInvocationDefined( invocationIndex ) )
1822                 {
1823                     break;
1824                 }
1825 
1826                 request.setBaseDirectory( basedir );
1827 
1828                 request.setPomFile( pomFile );
1829 
1830                 request.setGoals( goals );
1831 
1832                 request.setProfiles( profiles );
1833 
1834                 request.setMavenOpts( mavenOpts );
1835 
1836                 request.setOffline( false );
1837 
1838                 String customSettingsFile = invokerProperties.getSettingsFile( invocationIndex );
1839                 if ( customSettingsFile != null )
1840                 {
1841                     File interpolateSettingsFile = interpolateSettings( new File( customSettingsFile ) );
1842                     File mergeSettingsFile = mergeSettings( interpolateSettingsFile );
1843                     
1844                     request.setUserSettingsFile( mergeSettingsFile );
1845                 }
1846                 else
1847                 {
1848                     request.setUserSettingsFile( settingsFile );
1849                 }
1850 
1851                 Properties systemProperties =
1852                     getSystemProperties( basedir, invokerProperties.getSystemPropertiesFile( invocationIndex ) );
1853                 request.setProperties( systemProperties );
1854 
1855                 invokerProperties.configureInvocation( request, invocationIndex );
1856 
1857                 if ( getLog().isDebugEnabled() )
1858                 {
1859                     try
1860                     {
1861                         getLog().debug( "Using MAVEN_OPTS: " + request.getMavenOpts() );
1862                         getLog().debug( "Executing: " + new MavenCommandLineBuilder().build( request ) );
1863                     }
1864                     catch ( CommandLineConfigurationException e )
1865                     {
1866                         getLog().debug( "Failed to display command line: " + e.getMessage() );
1867                     }
1868                 }
1869 
1870                 InvocationResult result;
1871 
1872                 try
1873                 {
1874                     result = invoker.execute( request );
1875                 }
1876                 catch ( final MavenInvocationException e )
1877                 {
1878                     getLog().debug( "Error invoking Maven: " + e.getMessage(), e );
1879                     throw new RunFailureException( "Maven invocation failed. " + e.getMessage(),
1880                                                    BuildJob.Result.FAILURE_BUILD );
1881                 }
1882                 verify( result, invocationIndex, invokerProperties, logger );
1883             }
1884         }
1885         catch ( IOException e )
1886         {
1887             throw new MojoExecutionException( e.getMessage(), e );
1888         }
1889         finally
1890         {
1891             if ( selectorResult )
1892             {
1893                 runPostBuildHook( basedir, context, logger );
1894             }
1895             if ( logger != null )
1896             {
1897                 logger.close();
1898             }
1899         }
1900         return true;
1901     }
1902 
1903     private void runPostBuildHook( File basedir, Map<String, Object> context, FileLogger logger )
1904         throws MojoExecutionException, RunFailureException
1905     {
1906         try
1907         {
1908             scriptRunner.run( "post-build script", basedir, postBuildHookScript, context, logger,
1909                               BuildJob.Result.FAILURE_POST_HOOK, true );
1910         }
1911         catch ( IOException e )
1912         {
1913             throw new MojoExecutionException( e.getMessage(), e );
1914         }
1915     }
1916     private void setupLoggerForBuildJob( FileLogger logger, final InvocationRequest request )
1917     {
1918         if ( logger != null )
1919         {
1920             request.setErrorHandler( logger );
1921 
1922             request.setOutputHandler( logger );
1923         }
1924     }
1925 
1926     /**
1927      * Initializes the build logger for the specified project. This will write the logging information into
1928      * {@code build.log}.
1929      *
1930      * @param basedir The base directory of the project, must not be <code>null</code>.
1931      * @return The build logger or <code>null</code> if logging has been disabled.
1932      * @throws org.apache.maven.plugin.MojoExecutionException If the log file could not be created.
1933      */
1934     private FileLogger setupBuildLogFile( File basedir )
1935         throws MojoExecutionException
1936     {
1937         FileLogger logger = null;
1938 
1939         if ( !noLog )
1940         {
1941             File outputLog = new File( basedir, "build.log" );
1942             try
1943             {
1944                 if ( streamLogs )
1945                 {
1946                     logger = new FileLogger( outputLog, getLog() );
1947                 }
1948                 else
1949                 {
1950                     logger = new FileLogger( outputLog );
1951                 }
1952 
1953                 getLog().debug( "Build log initialized in: " + outputLog );
1954             }
1955             catch ( IOException e )
1956             {
1957                 throw new MojoExecutionException( "Error initializing build logfile in: " + outputLog, e );
1958             }
1959         }
1960 
1961         return logger;
1962     }
1963 
1964     /**
1965      * Gets the system properties to use for the specified project.
1966      *
1967      * @param basedir The base directory of the project, must not be <code>null</code>.
1968      * @param filename The filename to the properties file to load, may be <code>null</code> to use the default path
1969      *            given by {@link #testPropertiesFile}.
1970      * @return The system properties to use, may be empty but never <code>null</code>.
1971      * @throws org.apache.maven.plugin.MojoExecutionException If the properties file exists but could not be read.
1972      */
1973     private Properties getSystemProperties( final File basedir, final String filename )
1974         throws MojoExecutionException
1975     {
1976         Properties collectedTestProperties = new Properties();
1977 
1978         if ( properties != null )
1979         {
1980             // MINVOKER-118: property can have empty value, which is not accepted by collectedTestProperties
1981             for ( Map.Entry<String, String> entry : properties.entrySet() )
1982             {
1983                 if ( entry.getValue() != null )
1984                 {
1985                     collectedTestProperties.put( entry.getKey(), entry.getValue() );
1986                 }
1987             }
1988         }
1989 
1990         File propertiesFile = null;
1991         if ( filename != null )
1992         {
1993             propertiesFile = new File( basedir, filename );
1994         }
1995         else if ( testPropertiesFile != null )
1996         {
1997             propertiesFile = new File( basedir, testPropertiesFile );
1998         }
1999 
2000         if ( propertiesFile != null && propertiesFile.isFile() )
2001         {
2002             InputStream fin = null;
2003             try
2004             {
2005                 fin = new FileInputStream( propertiesFile );
2006 
2007                 Properties loadedProperties = new Properties();
2008                 loadedProperties.load( fin );
2009                 fin.close();
2010                 fin = null;
2011                 collectedTestProperties.putAll( loadedProperties );
2012             }
2013             catch ( IOException e )
2014             {
2015                 throw new MojoExecutionException( "Error reading system properties from " + propertiesFile );
2016             }
2017             finally
2018             {
2019                 IOUtil.close( fin );
2020             }
2021         }
2022 
2023         return collectedTestProperties;
2024     }
2025 
2026     /**
2027      * Verifies the invocation result.
2028      *
2029      * @param result The invocation result to check, must not be <code>null</code>.
2030      * @param invocationIndex The index of the invocation for which to check the exit code, must not be negative.
2031      * @param invokerProperties The invoker properties used to check the exit code, must not be <code>null</code>.
2032      * @param logger The build logger, may be <code>null</code> if logging is disabled.
2033      * @throws org.apache.maven.shared.scriptinterpreter.RunFailureException If the invocation result indicates a build
2034      *             failure.
2035      */
2036     private void verify( InvocationResult result, int invocationIndex, InvokerProperties invokerProperties,
2037                          FileLogger logger )
2038         throws RunFailureException
2039     {
2040         if ( result.getExecutionException() != null )
2041         {
2042             throw new RunFailureException( "The Maven invocation failed. "
2043                 + result.getExecutionException().getMessage(), BuildJob.Result.ERROR );
2044         }
2045         else if ( !invokerProperties.isExpectedResult( result.getExitCode(), invocationIndex ) )
2046         {
2047             StringBuilder buffer = new StringBuilder( 256 );
2048             buffer.append( "The build exited with code " ).append( result.getExitCode() ).append( ". " );
2049             if ( logger != null )
2050             {
2051                 buffer.append( "See " );
2052                 buffer.append( logger.getOutputFile().getAbsolutePath() );
2053                 buffer.append( " for details." );
2054             }
2055             else
2056             {
2057                 buffer.append( "See console output for details." );
2058             }
2059             throw new RunFailureException( buffer.toString(), BuildJob.Result.FAILURE_BUILD );
2060         }
2061     }
2062 
2063     /**
2064      * Gets the goal list for the specified project.
2065      *
2066      * @param basedir The base directory of the project, must not be <code>null</code>.
2067      * @return The list of goals to run when building the project, may be empty but never <code>null</code>.
2068      * @throws org.apache.maven.plugin.MojoExecutionException If the profile file could not be read.
2069      */
2070     List<String> getGoals( final File basedir )
2071         throws MojoExecutionException
2072     {
2073         try
2074         {
2075             // FIXME: Currently we have null for goalsFile which has been removed.
2076             // This might mean we can remove getGoals() at all ? Check this.
2077             return getTokens( basedir, null, goals );
2078         }
2079         catch ( IOException e )
2080         {
2081             throw new MojoExecutionException( "error reading goals", e );
2082         }
2083     }
2084 
2085     /**
2086      * Gets the profile list for the specified project.
2087      *
2088      * @param basedir The base directory of the project, must not be <code>null</code>.
2089      * @return The list of profiles to activate when building the project, may be empty but never <code>null</code>.
2090      * @throws org.apache.maven.plugin.MojoExecutionException If the profile file could not be read.
2091      */
2092     List<String> getProfiles( File basedir )
2093         throws MojoExecutionException
2094     {
2095         try
2096         {
2097             return getTokens( basedir, null, profiles );
2098         }
2099         catch ( IOException e )
2100         {
2101             throw new MojoExecutionException( "error reading profiles", e );
2102         }
2103     }
2104 
2105     private List<String> calculateExcludes()
2106         throws IOException
2107     {
2108         List<String> excludes =
2109             ( pomExcludes != null ) ? new ArrayList<String>( pomExcludes ) : new ArrayList<String>();
2110         if ( this.settingsFile != null )
2111         {
2112             String exclude = relativizePath( this.settingsFile, projectsDirectory.getCanonicalPath() );
2113             if ( exclude != null )
2114             {
2115                 excludes.add( exclude.replace( '\\', '/' ) );
2116                 getLog().debug( "Automatically excluded " + exclude + " from project scanning" );
2117             }
2118         }
2119         return excludes;
2120 
2121     }
2122 
2123     /**
2124      * @return The list of setupUp jobs.
2125      * @throws IOException
2126      * @see {@link #setupIncludes}
2127      */
2128     private BuildJob[] getSetupBuildJobsFromFolders()
2129         throws IOException
2130     {
2131         List<String> excludes = calculateExcludes();
2132 
2133         BuildJob[] setupPoms = scanProjectsDirectory( setupIncludes, excludes, BuildJob.Type.SETUP );
2134         if ( getLog().isDebugEnabled() )
2135         {
2136             getLog().debug( "Setup projects: " + Arrays.asList( setupPoms ) );
2137         }
2138 
2139         return setupPoms;
2140     }
2141 
2142     /**
2143      * Gets the build jobs that should be processed. Note that the order of the returned build jobs is significant.
2144      *
2145      * @return The build jobs to process, may be empty but never <code>null</code>.
2146      * @throws java.io.IOException If the projects directory could not be scanned.
2147      */
2148     BuildJob[] getBuildJobs()
2149         throws IOException
2150     {
2151         BuildJob[] buildJobs;
2152 
2153         if ( invokerTest == null )
2154         {
2155             List<String> excludes = calculateExcludes();
2156 
2157             BuildJob[] setupPoms = scanProjectsDirectory( setupIncludes, excludes, BuildJob.Type.SETUP );
2158             if ( getLog().isDebugEnabled() )
2159             {
2160                 getLog().debug( "Setup projects: " + Arrays.asList( setupPoms ) );
2161             }
2162 
2163             BuildJob[] normalPoms = scanProjectsDirectory( pomIncludes, excludes, BuildJob.Type.NORMAL );
2164 
2165             Map<String, BuildJob> uniquePoms = new LinkedHashMap<String, BuildJob>();
2166             for ( BuildJob setupPom : setupPoms )
2167             {
2168                 uniquePoms.put( setupPom.getProject(), setupPom );
2169             }
2170             for ( BuildJob normalPom : normalPoms )
2171             {
2172                 if ( !uniquePoms.containsKey( normalPom.getProject() ) )
2173                 {
2174                     uniquePoms.put( normalPom.getProject(), normalPom );
2175                 }
2176             }
2177 
2178             buildJobs = uniquePoms.values().toArray( new BuildJob[uniquePoms.size()] );
2179         }
2180         else
2181         {
2182             String[] testRegexes = StringUtils.split( invokerTest, "," );
2183             List<String> includes = new ArrayList<String>( testRegexes.length );
2184             List<String> excludes = new ArrayList<String>();
2185 
2186             for ( String regex : testRegexes )
2187             {
2188                 // user just use -Dinvoker.test=MWAR191,MNG111 to use a directory thats the end is not pom.xml
2189                 if ( regex.startsWith( "!" ) )
2190                 {
2191                     excludes.add( regex.substring( 1 ) );
2192                 }
2193                 else
2194                 {
2195                     includes.add( regex );
2196                 }
2197             }
2198 
2199             // it would be nice if we could figure out what types these are... but perhaps
2200             // not necessary for the -Dinvoker.test=xxx t
2201             buildJobs = scanProjectsDirectory( includes, excludes, BuildJob.Type.DIRECT );
2202         }
2203 
2204         relativizeProjectPaths( buildJobs );
2205 
2206         return buildJobs;
2207     }
2208 
2209     /**
2210      * Scans the projects directory for projects to build. Both (POM) files and mere directories will be matched by the
2211      * scanner patterns. If the patterns match a directory which contains a file named "pom.xml", the results will
2212      * include the path to this file rather than the directory path in order to avoid duplicate invocations of the same
2213      * project.
2214      *
2215      * @param includes The include patterns for the scanner, may be <code>null</code>.
2216      * @param excludes The exclude patterns for the scanner, may be <code>null</code> to exclude nothing.
2217      * @param type The type to assign to the resulting build jobs, must not be <code>null</code>.
2218      * @return The build jobs matching the patterns, never <code>null</code>.
2219      * @throws java.io.IOException If the project directory could not be scanned.
2220      */
2221     private BuildJob[] scanProjectsDirectory( List<String> includes, List<String> excludes, String type )
2222         throws IOException
2223     {
2224         if ( !projectsDirectory.isDirectory() )
2225         {
2226             return new BuildJob[0];
2227         }
2228 
2229         DirectoryScanner scanner = new DirectoryScanner();
2230         scanner.setBasedir( projectsDirectory.getCanonicalFile() );
2231         scanner.setFollowSymlinks( false );
2232         if ( includes != null )
2233         {
2234             scanner.setIncludes( includes.toArray( new String[includes.size()] ) );
2235         }
2236         if ( excludes != null )
2237         {
2238             scanner.setExcludes( excludes.toArray( new String[excludes.size()] ) );
2239         }
2240         scanner.addDefaultExcludes();
2241         scanner.scan();
2242 
2243         Map<String, BuildJob> matches = new LinkedHashMap<String, BuildJob>();
2244 
2245         for ( String includedFile : scanner.getIncludedFiles() )
2246         {
2247             matches.put( includedFile, new BuildJob( includedFile, type ) );
2248         }
2249 
2250         for ( String includedDir : scanner.getIncludedDirectories() )
2251         {
2252             String includedFile = includedDir + File.separatorChar + "pom.xml";
2253             if ( new File( scanner.getBasedir(), includedFile ).isFile() )
2254             {
2255                 matches.put( includedFile, new BuildJob( includedFile, type ) );
2256             }
2257             else
2258             {
2259                 matches.put( includedDir, new BuildJob( includedDir, type ) );
2260             }
2261         }
2262 
2263         return matches.values().toArray( new BuildJob[matches.size()] );
2264     }
2265 
2266     /**
2267      * Relativizes the project paths of the specified build jobs against the directory specified by
2268      * {@link #projectsDirectory} (if possible). If a project path does not denote a sub path of the projects directory,
2269      * it is returned as is.
2270      *
2271      * @param buildJobs The build jobs whose project paths should be relativized, must not be <code>null</code> nor
2272      *            contain <code>null</code> elements.
2273      * @throws java.io.IOException If any path could not be relativized.
2274      */
2275     private void relativizeProjectPaths( BuildJob[] buildJobs )
2276         throws IOException
2277     {
2278         String projectsDirPath = projectsDirectory.getCanonicalPath();
2279 
2280         for ( BuildJob buildJob : buildJobs )
2281         {
2282             String projectPath = buildJob.getProject();
2283 
2284             File file = new File( projectPath );
2285 
2286             if ( !file.isAbsolute() )
2287             {
2288                 file = new File( projectsDirectory, projectPath );
2289             }
2290 
2291             String relativizedPath = relativizePath( file, projectsDirPath );
2292 
2293             if ( relativizedPath == null )
2294             {
2295                 relativizedPath = projectPath;
2296             }
2297 
2298             buildJob.setProject( relativizedPath );
2299         }
2300     }
2301 
2302     /**
2303      * Relativizes the specified path against the given base directory. Besides relativization, the returned path will
2304      * also be normalized, e.g. directory references like ".." will be removed.
2305      *
2306      * @param path The path to relativize, must not be <code>null</code>.
2307      * @param basedir The (canonical path of the) base directory to relativize against, must not be <code>null</code>.
2308      * @return The relative path in normal form or <code>null</code> if the input path does not denote a sub path of the
2309      *         base directory.
2310      * @throws java.io.IOException If the path could not be relativized.
2311      */
2312     private String relativizePath( File path, String basedir )
2313         throws IOException
2314     {
2315         String relativizedPath = path.getCanonicalPath();
2316 
2317         if ( relativizedPath.startsWith( basedir ) )
2318         {
2319             relativizedPath = relativizedPath.substring( basedir.length() );
2320             if ( relativizedPath.startsWith( File.separator ) )
2321             {
2322                 relativizedPath = relativizedPath.substring( File.separator.length() );
2323             }
2324 
2325             return relativizedPath;
2326         }
2327         else
2328         {
2329             return null;
2330         }
2331     }
2332 
2333     /**
2334      * Returns the map-based value source used to interpolate POMs and other stuff.
2335      *
2336      * @param escapeXml {@code true}, to escape any XML special characters in the property values; {@code false}, to not
2337      * escape any property values.
2338      *
2339      * @return The map-based value source for interpolation, never <code>null</code>.
2340      */
2341     private Map<String, Object> getInterpolationValueSource( final boolean escapeXml )
2342     {
2343         Map<String, Object> props = new HashMap<String, Object>();
2344 
2345         if ( filterProperties != null )
2346         {
2347             props.putAll( filterProperties );
2348         }
2349         props.put( "basedir", this.project.getBasedir().getAbsolutePath() );
2350         props.put( "baseurl", toUrl( this.project.getBasedir().getAbsolutePath() ) );
2351         if ( settings.getLocalRepository() != null )
2352         {
2353             props.put( "localRepository", settings.getLocalRepository() );
2354             props.put( "localRepositoryUrl", toUrl( settings.getLocalRepository() ) );
2355         }
2356 
2357         return new CompositeMap( this.project, props, escapeXml );
2358     }
2359 
2360     /**
2361      * Converts the specified filesystem path to a URL. The resulting URL has no trailing slash regardless whether the
2362      * path denotes a file or a directory.
2363      *
2364      * @param filename The filesystem path to convert, must not be <code>null</code>.
2365      * @return The <code>file:</code> URL for the specified path, never <code>null</code>.
2366      */
2367     private static String toUrl( String filename )
2368     {
2369         /*
2370          * NOTE: Maven fails to properly handle percent-encoded "file:" URLs (WAGON-111) so don't use File.toURI() here
2371          * as-is but use the decoded path component in the URL.
2372          */
2373         String url = "file://" + new File( filename ).toURI().getPath();
2374         if ( url.endsWith( "/" ) )
2375         {
2376             url = url.substring( 0, url.length() - 1 );
2377         }
2378         return url;
2379     }
2380 
2381     /**
2382      * Gets goal/profile names for the specified project, either directly from the plugin configuration or from an
2383      * external token file.
2384      *
2385      * @param basedir The base directory of the test project, must not be <code>null</code>.
2386      * @param filename The (simple) name of an optional file in the project base directory from which to read
2387      *            goals/profiles, may be <code>null</code>.
2388      * @param defaultTokens The list of tokens to return in case the specified token file does not exist, may be
2389      *            <code>null</code>.
2390      * @return The list of goal/profile names, may be empty but never <code>null</code>.
2391      * @throws java.io.IOException If the token file exists but could not be parsed.
2392      */
2393     private List<String> getTokens( File basedir, String filename, List<String> defaultTokens )
2394         throws IOException
2395     {
2396         List<String> tokens = ( defaultTokens != null ) ? defaultTokens : new ArrayList<String>();
2397 
2398         if ( StringUtils.isNotEmpty( filename ) )
2399         {
2400             File tokenFile = new File( basedir, filename );
2401 
2402             if ( tokenFile.exists() )
2403             {
2404                 tokens = readTokens( tokenFile );
2405             }
2406         }
2407 
2408         return tokens;
2409     }
2410 
2411     /**
2412      * Reads the tokens from the specified file. Tokens are separated either by line terminators or commas. During
2413      * parsing, the file contents will be interpolated.
2414      *
2415      * @param tokenFile The file to read the tokens from, must not be <code>null</code>.
2416      * @return The list of tokens, may be empty but never <code>null</code>.
2417      * @throws java.io.IOException If the token file could not be read.
2418      */
2419     private List<String> readTokens( final File tokenFile )
2420         throws IOException
2421     {
2422         List<String> result = new ArrayList<String>();
2423 
2424         BufferedReader reader = null;
2425         try
2426         {
2427             Map<String, Object> composite = getInterpolationValueSource( false );
2428             reader = new BufferedReader( new InterpolationFilterReader( newReader( tokenFile ), composite ) );
2429 
2430             for ( String line = reader.readLine(); line != null; line = reader.readLine() )
2431             {
2432                 result.addAll( collectListFromCSV( line ) );
2433             }
2434 
2435             reader.close();
2436             reader = null;
2437         }
2438         finally
2439         {
2440             IOUtil.close( reader );
2441         }
2442 
2443         return result;
2444     }
2445 
2446     /**
2447      * Gets a list of comma separated tokens from the specified line.
2448      *
2449      * @param csv The line with comma separated tokens, may be <code>null</code>.
2450      * @return The list of tokens from the line, may be empty but never <code>null</code>.
2451      */
2452     private List<String> collectListFromCSV( final String csv )
2453     {
2454         final List<String> result = new ArrayList<String>();
2455 
2456         if ( ( csv != null ) && ( csv.trim().length() > 0 ) )
2457         {
2458             final StringTokenizer st = new StringTokenizer( csv, "," );
2459 
2460             while ( st.hasMoreTokens() )
2461             {
2462                 result.add( st.nextToken().trim() );
2463             }
2464         }
2465 
2466         return result;
2467     }
2468 
2469     /**
2470      * Interpolates the specified POM/settings file to a temporary file. The destination file may be same as the input
2471      * file, i.e. interpolation can be performed in-place.
2472      * <p>
2473      * <b>Note:</b>This methods expects the file to be a XML file and applies special XML escaping during interpolation.
2474      * </p>
2475      *
2476      * @param originalFile The XML file to interpolate, must not be <code>null</code>.
2477      * @param interpolatedFile The target file to write the interpolated contents of the original file to, must not be
2478      * <code>null</code>.
2479      *
2480      * @throws org.apache.maven.plugin.MojoExecutionException If the target file could not be created.
2481      */
2482     void buildInterpolatedFile( File originalFile, File interpolatedFile )
2483         throws MojoExecutionException
2484     {
2485         getLog().debug( "Interpolate " + originalFile.getPath() + " to " + interpolatedFile.getPath() );
2486 
2487         try
2488         {
2489             String xml;
2490 
2491             Reader reader = null;
2492             try
2493             {
2494                 // interpolation with token @...@
2495                 Map<String, Object> composite = getInterpolationValueSource( true );
2496                 reader =
2497                     new InterpolationFilterReader( ReaderFactory.newXmlReader( originalFile ), composite, "@", "@" );
2498 
2499                 xml = IOUtil.toString( reader );
2500 
2501                 reader.close();
2502                 reader = null;
2503             }
2504             finally
2505             {
2506                 IOUtil.close( reader );
2507             }
2508 
2509             Writer writer = null;
2510             try
2511             {
2512                 interpolatedFile.getParentFile().mkdirs();
2513                 writer = WriterFactory.newXmlWriter( interpolatedFile );
2514                 writer.write( xml );
2515                 writer.close();
2516                 writer = null;
2517             }
2518             finally
2519             {
2520                 IOUtil.close( writer );
2521             }
2522         }
2523         catch ( IOException e )
2524         {
2525             throw new MojoExecutionException( "Failed to interpolate file " + originalFile.getPath(), e );
2526         }
2527     }
2528 
2529     /**
2530      * Gets the (interpolated) invoker properties for an integration test.
2531      *
2532      * @param projectDirectory The base directory of the IT project, must not be <code>null</code>.
2533      * @return The invoker properties, may be empty but never <code>null</code>.
2534      * @throws org.apache.maven.plugin.MojoExecutionException If an I/O error occurred during reading the properties.
2535      */
2536     private InvokerProperties getInvokerProperties( final File projectDirectory )
2537         throws MojoExecutionException
2538     {
2539         Properties props = new Properties();
2540         if ( invokerPropertiesFile != null )
2541         {
2542             File propertiesFile = new File( projectDirectory, invokerPropertiesFile );
2543             if ( propertiesFile.isFile() )
2544             {
2545                 InputStream in = null;
2546                 try
2547                 {
2548                     in = new FileInputStream( propertiesFile );
2549                     props.load( in );
2550                     in.close();
2551                     in = null;
2552                 }
2553                 catch ( IOException e )
2554                 {
2555                     throw new MojoExecutionException( "Failed to read invoker properties: " + propertiesFile, e );
2556                 }
2557                 finally
2558                 {
2559                     IOUtil.close( in );
2560                 }
2561             }
2562 
2563             Interpolator interpolator = new RegexBasedInterpolator();
2564             interpolator.addValueSource( new MapBasedValueSource( getInterpolationValueSource( false ) ) );
2565             // CHECKSTYLE_OFF: LineLength
2566             for ( String key : props.stringPropertyNames() )
2567             {
2568                 String value = props.getProperty( key );
2569                 try
2570                 {
2571                     value = interpolator.interpolate( value, "" );
2572                 }
2573                 catch ( InterpolationException e )
2574                 {
2575                     throw new MojoExecutionException( "Failed to interpolate invoker properties: " + propertiesFile,
2576                                                       e );
2577                 }
2578                 props.setProperty( key, value );
2579             }
2580             // CHECKSTYLE_ON: LineLength
2581         }
2582         return new InvokerProperties( props );
2583     }
2584 
2585     protected boolean isParallelRun()
2586     {
2587         return parallelThreads > 1;
2588     }
2589 
2590 }