View Javadoc
1   package org.apache.maven.archetype.mojos;
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.FileNotFoundException;
26  import java.io.FileReader;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.Reader;
30  import java.io.StringWriter;
31  import java.io.Writer;
32  import java.util.Arrays;
33  import java.util.Collection;
34  import java.util.HashMap;
35  import java.util.LinkedHashMap;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.Properties;
39  import java.util.Set;
40  
41  import org.apache.commons.collections.CollectionUtils;
42  import org.apache.maven.archetype.ArchetypeGenerationRequest;
43  import org.apache.maven.archetype.ArchetypeGenerationResult;
44  import org.apache.maven.archetype.common.Constants;
45  import org.apache.maven.archetype.exception.ArchetypeNotConfigured;
46  import org.apache.maven.archetype.generator.ArchetypeGenerator;
47  import org.apache.maven.artifact.repository.ArtifactRepository;
48  import org.apache.maven.execution.MavenSession;
49  import org.apache.maven.plugin.AbstractMojo;
50  import org.apache.maven.plugin.MojoExecutionException;
51  import org.apache.maven.plugin.MojoFailureException;
52  import org.apache.maven.plugins.annotations.Component;
53  import org.apache.maven.plugins.annotations.Mojo;
54  import org.apache.maven.plugins.annotations.Parameter;
55  import org.apache.maven.project.DefaultProjectBuildingRequest;
56  import org.apache.maven.project.MavenProject;
57  import org.apache.maven.project.ProjectBuildingRequest;
58  import org.apache.maven.settings.Settings;
59  import org.apache.maven.shared.artifact.DefaultArtifactCoordinate;
60  import org.apache.maven.shared.artifact.resolve.ArtifactResolver;
61  import org.apache.maven.shared.artifact.resolve.ArtifactResolverException;
62  import org.apache.maven.shared.invoker.DefaultInvocationRequest;
63  import org.apache.maven.shared.invoker.InvocationRequest;
64  import org.apache.maven.shared.invoker.InvocationResult;
65  import org.apache.maven.shared.invoker.Invoker;
66  import org.apache.maven.shared.invoker.MavenInvocationException;
67  import org.apache.maven.shared.scriptinterpreter.RunFailureException;
68  import org.apache.maven.shared.scriptinterpreter.ScriptRunner;
69  import org.codehaus.plexus.util.FileUtils;
70  import org.codehaus.plexus.util.IOUtil;
71  import org.codehaus.plexus.util.InterpolationFilterReader;
72  import org.codehaus.plexus.util.ReaderFactory;
73  import org.codehaus.plexus.util.StringUtils;
74  import org.codehaus.plexus.util.WriterFactory;
75  import org.codehaus.plexus.util.introspection.ReflectionValueExtractor;
76  
77  /**
78   * <p>
79   * Execute the archetype integration tests, consisting in generating projects from the current archetype and optionally
80   * comparing generated projects with reference copy.
81   * </p>
82   * <p/>
83   * <p>
84   * Each IT consists of a sub-directory in <code>src/test/resources/projects</code> containing:
85   * </p>
86   * <ul>
87   * <li>a <code>goal.txt</code> file, containing a list of goals to run against the generated project (can be empty,
88   * content ignored before maven-archetype-plugin 2.1),</li>
89   * <li>an <code>archetype.properties</code> file, containing properties for project generation,</li>
90   * <li>an optional <code>reference/</code> directory containing a reference copy of the expected project created from
91   * the IT.</li>
92   * </ul>
93   * <p/>
94   * To let the IT create a Maven module below some other Maven project (being generated from another archetype)
95   * one can additionally specify an optional <code>archetype.pom.properties</code> file in the parent directory,
96   * specifying the archetype's <code>groupId</code>, <code>artifactId</code> and <code>version</code> along with its 
97   * <code>archetype.properties</code> file, containing properties for project generation. Both files are leveraged
98   * to create the parent project for this IT. Parent projects can be nested.
99   * An example structure for such an integration test looks like this
100  * <table>
101  * <tr>
102  * <th>File/Directory</code></td>
103  * <th>Description</td>
104  * </tr>
105  * <tr>
106  * <td><code>src/test/resources/projects/it1</code></td>
107  * <td>Directory for integration test 1</td>
108  * </tr>
109  * <tr>
110  * <td><code>src/test/resources/projects/it1/archetype.pom.properties</code></td>
111  * <td>GAV for the archetype from which to generate the parent</td>
112  * </tr>
113  * <tr>
114  * <td><code>src/test/resources/projects/it1/archetype.properties</code></td>
115  * <td>All required properties for the archetype being specified by <code>archetype.pom.properties</code> on this level</td>
116  * </tr>
117  * <tr>
118  * <td><code>src/test/resources/projects/it1/child</code></td>
119  * <td>Directory for maven module within integration test 1 (this folder's name is not relevant)</td>
120  * </tr>
121  * <tr>
122  * <td><code>src/test/resources/projects/it1/child/goal.txt</code></td>
123  * <td>The file containing the list of goals to be executed against the generated project</td>
124  * </tr>
125  * <tr>
126  * <td><code>src/test/resources/projects/it1/child/archetype.properties</code></td>
127  * <td>All required properties for this project's archetype</td>
128  * </tr>
129  * </table> 
130  * <p/>
131  * Notice that it is expected to be run as part as of a build after the <code>package</code> phase and not directly as a
132  * goal from CLI.
133  *
134  * @author rafale
135  */
136 @Mojo( name = "integration-test", requiresProject = true )
137 public class IntegrationTestMojo
138     extends AbstractMojo
139 {
140 
141     @Component
142     private ArchetypeGenerator archetypeGenerator;
143 
144     @Component
145     private Invoker invoker;
146     
147     @Component
148     private ArtifactResolver artifactResolver;
149     
150     @Parameter( defaultValue = "${project.remoteArtifactRepositories}", readonly = true, required = true )
151     protected List<ArtifactRepository> remoteRepositories;
152 
153     @Parameter( defaultValue = "${localRepository}", readonly = true, required = true )
154     protected ArtifactRepository localRepository;
155     
156     /**
157      * The archetype project to execute the integration tests on.
158      */
159     @Parameter( defaultValue = "${project}", readonly = true, required = true )
160     private MavenProject project;
161 
162     @Parameter( defaultValue = "${session}", readonly = true, required = true )
163     private MavenSession session;
164     
165     /**
166      * Skip the integration test.
167      */
168     @Parameter( property = "archetype.test.skip" )
169     private boolean skip = false;
170 
171     /**
172      * Directory of test projects
173      *
174      * @since 2.2
175      */
176     @Parameter( property = "archetype.test.projectsDirectory", defaultValue = "${project.build.testOutputDirectory}/projects", required = true )
177     private File testProjectsDirectory;
178 
179     /**
180      * Relative path of a cleanup/verification hook script to run after executing the build. This script may be written
181      * with either BeanShell or Groovy. If the file extension is omitted (e.g. <code>verify</code>), the
182      * plugin searches for the file by trying out the well-known extensions <code>.bsh</code> and <code>.groovy</code>.
183      * If this script exists for a particular project but returns any non-null value different from <code>true</code> or
184      * throws an exception, the corresponding build is flagged as a failure.
185      *
186      * @since 2.2
187      */
188     @Parameter( property = "archetype.test.verifyScript", defaultValue = "verify" )
189     private String postBuildHookScript;
190 
191     /**
192      * Suppress logging to the <code>build.log</code> file.
193      *
194      * @since 2.2
195      */
196     @Parameter( property = "archetype.test.noLog", defaultValue = "false" )
197     private boolean noLog;
198 
199     /**
200      * Flag used to determine whether the build logs should be output to the normal mojo log.
201      *
202      * @since 2.2
203      */
204     @Parameter( property = "archetype.test.streamLogs", defaultValue = "true" )
205     private boolean streamLogs;
206 
207     /**
208      * The file encoding for the post-build script.
209      *
210      * @since 2.2
211      */
212     @Parameter( property = "encoding", defaultValue = "${project.build.sourceEncoding}" )
213     private String encoding;
214 
215     /**
216      * The local repository to run maven instance.
217      *
218      * @since 2.2
219      */
220     @Parameter( property = "archetype.test.localRepositoryPath", defaultValue = "${settings.localRepository}", required = true )
221     private File localRepositoryPath;
222 
223     /**
224      * flag to enable show mvn version used for running its (cli option : -V,--show-version )
225      *
226      * @since 2.2
227      */
228     @Parameter( property = "archetype.test.showVersion", defaultValue = "false" )
229     private boolean showVersion;
230 
231     /**
232      * Ignores the EOL encoding for comparing files (default and original behaviour is false).
233      *
234      * @since 2.3
235      */
236     @Parameter( property = "archetype.test.ignoreEOLStyle", defaultValue = "false" )
237     private boolean ignoreEOLStyle;
238 
239     /**
240      * Whether to show debug statements in the build output.
241      *
242      * @since 2.2
243      */
244     @Parameter( property = "archetype.test.debug", defaultValue = "false" )
245     private boolean debug;
246 
247     /**
248      * A list of additional properties which will be used to filter tokens in settings.xml
249      *
250      * @since 2.2
251      */
252     @Parameter
253     private Map<String, String> filterProperties;
254 
255     /**
256      * The current user system settings for use in Maven.
257      *
258      * @since 2.2
259      */
260     @Parameter( defaultValue = "${settings}", required = true, readonly = true )
261     private Settings settings;
262 
263     /**
264      * Path to an alternate <code>settings.xml</code> to use for Maven invocation with all ITs. Note that the
265      * <code>&lt;localRepository&gt;</code> element of this settings file is always ignored, i.e. the path given by the
266      * parameter {@link #localRepositoryPath} is dominant.
267      *
268      * @since 2.2
269      */
270     @Parameter( property = "archetype.test.settingsFile" )
271     private File settingsFile;
272 
273     public void execute()
274         throws MojoExecutionException, MojoFailureException
275     {
276         if ( skip )
277         {
278             return;
279         }
280 
281         if ( !testProjectsDirectory.exists() )
282         {
283             getLog().warn( "No Archetype IT projects: root 'projects' directory not found." );
284 
285             return;
286         }
287 
288         File archetypeFile = project.getArtifact().getFile();
289 
290         if ( archetypeFile == null )
291         {
292             throw new MojoFailureException( "Unable to get the archetypes' artifact which should have just been built:"
293                                                 + " you probably launched 'mvn archetype:integration-test' instead of"
294                                                 + " 'mvn integration-test'." );
295         }
296 
297         try
298         {
299             List<File> projectsGoalFiles = FileUtils.getFiles( testProjectsDirectory, "**/goal.txt", "" );
300 
301             if ( projectsGoalFiles.size() == 0 )
302             {
303                 getLog().warn( "No Archetype IT projects: no directory with goal.txt found." );
304 
305                 return;
306             }
307 
308             StringWriter errorWriter = new StringWriter();
309             for ( File goalFile : projectsGoalFiles )
310             {
311                 try
312                 {
313                     processIntegrationTest( goalFile, archetypeFile );
314                 }
315                 catch ( IntegrationTestFailure ex )
316                 {
317                     errorWriter.write( "\nArchetype IT '" + goalFile.getParentFile().getName() + "' failed: " );
318                     errorWriter.write( ex.getMessage() );
319                 }
320             }
321 
322             String errors = errorWriter.toString();
323             if ( !StringUtils.isEmpty( errors ) )
324             {
325                 throw new MojoExecutionException( errors );
326             }
327         }
328         catch ( IOException ex )
329         {
330             throw new MojoFailureException( ex, ex.getMessage(), ex.getMessage() );
331         }
332     }
333 
334     /**
335      * Checks that actual directory content is the same as reference.
336      *
337      * @param reference the reference directory
338      * @param actual    the actual directory to compare with the reference
339      * @throws IntegrationTestFailure if content differs
340      */
341     private void assertDirectoryEquals( File reference, File actual )
342         throws IntegrationTestFailure, IOException
343     {
344         List<String> referenceFiles =
345             FileUtils.getFileAndDirectoryNames( reference, "**", null, false, true, true, true );
346         getLog().debug( "reference content: " + referenceFiles );
347 
348         List<String> actualFiles = FileUtils.getFileAndDirectoryNames( actual, "**", null, false, true, true, true );
349         getLog().debug( "actual content: " + referenceFiles );
350 
351         boolean fileNamesEquals = CollectionUtils.isEqualCollection( referenceFiles, actualFiles );
352 
353         if ( !fileNamesEquals )
354         {
355             getLog().debug( "Actual list of files is not the same as reference:" );
356             int missing = 0;
357             for ( String ref : referenceFiles )
358             {
359                 if ( actualFiles.contains( ref ) )
360                 {
361                     actualFiles.remove( ref );
362                     getLog().debug( "Contained " + ref );
363                 }
364                 else
365                 {
366                     missing++;
367                     getLog().error( "Not contained " + ref );
368                 }
369             }
370             getLog().error( "Remains " + actualFiles );
371 
372             throw new IntegrationTestFailure(
373                 "Reference and generated project differs (missing: " + missing + ", unexpected: " + actualFiles.size()
374                     + ")" );
375         }
376 
377         boolean contentEquals = true;
378 
379         for ( String file : referenceFiles )
380         {
381             File referenceFile = new File( reference, file );
382             File actualFile = new File( actual, file );
383 
384             if ( referenceFile.isDirectory() )
385             {
386                 if ( actualFile.isFile() )
387                 {
388                     getLog().warn( "File " + file + " is a directory in the reference but a file in actual" );
389                     contentEquals = false;
390                 }
391             }
392             else if ( actualFile.isDirectory() )
393             {
394                 if ( referenceFile.isFile() )
395                 {
396                     getLog().warn( "File " + file + " is a file in the reference but a directory in actual" );
397                     contentEquals = false;
398                 }
399             }
400             else if ( !contentEquals( referenceFile, actualFile ) )
401             {
402                 getLog().warn( "Contents of file " + file + " are not equal" );
403                 contentEquals = false;
404             }
405         }
406         if ( !contentEquals )
407         {
408             throw new IntegrationTestFailure( "Some content are not equals" );
409         }
410     }
411 
412     /**
413      * Uses the {@link #ignoreEOLStyle} attribute to compare the two files. If {@link #ignoreEOLStyle} is true,
414      * then the comparison does not take care about the EOL (aka newline) character.
415      */
416     private boolean contentEquals( File referenceFile, File actualFile )
417         throws IOException
418     {
419         // Original behaviour
420         if ( !ignoreEOLStyle )
421         {
422             getLog().warn( "Property ignoreEOLStyle was not set - files will be compared considering their EOL style!" );
423             return FileUtils.contentEquals( referenceFile, actualFile );
424         }
425 
426         getLog().debug( "Comparing files with EOL style ignored." );
427         BufferedReader referenceFileReader = null;
428         BufferedReader actualFileReader = null;
429         try
430         {
431             referenceFileReader = new BufferedReader( new FileReader( referenceFile ) );
432             actualFileReader = new BufferedReader( new FileReader( actualFile ) );
433 
434             String refLine = null;
435             String actualLine = null;
436 
437             do
438             {
439                 refLine = referenceFileReader.readLine();
440                 actualLine = actualFileReader.readLine();
441                 if ( !StringUtils.equals( refLine, actualLine ) )
442                 {
443                     return false;
444                 }
445             }
446             while ( refLine != null || actualLine != null );
447 
448             return true;
449         }
450         finally
451         {
452             IOUtil.close( referenceFileReader );
453             IOUtil.close( actualFileReader );
454         }
455     }
456 
457     private Properties loadProperties( final File propertiesFile )
458         throws IOException, FileNotFoundException
459     {
460         Properties properties = new Properties();
461 
462         InputStream in = null;
463         try
464         {
465             in = new FileInputStream( propertiesFile );
466 
467             properties.load( in );
468         }
469         finally
470         {
471             IOUtil.close( in );
472         }
473 
474         return properties;
475     }
476 
477     private void processIntegrationTest( File goalFile, File archetypeFile )
478         throws IntegrationTestFailure, MojoExecutionException
479     {
480         getLog().info( "Processing Archetype IT project: " + goalFile.getParentFile().getName() );
481 
482         try
483         {
484             Properties properties = getProperties( goalFile );
485 
486             File basedir = new File( goalFile.getParentFile(), "project" );
487 
488             FileUtils.deleteDirectory( basedir );
489 
490             FileUtils.mkdir( basedir.toString() );
491             
492             basedir = setupParentProjects( goalFile.getParentFile().getParentFile(), basedir );
493 
494             ArchetypeGenerationRequest request = generate( project.getGroupId(), project.getArtifactId(), project.getVersion(), archetypeFile, properties, basedir.toString() );
495 
496             File reference = new File( goalFile.getParentFile(), "reference" );
497 
498             if ( reference.exists() )
499             {
500                 // compare generated project with reference
501                 getLog().info( "Comparing generated project with reference content: " + reference );
502 
503                 assertDirectoryEquals( reference, new File( basedir, request.getArtifactId() ) );
504             }
505 
506             String goals = FileUtils.fileRead( goalFile );
507 
508             if ( StringUtils.isNotEmpty( goals ) )
509             {
510                 invokePostArchetypeGenerationGoals( goals.trim(), new File( basedir, request.getArtifactId() ),
511                                                     goalFile );
512             }
513         }
514         catch ( IOException ioe )
515         {
516             throw new IntegrationTestFailure( ioe );
517         }
518     }
519 
520     private ArchetypeGenerationRequest generate( String archetypeGroupId, String archetypeArtifactId, String archetypeVersion, File archetypeFile, Properties properties, String basedir ) throws IntegrationTestFailure
521     {
522         //@formatter:off
523         ArchetypeGenerationRequest request =
524             new ArchetypeGenerationRequest().setArchetypeGroupId( archetypeGroupId ).setArchetypeArtifactId(
525                 archetypeArtifactId ).setArchetypeVersion( archetypeVersion ).setGroupId(
526                 properties.getProperty( Constants.GROUP_ID ) ).setArtifactId(
527                 properties.getProperty( Constants.ARTIFACT_ID ) ).setVersion(
528                 properties.getProperty( Constants.VERSION ) ).setPackage(
529                 properties.getProperty( Constants.PACKAGE ) ).setOutputDirectory( basedir ).setProperties(
530                 properties );
531         //@formatter:on
532 
533         ArchetypeGenerationResult result = new ArchetypeGenerationResult();
534 
535         archetypeGenerator.generateArchetype( request, archetypeFile, result );
536 
537         if ( result.getCause() != null )
538         {
539             if ( result.getCause() instanceof ArchetypeNotConfigured )
540             {
541                 ArchetypeNotConfigured anc = (ArchetypeNotConfigured) result.getCause();
542 
543                 throw new IntegrationTestFailure(
544                     "Missing required properties in archetype.properties: " + StringUtils.join(
545                         anc.getMissingProperties().iterator(), ", " ), anc );
546             }
547 
548             throw new IntegrationTestFailure( result.getCause().getMessage(), result.getCause() );
549         }
550         return request;
551     }
552 
553     private File setupParentProjects( File configFolder, File buildFolder )
554         throws IOException, MojoExecutionException, IntegrationTestFailure
555     {
556         // look for 'archetype.pom.properties'
557         File archetypePomPropertiesFile = new File( configFolder, "archetype.pom.properties" );
558         if ( !archetypePomPropertiesFile.exists() ) 
559         {
560             getLog().debug( "No 'archetype.pom.properties' file found in " + configFolder );
561             return buildFolder;
562         }
563         
564         // go up to the parent configuration folder
565         buildFolder = setupParentProjects( configFolder.getParentFile(), buildFolder );
566         
567         Properties archetypePomProperties = loadProperties( archetypePomPropertiesFile );
568         String groupId = archetypePomProperties.getProperty( Constants.GROUP_ID );
569         if ( StringUtils.isEmpty( groupId ) )
570         {
571             throw new MojoExecutionException( "Property " + Constants.GROUP_ID + " not set in " + archetypePomPropertiesFile );
572         }
573         String artifactId = archetypePomProperties.getProperty( Constants.ARTIFACT_ID );
574         if ( StringUtils.isEmpty( artifactId ) )
575         {
576             throw new MojoExecutionException( "Property " + Constants.ARTIFACT_ID + " not set in " + archetypePomPropertiesFile );
577         }
578         String version = archetypePomProperties.getProperty( Constants.VERSION );
579         if ( StringUtils.isEmpty( version ) )
580         {
581             throw new MojoExecutionException( "Property " + Constants.VERSION + " not set in " + archetypePomPropertiesFile );
582         }
583         
584         File archetypeFile;
585         try
586         {
587             archetypeFile = getArchetypeFile( groupId, artifactId, version );
588         }
589         catch ( ArtifactResolverException e )
590         {
591             throw new MojoExecutionException( "Could not resolve archetype artifact " , e );
592         }
593         Properties archetypeProperties = getProperties( archetypePomPropertiesFile );
594         getLog().info( "Setting up parent project in " + buildFolder );
595         ArchetypeGenerationRequest request = generate( groupId, artifactId, version, archetypeFile, archetypeProperties, buildFolder.toString() );
596         return new File( buildFolder, request.getArtifactId() );
597     }
598 
599     private File getArchetypeFile( String groupId, String artifactId, String version ) 
600         throws ArtifactResolverException
601     {
602         ProjectBuildingRequest buildingRequest = new DefaultProjectBuildingRequest( session.getProjectBuildingRequest() );
603         if ( localRepository != null )
604         {
605             buildingRequest = buildingRequest.setLocalRepository( localRepository );
606         }
607         if ( remoteRepositories != null && !remoteRepositories.isEmpty()  )
608         {
609             buildingRequest = buildingRequest.setRemoteRepositories( remoteRepositories );
610         }
611 
612         DefaultArtifactCoordinate coordinate = new DefaultArtifactCoordinate();
613         coordinate.setGroupId( groupId );
614         coordinate.setArtifactId( artifactId );
615         coordinate.setVersion( version );
616         
617         return artifactResolver.resolveArtifact( buildingRequest, coordinate ).getArtifact().getFile();
618     }
619 
620     private Properties getProperties( File goalFile )
621         throws IOException
622     {
623         File propertiesFile = new File( goalFile.getParentFile(), "archetype.properties" );
624 
625         return loadProperties( propertiesFile );
626     }
627 
628     private void invokePostArchetypeGenerationGoals( String goals, File basedir, File goalFile )
629         throws IntegrationTestFailure, IOException, MojoExecutionException
630     {
631         FileLogger logger = setupLogger( basedir );
632 
633         if ( !StringUtils.isBlank( goals ) )
634         {
635 
636             getLog().info( "Invoking post-archetype-generation goals: " + goals );
637 
638             if ( !localRepositoryPath.exists() )
639             {
640                 localRepositoryPath.mkdirs();
641             }
642 
643             //@formatter:off
644             InvocationRequest request = new DefaultInvocationRequest().setBaseDirectory( basedir ).setGoals(
645                 Arrays.asList( StringUtils.split( goals, "," ) ) ).setLocalRepositoryDirectory(
646                 localRepositoryPath ).setInteractive( false ).setShowErrors( true );
647             //@formatter:on
648 
649             request.setDebug( debug );
650 
651             request.setShowVersion( showVersion );
652 
653             if ( logger != null )
654             {
655                 request.setErrorHandler( logger );
656 
657                 request.setOutputHandler( logger );
658             }
659 
660             File interpolatedSettingsFile = null;
661             if ( settingsFile != null )
662             {
663                 File interpolatedSettingsDirectory =
664                     new File( project.getBuild().getOutputDirectory(), "archetype-it" );
665                 if ( interpolatedSettingsDirectory.exists() )
666                 {
667                     FileUtils.deleteDirectory( interpolatedSettingsDirectory );
668                 }
669                 interpolatedSettingsDirectory.mkdir();
670                 interpolatedSettingsFile =
671                     new File( interpolatedSettingsDirectory, "interpolated-" + settingsFile.getName() );
672 
673                 buildInterpolatedFile( settingsFile, interpolatedSettingsFile );
674 
675                 request.setUserSettingsFile( interpolatedSettingsFile );
676             }
677 
678             try
679             {
680                 InvocationResult result = invoker.execute( request );
681 
682                 getLog().info( "Post-archetype-generation invoker exit code: " + result.getExitCode() );
683 
684                 if ( result.getExitCode() != 0 )
685                 {
686                     throw new IntegrationTestFailure( "Execution failure: exit code = " + result.getExitCode(),
687                                                       result.getExecutionException() );
688                 }
689             }
690             catch ( MavenInvocationException e )
691             {
692                 throw new IntegrationTestFailure( "Cannot run additions goals.", e );
693             }
694         }
695         else
696         {
697             getLog().info( "No post-archetype-generation goals to invoke." );
698         }
699         // verify result
700         ScriptRunner scriptRunner = new ScriptRunner( getLog() );
701         scriptRunner.setScriptEncoding( encoding );
702 
703         Map<String, Object> context = new LinkedHashMap<String, Object>();
704         context.put( "projectDir", basedir );
705 
706         try
707         {
708             scriptRunner.run( "post-build script", goalFile.getParentFile(), postBuildHookScript, context, logger,
709                               "failure post script", true );
710         }
711         catch ( RunFailureException e )
712         {
713             throw new IntegrationTestFailure( "post build script failure failure: " + e.getMessage(), e );
714         }
715     }
716 
717     private FileLogger setupLogger( File basedir )
718         throws IOException
719     {
720         FileLogger logger = null;
721 
722         if ( !noLog )
723         {
724             File outputLog = new File( basedir, "build.log" );
725 
726             if ( streamLogs )
727             {
728                 logger = new FileLogger( outputLog, getLog() );
729             }
730             else
731             {
732                 logger = new FileLogger( outputLog );
733             }
734 
735             getLog().debug( "build log initialized in: " + outputLog );
736 
737         }
738 
739         return logger;
740     }
741 
742     class IntegrationTestFailure
743         extends Exception
744     {
745         IntegrationTestFailure()
746         {
747             super();
748         }
749 
750         IntegrationTestFailure( String message )
751         {
752             super( message );
753         }
754 
755         IntegrationTestFailure( Throwable cause )
756         {
757             super( cause );
758         }
759 
760         IntegrationTestFailure( String message, Throwable cause )
761         {
762             super( message, cause );
763         }
764     }
765 
766     /**
767      * Returns the map-based value source used to interpolate settings and other stuff.
768      *
769      * @return The map-based value source for interpolation, never <code>null</code>.
770      */
771     private Map<String, Object> getInterpolationValueSource()
772     {
773         Map<String, Object> props = new HashMap<String, Object>();
774         if ( filterProperties != null )
775         {
776             props.putAll( filterProperties );
777         }
778         if ( filterProperties != null )
779         {
780             props.putAll( filterProperties );
781         }
782         props.put( "basedir", this.project.getBasedir().getAbsolutePath() );
783         props.put( "baseurl", toUrl( this.project.getBasedir().getAbsolutePath() ) );
784         if ( settings.getLocalRepository() != null )
785         {
786             props.put( "localRepository", settings.getLocalRepository() );
787             props.put( "localRepositoryUrl", toUrl( settings.getLocalRepository() ) );
788         }
789         return new CompositeMap( this.project, props );
790     }
791 
792     protected void buildInterpolatedFile( File originalFile, File interpolatedFile )
793         throws MojoExecutionException
794     {
795         getLog().debug( "Interpolate " + originalFile.getPath() + " to " + interpolatedFile.getPath() );
796 
797         try
798         {
799             String xml;
800 
801             Reader reader = null;
802             try
803             {
804                 // interpolation with token @...@
805                 Map<String, Object> composite = getInterpolationValueSource();
806                 reader = ReaderFactory.newXmlReader( originalFile );
807                 reader = new InterpolationFilterReader( reader, composite, "@", "@" );
808                 xml = IOUtil.toString( reader );
809             }
810             finally
811             {
812                 IOUtil.close( reader );
813             }
814 
815             Writer writer = null;
816             try
817             {
818                 interpolatedFile.getParentFile().mkdirs();
819                 writer = WriterFactory.newXmlWriter( interpolatedFile );
820                 writer.write( xml );
821                 writer.flush();
822             }
823             finally
824             {
825                 IOUtil.close( writer );
826             }
827         }
828         catch ( IOException e )
829         {
830             throw new MojoExecutionException( "Failed to interpolate file " + originalFile.getPath(), e );
831         }
832     }
833 
834     private static class CompositeMap
835         implements Map<String, Object>
836     {
837 
838         /**
839          * The Maven project from which to extract interpolated values, never <code>null</code>.
840          */
841         private MavenProject mavenProject;
842 
843         /**
844          * The set of additional properties from which to extract interpolated values, never <code>null</code>.
845          */
846         private Map<String, Object> properties;
847 
848         /**
849          * Creates a new interpolation source backed by the specified Maven project and some user-specified properties.
850          *
851          * @param mavenProject The Maven project from which to extract interpolated values, must not be
852          *                     <code>null</code>.
853          * @param properties   The set of additional properties from which to extract interpolated values, may be
854          *                     <code>null</code>.
855          */
856         protected CompositeMap( MavenProject mavenProject, Map<String, Object> properties )
857         {
858             if ( mavenProject == null )
859             {
860                 throw new IllegalArgumentException( "no project specified" );
861             }
862             this.mavenProject = mavenProject;
863             this.properties = properties == null ? (Map) new Properties() : properties;
864         }
865 
866         /**
867          * {@inheritDoc}
868          *
869          * @see java.util.Map#clear()
870          */
871         public void clear()
872         {
873             // nothing here
874         }
875 
876         /**
877          * {@inheritDoc}
878          *
879          * @see java.util.Map#containsKey(java.lang.Object)
880          */
881         public boolean containsKey( Object key )
882         {
883             if ( !( key instanceof String ) )
884             {
885                 return false;
886             }
887 
888             String expression = (String) key;
889             if ( expression.startsWith( "project." ) || expression.startsWith( "pom." ) )
890             {
891                 try
892                 {
893                     Object evaluated = ReflectionValueExtractor.evaluate( expression, this.mavenProject );
894                     if ( evaluated != null )
895                     {
896                         return true;
897                     }
898                 }
899                 catch ( Exception e )
900                 {
901                     // uhm do we have to throw a RuntimeException here ?
902                 }
903             }
904 
905             return properties.containsKey( key ) || mavenProject.getProperties().containsKey( key );
906         }
907 
908         /**
909          * {@inheritDoc}
910          *
911          * @see java.util.Map#containsValue(java.lang.Object)
912          */
913         public boolean containsValue( Object value )
914         {
915             throw new UnsupportedOperationException();
916         }
917 
918         /**
919          * {@inheritDoc}
920          *
921          * @see java.util.Map#entrySet()
922          */
923         public Set<Entry<String, Object>> entrySet()
924         {
925             throw new UnsupportedOperationException();
926         }
927 
928         /**
929          * {@inheritDoc}
930          *
931          * @see java.util.Map#get(java.lang.Object)
932          */
933         public Object get( Object key )
934         {
935             if ( !( key instanceof String ) )
936             {
937                 return null;
938             }
939 
940             String expression = (String) key;
941             if ( expression.startsWith( "project." ) || expression.startsWith( "pom." ) )
942             {
943                 try
944                 {
945                     Object evaluated = ReflectionValueExtractor.evaluate( expression, this.mavenProject );
946                     if ( evaluated != null )
947                     {
948                         return evaluated;
949                     }
950                 }
951                 catch ( Exception e )
952                 {
953                     // uhm do we have to throw a RuntimeException here ?
954                 }
955             }
956 
957             Object value = properties.get( key );
958 
959             return ( value != null ? value : this.mavenProject.getProperties().get( key ) );
960 
961         }
962 
963         /**
964          * {@inheritDoc}
965          *
966          * @see java.util.Map#isEmpty()
967          */
968         public boolean isEmpty()
969         {
970             return this.mavenProject == null && this.mavenProject.getProperties().isEmpty()
971                 && this.properties.isEmpty();
972         }
973 
974         /**
975          * {@inheritDoc}
976          *
977          * @see java.util.Map#keySet()
978          */
979         public Set<String> keySet()
980         {
981             throw new UnsupportedOperationException();
982         }
983 
984         /**
985          * {@inheritDoc}
986          *
987          * @see java.util.Map#put(java.lang.Object, java.lang.Object)
988          */
989         public Object put( String key, Object value )
990         {
991             throw new UnsupportedOperationException();
992         }
993 
994         /**
995          * {@inheritDoc}
996          *
997          * @see java.util.Map#putAll(java.util.Map)
998          */
999         public void putAll( Map<? extends String, ? extends Object> t )
1000         {
1001             throw new UnsupportedOperationException();
1002         }
1003 
1004         /**
1005          * {@inheritDoc}
1006          *
1007          * @see java.util.Map#remove(java.lang.Object)
1008          */
1009         public Object remove( Object key )
1010         {
1011             throw new UnsupportedOperationException();
1012         }
1013 
1014         /**
1015          * {@inheritDoc}
1016          *
1017          * @see java.util.Map#size()
1018          */
1019         public int size()
1020         {
1021             throw new UnsupportedOperationException();
1022         }
1023 
1024         /**
1025          * {@inheritDoc}
1026          *
1027          * @see java.util.Map#values()
1028          */
1029         public Collection<Object> values()
1030         {
1031             throw new UnsupportedOperationException();
1032         }
1033     }
1034 
1035     /**
1036      * Converts the specified filesystem path to a URL. The resulting URL has no trailing slash regardless whether the
1037      * path denotes a file or a directory.
1038      *
1039      * @param filename The filesystem path to convert, must not be <code>null</code>.
1040      * @return The <code>file:</code> URL for the specified path, never <code>null</code>.
1041      */
1042     private static String toUrl( String filename )
1043     {
1044         /*
1045          * NOTE: Maven fails to properly handle percent-encoded "file:" URLs (WAGON-111) so don't use File.toURI() here
1046          * as-is but use the decoded path component in the URL.
1047          */
1048         String url = "file://" + new File( filename ).toURI().getPath();
1049         if ( url.endsWith( "/" ) )
1050         {
1051             url = url.substring( 0, url.length() - 1 );
1052         }
1053         return url;
1054     }
1055 }