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