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