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