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