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