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