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