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