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