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