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