View Javadoc

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