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