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