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