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