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