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