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