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