blob: 7e7d435c1756873f5f4ca1efbd36179948cfcf1a [file] [log] [blame]
package org.apache.maven.archetype.mojos;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.apache.commons.collections.CollectionUtils;
import org.apache.maven.archetype.ArchetypeGenerationRequest;
import org.apache.maven.archetype.ArchetypeGenerationResult;
import org.apache.maven.archetype.common.Constants;
import org.apache.maven.archetype.exception.ArchetypeNotConfigured;
import org.apache.maven.archetype.generator.ArchetypeGenerator;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.DefaultProjectBuildingRequest;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuildingRequest;
import org.apache.maven.settings.Settings;
import org.apache.maven.shared.artifact.DefaultArtifactCoordinate;
import org.apache.maven.shared.artifact.resolve.ArtifactResolver;
import org.apache.maven.shared.artifact.resolve.ArtifactResolverException;
import org.apache.maven.shared.invoker.DefaultInvocationRequest;
import org.apache.maven.shared.invoker.InvocationRequest;
import org.apache.maven.shared.invoker.InvocationResult;
import org.apache.maven.shared.invoker.Invoker;
import org.apache.maven.shared.invoker.MavenInvocationException;
import org.apache.maven.shared.scriptinterpreter.RunFailureException;
import org.apache.maven.shared.scriptinterpreter.ScriptRunner;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.InterpolationFilterReader;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.WriterFactory;
import org.codehaus.plexus.util.introspection.ReflectionValueExtractor;
/**
* <p>Execute the archetype integration tests, consisting in generating projects from the current archetype and optionally
* comparing generated projects with reference copy.</p>
*
* <p>Each IT consists of a sub-directory in <code>src/test/resources/projects</code> containing:</p>
*
* <ul>
* <li>a <code>goal.txt</code> file, containing a list of goals to run against the generated project (can be empty,
* content ignored before maven-archetype-plugin 2.1),</li>
* <li>an <code>archetype.properties</code> file, containing properties for project generation,</li>
* <li>an optional <code>reference/</code> directory containing a reference copy of the expected project created from
* the IT.</li>
* </ul>
* <p>To let the IT create a Maven module below some other Maven project (being generated from another archetype)
* one can additionally specify an optional <code>archetype.pom.properties</code> file in the parent directory,
* specifying the archetype's <code>groupId</code>, <code>artifactId</code> and <code>version</code> along with its
* <code>archetype.properties</code> file, containing properties for project generation. Both files are leveraged
* to create the parent project for this IT. Parent projects can be nested.</p>
*
* <p>An example structure for such an integration test looks like this</p>
* <table summary="integration test folder structure">
* <tr>
* <th>File/Directory</th>
* <th>Description</th>
* </tr>
* <tr>
* <td><code>src/test/resources/projects/it1</code></td>
* <td>Directory for integration test 1</td>
* </tr>
* <tr>
* <td><code>src/test/resources/projects/it1/archetype.pom.properties</code></td>
* <td>GAV for the archetype from which to generate the parent</td>
* </tr>
* <tr>
* <td><code>src/test/resources/projects/it1/archetype.properties</code></td>
* <td>All required properties for the archetype being specified by <code>archetype.pom.properties</code> on this level</td>
* </tr>
* <tr>
* <td><code>src/test/resources/projects/it1/child</code></td>
* <td>Directory for maven module within integration test 1 (this folder's name is not relevant)</td>
* </tr>
* <tr>
* <td><code>src/test/resources/projects/it1/child/goal.txt</code></td>
* <td>The file containing the list of goals to be executed against the generated project</td>
* </tr>
* <tr>
* <td><code>src/test/resources/projects/it1/child/archetype.properties</code></td>
* <td>All required properties for this project's archetype</td>
* </tr>
* </table>
*
* <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
* goal from CLI.</p>
*
* @author rafale
*/
@Mojo( name = "integration-test", requiresProject = true )
public class IntegrationTestMojo
extends AbstractMojo
{
@Component
private ArchetypeGenerator archetypeGenerator;
@Component
private Invoker invoker;
@Component
private ArtifactResolver artifactResolver;
@Parameter( defaultValue = "${project.remoteArtifactRepositories}", readonly = true, required = true )
protected List<ArtifactRepository> remoteRepositories;
@Parameter( defaultValue = "${localRepository}", readonly = true, required = true )
protected ArtifactRepository localRepository;
/**
* The archetype project to execute the integration tests on.
*/
@Parameter( defaultValue = "${project}", readonly = true, required = true )
private MavenProject project;
@Parameter( defaultValue = "${session}", readonly = true, required = true )
private MavenSession session;
/**
* Skip the integration test.
*/
@Parameter( property = "archetype.test.skip" )
private boolean skip = false;
/**
* Directory of test projects
*
* @since 2.2
*/
@Parameter( property = "archetype.test.projectsDirectory", defaultValue = "${project.build.testOutputDirectory}/projects", required = true )
private File testProjectsDirectory;
/**
* Relative path of a cleanup/verification hook script to run after executing the build. This script may be written
* with either BeanShell or Groovy. If the file extension is omitted (e.g. <code>verify</code>), the
* plugin searches for the file by trying out the well-known extensions <code>.bsh</code> and <code>.groovy</code>.
* If this script exists for a particular project but returns any non-null value different from <code>true</code> or
* throws an exception, the corresponding build is flagged as a failure.
*
* @since 2.2
*/
@Parameter( property = "archetype.test.verifyScript", defaultValue = "verify" )
private String postBuildHookScript;
/**
* Suppress logging to the <code>build.log</code> file.
*
* @since 2.2
*/
@Parameter( property = "archetype.test.noLog", defaultValue = "false" )
private boolean noLog;
/**
* Flag used to determine whether the build logs should be output to the normal mojo log.
*
* @since 2.2
*/
@Parameter( property = "archetype.test.streamLogs", defaultValue = "true" )
private boolean streamLogs;
/**
* The file encoding for the post-build script.
*
* @since 2.2
*/
@Parameter( property = "encoding", defaultValue = "${project.build.sourceEncoding}" )
private String encoding;
/**
* The local repository to run maven instance.
*
* @since 2.2
*/
@Parameter( property = "archetype.test.localRepositoryPath", defaultValue = "${settings.localRepository}", required = true )
private File localRepositoryPath;
/**
* flag to enable show mvn version used for running its (cli option : -V,--show-version )
*
* @since 2.2
*/
@Parameter( property = "archetype.test.showVersion", defaultValue = "false" )
private boolean showVersion;
/**
* Ignores the EOL encoding for comparing files (default and original behaviour is false).
*
* @since 2.3
*/
@Parameter( property = "archetype.test.ignoreEOLStyle", defaultValue = "false" )
private boolean ignoreEOLStyle;
/**
* Whether to show debug statements in the build output.
*
* @since 2.2
*/
@Parameter( property = "archetype.test.debug", defaultValue = "false" )
private boolean debug;
/**
* A list of additional properties which will be used to filter tokens in settings.xml
*
* @since 2.2
*/
@Parameter
private Map<String, String> filterProperties;
/**
* The current user system settings for use in Maven.
*
* @since 2.2
*/
@Parameter( defaultValue = "${settings}", required = true, readonly = true )
private Settings settings;
/**
* Path to an alternate <code>settings.xml</code> to use for Maven invocation with all ITs. Note that the
* <code>&lt;localRepository&gt;</code> element of this settings file is always ignored, i.e. the path given by the
* parameter {@link #localRepositoryPath} is dominant.
*
* @since 2.2
*/
@Parameter( property = "archetype.test.settingsFile" )
private File settingsFile;
/**
* Common set of properties to pass in on each project's command line, via -D parameters.
*
* @since 3.0.2
*/
@Parameter
private Map<String, String> properties = new HashMap<>();
@Override
public void execute()
throws MojoExecutionException, MojoFailureException
{
if ( skip )
{
return;
}
if ( !testProjectsDirectory.exists() )
{
getLog().warn( "No Archetype IT projects: root 'projects' directory not found." );
return;
}
File archetypeFile = project.getArtifact().getFile();
if ( archetypeFile == null )
{
throw new MojoFailureException( "Unable to get the archetypes' artifact which should have just been built:"
+ " you probably launched 'mvn archetype:integration-test' instead of"
+ " 'mvn integration-test'." );
}
try
{
List<File> projectsGoalFiles = FileUtils.getFiles( testProjectsDirectory, "**/goal.txt", "" );
if ( projectsGoalFiles.size() == 0 )
{
getLog().warn( "No Archetype IT projects: no directory with goal.txt found." );
return;
}
StringWriter errorWriter = new StringWriter();
for ( File goalFile : projectsGoalFiles )
{
try
{
processIntegrationTest( goalFile, archetypeFile );
}
catch ( IntegrationTestFailure ex )
{
errorWriter.write( "\nArchetype IT '" + goalFile.getParentFile().getName() + "' failed: " );
errorWriter.write( ex.getMessage() );
}
}
String errors = errorWriter.toString();
if ( !StringUtils.isEmpty( errors ) )
{
throw new MojoExecutionException( errors );
}
}
catch ( IOException ex )
{
throw new MojoFailureException( ex, ex.getMessage(), ex.getMessage() );
}
}
/**
* Checks that actual directory content is the same as reference.
*
* @param reference the reference directory
* @param actual the actual directory to compare with the reference
* @throws IntegrationTestFailure if content differs
*/
private void assertDirectoryEquals( File reference, File actual )
throws IntegrationTestFailure, IOException
{
List<String> referenceFiles =
FileUtils.getFileAndDirectoryNames( reference, "**", null, false, true, true, true );
getLog().debug( "reference content: " + referenceFiles );
List<String> actualFiles = FileUtils.getFileAndDirectoryNames( actual, "**", null, false, true, true, true );
getLog().debug( "actual content: " + referenceFiles );
boolean fileNamesEquals = CollectionUtils.isEqualCollection( referenceFiles, actualFiles );
if ( !fileNamesEquals )
{
getLog().debug( "Actual list of files is not the same as reference:" );
int missing = 0;
for ( String ref : referenceFiles )
{
if ( actualFiles.contains( ref ) )
{
actualFiles.remove( ref );
getLog().debug( "Contained " + ref );
}
else
{
missing++;
getLog().error( "Not contained " + ref );
}
}
getLog().error( "Remains " + actualFiles );
throw new IntegrationTestFailure(
"Reference and generated project differs (missing: " + missing + ", unexpected: " + actualFiles.size()
+ ")" );
}
boolean contentEquals = true;
for ( String file : referenceFiles )
{
File referenceFile = new File( reference, file );
File actualFile = new File( actual, file );
if ( referenceFile.isDirectory() )
{
if ( actualFile.isFile() )
{
getLog().warn( "File " + file + " is a directory in the reference but a file in actual" );
contentEquals = false;
}
}
else if ( actualFile.isDirectory() )
{
if ( referenceFile.isFile() )
{
getLog().warn( "File " + file + " is a file in the reference but a directory in actual" );
contentEquals = false;
}
}
else if ( !contentEquals( referenceFile, actualFile ) )
{
getLog().warn( "Contents of file " + file + " are not equal" );
contentEquals = false;
}
}
if ( !contentEquals )
{
throw new IntegrationTestFailure( "Some content are not equals" );
}
}
/**
* Uses the {@link #ignoreEOLStyle} attribute to compare the two files. If {@link #ignoreEOLStyle} is true,
* then the comparison does not take care about the EOL (aka newline) character.
*/
private boolean contentEquals( File referenceFile, File actualFile )
throws IOException
{
// Original behaviour
if ( !ignoreEOLStyle )
{
getLog().warn( "Property ignoreEOLStyle was not set - files will be compared considering their EOL style!" );
return FileUtils.contentEquals( referenceFile, actualFile );
}
getLog().debug( "Comparing files with EOL style ignored." );
try ( BufferedReader referenceFileReader = new BufferedReader( new FileReader( referenceFile ) );
BufferedReader actualFileReader = new BufferedReader( new FileReader( actualFile ) ) )
{
String refLine = null;
String actualLine = null;
do
{
refLine = referenceFileReader.readLine();
actualLine = actualFileReader.readLine();
if ( !StringUtils.equals( refLine, actualLine ) )
{
return false;
}
}
while ( refLine != null || actualLine != null );
return true;
}
}
private Properties loadProperties( final File propertiesFile )
throws IOException
{
Properties properties = new Properties();
try ( InputStream in = new FileInputStream( propertiesFile ) )
{
properties.load( in );
}
return properties;
}
private void processIntegrationTest( File goalFile, File archetypeFile )
throws IntegrationTestFailure, MojoExecutionException
{
getLog().info( "Processing Archetype IT project: " + goalFile.getParentFile().getName() );
try
{
Properties properties = getProperties( goalFile );
File basedir = new File( goalFile.getParentFile(), "project" );
FileUtils.deleteDirectory( basedir );
FileUtils.mkdir( basedir.toString() );
basedir = setupParentProjects( goalFile.getParentFile().getParentFile(), basedir );
ArchetypeGenerationRequest request = generate( project.getGroupId(), project.getArtifactId(), project.getVersion(), archetypeFile, properties, basedir.toString() );
File reference = new File( goalFile.getParentFile(), "reference" );
if ( reference.exists() )
{
// compare generated project with reference
getLog().info( "Comparing generated project with reference content: " + reference );
assertDirectoryEquals( reference, new File( basedir, request.getArtifactId() ) );
}
String goals = FileUtils.fileRead( goalFile );
if ( StringUtils.isNotEmpty( goals ) )
{
invokePostArchetypeGenerationGoals( goals.trim(), new File( basedir, request.getArtifactId() ),
goalFile );
}
}
catch ( IOException ioe )
{
throw new IntegrationTestFailure( ioe );
}
}
private ArchetypeGenerationRequest generate( String archetypeGroupId, String archetypeArtifactId, String archetypeVersion, File archetypeFile, Properties properties, String basedir ) throws IntegrationTestFailure
{
//@formatter:off
ArchetypeGenerationRequest request =
new ArchetypeGenerationRequest().setArchetypeGroupId( archetypeGroupId ).setArchetypeArtifactId(
archetypeArtifactId ).setArchetypeVersion( archetypeVersion ).setGroupId(
properties.getProperty( Constants.GROUP_ID ) ).setArtifactId(
properties.getProperty( Constants.ARTIFACT_ID ) ).setVersion(
properties.getProperty( Constants.VERSION ) ).setPackage(
properties.getProperty( Constants.PACKAGE ) ).setOutputDirectory( basedir ).setProperties(
properties );
//@formatter:on
ArchetypeGenerationResult result = new ArchetypeGenerationResult();
archetypeGenerator.generateArchetype( request, archetypeFile, result );
if ( result.getCause() != null )
{
if ( result.getCause() instanceof ArchetypeNotConfigured )
{
ArchetypeNotConfigured anc = (ArchetypeNotConfigured) result.getCause();
throw new IntegrationTestFailure(
"Missing required properties in archetype.properties: " + StringUtils.join(
anc.getMissingProperties().iterator(), ", " ), anc );
}
throw new IntegrationTestFailure( result.getCause().getMessage(), result.getCause() );
}
return request;
}
private File setupParentProjects( File configFolder, File buildFolder )
throws IOException, MojoExecutionException, IntegrationTestFailure
{
// look for 'archetype.pom.properties'
File archetypePomPropertiesFile = new File( configFolder, "archetype.pom.properties" );
if ( !archetypePomPropertiesFile.exists() )
{
getLog().debug( "No 'archetype.pom.properties' file found in " + configFolder );
return buildFolder;
}
// go up to the parent configuration folder
buildFolder = setupParentProjects( configFolder.getParentFile(), buildFolder );
Properties archetypePomProperties = loadProperties( archetypePomPropertiesFile );
String groupId = archetypePomProperties.getProperty( Constants.GROUP_ID );
if ( StringUtils.isEmpty( groupId ) )
{
throw new MojoExecutionException( "Property " + Constants.GROUP_ID + " not set in " + archetypePomPropertiesFile );
}
String artifactId = archetypePomProperties.getProperty( Constants.ARTIFACT_ID );
if ( StringUtils.isEmpty( artifactId ) )
{
throw new MojoExecutionException( "Property " + Constants.ARTIFACT_ID + " not set in " + archetypePomPropertiesFile );
}
String version = archetypePomProperties.getProperty( Constants.VERSION );
if ( StringUtils.isEmpty( version ) )
{
throw new MojoExecutionException( "Property " + Constants.VERSION + " not set in " + archetypePomPropertiesFile );
}
File archetypeFile;
try
{
archetypeFile = getArchetypeFile( groupId, artifactId, version );
}
catch ( ArtifactResolverException e )
{
throw new MojoExecutionException( "Could not resolve archetype artifact " , e );
}
Properties archetypeProperties = getProperties( archetypePomPropertiesFile );
getLog().info( "Setting up parent project in " + buildFolder );
ArchetypeGenerationRequest request = generate( groupId, artifactId, version, archetypeFile, archetypeProperties, buildFolder.toString() );
return new File( buildFolder, request.getArtifactId() );
}
private File getArchetypeFile( String groupId, String artifactId, String version )
throws ArtifactResolverException
{
ProjectBuildingRequest buildingRequest = new DefaultProjectBuildingRequest( session.getProjectBuildingRequest() );
if ( localRepository != null )
{
buildingRequest = buildingRequest.setLocalRepository( localRepository );
}
if ( remoteRepositories != null && !remoteRepositories.isEmpty() )
{
buildingRequest = buildingRequest.setRemoteRepositories( remoteRepositories );
}
DefaultArtifactCoordinate coordinate = new DefaultArtifactCoordinate();
coordinate.setGroupId( groupId );
coordinate.setArtifactId( artifactId );
coordinate.setVersion( version );
return artifactResolver.resolveArtifact( buildingRequest, coordinate ).getArtifact().getFile();
}
private Properties getProperties( File goalFile )
throws IOException
{
File propertiesFile = new File( goalFile.getParentFile(), "archetype.properties" );
return loadProperties( propertiesFile );
}
private void invokePostArchetypeGenerationGoals( String goals, File basedir, File goalFile )
throws IntegrationTestFailure, IOException, MojoExecutionException
{
FileLogger logger = setupLogger( basedir );
if ( !StringUtils.isBlank( goals ) )
{
getLog().info( "Invoking post-archetype-generation goals: " + goals );
if ( !localRepositoryPath.exists() )
{
localRepositoryPath.mkdirs();
}
//@formatter:off
InvocationRequest request = new DefaultInvocationRequest().setBaseDirectory( basedir ).setGoals(
Arrays.asList( StringUtils.split( goals, "," ) ) ).setLocalRepositoryDirectory(
localRepositoryPath ).setBatchMode( true ).setShowErrors( true );
//@formatter:on
request.setDebug( debug );
request.setShowVersion( showVersion );
if ( logger != null )
{
request.setErrorHandler( logger );
request.setOutputHandler( logger );
}
if ( !properties.isEmpty() )
{
Properties props = new Properties();
for ( Map.Entry<String, String> entry : properties.entrySet() )
{
if ( entry.getValue() != null )
{
props.setProperty( entry.getKey(), entry.getValue() );
}
}
request.setProperties( props );
}
File interpolatedSettingsFile = null;
if ( settingsFile != null )
{
File interpolatedSettingsDirectory =
new File( project.getBuild().getOutputDirectory(), "archetype-it" );
if ( interpolatedSettingsDirectory.exists() )
{
FileUtils.deleteDirectory( interpolatedSettingsDirectory );
}
interpolatedSettingsDirectory.mkdir();
interpolatedSettingsFile =
new File( interpolatedSettingsDirectory, "interpolated-" + settingsFile.getName() );
buildInterpolatedFile( settingsFile, interpolatedSettingsFile );
request.setUserSettingsFile( interpolatedSettingsFile );
}
try
{
InvocationResult result = invoker.execute( request );
getLog().info( "Post-archetype-generation invoker exit code: " + result.getExitCode() );
if ( result.getExitCode() != 0 )
{
throw new IntegrationTestFailure( "Execution failure: exit code = " + result.getExitCode(),
result.getExecutionException() );
}
}
catch ( MavenInvocationException e )
{
throw new IntegrationTestFailure( "Cannot run additions goals.", e );
}
}
else
{
getLog().info( "No post-archetype-generation goals to invoke." );
}
// verify result
ScriptRunner scriptRunner = new ScriptRunner( getLog() );
scriptRunner.setScriptEncoding( encoding );
Map<String, Object> context = new LinkedHashMap<>();
context.put( "projectDir", basedir );
try
{
scriptRunner.run( "post-build script", goalFile.getParentFile(), postBuildHookScript, context, logger,
"failure post script", true );
}
catch ( RunFailureException e )
{
throw new IntegrationTestFailure( "post build script failure failure: " + e.getMessage(), e );
}
}
private FileLogger setupLogger( File basedir )
throws IOException
{
FileLogger logger = null;
if ( !noLog )
{
File outputLog = new File( basedir, "build.log" );
if ( streamLogs )
{
logger = new FileLogger( outputLog, getLog() );
}
else
{
logger = new FileLogger( outputLog );
}
getLog().debug( "build log initialized in: " + outputLog );
}
return logger;
}
class IntegrationTestFailure
extends Exception
{
IntegrationTestFailure()
{
super();
}
IntegrationTestFailure( String message )
{
super( message );
}
IntegrationTestFailure( Throwable cause )
{
super( cause );
}
IntegrationTestFailure( String message, Throwable cause )
{
super( message, cause );
}
}
/**
* Returns the map-based value source used to interpolate settings and other stuff.
*
* @return The map-based value source for interpolation, never <code>null</code>.
*/
private Map<String, Object> getInterpolationValueSource()
{
Map<String, Object> props = new HashMap<>();
if ( filterProperties != null )
{
props.putAll( filterProperties );
}
if ( filterProperties != null )
{
props.putAll( filterProperties );
}
props.put( "basedir", this.project.getBasedir().getAbsolutePath() );
props.put( "baseurl", toUrl( this.project.getBasedir().getAbsolutePath() ) );
if ( settings.getLocalRepository() != null )
{
props.put( "localRepository", settings.getLocalRepository() );
props.put( "localRepositoryUrl", toUrl( settings.getLocalRepository() ) );
}
return new CompositeMap( this.project, props );
}
protected void buildInterpolatedFile( File originalFile, File interpolatedFile )
throws MojoExecutionException
{
getLog().debug( "Interpolate " + originalFile.getPath() + " to " + interpolatedFile.getPath() );
try
{
String xml;
// interpolation with token @...@
Map<String, Object> composite = getInterpolationValueSource();
try ( Reader xmlStreamReader = ReaderFactory.newXmlReader( originalFile );
Reader reader = new InterpolationFilterReader( xmlStreamReader, composite, "@", "@" ) )
{
xml = IOUtil.toString( reader );
}
try ( Writer writer = WriterFactory.newXmlWriter( interpolatedFile ) )
{
interpolatedFile.getParentFile().mkdirs();
writer.write( xml );
writer.flush();
}
}
catch ( IOException e )
{
throw new MojoExecutionException( "Failed to interpolate file " + originalFile.getPath(), e );
}
}
private static class CompositeMap
implements Map<String, Object>
{
/**
* The Maven project from which to extract interpolated values, never <code>null</code>.
*/
private MavenProject mavenProject;
/**
* The set of additional properties from which to extract interpolated values, never <code>null</code>.
*/
private Map<String, Object> properties;
/**
* Creates a new interpolation source backed by the specified Maven project and some user-specified properties.
*
* @param mavenProject The Maven project from which to extract interpolated values, must not be
* <code>null</code>.
* @param properties The set of additional properties from which to extract interpolated values, may be
* <code>null</code>.
*/
protected CompositeMap( MavenProject mavenProject, Map<String, Object> properties )
{
if ( mavenProject == null )
{
throw new IllegalArgumentException( "no project specified" );
}
this.mavenProject = mavenProject;
this.properties = properties == null ? (Map) new Properties() : properties;
}
/**
* {@inheritDoc}
*
* @see java.util.Map#clear()
*/
@Override
public void clear()
{
// nothing here
}
/**
* {@inheritDoc}
*
* @see java.util.Map#containsKey(java.lang.Object)
*/
@Override
public boolean containsKey( Object key )
{
if ( !( key instanceof String ) )
{
return false;
}
String expression = (String) key;
if ( expression.startsWith( "project." ) || expression.startsWith( "pom." ) )
{
try
{
Object evaluated = ReflectionValueExtractor.evaluate( expression, this.mavenProject );
if ( evaluated != null )
{
return true;
}
}
catch ( Exception e )
{
// uhm do we have to throw a RuntimeException here ?
}
}
return properties.containsKey( key ) || mavenProject.getProperties().containsKey( key );
}
/**
* {@inheritDoc}
*
* @see java.util.Map#containsValue(java.lang.Object)
*/
@Override
public boolean containsValue( Object value )
{
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*
* @see java.util.Map#entrySet()
*/
@Override
public Set<Entry<String, Object>> entrySet()
{
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*
* @see java.util.Map#get(java.lang.Object)
*/
@Override
public Object get( Object key )
{
if ( !( key instanceof String ) )
{
return null;
}
String expression = (String) key;
if ( expression.startsWith( "project." ) || expression.startsWith( "pom." ) )
{
try
{
Object evaluated = ReflectionValueExtractor.evaluate( expression, this.mavenProject );
if ( evaluated != null )
{
return evaluated;
}
}
catch ( Exception e )
{
// uhm do we have to throw a RuntimeException here ?
}
}
Object value = properties.get( key );
return ( value != null ? value : this.mavenProject.getProperties().get( key ) );
}
/**
* {@inheritDoc}
*
* @see java.util.Map#isEmpty()
*/
@Override
public boolean isEmpty()
{
return this.mavenProject == null && this.mavenProject.getProperties().isEmpty()
&& this.properties.isEmpty();
}
/**
* {@inheritDoc}
*
* @see java.util.Map#keySet()
*/
@Override
public Set<String> keySet()
{
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*
* @see java.util.Map#put(java.lang.Object, java.lang.Object)
*/
@Override
public Object put( String key, Object value )
{
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*
* @see java.util.Map#putAll(java.util.Map)
*/
@Override
public void putAll( Map<? extends String, ? extends Object> t )
{
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*
* @see java.util.Map#remove(java.lang.Object)
*/
@Override
public Object remove( Object key )
{
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*
* @see java.util.Map#size()
*/
@Override
public int size()
{
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*
* @see java.util.Map#values()
*/
@Override
public Collection<Object> values()
{
throw new UnsupportedOperationException();
}
}
/**
* Converts the specified filesystem path to a URL. The resulting URL has no trailing slash regardless whether the
* path denotes a file or a directory.
*
* @param filename The filesystem path to convert, must not be <code>null</code>.
* @return The <code>file:</code> URL for the specified path, never <code>null</code>.
*/
private static String toUrl( String filename )
{
/*
* NOTE: Maven fails to properly handle percent-encoded "file:" URLs (WAGON-111) so don't use File.toURI() here
* as-is but use the decoded path component in the URL.
*/
String url = "file://" + new File( filename ).toURI().getPath();
if ( url.endsWith( "/" ) )
{
url = url.substring( 0, url.length() - 1 );
}
return url;
}
}