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