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