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