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