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