1 package org.apache.maven.plugin.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.model.Model;
24 import org.apache.maven.model.Profile;
25 import org.apache.maven.plugin.AbstractMojo;
26 import org.apache.maven.plugin.MojoExecutionException;
27 import org.apache.maven.plugin.MojoFailureException;
28 import org.apache.maven.plugin.invoker.model.BuildJob;
29 import org.apache.maven.plugin.invoker.model.io.xpp3.BuildJobXpp3Writer;
30 import org.apache.maven.plugin.registry.TrackableBase;
31 import org.apache.maven.project.MavenProject;
32 import org.apache.maven.settings.Settings;
33 import org.apache.maven.settings.SettingsUtils;
34 import org.apache.maven.settings.io.xpp3.SettingsXpp3Reader;
35 import org.apache.maven.settings.io.xpp3.SettingsXpp3Writer;
36 import org.apache.maven.shared.invoker.CommandLineConfigurationException;
37 import org.apache.maven.shared.invoker.DefaultInvocationRequest;
38 import org.apache.maven.shared.invoker.InvocationRequest;
39 import org.apache.maven.shared.invoker.InvocationResult;
40 import org.apache.maven.shared.invoker.Invoker;
41 import org.apache.maven.shared.invoker.MavenCommandLineBuilder;
42 import org.apache.maven.shared.invoker.MavenInvocationException;
43 import org.apache.maven.shared.scriptinterpreter.RunErrorException;
44 import org.apache.maven.shared.scriptinterpreter.RunFailureException;
45 import org.apache.maven.shared.scriptinterpreter.ScriptRunner;
46 import org.codehaus.plexus.interpolation.InterpolationException;
47 import org.codehaus.plexus.interpolation.Interpolator;
48 import org.codehaus.plexus.interpolation.MapBasedValueSource;
49 import org.codehaus.plexus.interpolation.RegexBasedInterpolator;
50 import org.codehaus.plexus.util.DirectoryScanner;
51 import org.codehaus.plexus.util.FileUtils;
52 import org.codehaus.plexus.util.IOUtil;
53 import org.codehaus.plexus.util.InterpolationFilterReader;
54 import org.codehaus.plexus.util.ReaderFactory;
55 import org.codehaus.plexus.util.StringUtils;
56 import org.codehaus.plexus.util.WriterFactory;
57 import org.codehaus.plexus.util.xml.XmlStreamReader;
58 import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
59
60 import java.io.BufferedReader;
61 import java.io.File;
62 import java.io.FileInputStream;
63 import java.io.FileOutputStream;
64 import java.io.FileWriter;
65 import java.io.IOException;
66 import java.io.InputStream;
67 import java.io.OutputStreamWriter;
68 import java.io.Reader;
69 import java.io.Writer;
70 import java.text.DecimalFormat;
71 import java.text.DecimalFormatSymbols;
72 import java.util.ArrayList;
73 import java.util.Arrays;
74 import java.util.Collection;
75 import java.util.Collections;
76 import java.util.HashMap;
77 import java.util.LinkedHashMap;
78 import java.util.LinkedHashSet;
79 import java.util.List;
80 import java.util.Locale;
81 import java.util.Map;
82 import java.util.Properties;
83 import java.util.Set;
84 import java.util.StringTokenizer;
85 import java.util.TreeSet;
86 import java.util.concurrent.ExecutorService;
87 import java.util.concurrent.Executors;
88 import java.util.concurrent.TimeUnit;
89
90 /**
91 * Provides common code for mojos invoking sub builds.
92 *
93 * @author Stephen Connolly
94 * @since 15-Aug-2009 09:09:29
95 */
96 public abstract class AbstractInvokerMojo
97 extends AbstractMojo
98 {
99
100 /**
101 * Flag used to suppress certain invocations. This is useful in tailoring the build using profiles.
102 *
103 * @parameter expression="${invoker.skip}" default-value="false"
104 * @since 1.1
105 */
106 private boolean skipInvocation;
107
108 /**
109 * Flag used to suppress the summary output notifying of successes and failures. If set to <code>true</code>, the
110 * only indication of the build's success or failure will be the effect it has on the main build (if it fails, the
111 * main build should fail as well). If {@link #streamLogs} is enabled, the sub-build summary will also provide an
112 * indication.
113 *
114 * @parameter default-value="false"
115 */
116 protected boolean suppressSummaries;
117
118 /**
119 * Flag used to determine whether the build logs should be output to the normal mojo log.
120 *
121 * @parameter expression="${invoker.streamLogs}" default-value="false"
122 */
123 private boolean streamLogs;
124
125 /**
126 * The local repository for caching artifacts. It is strongly recommended to specify a path to an isolated
127 * repository like <code>${project.build.directory}/it-repo</code>. Otherwise, your ordinary local repository will
128 * be used, potentially soiling it with broken artifacts.
129 *
130 * @parameter expression="${invoker.localRepositoryPath}" default-value="${settings.localRepository}"
131 */
132 private File localRepositoryPath;
133
134 /**
135 * Directory to search for integration tests.
136 *
137 * @parameter expression="${invoker.projectsDirectory}" default-value="${basedir}/src/it/"
138 */
139 private File projectsDirectory;
140
141 /**
142 * Base directory where all build reports are written to.
143 * Every execution of an integration test will produce an XML file which contains the information
144 * about success or failure of that particular build job. The format of the resulting XML
145 * file is documented in the given <a href="./build-job.html">build-job</a> reference.
146 *
147 * @parameter expression="${invoker.reportsDirectory}" default-value="${project.build.directory}/invoker-reports"
148 * @since 1.4
149 */
150 private File reportsDirectory;
151
152 /**
153 * A flag to disable the generation of build reports.
154 *
155 * @parameter expression="${invoker.disableReports}" default-value="false"
156 * @since 1.4
157 */
158 private boolean disableReports;
159
160 /**
161 * Directory to which projects should be cloned prior to execution. If not specified, each integration test will be
162 * run in the directory in which the corresponding IT POM was found. In this case, you most likely want to configure
163 * your SCM to ignore <code>target</code> and <code>build.log</code> in the test's base directory.
164 *
165 * @parameter
166 * @since 1.1
167 */
168 private File cloneProjectsTo;
169
170 /**
171 * Some files are normally excluded when copying the IT projects from the directory specified by the parameter
172 * projectsDirectory to the directory given by cloneProjectsTo (e.g. <code>.svn</code>, <code>CVS</code>,
173 * <code>*~</code>, etc). Setting this parameter to <code>true</code> will cause all files to be copied to the
174 * cloneProjectsTo directory.
175 *
176 * @parameter default-value="false"
177 * @since 1.2
178 */
179 private boolean cloneAllFiles;
180
181 /**
182 * Ensure the {@link #cloneProjectsTo} directory is not polluted with files from earlier invoker runs.
183 *
184 * @parameter default-value="false"
185 * @since 1.6
186 */
187 private boolean cloneClean;
188
189 /**
190 * A single POM to build, skipping any scanning parameters and behavior.
191 *
192 * @parameter expression="${invoker.pom}"
193 */
194 private File pom;
195
196 /**
197 * Include patterns for searching the integration test directory for projects. This parameter is meant to be set
198 * from the POM. If this parameter is not set, the plugin will search for all <code>pom.xml</code> files one
199 * directory below {@link #projectsDirectory} (i.e. <code>*/pom.xml</code>).<br>
200 * <br>
201 * Starting with version 1.3, mere directories can also be matched by these patterns. For example, the include
202 * pattern <code>*</code> will run Maven builds on all immediate sub directories of {@link #projectsDirectory},
203 * regardless if they contain a <code>pom.xml</code>. This allows to perform builds that need/should not depend on
204 * the existence of a POM.
205 *
206 * @parameter
207 */
208 private List<String> pomIncludes = Collections.singletonList( "*/pom.xml" );
209
210 /**
211 * Exclude patterns for searching the integration test directory. This parameter is meant to be set from the POM. By
212 * default, no POM files are excluded. For the convenience of using an include pattern like <code>*</code>, the
213 * custom settings file specified by the parameter {@link #settingsFile} will always be excluded automatically.
214 *
215 * @parameter
216 */
217 private List<String> pomExcludes = Collections.emptyList();
218
219 /**
220 * Include patterns for searching the projects directory for projects that need to be run before the other projects.
221 * This parameter allows to declare projects that perform setup tasks like installing utility artifacts into the
222 * local repository. Projects matched by these patterns are implicitly excluded from the scan for ordinary projects.
223 * Also, the exclusions defined by the parameter {@link #pomExcludes} apply to the setup projects, too. Default
224 * value is: <code>setup*/pom.xml</code>.
225 *
226 * @parameter
227 * @since 1.3
228 */
229 private List<String> setupIncludes = Collections.singletonList( "setup*/pom.xml" );
230
231 /**
232 * The list of goals to execute on each project. Default value is: <code>package</code>.
233 *
234 * @parameter
235 */
236 private List<String> goals = Collections.singletonList( "package" );
237
238 /**
239 * The name of the project-specific file that contains the enumeration of goals to execute for that test.
240 *
241 * @parameter expression="${invoker.goalsFile}" default-value="goals.txt"
242 * @deprecated As of version 1.2, the key <code>invoker.goals</code> from the properties file specified by the
243 * parameter {@link #invokerPropertiesFile} should be used instead.
244 */
245 private String goalsFile;
246
247 /**
248 * @component
249 */
250 private Invoker invoker;
251
252 /**
253 * Relative path of a selector script to run prior in order to decide if the build should be executed. This script
254 * may be written with either BeanShell or Groovy. If the file extension is omitted (e.g. <code>selector</code>),
255 * the plugin searches for the file by trying out the well-known extensions <code>.bsh</code> and <code>.groovy</code>.
256 * If this script exists for a particular project but returns any non-null value different from <code>true</code>,
257 * the corresponding build is flagged as skipped. In this case, none of the pre-build hook script,
258 * Maven nor the post-build hook script will be invoked. If this script throws an exception, the corresponding
259 * build is flagged as in error, and none of the pre-build hook script, Maven not the post-build hook script will
260 * be invoked.
261 *
262 * @parameter expression="${invoker.selectorScript}" default-value="selector"
263 * @since 1.5
264 */
265 private String selectorScript;
266
267 /**
268 * Relative path of a pre-build hook script to run prior to executing the build. This script may be written with
269 * either BeanShell or Groovy (since 1.3). If the file extension is omitted (e.g. <code>prebuild</code>), the plugin
270 * searches for the file by trying out the well-known extensions <code>.bsh</code> and <code>.groovy</code>. If this
271 * script exists for a particular project but returns any non-null value different from <code>true</code> or throws
272 * an exception, the corresponding build is flagged as a failure. In this case, neither Maven nor the post-build
273 * hook script will be invoked.
274 *
275 * @parameter expression="${invoker.preBuildHookScript}" default-value="prebuild"
276 */
277 private String preBuildHookScript;
278
279 /**
280 * Relative path of a cleanup/verification hook script to run after executing the build. This script may be written
281 * with either BeanShell or Groovy (since 1.3). If the file extension is omitted (e.g. <code>verify</code>), the
282 * plugin searches for the file by trying out the well-known extensions <code>.bsh</code> and <code>.groovy</code>.
283 * If this script exists for a particular project but returns any non-null value different from <code>true</code> or
284 * throws an exception, the corresponding build is flagged as a failure.
285 *
286 * @parameter expression="${invoker.postBuildHookScript}" default-value="postbuild"
287 */
288 private String postBuildHookScript;
289
290 /**
291 * Location of a properties file that defines CLI properties for the test.
292 *
293 * @parameter expression="${invoker.testPropertiesFile}" default-value="test.properties"
294 */
295 private String testPropertiesFile;
296
297 /**
298 * Common set of test properties to pass in on each IT's command line, via -D parameters.
299 *
300 * @parameter
301 * @deprecated As of version 1.1, use the {@link #properties} parameter instead.
302 */
303 private Properties testProperties;
304
305 /**
306 * Common set of properties to pass in on each project's command line, via -D parameters.
307 *
308 * @parameter
309 * @since 1.1
310 */
311 private Map<String, String> properties;
312
313 /**
314 * Whether to show errors in the build output.
315 *
316 * @parameter expression="${invoker.showErrors}" default-value="false"
317 */
318 private boolean showErrors;
319
320 /**
321 * Whether to show debug statements in the build output.
322 *
323 * @parameter expression="${invoker.debug}" default-value="false"
324 */
325 private boolean debug;
326
327 /**
328 * Suppress logging to the <code>build.log</code> file.
329 *
330 * @parameter expression="${invoker.noLog}" default-value="false"
331 */
332 private boolean noLog;
333
334 /**
335 * List of profile identifiers to explicitly trigger in the build.
336 *
337 * @parameter
338 * @since 1.1
339 */
340 private List<String> profiles;
341
342 /**
343 * List of properties which will be used to interpolate goal files.
344 *
345 * @parameter
346 * @since 1.1
347 * @deprecated As of version 1.3, the parameter {@link #filterProperties} should be used instead.
348 */
349 private Properties interpolationsProperties;
350
351 /**
352 * A list of additional properties which will be used to filter tokens in POMs and goal files.
353 *
354 * @parameter
355 * @since 1.3
356 */
357 private Map<String, String> filterProperties;
358
359 /**
360 * The Maven Project Object
361 *
362 * @parameter expression="${project}"
363 * @required
364 * @readonly
365 * @since 1.1
366 */
367 private MavenProject project;
368
369 /**
370 * A comma separated list of project names to run. Specify this parameter to run individual tests by file name,
371 * overriding the {@link #setupIncludes}, {@link #pomIncludes} and {@link #pomExcludes} parameters. Each pattern you
372 * specify here will be used to create an include pattern formatted like
373 * <code>${projectsDirectory}/<i>pattern</i></code>, so you can just type
374 * <code>-Dinvoker.test=FirstTest,SecondTest</code> to run builds in <code>${projectsDirectory}/FirstTest</code> and
375 * <code>${projectsDirectory}/SecondTest</code>.
376 *
377 * @parameter expression="${invoker.test}"
378 * @since 1.1
379 */
380 private String invokerTest;
381
382 /**
383 * The name of the project-specific file that contains the enumeration of profiles to use for that test. <b>If the
384 * file exists and is empty no profiles will be used even if the parameter {@link #profiles} is set.</b>
385 *
386 * @parameter expression="${invoker.profilesFile}" default-value="profiles.txt"
387 * @since 1.1
388 * @deprecated As of version 1.2, the key <code>invoker.profiles</code> from the properties file specified by the
389 * parameter {@link #invokerPropertiesFile} should be used instead.
390 */
391 private String profilesFile;
392
393 /**
394 * Path to an alternate <code>settings.xml</code> to use for Maven invocation with all ITs. Note that the
395 * <code><localRepository></code> element of this settings file is always ignored, i.e. the path given by the
396 * parameter {@link #localRepositoryPath} is dominant.
397 *
398 * @parameter expression="${invoker.settingsFile}"
399 * @since 1.2
400 */
401 private File settingsFile;
402
403 /**
404 * The <code>MAVEN_OPTS</code> environment variable to use when invoking Maven. This value can be overridden for
405 * individual integration tests by using {@link #invokerPropertiesFile}.
406 *
407 * @parameter expression="${invoker.mavenOpts}"
408 * @since 1.2
409 */
410 private String mavenOpts;
411
412 /**
413 * The home directory of the Maven installation to use for the forked builds. Defaults to the current Maven
414 * installation.
415 *
416 * @parameter expression="${invoker.mavenHome}"
417 * @since 1.3
418 */
419 private File mavenHome;
420
421 /**
422 * The <code>JAVA_HOME</code> environment variable to use for forked Maven invocations. Defaults to the current Java
423 * home directory.
424 *
425 * @parameter expression="${invoker.javaHome}"
426 * @since 1.3
427 */
428 private File javaHome;
429
430 /**
431 * The file encoding for the pre-/post-build scripts and the list files for goals and profiles.
432 *
433 * @parameter expression="${encoding}" default-value="${project.build.sourceEncoding}"
434 * @since 1.2
435 */
436 private String encoding;
437
438 /**
439 * The current user system settings for use in Maven.
440 *
441 * @parameter expression="${settings}"
442 * @required
443 * @readonly
444 * @since 1.2
445 */
446 private Settings settings;
447
448 /**
449 * A flag whether the test class path of the project under test should be included in the class path of the
450 * pre-/post-build scripts. If set to <code>false</code>, the class path of script interpreter consists only of
451 * the <a href="dependencies.html">runtime dependencies</a> of the Maven Invoker Plugin. If set the
452 * <code>true</code>, the project's test class path will be prepended to the interpreter class path. Among
453 * others, this feature allows the scripts to access utility classes from the test sources of your project.
454 *
455 * @parameter expression="${invoker.addTestClassPath}" default-value="false"
456 * @since 1.2
457 */
458 private boolean addTestClassPath;
459
460 /**
461 * The test class path of the project under test.
462 *
463 * @parameter default-value="${project.testClasspathElements}"
464 * @readonly
465 */
466 private List<String> testClassPath;
467
468 /**
469 * The name of an optional project-specific file that contains properties used to specify settings for an individual
470 * Maven invocation. Any property present in the file will override the corresponding setting from the plugin
471 * configuration. The values of the properties are filtered and may use expressions like
472 * <code>${project.version}</code> to reference project properties or values from the parameter
473 * {@link #filterProperties}. The snippet below describes the supported properties:
474 * <p/>
475 * <pre>
476 * # A comma or space separated list of goals/phases to execute, may
477 * # specify an empty list to execute the default goal of the IT project
478 * invoker.goals = clean install
479 *
480 * # Optionally, a list of goals to run during further invocations of Maven
481 * invoker.goals.2 = ${project.groupId}:${project.artifactId}:${project.version}:run
482 *
483 * # A comma or space separated list of profiles to activate
484 * invoker.profiles = its,jdk15
485 *
486 * # The path to an alternative POM or base directory to invoke Maven on, defaults to the
487 * # project that was originally specified in the plugin configuration
488 * # Since plugin version 1.4
489 * invoker.project = sub-module
490 *
491 * # The value for the environment variable MAVEN_OPTS
492 * invoker.mavenOpts = -Dfile.encoding=UTF-16 -Xms32m -Xmx256m
493 *
494 * # Possible values are "fail-fast" (default), "fail-at-end" and "fail-never"
495 * invoker.failureBehavior = fail-never
496 *
497 * # The expected result of the build, possible values are "success" (default) and "failure"
498 * invoker.buildResult = failure
499 *
500 * # A boolean value controlling the aggregator mode of Maven, defaults to "false"
501 * invoker.nonRecursive = true
502 *
503 * # A boolean value controlling the network behavior of Maven, defaults to "false"
504 * # Since plugin version 1.4
505 * invoker.offline = true
506 *
507 * # The path to the properties file from which to load system properties, defaults to the
508 * # filename given by the plugin parameter testPropertiesFile
509 * # Since plugin version 1.4
510 * invoker.systemPropertiesFile = test.properties
511 *
512 * # An optional human friendly name for this build job to be included in the build reports.
513 * # Since plugin version 1.4
514 * invoker.name = Test Build 01
515 *
516 * # An optional description for this build job to be included in the build reports.
517 * # Since plugin version 1.4
518 * invoker.description = Checks the support for build reports.
519 *
520 * # A comma separated list of JRE versions on which this build job should be run.
521 * # Since plugin version 1.4
522 * invoker.java.version = 1.4+, !1.4.1, 1.7-
523 *
524 * # A comma separated list of OS families on which this build job should be run.
525 * # Since plugin version 1.4
526 * invoker.os.family = !windows, unix, mac
527 *
528 * # A comma separated list of Maven versions on which this build should be run.
529 * # Since plugin version 1.5
530 * invoker.maven.version = 2.0.10+, !2.1.0, !2.2.0
531 * </pre>
532 *
533 * @parameter expression="${invoker.invokerPropertiesFile}" default-value="invoker.properties"
534 * @since 1.2
535 */
536 private String invokerPropertiesFile;
537
538 /**
539 * flag to enable show mvn version used for running its (cli option : -V,--show-version )
540 *
541 * @parameter expression="${invoker.showVersion}" default-value="false"
542 * @since 1.4
543 */
544 private boolean showVersion;
545
546 /**
547 * number of threads for running tests in parallel.
548 * This will be the number of maven forked process in parallel.
549 *
550 * @parameter expression="${invoker.parallelThreads}" default-value="1"
551 * @since 1.6
552 */
553 private int parallelThreads;
554
555 /**
556 * @parameter expression="${plugin.artifacts}"
557 * @required
558 * @readonly
559 * @since 1.6
560 */
561 private List<Artifact> pluginArtifacts;
562
563
564 /**
565 * If enable and if you have a settings file configured for the execution, it will be merged with your user settings.
566 *
567 * @parameter expression="${invoker.mergeUserSettings}" default-value="false"
568 * @since 1.6
569 */
570 private boolean mergeUserSettings;
571
572 /**
573 * The scripter runner that is responsible to execute hook scripts.
574 */
575 private ScriptRunner scriptRunner;
576
577 /**
578 * A string used to prefix the file name of the filtered POMs in case the POMs couldn't be filtered in-place (i.e.
579 * the projects were not cloned to a temporary directory), can be <code>null</code>. This will be set to
580 * <code>null</code> if the POMs have already been filtered during cloning.
581 */
582 private String filteredPomPrefix = "interpolated-";
583
584 /**
585 * The format for elapsed build time.
586 */
587 private final DecimalFormat secFormat = new DecimalFormat( "(0.0 s)", new DecimalFormatSymbols( Locale.ENGLISH ) );
588
589 /**
590 * Invokes Maven on the configured test projects.
591 *
592 * @throws org.apache.maven.plugin.MojoExecutionException
593 * If the goal encountered severe errors.
594 * @throws org.apache.maven.plugin.MojoFailureException
595 * If any of the Maven builds failed.
596 */
597 public void execute()
598 throws MojoExecutionException, MojoFailureException
599 {
600 if ( skipInvocation )
601 {
602 getLog().info( "Skipping invocation per configuration."
603 + " If this is incorrect, ensure the skipInvocation parameter is not set to true." );
604 return;
605 }
606
607 // done it here to prevent issues with concurrent access in case of parallel run
608 if ( !disableReports && !reportsDirectory.exists() )
609 {
610 reportsDirectory.mkdirs();
611 }
612
613 BuildJob[] buildJobs;
614 if ( pom != null )
615 {
616 try
617 {
618 projectsDirectory = pom.getCanonicalFile().getParentFile();
619 }
620 catch ( IOException e )
621 {
622 throw new MojoExecutionException(
623 "Failed to discover projectsDirectory from pom File parameter." + " Reason: " + e.getMessage(), e );
624 }
625
626 buildJobs = new BuildJob[]{ new BuildJob( pom.getName(), BuildJob.Type.NORMAL ) };
627 }
628 else
629 {
630 try
631 {
632 buildJobs = getBuildJobs();
633 }
634 catch ( final IOException e )
635 {
636 throw new MojoExecutionException(
637 "Error retrieving POM list from includes, excludes, " + "and projects directory. Reason: "
638 + e.getMessage(), e );
639 }
640 }
641
642 if ( ( buildJobs == null ) || ( buildJobs.length < 1 ) )
643 {
644 getLog().info( "No projects were selected for execution." );
645 return;
646 }
647
648 if ( StringUtils.isEmpty( encoding ) )
649 {
650 getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
651 + ", i.e. build is platform dependent!" );
652 }
653
654 final List<String> scriptClassPath;
655 if ( addTestClassPath )
656 {
657 scriptClassPath = new ArrayList<String>( testClassPath );
658 for ( Artifact pluginArtifact : pluginArtifacts )
659 {
660 scriptClassPath.remove( pluginArtifact.getFile().getAbsolutePath() );
661 }
662 }
663 else
664 {
665 scriptClassPath = null;
666 }
667 scriptRunner = new ScriptRunner( getLog() );
668 scriptRunner.setScriptEncoding( encoding );
669 scriptRunner.setGlobalVariable( "localRepositoryPath", localRepositoryPath );
670 scriptRunner.setClassPath( scriptClassPath );
671
672 Collection<String> collectedProjects = new LinkedHashSet<String>();
673 for ( int i = 0; i < buildJobs.length; i++ )
674 {
675 collectProjects( projectsDirectory, buildJobs[i].getProject(), collectedProjects, true );
676 }
677
678 File projectsDir = projectsDirectory;
679
680 if ( cloneProjectsTo != null )
681 {
682 cloneProjects( collectedProjects );
683 projectsDir = cloneProjectsTo;
684 }
685 else
686 {
687 getLog().warn( "Filtering of parent/child POMs is not supported without cloning the projects" );
688 }
689
690 runBuilds( projectsDir, buildJobs );
691
692 processResults( new InvokerSession( buildJobs ) );
693 }
694
695 /**
696 * Processes the results of invoking the build jobs.
697 *
698 * @param invokerSession The session with the build jobs, must not be <code>null</code>.
699 * @throws MojoFailureException If the mojo had failed as a result of invoking the build jobs.
700 * @since 1.4
701 */
702 abstract void processResults( InvokerSession invokerSession )
703 throws MojoFailureException;
704
705 /**
706 * Creates a new reader for the specified file, using the plugin's {@link #encoding} parameter.
707 *
708 * @param file The file to create a reader for, must not be <code>null</code>.
709 * @return The reader for the file, never <code>null</code>.
710 * @throws java.io.IOException If the specified file was not found or the configured encoding is not supported.
711 */
712 private Reader newReader( File file )
713 throws IOException
714 {
715 if ( StringUtils.isNotEmpty( encoding ) )
716 {
717 return ReaderFactory.newReader( file, encoding );
718 }
719 else
720 {
721 return ReaderFactory.newPlatformReader( file );
722 }
723 }
724
725 /**
726 * Collects all projects locally reachable from the specified project. The method will as such try to read the POM
727 * and recursively follow its parent/module elements.
728 *
729 * @param projectsDir The base directory of all projects, must not be <code>null</code>.
730 * @param projectPath The relative path of the current project, can denote either the POM or its base directory,
731 * must not be <code>null</code>.
732 * @param projectPaths The set of already collected projects to add new projects to, must not be <code>null</code>.
733 * This set will hold the relative paths to either a POM file or a project base directory.
734 * @param included A flag indicating whether the specified project has been explicitly included via the parameter
735 * {@link #pomIncludes}. Such projects will always be added to the result set even if there is no
736 * corresponding POM.
737 * @throws org.apache.maven.plugin.MojoExecutionException
738 * If the project tree could not be traversed.
739 */
740 private void collectProjects( File projectsDir, String projectPath, Collection<String> projectPaths,
741 boolean included )
742 throws MojoExecutionException
743 {
744 projectPath = projectPath.replace( '\\', '/' );
745 File pomFile = new File( projectsDir, projectPath );
746 if ( pomFile.isDirectory() )
747 {
748 pomFile = new File( pomFile, "pom.xml" );
749 if ( !pomFile.exists() )
750 {
751 if ( included )
752 {
753 projectPaths.add( projectPath );
754 }
755 return;
756 }
757 if ( !projectPath.endsWith( "/" ) )
758 {
759 projectPath += '/';
760 }
761 projectPath += "pom.xml";
762 }
763 else if ( !pomFile.isFile() )
764 {
765 return;
766 }
767 if ( !projectPaths.add( projectPath ) )
768 {
769 return;
770 }
771 getLog().debug( "Collecting parent/child projects of " + projectPath );
772
773 Model model = PomUtils.loadPom( pomFile );
774
775 try
776 {
777 String projectsRoot = projectsDir.getCanonicalPath();
778 String projectDir = pomFile.getParent();
779
780 String parentPath = "../pom.xml";
781 if ( model.getParent() != null && StringUtils.isNotEmpty( model.getParent().getRelativePath() ) )
782 {
783 parentPath = model.getParent().getRelativePath();
784 }
785 String parent = relativizePath( new File( projectDir, parentPath ), projectsRoot );
786 if ( parent != null )
787 {
788 collectProjects( projectsDir, parent, projectPaths, false );
789 }
790
791 Collection<String> modulePaths = new LinkedHashSet<String>();
792
793 modulePaths.addAll( (List<String>) model.getModules() );
794
795 for ( Profile profile : (List<Profile>) model.getProfiles() )
796 {
797 modulePaths.addAll( (List<String>) profile.getModules() );
798 }
799
800 for ( String modulePath : modulePaths )
801 {
802 String module = relativizePath( new File( projectDir, modulePath ), projectsRoot );
803 if ( module != null )
804 {
805 collectProjects( projectsDir, module, projectPaths, false );
806 }
807 }
808 }
809 catch ( IOException e )
810 {
811 throw new MojoExecutionException( "Failed to analyze POM: " + pomFile, e );
812 }
813 }
814
815 /**
816 * Copies the specified projects to the directory given by {@link #cloneProjectsTo}. A project may either be denoted
817 * by a path to a POM file or merely by a path to a base directory. During cloning, the POM files will be filtered.
818 *
819 * @param projectPaths The paths to the projects to clone, relative to the projects directory, must not be
820 * <code>null</code> nor contain <code>null</code> elements.
821 * @throws org.apache.maven.plugin.MojoExecutionException
822 * If the the projects could not be copied/filtered.
823 */
824 private void cloneProjects( Collection<String> projectPaths )
825 throws MojoExecutionException
826 {
827 if ( !cloneProjectsTo.mkdirs() && cloneClean )
828 {
829 try
830 {
831 FileUtils.cleanDirectory( cloneProjectsTo );
832 }
833 catch ( IOException e )
834 {
835 throw new MojoExecutionException(
836 "Could not clean the cloneProjectsTo directory. Reason: " + e.getMessage(), e );
837 }
838 }
839
840 // determine project directories to clone
841 Collection<String> dirs = new LinkedHashSet<String>();
842 for ( String projectPath : projectPaths )
843 {
844 if ( !new File( projectsDirectory, projectPath ).isDirectory() )
845 {
846 projectPath = getParentPath( projectPath );
847 }
848 dirs.add( projectPath );
849 }
850
851 boolean filter = false;
852
853 // clone project directories
854 try
855 {
856 filter = !cloneProjectsTo.getCanonicalFile().equals( projectsDirectory.getCanonicalFile() );
857
858 List<String> clonedSubpaths = new ArrayList<String>();
859
860 for ( String subpath : dirs )
861 {
862 // skip this project if its parent directory is also scheduled for cloning
863 if ( !".".equals( subpath ) && dirs.contains( getParentPath( subpath ) ) )
864 {
865 continue;
866 }
867
868 // avoid copying subdirs that are already cloned.
869 if ( !alreadyCloned( subpath, clonedSubpaths ) )
870 {
871 // avoid creating new files that point to dir/.
872 if ( ".".equals( subpath ) )
873 {
874 String cloneSubdir = relativizePath( cloneProjectsTo, projectsDirectory.getCanonicalPath() );
875
876 // avoid infinite recursion if the cloneTo path is a subdirectory.
877 if ( cloneSubdir != null )
878 {
879 File temp = File.createTempFile( "pre-invocation-clone.", "" );
880 temp.delete();
881 temp.mkdirs();
882
883 copyDirectoryStructure( projectsDirectory, temp );
884
885 FileUtils.deleteDirectory( new File( temp, cloneSubdir ) );
886
887 copyDirectoryStructure( temp, cloneProjectsTo );
888 }
889 else
890 {
891 copyDirectoryStructure( projectsDirectory, cloneProjectsTo );
892 }
893 }
894 else
895 {
896 File srcDir = new File( projectsDirectory, subpath );
897 File dstDir = new File( cloneProjectsTo, subpath );
898 copyDirectoryStructure( srcDir, dstDir );
899 }
900
901 clonedSubpaths.add( subpath );
902 }
903 }
904 }
905 catch ( IOException e )
906 {
907 throw new MojoExecutionException(
908 "Failed to clone projects from: " + projectsDirectory + " to: " + cloneProjectsTo + ". Reason: "
909 + e.getMessage(), e );
910 }
911
912 // filter cloned POMs
913 if ( filter )
914 {
915 for ( String projectPath : projectPaths )
916 {
917 File pomFile = new File( cloneProjectsTo, projectPath );
918 if ( pomFile.isFile() )
919 {
920 buildInterpolatedFile( pomFile, pomFile );
921 }
922 }
923 filteredPomPrefix = null;
924 }
925 }
926
927 /**
928 * Gets the parent path of the specified relative path.
929 *
930 * @param path The relative path whose parent should be retrieved, must not be <code>null</code>.
931 * @return The parent path or "." if the specified path has no parent, never <code>null</code>.
932 */
933 private String getParentPath( String path )
934 {
935 int lastSep = Math.max( path.lastIndexOf( '/' ), path.lastIndexOf( '\\' ) );
936 return ( lastSep < 0 ) ? "." : path.substring( 0, lastSep );
937 }
938
939 /**
940 * Copied a directory structure with deafault exclusions (.svn, CVS, etc)
941 *
942 * @param sourceDir The source directory to copy, must not be <code>null</code>.
943 * @param destDir The target directory to copy to, must not be <code>null</code>.
944 * @throws java.io.IOException If the directory structure could not be copied.
945 */
946 private void copyDirectoryStructure( File sourceDir, File destDir )
947 throws IOException
948 {
949 DirectoryScanner scanner = new DirectoryScanner();
950 scanner.setBasedir( sourceDir );
951 if ( !cloneAllFiles )
952 {
953 scanner.addDefaultExcludes();
954 }
955 scanner.scan();
956
957 /*
958 * NOTE: Make sure the destination directory is always there (even if empty) to support POM-less ITs.
959 */
960 destDir.mkdirs();
961 String[] includedDirs = scanner.getIncludedDirectories();
962 for ( int i = 0; i < includedDirs.length; ++i )
963 {
964 File clonedDir = new File( destDir, includedDirs[i] );
965 clonedDir.mkdirs();
966 }
967
968 String[] includedFiles = scanner.getIncludedFiles();
969 for ( int i = 0; i < includedFiles.length; ++i )
970 {
971 File sourceFile = new File( sourceDir, includedFiles[i] );
972 File destFile = new File( destDir, includedFiles[i] );
973 FileUtils.copyFile( sourceFile, destFile );
974 }
975 }
976
977 /**
978 * Determines whether the specified sub path has already been cloned, i.e. whether one of its ancestor directories
979 * was already cloned.
980 *
981 * @param subpath The sub path to check, must not be <code>null</code>.
982 * @param clonedSubpaths The list of already cloned paths, must not be <code>null</code> nor contain
983 * <code>null</code> elements.
984 * @return <code>true</code> if the specified path has already been cloned, <code>false</code> otherwise.
985 */
986 static boolean alreadyCloned( String subpath, List<String> clonedSubpaths )
987 {
988 for ( String path : clonedSubpaths )
989 {
990 if ( ".".equals( path ) || subpath.equals( path ) || subpath.startsWith( path + File.separator ) )
991 {
992 return true;
993 }
994 }
995
996 return false;
997 }
998
999 /**
1000 * Runs the specified build jobs.
1001 *
1002 * @param projectsDir The base directory of all projects, must not be <code>null</code>.
1003 * @param buildJobs The build jobs to run must not be <code>null</code> nor contain <code>null</code> elements.
1004 * @throws org.apache.maven.plugin.MojoExecutionException
1005 * If any build could not be launched.
1006 */
1007 private void runBuilds( final File projectsDir, BuildJob[] buildJobs )
1008 throws MojoExecutionException
1009 {
1010 if ( !localRepositoryPath.exists() )
1011 {
1012 localRepositoryPath.mkdirs();
1013 }
1014
1015 //-----------------------------------------------
1016 // interpolate settings file
1017 //-----------------------------------------------
1018
1019 File interpolatedSettingsFile = null;
1020 if ( settingsFile != null )
1021 {
1022 if ( cloneProjectsTo != null )
1023 {
1024 interpolatedSettingsFile = new File( cloneProjectsTo, "interpolated-" + settingsFile.getName() );
1025 }
1026 else
1027 {
1028 interpolatedSettingsFile =
1029 new File( settingsFile.getParentFile(), "interpolated-" + settingsFile.getName() );
1030 }
1031 buildInterpolatedFile( settingsFile, interpolatedSettingsFile );
1032 }
1033
1034 //-----------------------------------------------
1035 // merge settings file
1036 //-----------------------------------------------
1037
1038 SettingsXpp3Writer settingsWriter = new SettingsXpp3Writer();
1039
1040 File mergedSettingsFile;
1041 Settings mergedSettings = this.settings;
1042 if ( mergeUserSettings )
1043 {
1044 if ( interpolatedSettingsFile != null )
1045 {
1046 // Have to merge the specified settings file (dominant) and the one of the invoking Maven process
1047 Reader reader = null;
1048 try
1049 {
1050 reader = new XmlStreamReader( interpolatedSettingsFile );
1051 SettingsXpp3Reader settingsReader = new SettingsXpp3Reader();
1052 Settings dominantSettings = settingsReader.read( reader );
1053 Settings recessiveSettings = this.settings;
1054
1055 SettingsUtils.merge( dominantSettings, recessiveSettings, TrackableBase.USER_LEVEL );
1056
1057 mergedSettings = dominantSettings;
1058 getLog().debug( "Merged specified settings file with settings of invoking process" );
1059 }
1060 catch ( XmlPullParserException e )
1061 {
1062 throw new MojoExecutionException( "Could not read specified settings file", e );
1063 }
1064 catch ( IOException e )
1065 {
1066 throw new MojoExecutionException( "Could not read specified settings file", e );
1067 }
1068 finally
1069 {
1070 IOUtil.close( reader );
1071 }
1072 }
1073 }
1074 if ( this.settingsFile != null && !mergeUserSettings )
1075 {
1076 mergedSettingsFile = interpolatedSettingsFile;
1077 }
1078 else
1079 {
1080 try
1081 {
1082 mergedSettingsFile = File.createTempFile( "invoker-settings", ".xml" );
1083
1084 FileWriter fileWriter = null;
1085 try
1086 {
1087 fileWriter = new FileWriter( mergedSettingsFile );
1088 settingsWriter.write( fileWriter, mergedSettings );
1089 }
1090 finally
1091 {
1092 IOUtil.close( fileWriter );
1093 }
1094
1095 if ( getLog().isDebugEnabled() )
1096 {
1097 getLog().debug(
1098 "Created temporary file for invoker settings.xml: " + mergedSettingsFile.getAbsolutePath() );
1099 }
1100
1101 }
1102 catch ( IOException e )
1103 {
1104 throw new MojoExecutionException( "Could not create temporary file for invoker settings.xml", e );
1105 }
1106 }
1107 final File finalSettingsFile = mergedSettingsFile;
1108
1109 try
1110 {
1111 if ( isParallelRun() )
1112 {
1113 getLog().info( "use parallelThreads " + parallelThreads );
1114
1115 ExecutorService executorService = Executors.newFixedThreadPool( parallelThreads );
1116 for ( int i = 0; i < buildJobs.length; i++ )
1117 {
1118 final BuildJob project = buildJobs[i];
1119 executorService.execute( new Runnable()
1120 {
1121
1122 public void run()
1123 {
1124 try
1125 {
1126 runBuild( projectsDir, project, finalSettingsFile );
1127 }
1128 catch ( MojoExecutionException e )
1129 {
1130 throw new RuntimeException( e.getMessage(), e );
1131 }
1132 }
1133 } );
1134 }
1135
1136 try
1137 {
1138 executorService.shutdown();
1139 // TODO add a configurable time out
1140 executorService.awaitTermination( Long.MAX_VALUE, TimeUnit.MILLISECONDS );
1141 }
1142 catch ( InterruptedException e )
1143 {
1144 throw new MojoExecutionException( e.getMessage(), e );
1145 }
1146
1147 }
1148 else
1149 {
1150 for ( int i = 0; i < buildJobs.length; i++ )
1151 {
1152 BuildJob project = buildJobs[i];
1153 runBuild( projectsDir, project, finalSettingsFile );
1154 }
1155 }
1156 }
1157 finally
1158 {
1159 if ( interpolatedSettingsFile != null && cloneProjectsTo == null )
1160 {
1161 interpolatedSettingsFile.delete();
1162 }
1163 if ( mergedSettingsFile != null && mergedSettingsFile.exists() )
1164 {
1165 mergedSettingsFile.delete();
1166 }
1167
1168 }
1169 }
1170
1171 /**
1172 * Runs the specified project.
1173 *
1174 * @param projectsDir The base directory of all projects, must not be <code>null</code>.
1175 * @param buildJob The build job to run, must not be <code>null</code>.
1176 * @param settingsFile The (already interpolated) user settings file for the build, may be <code>null</code> to use
1177 * the current user settings.
1178 * @throws org.apache.maven.plugin.MojoExecutionException
1179 * If the project could not be launched.
1180 */
1181 private void runBuild( File projectsDir, BuildJob buildJob, File settingsFile )
1182 throws MojoExecutionException
1183 {
1184 File pomFile = new File( projectsDir, buildJob.getProject() );
1185 File basedir;
1186 if ( pomFile.isDirectory() )
1187 {
1188 basedir = pomFile;
1189 pomFile = new File( basedir, "pom.xml" );
1190 if ( !pomFile.exists() )
1191 {
1192 pomFile = null;
1193 }
1194 else
1195 {
1196 buildJob.setProject( buildJob.getProject() + File.separator + "pom.xml" );
1197 }
1198 }
1199 else
1200 {
1201 basedir = pomFile.getParentFile();
1202 }
1203
1204 getLog().info( "Building: " + buildJob.getProject() );
1205
1206 File interpolatedPomFile = null;
1207 if ( pomFile != null )
1208 {
1209 if ( filteredPomPrefix != null )
1210 {
1211 interpolatedPomFile = new File( basedir, filteredPomPrefix + pomFile.getName() );
1212 buildInterpolatedFile( pomFile, interpolatedPomFile );
1213 }
1214 else
1215 {
1216 interpolatedPomFile = pomFile;
1217 }
1218 }
1219
1220 InvokerProperties invokerProperties = getInvokerProperties( basedir );
1221
1222 // let's set what details we can
1223 buildJob.setName( invokerProperties.getJobName() );
1224 buildJob.setDescription( invokerProperties.getJobDescription() );
1225
1226 try
1227 {
1228 if ( isSelected( invokerProperties ) )
1229 {
1230 long milliseconds = System.currentTimeMillis();
1231 boolean executed;
1232 try
1233 {
1234 executed = runBuild( basedir, interpolatedPomFile, settingsFile, invokerProperties );
1235 }
1236 finally
1237 {
1238 milliseconds = System.currentTimeMillis() - milliseconds;
1239 buildJob.setTime( milliseconds / 1000.0 );
1240 }
1241
1242 if ( executed )
1243 {
1244
1245 buildJob.setResult( BuildJob.Result.SUCCESS );
1246
1247 if ( !suppressSummaries )
1248 {
1249 getLog().info( "..SUCCESS " + formatTime( buildJob.getTime() ) );
1250 }
1251 }
1252 else
1253 {
1254 buildJob.setResult( BuildJob.Result.SKIPPED );
1255
1256 if ( !suppressSummaries )
1257 {
1258 getLog().info( "..SKIPPED " + formatTime( buildJob.getTime() ) );
1259 }
1260 }
1261 }
1262 else
1263 {
1264 buildJob.setResult( BuildJob.Result.SKIPPED );
1265
1266 if ( !suppressSummaries )
1267 {
1268 getLog().info( "..SKIPPED " );
1269 }
1270 }
1271 }
1272 catch ( RunErrorException e )
1273 {
1274 buildJob.setResult( BuildJob.Result.ERROR );
1275 buildJob.setFailureMessage( e.getMessage() );
1276
1277 if ( !suppressSummaries )
1278 {
1279 getLog().info( "..ERROR " + formatTime( buildJob.getTime() ) );
1280 getLog().info( " " + e.getMessage() );
1281 }
1282 }
1283 catch ( RunFailureException e )
1284 {
1285 buildJob.setResult( e.getType() );
1286 buildJob.setFailureMessage( e.getMessage() );
1287
1288 if ( !suppressSummaries )
1289 {
1290 getLog().info( "..FAILED " + formatTime( buildJob.getTime() ) );
1291 getLog().info( " " + e.getMessage() );
1292 }
1293 }
1294 finally
1295 {
1296 if ( interpolatedPomFile != null && StringUtils.isNotEmpty( filteredPomPrefix ) )
1297 {
1298 interpolatedPomFile.delete();
1299 }
1300 writeBuildReport( buildJob );
1301 }
1302 }
1303
1304 /**
1305 * Determines whether selector conditions of the specified invoker properties match the current environment.
1306 *
1307 * @param invokerProperties The invoker properties to check, must not be <code>null</code>.
1308 * @return <code>true</code> if the job corresponding to the properties should be run, <code>false</code> otherwise.
1309 */
1310 private boolean isSelected( InvokerProperties invokerProperties )
1311 {
1312 if ( !SelectorUtils.isMavenVersion( invokerProperties.getMavenVersion() ) )
1313 {
1314 return false;
1315 }
1316
1317 if ( !SelectorUtils.isJreVersion( invokerProperties.getJreVersion() ) )
1318 {
1319 return false;
1320 }
1321
1322 if ( !SelectorUtils.isOsFamily( invokerProperties.getOsFamily() ) )
1323 {
1324 return false;
1325 }
1326
1327 return true;
1328 }
1329
1330 /**
1331 * Writes the XML report for the specified build job unless report generation has been disabled.
1332 *
1333 * @param buildJob The build job whose report should be written, must not be <code>null</code>.
1334 * @throws org.apache.maven.plugin.MojoExecutionException
1335 * If the report could not be written.
1336 */
1337 private void writeBuildReport( BuildJob buildJob )
1338 throws MojoExecutionException
1339 {
1340 if ( disableReports )
1341 {
1342 return;
1343 }
1344
1345 String safeFileName = buildJob.getProject().replace( '/', '_' ).replace( '\\', '_' ).replace( ' ', '_' );
1346 if ( safeFileName.endsWith( "_pom.xml" ) )
1347 {
1348 safeFileName = safeFileName.substring( 0, safeFileName.length() - "_pom.xml".length() );
1349 }
1350
1351 File reportFile = new File( reportsDirectory, "BUILD-" + safeFileName + ".xml" );
1352 try
1353 {
1354 FileOutputStream fos = new FileOutputStream( reportFile );
1355 try
1356 {
1357 Writer osw = new OutputStreamWriter( fos, buildJob.getModelEncoding() );
1358 BuildJobXpp3Writer writer = new BuildJobXpp3Writer();
1359 writer.write( osw, buildJob );
1360 osw.close();
1361 }
1362 finally
1363 {
1364 fos.close();
1365 }
1366 }
1367 catch ( IOException e )
1368 {
1369 throw new MojoExecutionException( "Failed to write build report " + reportFile, e );
1370 }
1371 }
1372
1373 /**
1374 * Formats the specified build duration time.
1375 *
1376 * @param seconds The duration of the build.
1377 * @return The formatted time, never <code>null</code>.
1378 */
1379 private String formatTime( double seconds )
1380 {
1381 return secFormat.format( seconds );
1382 }
1383
1384 /**
1385 * Runs the specified project.
1386 *
1387 * @param basedir The base directory of the project, must not be <code>null</code>.
1388 * @param pomFile The (already interpolated) POM file, may be <code>null</code> for a POM-less Maven invocation.
1389 * @param settingsFile The (already interpolated) user settings file for the build, may be <code>null</code>. Will be
1390 * merged with the settings file of the invoking Maven process.
1391 * @param invokerProperties The properties to use.
1392 * @return <code>true</code> if the project was launched or <code>false</code> if the selector script indicated that
1393 * the project should be skipped.
1394 * @throws org.apache.maven.plugin.MojoExecutionException
1395 * If the project could not be launched.
1396 * @throws org.apache.maven.shared.scriptinterpreter.RunFailureException
1397 * If either a hook script or the build itself failed.
1398 */
1399 private boolean runBuild( File basedir, File pomFile, File settingsFile, InvokerProperties invokerProperties )
1400 throws MojoExecutionException, RunFailureException
1401 {
1402 if ( getLog().isDebugEnabled() && !invokerProperties.getProperties().isEmpty() )
1403 {
1404 Properties props = invokerProperties.getProperties();
1405 getLog().debug( "Using invoker properties:" );
1406 for ( String key : new TreeSet<String>( (Set) props.keySet() ) )
1407 {
1408 String value = props.getProperty( key );
1409 getLog().debug( " " + key + " = " + value );
1410 }
1411 }
1412
1413 List<String> goals = getGoals( basedir );
1414
1415 List<String> profiles = getProfiles( basedir );
1416
1417 Map<String, Object> context = new LinkedHashMap<String, Object>();
1418
1419 FileLogger logger = setupLogger( basedir );
1420 try
1421 {
1422 try
1423 {
1424 scriptRunner.run( "selector script", basedir, selectorScript, context, logger, BuildJob.Result.SKIPPED,
1425 false );
1426 }
1427 catch ( RunErrorException e )
1428 {
1429 throw e;
1430 }
1431 catch ( RunFailureException e )
1432 {
1433 return false;
1434 }
1435
1436 scriptRunner.run( "pre-build script", basedir, preBuildHookScript, context, logger,
1437 BuildJob.Result.FAILURE_PRE_HOOK, false );
1438
1439 final InvocationRequest request = new DefaultInvocationRequest();
1440
1441 request.setLocalRepositoryDirectory( localRepositoryPath );
1442
1443 request.setInteractive( false );
1444
1445 request.setShowErrors( showErrors );
1446
1447 request.setDebug( debug );
1448
1449 request.setShowVersion( showVersion );
1450
1451 if ( logger != null )
1452 {
1453 request.setErrorHandler( logger );
1454
1455 request.setOutputHandler( logger );
1456 }
1457
1458 if ( mavenHome != null )
1459 {
1460 invoker.setMavenHome( mavenHome );
1461 request.addShellEnvironment( "M2_HOME", mavenHome.getAbsolutePath() );
1462 }
1463
1464 if ( javaHome != null )
1465 {
1466 request.setJavaHome( javaHome );
1467 }
1468
1469 for ( int invocationIndex = 1; ; invocationIndex++ )
1470 {
1471 if ( invocationIndex > 1 && !invokerProperties.isInvocationDefined( invocationIndex ) )
1472 {
1473 break;
1474 }
1475
1476 request.setBaseDirectory( basedir );
1477
1478 request.setPomFile( pomFile );
1479
1480 request.setGoals( goals );
1481
1482 request.setProfiles( profiles );
1483
1484 request.setMavenOpts( mavenOpts );
1485
1486 request.setOffline( false );
1487
1488 request.setUserSettingsFile( settingsFile );
1489
1490 Properties systemProperties =
1491 getSystemProperties( basedir, invokerProperties.getSystemPropertiesFile( invocationIndex ) );
1492 request.setProperties( systemProperties );
1493
1494 invokerProperties.configureInvocation( request, invocationIndex );
1495
1496 if ( getLog().isDebugEnabled() )
1497 {
1498 try
1499 {
1500 getLog().debug( "Using MAVEN_OPTS: " + request.getMavenOpts() );
1501 getLog().debug( "Executing: " + new MavenCommandLineBuilder().build( request ) );
1502 }
1503 catch ( CommandLineConfigurationException e )
1504 {
1505 getLog().debug( "Failed to display command line: " + e.getMessage() );
1506 }
1507 }
1508
1509 InvocationResult result;
1510
1511 try
1512 {
1513 result = invoker.execute( request );
1514 }
1515 catch ( final MavenInvocationException e )
1516 {
1517 getLog().debug( "Error invoking Maven: " + e.getMessage(), e );
1518 throw new RunFailureException( "Maven invocation failed. " + e.getMessage(),
1519 BuildJob.Result.FAILURE_BUILD );
1520 }
1521
1522 verify( result, invocationIndex, invokerProperties, logger );
1523 }
1524
1525 scriptRunner.run( "post-build script", basedir, postBuildHookScript, context, logger,
1526 BuildJob.Result.FAILURE_POST_HOOK, true );
1527 }
1528 catch ( IOException e )
1529 {
1530 throw new MojoExecutionException( e.getMessage(), e );
1531 }
1532 finally
1533 {
1534 if ( logger != null )
1535 {
1536 logger.close();
1537 }
1538 }
1539 return true;
1540 }
1541
1542 /**
1543 * Initializes the build logger for the specified project.
1544 *
1545 * @param basedir The base directory of the project, must not be <code>null</code>.
1546 * @return The build logger or <code>null</code> if logging has been disabled.
1547 * @throws org.apache.maven.plugin.MojoExecutionException
1548 * If the log file could not be created.
1549 */
1550 private FileLogger setupLogger( File basedir )
1551 throws MojoExecutionException
1552 {
1553 FileLogger logger = null;
1554
1555 if ( !noLog )
1556 {
1557 File outputLog = new File( basedir, "build.log" );
1558 try
1559 {
1560 if ( streamLogs )
1561 {
1562 logger = new FileLogger( outputLog, getLog() );
1563 }
1564 else
1565 {
1566 logger = new FileLogger( outputLog );
1567 }
1568
1569 getLog().debug( "build log initialized in: " + outputLog );
1570 }
1571 catch ( IOException e )
1572 {
1573 throw new MojoExecutionException( "Error initializing build logfile in: " + outputLog, e );
1574 }
1575 }
1576
1577 return logger;
1578 }
1579
1580 /**
1581 * Gets the system properties to use for the specified project.
1582 *
1583 * @param basedir The base directory of the project, must not be <code>null</code>.
1584 * @param filename The filename to the properties file to load, may be <code>null</code> to use the default path
1585 * given by {@link #testPropertiesFile}.
1586 * @return The system properties to use, may be empty but never <code>null</code>.
1587 * @throws org.apache.maven.plugin.MojoExecutionException
1588 * If the properties file exists but could not be read.
1589 */
1590 private Properties getSystemProperties( final File basedir, final String filename )
1591 throws MojoExecutionException
1592 {
1593 Properties collectedTestProperties = new Properties();
1594
1595 if ( testProperties != null )
1596 {
1597 collectedTestProperties.putAll( testProperties );
1598 }
1599
1600 if ( properties != null )
1601 {
1602 collectedTestProperties.putAll( properties );
1603 }
1604
1605 File propertiesFile = null;
1606 if ( filename != null )
1607 {
1608 propertiesFile = new File( basedir, filename );
1609 }
1610 else if ( testPropertiesFile != null )
1611 {
1612 propertiesFile = new File( basedir, testPropertiesFile );
1613 }
1614
1615 if ( propertiesFile != null && propertiesFile.isFile() )
1616 {
1617 InputStream fin = null;
1618 try
1619 {
1620 fin = new FileInputStream( propertiesFile );
1621
1622 Properties loadedProperties = new Properties();
1623 loadedProperties.load( fin );
1624 collectedTestProperties.putAll( loadedProperties );
1625 }
1626 catch ( IOException e )
1627 {
1628 throw new MojoExecutionException( "Error reading system properties from " + propertiesFile );
1629 }
1630 finally
1631 {
1632 IOUtil.close( fin );
1633 }
1634 }
1635
1636 return collectedTestProperties;
1637 }
1638
1639 /**
1640 * Verifies the invocation result.
1641 *
1642 * @param result The invocation result to check, must not be <code>null</code>.
1643 * @param invocationIndex The index of the invocation for which to check the exit code, must not be negative.
1644 * @param invokerProperties The invoker properties used to check the exit code, must not be <code>null</code>.
1645 * @param logger The build logger, may be <code>null</code> if logging is disabled.
1646 * @throws org.apache.maven.shared.scriptinterpreter.RunFailureException
1647 * If the invocation result indicates a build failure.
1648 */
1649 private void verify( InvocationResult result, int invocationIndex, InvokerProperties invokerProperties,
1650 FileLogger logger )
1651 throws RunFailureException
1652 {
1653 if ( result.getExecutionException() != null )
1654 {
1655 throw new RunFailureException(
1656 "The Maven invocation failed. " + result.getExecutionException().getMessage(), BuildJob.Result.ERROR );
1657 }
1658 else if ( !invokerProperties.isExpectedResult( result.getExitCode(), invocationIndex ) )
1659 {
1660 StringBuffer buffer = new StringBuffer( 256 );
1661 buffer.append( "The build exited with code " ).append( result.getExitCode() ).append( ". " );
1662 if ( logger != null )
1663 {
1664 buffer.append( "See " );
1665 buffer.append( logger.getOutputFile().getAbsolutePath() );
1666 buffer.append( " for details." );
1667 }
1668 else
1669 {
1670 buffer.append( "See console output for details." );
1671 }
1672 throw new RunFailureException( buffer.toString(), BuildJob.Result.FAILURE_BUILD );
1673 }
1674 }
1675
1676 /**
1677 * Gets the goal list for the specified project.
1678 *
1679 * @param basedir The base directory of the project, must not be <code>null</code>.
1680 * @return The list of goals to run when building the project, may be empty but never <code>null</code>.
1681 * @throws org.apache.maven.plugin.MojoExecutionException
1682 * If the profile file could not be read.
1683 */
1684 List<String> getGoals( final File basedir )
1685 throws MojoExecutionException
1686 {
1687 try
1688 {
1689 return getTokens( basedir, goalsFile, goals );
1690 }
1691 catch ( IOException e )
1692 {
1693 throw new MojoExecutionException( "error reading goals", e );
1694 }
1695 }
1696
1697 /**
1698 * Gets the profile list for the specified project.
1699 *
1700 * @param basedir The base directory of the project, must not be <code>null</code>.
1701 * @return The list of profiles to activate when building the project, may be empty but never <code>null</code>.
1702 * @throws org.apache.maven.plugin.MojoExecutionException
1703 * If the profile file could not be read.
1704 */
1705 List<String> getProfiles( File basedir )
1706 throws MojoExecutionException
1707 {
1708 try
1709 {
1710 return getTokens( basedir, profilesFile, profiles );
1711 }
1712 catch ( IOException e )
1713 {
1714 throw new MojoExecutionException( "error reading profiles", e );
1715 }
1716 }
1717
1718 /**
1719 * Gets the build jobs that should be processed. Note that the order of the returned build jobs is significant.
1720 *
1721 * @return The build jobs to process, may be empty but never <code>null</code>.
1722 * @throws java.io.IOException If the projects directory could not be scanned.
1723 */
1724 BuildJob[] getBuildJobs()
1725 throws IOException
1726 {
1727 BuildJob[] buildJobs;
1728
1729 if ( ( pom != null ) && pom.exists() )
1730 {
1731 buildJobs = new BuildJob[]{ new BuildJob( pom.getAbsolutePath(), BuildJob.Type.NORMAL ) };
1732 }
1733 else if ( invokerTest != null )
1734 {
1735 String[] testRegexes = StringUtils.split( invokerTest, "," );
1736 List<String> includes = new ArrayList<String>( testRegexes.length );
1737
1738 for ( int i = 0, size = testRegexes.length; i < size; i++ )
1739 {
1740 // user just use -Dinvoker.test=MWAR191,MNG111 to use a directory thats the end is not pom.xml
1741 includes.add( testRegexes[i] );
1742 }
1743
1744 // it would be nice if we could figure out what types these are... but perhaps
1745 // not necessary for the -Dinvoker.test=xxx t
1746 buildJobs = scanProjectsDirectory( includes, null, BuildJob.Type.DIRECT );
1747 }
1748 else
1749 {
1750 List<String> excludes =
1751 ( pomExcludes != null ) ? new ArrayList<String>( pomExcludes ) : new ArrayList<String>();
1752 if ( this.settingsFile != null )
1753 {
1754 String exclude = relativizePath( this.settingsFile, projectsDirectory.getCanonicalPath() );
1755 if ( exclude != null )
1756 {
1757 excludes.add( exclude.replace( '\\', '/' ) );
1758 getLog().debug( "Automatically excluded " + exclude + " from project scanning" );
1759 }
1760 }
1761
1762 BuildJob[] setupPoms = scanProjectsDirectory( setupIncludes, excludes, BuildJob.Type.SETUP );
1763 getLog().debug( "Setup projects: " + Arrays.asList( setupPoms ) );
1764
1765 BuildJob[] normalPoms = scanProjectsDirectory( pomIncludes, excludes, BuildJob.Type.NORMAL );
1766
1767 Map<String, BuildJob> uniquePoms = new LinkedHashMap<String, BuildJob>();
1768 for ( int i = 0; i < setupPoms.length; i++ )
1769 {
1770 uniquePoms.put( setupPoms[i].getProject(), setupPoms[i] );
1771 }
1772 for ( int i = 0; i < normalPoms.length; i++ )
1773 {
1774 if ( !uniquePoms.containsKey( normalPoms[i].getProject() ) )
1775 {
1776 uniquePoms.put( normalPoms[i].getProject(), normalPoms[i] );
1777 }
1778 }
1779
1780 buildJobs = uniquePoms.values().toArray( new BuildJob[uniquePoms.size()] );
1781 }
1782
1783 relativizeProjectPaths( buildJobs );
1784
1785 return buildJobs;
1786 }
1787
1788 /**
1789 * Scans the projects directory for projects to build. Both (POM) files and mere directories will be matched by the
1790 * scanner patterns. If the patterns match a directory which contains a file named "pom.xml", the results will
1791 * include the path to this file rather than the directory path in order to avoid duplicate invocations of the same
1792 * project.
1793 *
1794 * @param includes The include patterns for the scanner, may be <code>null</code>.
1795 * @param excludes The exclude patterns for the scanner, may be <code>null</code> to exclude nothing.
1796 * @param type The type to assign to the resulting build jobs, must not be <code>null</code>.
1797 * @return The build jobs matching the patterns, never <code>null</code>.
1798 * @throws java.io.IOException If the project directory could not be scanned.
1799 */
1800 private BuildJob[] scanProjectsDirectory( List<String> includes, List<String> excludes, String type )
1801 throws IOException
1802 {
1803 if ( !projectsDirectory.isDirectory() )
1804 {
1805 return new BuildJob[0];
1806 }
1807
1808 DirectoryScanner scanner = new DirectoryScanner();
1809 scanner.setBasedir( projectsDirectory.getCanonicalFile() );
1810 scanner.setFollowSymlinks( false );
1811 if ( includes != null )
1812 {
1813 scanner.setIncludes( includes.toArray( new String[includes.size()] ) );
1814 }
1815 if ( excludes != null )
1816 {
1817 scanner.setExcludes( excludes.toArray( new String[excludes.size()] ) );
1818 }
1819 scanner.addDefaultExcludes();
1820 scanner.scan();
1821
1822 Map<String, BuildJob> matches = new LinkedHashMap<String, BuildJob>();
1823
1824 String[] includedFiles = scanner.getIncludedFiles();
1825 for ( int i = 0; i < includedFiles.length; i++ )
1826 {
1827 matches.put( includedFiles[i], new BuildJob( includedFiles[i], type ) );
1828 }
1829
1830 String[] includedDirs = scanner.getIncludedDirectories();
1831 for ( int i = 0; i < includedDirs.length; i++ )
1832 {
1833 String includedFile = includedDirs[i] + File.separatorChar + "pom.xml";
1834 if ( new File( scanner.getBasedir(), includedFile ).isFile() )
1835 {
1836 matches.put( includedFile, new BuildJob( includedFile, type ) );
1837 }
1838 else
1839 {
1840 matches.put( includedDirs[i], new BuildJob( includedDirs[i], type ) );
1841 }
1842 }
1843
1844 return matches.values().toArray( new BuildJob[matches.size()] );
1845 }
1846
1847 /**
1848 * Relativizes the project paths of the specified build jobs against the directory specified by
1849 * {@link #projectsDirectory} (if possible). If a project path does not denote a sub path of the projects directory,
1850 * it is returned as is.
1851 *
1852 * @param buildJobs The build jobs whose project paths should be relativized, must not be <code>null</code> nor
1853 * contain <code>null</code> elements.
1854 * @throws java.io.IOException If any path could not be relativized.
1855 */
1856 private void relativizeProjectPaths( BuildJob[] buildJobs )
1857 throws IOException
1858 {
1859 String projectsDirPath = projectsDirectory.getCanonicalPath();
1860
1861 for ( int i = 0; i < buildJobs.length; i++ )
1862 {
1863 String projectPath = buildJobs[i].getProject();
1864
1865 File file = new File( projectPath );
1866
1867 if ( !file.isAbsolute() )
1868 {
1869 file = new File( projectsDirectory, projectPath );
1870 }
1871
1872 String relativizedPath = relativizePath( file, projectsDirPath );
1873
1874 if ( relativizedPath == null )
1875 {
1876 relativizedPath = projectPath;
1877 }
1878
1879 buildJobs[i].setProject( relativizedPath );
1880 }
1881 }
1882
1883 /**
1884 * Relativizes the specified path against the given base directory. Besides relativization, the returned path will
1885 * also be normalized, e.g. directory references like ".." will be removed.
1886 *
1887 * @param path The path to relativize, must not be <code>null</code>.
1888 * @param basedir The (canonical path of the) base directory to relativize against, must not be <code>null</code>.
1889 * @return The relative path in normal form or <code>null</code> if the input path does not denote a sub path of the
1890 * base directory.
1891 * @throws java.io.IOException If the path could not be relativized.
1892 */
1893 private String relativizePath( File path, String basedir )
1894 throws IOException
1895 {
1896 String relativizedPath = path.getCanonicalPath();
1897
1898 if ( relativizedPath.startsWith( basedir ) )
1899 {
1900 relativizedPath = relativizedPath.substring( basedir.length() );
1901 if ( relativizedPath.startsWith( File.separator ) )
1902 {
1903 relativizedPath = relativizedPath.substring( File.separator.length() );
1904 }
1905
1906 return relativizedPath;
1907 }
1908 else
1909 {
1910 return null;
1911 }
1912 }
1913
1914 /**
1915 * Returns the map-based value source used to interpolate POMs and other stuff.
1916 *
1917 * @return The map-based value source for interpolation, never <code>null</code>.
1918 */
1919 private Map<String, Object> getInterpolationValueSource()
1920 {
1921 Map<String, Object> props = new HashMap<String, Object>();
1922 if ( interpolationsProperties != null )
1923 {
1924 props.putAll( (Map) interpolationsProperties );
1925 }
1926 if ( filterProperties != null )
1927 {
1928 props.putAll( filterProperties );
1929 }
1930 props.put( "basedir", this.project.getBasedir().getAbsolutePath() );
1931 props.put( "baseurl", toUrl( this.project.getBasedir().getAbsolutePath() ) );
1932 if ( settings.getLocalRepository() != null )
1933 {
1934 props.put( "localRepository", settings.getLocalRepository() );
1935 props.put( "localRepositoryUrl", toUrl( settings.getLocalRepository() ) );
1936 }
1937 return new CompositeMap( this.project, props );
1938 }
1939
1940 /**
1941 * Converts the specified filesystem path to a URL. The resulting URL has no trailing slash regardless whether the
1942 * path denotes a file or a directory.
1943 *
1944 * @param filename The filesystem path to convert, must not be <code>null</code>.
1945 * @return The <code>file:</code> URL for the specified path, never <code>null</code>.
1946 */
1947 private static String toUrl( String filename )
1948 {
1949 /*
1950 * NOTE: Maven fails to properly handle percent-encoded "file:" URLs (WAGON-111) so don't use File.toURI() here
1951 * as-is but use the decoded path component in the URL.
1952 */
1953 String url = "file://" + new File( filename ).toURI().getPath();
1954 if ( url.endsWith( "/" ) )
1955 {
1956 url = url.substring( 0, url.length() - 1 );
1957 }
1958 return url;
1959 }
1960
1961 /**
1962 * Gets goal/profile names for the specified project, either directly from the plugin configuration or from an
1963 * external token file.
1964 *
1965 * @param basedir The base directory of the test project, must not be <code>null</code>.
1966 * @param filename The (simple) name of an optional file in the project base directory from which to read
1967 * goals/profiles, may be <code>null</code>.
1968 * @param defaultTokens The list of tokens to return in case the specified token file does not exist, may be
1969 * <code>null</code>.
1970 * @return The list of goal/profile names, may be empty but never <code>null</code>.
1971 * @throws java.io.IOException If the token file exists but could not be parsed.
1972 */
1973 private List<String> getTokens( File basedir, String filename, List<String> defaultTokens )
1974 throws IOException
1975 {
1976 List<String> tokens = ( defaultTokens != null ) ? defaultTokens : new ArrayList<String>();
1977
1978 if ( StringUtils.isNotEmpty( filename ) )
1979 {
1980 File tokenFile = new File( basedir, filename );
1981
1982 if ( tokenFile.exists() )
1983 {
1984 tokens = readTokens( tokenFile );
1985 }
1986 }
1987
1988 return tokens;
1989 }
1990
1991 /**
1992 * Reads the tokens from the specified file. Tokens are separated either by line terminators or commas. During
1993 * parsing, the file contents will be interpolated.
1994 *
1995 * @param tokenFile The file to read the tokens from, must not be <code>null</code>.
1996 * @return The list of tokens, may be empty but never <code>null</code>.
1997 * @throws java.io.IOException If the token file could not be read.
1998 */
1999 private List<String> readTokens( final File tokenFile )
2000 throws IOException
2001 {
2002 List<String> result = new ArrayList<String>();
2003
2004 BufferedReader reader = null;
2005 try
2006 {
2007 Map<String, Object> composite = getInterpolationValueSource();
2008 reader = new BufferedReader( new InterpolationFilterReader( newReader( tokenFile ), composite ) );
2009
2010 String line = null;
2011 while ( ( line = reader.readLine() ) != null )
2012 {
2013 result.addAll( collectListFromCSV( line ) );
2014 }
2015 }
2016 finally
2017 {
2018 IOUtil.close( reader );
2019 }
2020
2021 return result;
2022 }
2023
2024 /**
2025 * Gets a list of comma separated tokens from the specified line.
2026 *
2027 * @param csv The line with comma separated tokens, may be <code>null</code>.
2028 * @return The list of tokens from the line, may be empty but never <code>null</code>.
2029 */
2030 private List<String> collectListFromCSV( final String csv )
2031 {
2032 final List<String> result = new ArrayList<String>();
2033
2034 if ( ( csv != null ) && ( csv.trim().length() > 0 ) )
2035 {
2036 final StringTokenizer st = new StringTokenizer( csv, "," );
2037
2038 while ( st.hasMoreTokens() )
2039 {
2040 result.add( st.nextToken().trim() );
2041 }
2042 }
2043
2044 return result;
2045 }
2046
2047 /**
2048 * Interpolates the specified POM/settings file to a temporary file. The destination file may be same as the input
2049 * file, i.e. interpolation can be performed in-place.
2050 *
2051 * @param originalFile The XML file to interpolate, must not be <code>null</code>.
2052 * @param interpolatedFile The target file to write the interpolated contents of the original file to, must not be
2053 * <code>null</code>.
2054 * @throws org.apache.maven.plugin.MojoExecutionException
2055 * If the target file could not be created.
2056 */
2057 void buildInterpolatedFile( File originalFile, File interpolatedFile )
2058 throws MojoExecutionException
2059 {
2060 getLog().debug( "Interpolate " + originalFile.getPath() + " to " + interpolatedFile.getPath() );
2061
2062 try
2063 {
2064 String xml;
2065
2066 Reader reader = null;
2067 try
2068 {
2069 // interpolation with token @...@
2070 Map<String, Object> composite = getInterpolationValueSource();
2071 reader = ReaderFactory.newXmlReader( originalFile );
2072 reader = new InterpolationFilterReader( reader, composite, "@", "@" );
2073 xml = IOUtil.toString( reader );
2074 }
2075 finally
2076 {
2077 IOUtil.close( reader );
2078 }
2079
2080 Writer writer = null;
2081 try
2082 {
2083 interpolatedFile.getParentFile().mkdirs();
2084 writer = WriterFactory.newXmlWriter( interpolatedFile );
2085 writer.write( xml );
2086 writer.flush();
2087 }
2088 finally
2089 {
2090 IOUtil.close( writer );
2091 }
2092 }
2093 catch ( IOException e )
2094 {
2095 throw new MojoExecutionException( "Failed to interpolate file " + originalFile.getPath(), e );
2096 }
2097 }
2098
2099 /**
2100 * Gets the (interpolated) invoker properties for an integration test.
2101 *
2102 * @param projectDirectory The base directory of the IT project, must not be <code>null</code>.
2103 * @return The invoker properties, may be empty but never <code>null</code>.
2104 * @throws org.apache.maven.plugin.MojoExecutionException
2105 * If an I/O error occurred during reading the properties.
2106 */
2107 private InvokerProperties getInvokerProperties( final File projectDirectory )
2108 throws MojoExecutionException
2109 {
2110 Properties props = new Properties();
2111 if ( invokerPropertiesFile != null )
2112 {
2113 File propertiesFile = new File( projectDirectory, invokerPropertiesFile );
2114 if ( propertiesFile.isFile() )
2115 {
2116 InputStream in = null;
2117 try
2118 {
2119 in = new FileInputStream( propertiesFile );
2120 props.load( in );
2121 }
2122 catch ( IOException e )
2123 {
2124 throw new MojoExecutionException( "Failed to read invoker properties: " + propertiesFile, e );
2125 }
2126 finally
2127 {
2128 IOUtil.close( in );
2129 }
2130 }
2131
2132 Interpolator interpolator = new RegexBasedInterpolator();
2133 interpolator.addValueSource( new MapBasedValueSource( getInterpolationValueSource() ) );
2134 for ( String key : (Set<String>) ( (Map) props ).keySet() )
2135 {
2136 String value = props.getProperty( key );
2137 try
2138 {
2139 value = interpolator.interpolate( value, "" );
2140 }
2141 catch ( InterpolationException e )
2142 {
2143 throw new MojoExecutionException( "Failed to interpolate invoker properties: " + propertiesFile,
2144 e );
2145 }
2146 props.setProperty( key, value );
2147 }
2148 }
2149 return new InvokerProperties( props );
2150 }
2151
2152 protected boolean isParallelRun()
2153 {
2154 return parallelThreads > 1;
2155 }
2156
2157 }