blob: bc828efaca658572454d97df35f616e9236aa900 [file] [log] [blame]
package org.apache.maven.plugin.resources.remote;
/*
* 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.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import org.apache.commons.io.output.DeferredFileOutputStream;
import org.apache.maven.ProjectDependenciesResolver;
import org.apache.maven.archiver.MavenArchiver;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DefaultArtifact;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.lifecycle.internal.ProjectArtifactFactory;
import org.apache.maven.model.Model;
import org.apache.maven.model.Organization;
import org.apache.maven.model.Resource;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.resources.remote.io.xpp3.RemoteResourcesBundleXpp3Reader;
import org.apache.maven.plugin.resources.remote.io.xpp3.SupplementalDataModelXpp3Reader;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
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.ProjectBuilder;
import org.apache.maven.project.ProjectBuildingException;
import org.apache.maven.project.ProjectBuildingRequest;
import org.apache.maven.project.ProjectBuildingResult;
import org.apache.maven.project.artifact.InvalidDependencyVersionException;
import org.apache.maven.repository.RepositorySystem;
import org.apache.maven.shared.artifact.filter.collection.ArtifactFilterException;
import org.apache.maven.shared.artifact.filter.collection.ArtifactIdFilter;
import org.apache.maven.shared.artifact.filter.collection.FilterArtifacts;
import org.apache.maven.shared.artifact.filter.collection.GroupIdFilter;
import org.apache.maven.shared.artifact.filter.collection.ProjectTransitivityFilter;
import org.apache.maven.shared.artifact.filter.collection.ScopeFilter;
import org.apache.maven.shared.filtering.MavenFileFilter;
import org.apache.maven.shared.filtering.MavenFileFilterRequest;
import org.apache.maven.shared.filtering.MavenFilteringException;
import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResolver;
import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResolverException;
import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResult;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.exception.MethodInvocationException;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.exception.VelocityException;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.RuntimeServices;
import org.apache.velocity.runtime.log.LogChute;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
import org.codehaus.plexus.resource.ResourceManager;
import org.codehaus.plexus.resource.loader.FileResourceLoader;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.WriterFactory;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
/**
* <p>
* Pull down resourceBundles containing remote resources and process the resources contained inside. When that is done,
* the resources are injected into the current (in-memory) Maven project, making them available to the process-resources
* phase.
* </p>
* <p>
* Resources that end in ".vm" are treated as Velocity templates. For those, the ".vm" is stripped off for the final
* artifact name and it's fed through Velocity to have properties expanded, conditions processed, etc...
* </p>
* <p/>
* Resources that don't end in ".vm" are copied "as is".
*/
// NOTE: Removed the following in favor of maven-artifact-resolver library, for MRRESOURCES-41
// If I leave this intact, interdependent projects within the reactor that haven't been built
// (remember, this runs in the generate-resources phase) will cause the build to fail.
//
// @requiresDependencyResolution test
@Mojo( name = "process", defaultPhase = LifecyclePhase.GENERATE_RESOURCES, threadSafe = true )
public class ProcessRemoteResourcesMojo
extends AbstractMojo
implements LogChute
{
private static final String TEMPLATE_SUFFIX = ".vm";
/**
* <p>
* In cases where a local resource overrides one from a remote resource bundle, that resource should be filtered if
* the resource set specifies it. In those cases, this parameter defines the list of delimiters for filterable
* expressions. These delimiters are specified in the form 'beginToken*endToken'. If no '*' is given, the delimiter
* is assumed to be the same for start and end.
* </p>
* <p>
* So, the default filtering delimiters might be specified as:
* </p>
*
* <pre>
* &lt;delimiters&gt;
* &lt;delimiter&gt;${*}&lt/delimiter&gt;
* &lt;delimiter&gt;@&lt/delimiter&gt;
* &lt;/delimiters&gt;
* </pre>
* <p/>
* Since the '@' delimiter is the same on both ends, we don't need to specify '@*@' (though we can).
*
* @since 1.1
*/
@Parameter
protected List<String> filterDelimiters;
/**
* @since 1.1
*/
@Parameter( defaultValue = "true" )
protected boolean useDefaultFilterDelimiters;
/**
* If true, only generate resources in the directory of the root project in a multimodule build.
* Dependencies from all modules will be aggregated before resource-generation takes place.
*
* @since 1.1
*/
@Parameter( defaultValue = "false" )
protected boolean runOnlyAtExecutionRoot;
/**
* Used for calculation of execution-root for {@link ProcessRemoteResourcesMojo#runOnlyAtExecutionRoot}.
*/
@Parameter( defaultValue = "${basedir}", readonly = true, required = true )
protected File basedir;
/**
* The character encoding scheme to be applied when filtering resources.
*/
@Parameter( property = "encoding", defaultValue = "${project.build.sourceEncoding}" )
protected String encoding;
/**
* The local repository taken from Maven's runtime. Typically <code>$HOME/.m2/repository</code>.
*/
@Parameter( defaultValue = "${localRepository}", readonly = true, required = true )
private ArtifactRepository localRepository;
/**
* List of Remote Repositories used by the resolver.
*/
@Parameter( defaultValue = "${project.remoteArtifactRepositories}", readonly = true, required = true )
private List<ArtifactRepository> remoteArtifactRepositories;
/**
* The current Maven project.
*/
@Parameter( defaultValue = "${project}", readonly = true, required = true )
private MavenProject project;
/**
* The directory where processed resources will be placed for packaging.
*/
@Parameter( defaultValue = "${project.build.directory}/maven-shared-archive-resources" )
private File outputDirectory;
/**
* The directory containing extra information appended to the generated resources.
*/
@Parameter( defaultValue = "${basedir}/src/main/appended-resources" )
private File appendedResourcesDirectory;
/**
* Supplemental model data. Useful when processing
* artifacts with incomplete POM metadata.
* <p/>
* By default, this Mojo looks for supplemental model data in the file
* "<code>${appendedResourcesDirectory}/supplemental-models.xml</code>".
*
* @since 1.0-alpha-5
*/
@Parameter
private String[] supplementalModels;
/**
* List of artifacts that are added to the search path when looking
* for supplementalModels, expressed with <code>groupId:artifactId:version[:type[:classifier]]</code> format.
*
* @since 1.1
*/
@Parameter
private List<String> supplementalModelArtifacts;
/**
* Map of artifacts to supplemental project object models.
*/
private Map<String, Model> supplementModels;
/**
* Merges supplemental data model with artifact metadata. Useful when processing artifacts with
* incomplete POM metadata.
*/
private ModelInheritanceAssembler inheritanceAssembler = new ModelInheritanceAssembler();
/**
* The resource bundles that will be retrieved and processed,
* expressed with <code>groupId:artifactId:version[:type[:classifier]]</code> format.
*/
@Parameter( required = true )
private List<String> resourceBundles;
/**
* Skip remote-resource processing
*
* @since 1.0-alpha-5
*/
@Parameter( property = "remoteresources.skip", defaultValue = "false" )
private boolean skip;
/**
* Attaches the resources to the main build of the project as a resource directory.
*
* @since 1.5
*/
@Parameter( defaultValue = "true", property = "attachToMain" )
private boolean attachToMain;
/**
* Attaches the resources to the test build of the project as a resource directory.
*
* @since 1.5
*/
@Parameter( defaultValue = "true", property = "attachToTest" )
private boolean attachToTest;
/**
* Additional properties to be passed to Velocity.
* <p/>
* Several properties are automatically added:<ul>
* <li><code>project</code> - the current MavenProject </li>
* <li><code>projects</code> - the list of dependency projects</li>
* <li><code>projectsSortedByOrganization</code> - the list of dependency projects sorted by organization</li>
* <li><code>projectTimespan</code> - the timespan of the current project (requires inceptionYear in pom)</li>
* <li><code>locator</code> - the ResourceManager that can be used to retrieve additional resources</li>
* </ul>
* See <a
* href="https://maven.apache.org/ref/current/maven-project/apidocs/org/apache/maven/project/MavenProject.html"> the
* javadoc for MavenProject</a> for information about the properties on the MavenProject.
*/
@Parameter
private Map<String, Object> properties = new HashMap<>();
/**
* Whether to include properties defined in the project when filtering resources.
*
* @since 1.2
*/
@Parameter( defaultValue = "false" )
protected boolean includeProjectProperties = false;
/**
* When the result of velocity transformation fits in memory, it is compared with the actual contents on disk
* to eliminate unnecessary destination file overwrite. This improves build times since further build steps
* typically rely on the modification date.
*
* @since 1.6
*/
@Parameter( defaultValue = "5242880" )
protected int velocityFilterInMemoryThreshold = 5 * 1024 * 1024;
/**
* The list of resources defined for the project.
*/
@Parameter( defaultValue = "${project.resources}", readonly = true, required = true )
private List<Resource> resources;
/**
* Repository system, needed to create Artifact and Repository objects.
*/
@Component
protected RepositorySystem repositorySystem;
/**
* Artifact Resolver, needed to resolve and download the {@code resourceBundles}.
*/
@Component
private ArtifactResolver artifactResolver;
/**
* Filtering support, for local resources that override those in the remote bundle.
*/
@Component
private MavenFileFilter fileFilter;
/**
* Project artifact factory, needed to create artifacts for the project dependencies.
*/
@Component
private ProjectArtifactFactory projectArtifactFactory;
/**
* The Maven session.
*/
@Parameter( defaultValue = "${session}", readonly = true, required = true )
private MavenSession mavenSession;
/**
* ProjectBuilder, needed to create projects from the artifacts.
*/
@Component( role = ProjectBuilder.class )
private ProjectBuilder projectBuilder;
/**
*/
@Component
private ResourceManager locator;
/**
* Scope to include. An Empty string indicates all scopes (default is "runtime").
*
* @since 1.0
*/
@Parameter( property = "includeScope", defaultValue = "runtime" )
protected String includeScope;
/**
* Scope to exclude. An Empty string indicates no scopes (default).
*
* @since 1.0
*/
@Parameter( property = "excludeScope", defaultValue = "" )
protected String excludeScope;
/**
* When resolving project dependencies, specify the scopes to include.
* The default is the same as "includeScope" if there are no exclude scopes set.
* Otherwise, it defaults to "test" to grab all the dependencies so the
* exclude filters can filter out what is not needed.
*
* @since 1.5
*/
@Parameter
private String[] resolveScopes;
/**
* Comma separated list of Artifact names too exclude.
*
* @since 1.0
*/
@Parameter( property = "excludeArtifactIds", defaultValue = "" )
protected String excludeArtifactIds;
/**
* Comma separated list of Artifact names to include.
*
* @since 1.0
*/
@Parameter( property = "includeArtifactIds", defaultValue = "" )
protected String includeArtifactIds;
/**
* Comma separated list of GroupId Names to exclude.
*
* @since 1.0
*/
@Parameter( property = "excludeGroupIds", defaultValue = "" )
protected String excludeGroupIds;
/**
* Comma separated list of GroupIds to include.
*
* @since 1.0
*/
@Parameter( property = "includeGroupIds", defaultValue = "" )
protected String includeGroupIds;
/**
* If we should exclude transitive dependencies
*
* @since 1.0
*/
@Parameter( property = "excludeTransitive", defaultValue = "false" )
protected boolean excludeTransitive;
/**
* Timestamp for reproducible output archive entries, either formatted as ISO 8601
* <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
* <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
*/
@Parameter( defaultValue = "${project.build.outputTimestamp}" )
private String outputTimestamp;
/**
*/
@Component( hint = "default" )
protected ProjectDependenciesResolver dependencyResolver;
private VelocityEngine velocity;
@Override
@SuppressWarnings( "unchecked" )
public void execute()
throws MojoExecutionException
{
if ( skip )
{
getLog().info( "Skipping remote resources execution." );
return;
}
if ( StringUtils.isEmpty( encoding ) )
{
getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
+ ", i.e. build is platform dependent!" );
}
if ( runOnlyAtExecutionRoot && !project.isExecutionRoot() )
{
getLog().info( "Skipping remote-resource generation in this project because it's not the Execution Root" );
return;
}
if ( resolveScopes == null )
{
resolveScopes =
new String[] { StringUtils.isEmpty( excludeScope ) ? this.includeScope : Artifact.SCOPE_TEST };
}
if ( supplementalModels == null )
{
File sups = new File( appendedResourcesDirectory, "supplemental-models.xml" );
if ( sups.exists() )
{
try
{
supplementalModels = new String[] { sups.toURI().toURL().toString() };
}
catch ( MalformedURLException e )
{
// ignore
getLog().debug( "URL issue with supplemental-models.xml: " + e.toString() );
}
}
}
configureLocator();
if ( includeProjectProperties )
{
final Properties projectProperties = project.getProperties();
for ( Object key : projectProperties.keySet() )
{
properties.put( key.toString(), projectProperties.get( key ).toString() );
}
}
ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
try
{
validate();
List<File> resourceBundleArtifacts = downloadBundles( resourceBundles );
supplementModels = loadSupplements( supplementalModels );
ClassLoader classLoader = initalizeClassloader( resourceBundleArtifacts );
Thread.currentThread().setContextClassLoader( classLoader );
velocity = new VelocityEngine();
velocity.setProperty( RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, this );
velocity.setProperty( "resource.loader", "classpath" );
velocity.setProperty( "classpath.resource.loader.class", ClasspathResourceLoader.class.getName() );
velocity.init();
VelocityContext context = buildVelocityContext( properties );
processResourceBundles( classLoader, context );
if ( outputDirectory.exists() )
{
// ----------------------------------------------------------------------------
// Push our newly generated resources directory into the MavenProject so that
// these resources can be picked up by the process-resources phase.
// ----------------------------------------------------------------------------
Resource resource = new Resource();
resource.setDirectory( outputDirectory.getAbsolutePath() );
// MRRESOURCES-61 handle main and test resources separately
if ( attachToMain )
{
project.getResources().add( resource );
}
if ( attachToTest )
{
project.getTestResources().add( resource );
}
// ----------------------------------------------------------------------------
// Write out archiver dot file
// ----------------------------------------------------------------------------
try
{
File dotFile = new File( project.getBuild().getDirectory(), ".plxarc" );
FileUtils.mkdir( dotFile.getParentFile().getAbsolutePath() );
FileUtils.fileWrite( dotFile.getAbsolutePath(), outputDirectory.getName() );
}
catch ( IOException e )
{
throw new MojoExecutionException( "Error creating dot file for archiving instructions.", e );
}
}
}
finally
{
Thread.currentThread().setContextClassLoader( origLoader );
}
}
private void configureLocator()
throws MojoExecutionException
{
if ( supplementalModelArtifacts != null && !supplementalModelArtifacts.isEmpty() )
{
List<File> artifacts = downloadBundles( supplementalModelArtifacts );
for ( File artifact : artifacts )
{
if ( artifact.isDirectory() )
{
locator.addSearchPath( FileResourceLoader.ID, artifact.getAbsolutePath() );
}
else
{
try
{
locator.addSearchPath( "jar", "jar:" + artifact.toURI().toURL().toExternalForm() );
}
catch ( MalformedURLException e )
{
throw new MojoExecutionException( "Could not use jar " + artifact.getAbsolutePath(), e );
}
}
}
}
locator.addSearchPath( FileResourceLoader.ID, project.getFile().getParentFile().getAbsolutePath() );
if ( appendedResourcesDirectory != null )
{
locator.addSearchPath( FileResourceLoader.ID, appendedResourcesDirectory.getAbsolutePath() );
}
locator.addSearchPath( "url", "" );
locator.setOutputDirectory( new File( project.getBuild().getDirectory() ) );
}
@SuppressWarnings( "unchecked" )
protected List<MavenProject> getProjects()
{
List<MavenProject> projects = new ArrayList<>();
// add filters in well known order, least specific to most specific
FilterArtifacts filter = new FilterArtifacts();
Set<Artifact> artifacts = resolveProjectArtifacts();
if ( this.excludeTransitive )
{
Set<Artifact> depArtifacts;
if ( runOnlyAtExecutionRoot )
{
depArtifacts = aggregateProjectDependencyArtifacts();
}
else
{
depArtifacts = project.getDependencyArtifacts();
}
filter.addFilter( new ProjectTransitivityFilter( depArtifacts, true ) );
}
filter.addFilter( new ScopeFilter( this.includeScope, this.excludeScope ) );
filter.addFilter( new GroupIdFilter( this.includeGroupIds, this.excludeGroupIds ) );
filter.addFilter( new ArtifactIdFilter( this.includeArtifactIds, this.excludeArtifactIds ) );
// perform filtering
try
{
artifacts = filter.filter( artifacts );
}
catch ( ArtifactFilterException e )
{
throw new IllegalStateException( e.getMessage(), e );
}
for ( Artifact artifact : artifacts )
{
List<ArtifactRepository> remoteRepo = remoteArtifactRepositories;
if ( artifact.isSnapshot() )
{
VersionRange rng = VersionRange.createFromVersion( artifact.getBaseVersion() );
artifact = new DefaultArtifact( artifact.getGroupId(), artifact.getArtifactId(), rng,
artifact.getType(), artifact.getClassifier(), artifact.getScope(),
artifact.getArtifactHandler(), artifact.isOptional() );
}
getLog().debug( "Building project for " + artifact );
MavenProject p;
try
{
ProjectBuildingRequest req = new DefaultProjectBuildingRequest()
.setLocalRepository( localRepository )
.setRemoteRepositories( remoteRepo );
ProjectBuildingResult res = projectBuilder.build( artifact, req );
p = res.getProject();
}
catch ( ProjectBuildingException e )
{
getLog().warn( "Invalid project model for artifact [" + artifact.getArtifactId() + ":"
+ artifact.getGroupId() + ":" + artifact.getVersion() + "]. "
+ "It will be ignored by the remote resources Mojo." );
continue;
}
String supplementKey =
generateSupplementMapKey( p.getModel().getGroupId(), p.getModel().getArtifactId() );
if ( supplementModels.containsKey( supplementKey ) )
{
Model mergedModel = mergeModels( p.getModel(), supplementModels.get( supplementKey ) );
MavenProject mergedProject = new MavenProject( mergedModel );
projects.add( mergedProject );
mergedProject.setArtifact( artifact );
mergedProject.setVersion( artifact.getVersion() );
getLog().debug( "Adding project with groupId [" + mergedProject.getGroupId() + "] (supplemented)" );
}
else
{
projects.add( p );
getLog().debug( "Adding project with groupId [" + p.getGroupId() + "]" );
}
}
Collections.sort( projects, new ProjectComparator() );
return projects;
}
private Set<Artifact> resolveProjectArtifacts()
{
try
{
if ( runOnlyAtExecutionRoot )
{
List<MavenProject> projects = mavenSession.getProjects();
return dependencyResolver.resolve( projects, Arrays.asList( resolveScopes ), mavenSession );
}
else
{
return dependencyResolver.resolve( project, Arrays.asList( resolveScopes ), mavenSession );
}
}
catch ( ArtifactResolutionException e )
{
throw new IllegalStateException( "Failed to resolve dependencies for one or more projects in the reactor. "
+ "Reason: " + e.getMessage(), e );
}
catch ( ArtifactNotFoundException e )
{
throw new IllegalStateException( "Failed to resolve dependencies for one or more projects in the reactor. "
+ "Reason: " + e.getMessage(), e );
}
}
@SuppressWarnings( "unchecked" )
private Set<Artifact> aggregateProjectDependencyArtifacts()
{
Set<Artifact> artifacts = new LinkedHashSet<>();
List<MavenProject> projects = mavenSession.getProjects();
for ( MavenProject p : projects )
{
if ( p.getDependencyArtifacts() == null )
{
try
{
Set<Artifact> depArtifacts = projectArtifactFactory.createArtifacts( p );
if ( depArtifacts != null && !depArtifacts.isEmpty() )
{
for ( Artifact artifact : depArtifacts )
{
if ( artifact.getVersion() == null && artifact.getVersionRange() != null )
{
// Version is required for equality comparison between artifacts,
// but it is not needed for our purposes of filtering out
// transitive dependencies (which requires only groupId/artifactId).
// Therefore set an arbitrary version if missing.
artifact.setResolvedVersion( Artifact.LATEST_VERSION );
}
artifacts.add( artifact );
}
}
}
catch ( InvalidDependencyVersionException e )
{
throw new IllegalStateException( "Failed to create dependency artifacts for: " + p.getId()
+ ". Reason: " + e.getMessage(), e );
}
}
}
return artifacts;
}
protected Map<Organization, List<MavenProject>> getProjectsSortedByOrganization( List<MavenProject> projects )
{
Map<Organization, List<MavenProject>> organizations =
new TreeMap<>( new OrganizationComparator() );
List<MavenProject> unknownOrganization = new ArrayList<>();
for ( MavenProject p : projects )
{
if ( p.getOrganization() != null && StringUtils.isNotEmpty( p.getOrganization().getName() ) )
{
List<MavenProject> sortedProjects = organizations.get( p.getOrganization() );
if ( sortedProjects == null )
{
sortedProjects = new ArrayList<>();
}
sortedProjects.add( p );
organizations.put( p.getOrganization(), sortedProjects );
}
else
{
unknownOrganization.add( p );
}
}
if ( !unknownOrganization.isEmpty() )
{
Organization unknownOrg = new Organization();
unknownOrg.setName( "an unknown organization" );
organizations.put( unknownOrg, unknownOrganization );
}
return organizations;
}
protected boolean copyResourceIfExists( File file, String relFileName, VelocityContext context )
throws IOException, MojoExecutionException
{
for ( Resource resource : resources )
{
File resourceDirectory = new File( resource.getDirectory() );
if ( !resourceDirectory.exists() )
{
continue;
}
// TODO - really should use the resource includes/excludes and name mapping
File source = new File( resourceDirectory, relFileName );
File templateSource = new File( resourceDirectory, relFileName + TEMPLATE_SUFFIX );
if ( !source.exists() && templateSource.exists() )
{
source = templateSource;
}
if ( source.exists() && !source.equals( file ) )
{
if ( source == templateSource )
{
try ( DeferredFileOutputStream os =
new DeferredFileOutputStream( velocityFilterInMemoryThreshold, file ) )
{
try ( Reader reader = getReader( source ); Writer writer = getWriter( os ) )
{
velocity.evaluate( context, writer, "", reader );
}
catch ( ParseErrorException | MethodInvocationException | ResourceNotFoundException e )
{
throw new MojoExecutionException( "Error rendering velocity resource: " + source, e );
}
fileWriteIfDiffers( os );
}
}
else if ( resource.isFiltering() )
{
MavenFileFilterRequest req = setupRequest( resource, source, file );
try
{
fileFilter.copyFile( req );
}
catch ( MavenFilteringException e )
{
throw new MojoExecutionException( "Error filtering resource: " + source, e );
}
}
else
{
FileUtils.copyFile( source, file );
}
// exclude the original (so eclipse doesn't complain about duplicate resources)
resource.addExclude( relFileName );
return true;
}
}
return false;
}
private Reader getReader( File source ) throws IOException
{
if ( encoding != null )
{
return new InputStreamReader( new FileInputStream( source ), encoding );
}
else
{
return ReaderFactory.newPlatformReader( source );
}
}
private Writer getWriter( OutputStream os ) throws IOException
{
if ( encoding != null )
{
return new OutputStreamWriter( os, encoding );
}
else
{
return WriterFactory.newPlatformWriter( os );
}
}
/**
* If the transformation result fits in memory and the destination file already exists
* then both are compared.
* <p>If destination file is byte-by-byte equal, then it is not overwritten.
* This improves subsequent compilation times since upstream plugins property see that
* the resource was not modified.
* <p>Note: the method should be called after {@link org.apache.commons.io.output.DeferredFileOutputStream#close}
*
* @param outStream Deferred stream
* @throws IOException
*/
private void fileWriteIfDiffers( DeferredFileOutputStream outStream )
throws IOException
{
File file = outStream.getFile();
if ( outStream.isThresholdExceeded() )
{
getLog().info( "File " + file + " was overwritten due to content limit threshold "
+ outStream.getThreshold() + " reached" );
return;
}
boolean needOverwrite = true;
if ( file.exists() )
{
try ( InputStream is = new FileInputStream( file ) )
{
final InputStream newContents = new ByteArrayInputStream( outStream.getData() );
needOverwrite = !IOUtil.contentEquals( is, newContents );
if ( getLog().isDebugEnabled() )
{
getLog().debug( "File " + file + " contents "
+ ( needOverwrite ? "differs" : "does not differ" ) );
}
}
}
if ( !needOverwrite )
{
getLog().debug( "File " + file + " is up to date" );
return;
}
getLog().debug( "Writing " + file );
try ( OutputStream os = new FileOutputStream( file ) )
{
outStream.writeTo( os );
}
}
private MavenFileFilterRequest setupRequest( Resource resource, File source, File file )
{
MavenFileFilterRequest req = new MavenFileFilterRequest();
req.setFrom( source );
req.setTo( file );
req.setFiltering( resource.isFiltering() );
req.setMavenProject( project );
req.setMavenSession( mavenSession );
req.setInjectProjectBuildFilters( true );
if ( encoding != null )
{
req.setEncoding( encoding );
}
if ( filterDelimiters != null && !filterDelimiters.isEmpty() )
{
LinkedHashSet<String> delims = new LinkedHashSet<>();
if ( useDefaultFilterDelimiters )
{
delims.addAll( req.getDelimiters() );
}
for ( String delim : filterDelimiters )
{
if ( delim == null )
{
delims.add( "${*}" );
}
else
{
delims.add( delim );
}
}
req.setDelimiters( delims );
}
return req;
}
protected void validate()
throws MojoExecutionException
{
int bundleCount = 1;
for ( String artifactDescriptor : resourceBundles )
{
// groupId:artifactId:version, groupId:artifactId:version:type
// or groupId:artifactId:version:type:classifier
String[] s = StringUtils.split( artifactDescriptor, ":" );
if ( s.length < 3 || s.length > 5 )
{
String position;
if ( bundleCount == 1 )
{
position = "1st";
}
else if ( bundleCount == 2 )
{
position = "2nd";
}
else if ( bundleCount == 3 )
{
position = "3rd";
}
else
{
position = bundleCount + "th";
}
throw new MojoExecutionException( "The " + position
+ " resource bundle configured must specify a groupId, artifactId, "
+ " version and, optionally, type and classifier for a remote resource bundle. "
+ "Must be of the form <resourceBundle>groupId:artifactId:version</resourceBundle>, "
+ "<resourceBundle>groupId:artifactId:version:type</resourceBundle> or "
+ "<resourceBundle>groupId:artifactId:version:type:classifier</resourceBundle>" );
}
bundleCount++;
}
}
private static final String KEY_PROJECTS = "projects";
private static final String KEY_PROJECTS_ORGS = "projectsSortedByOrganization";
protected VelocityContext buildVelocityContext( Map<String, Object> properties )
throws MojoExecutionException
{
// the following properties are expensive to calculate, so we provide them lazily
VelocityContext context = new VelocityContext( properties )
{
@Override
public Object internalGet( String key )
{
Object result = super.internalGet( key );
if ( result == null && key != null && key.startsWith( KEY_PROJECTS ) && containsKey( key ) )
{
// calculate and put projects* properties
List<MavenProject> projects = getProjects();
put( KEY_PROJECTS, projects );
put( KEY_PROJECTS_ORGS, getProjectsSortedByOrganization( projects ) );
return super.internalGet( key );
}
return result;
}
};
// to have a consistent getKeys()/containsKey() behaviour, keys must be present from the start
context.put( KEY_PROJECTS, null );
context.put( KEY_PROJECTS_ORGS, null );
// the following properties are cheap to calculate, so we provide them eagerly
// Reproducible Builds: try to use reproducible output timestamp
MavenArchiver archiver = new MavenArchiver();
Date outputDate = archiver.parseOutputTimestamp( outputTimestamp );
String inceptionYear = project.getInceptionYear();
String year = new SimpleDateFormat( "yyyy" ).format( ( outputDate == null ) ? new Date() : outputDate );
if ( StringUtils.isEmpty( inceptionYear ) )
{
if ( getLog().isDebugEnabled() )
{
getLog().debug( "inceptionYear not specified, defaulting to " + year );
}
inceptionYear = year;
}
context.put( "project", project );
context.put( "presentYear", year );
context.put( "locator", locator );
if ( inceptionYear.equals( year ) )
{
context.put( "projectTimespan", year );
}
else
{
context.put( "projectTimespan", inceptionYear + "-" + year );
}
return context;
}
private List<File> downloadBundles( List<String> bundles )
throws MojoExecutionException
{
List<File> bundleArtifacts = new ArrayList<>();
for ( String artifactDescriptor : bundles )
{
getLog().info( "Preparing remote bundle " + artifactDescriptor );
// groupId:artifactId:version[:type[:classifier]]
String[] s = artifactDescriptor.split( ":" );
File artifactFile = null;
// check if the artifact is part of the reactor
if ( mavenSession != null )
{
List<MavenProject> list = mavenSession.getProjects();
for ( MavenProject p : list )
{
if ( s[0].equals( p.getGroupId() ) && s[1].equals( p.getArtifactId() )
&& s[2].equals( p.getVersion() ) )
{
if ( s.length >= 4 && "test-jar".equals( s[3] ) )
{
artifactFile = new File( p.getBuild().getTestOutputDirectory() );
}
else
{
artifactFile = new File( p.getBuild().getOutputDirectory() );
}
}
}
}
if ( artifactFile == null || !artifactFile.exists() )
{
String type = ( s.length >= 4 ? s[3] : "jar" );
String classifier = ( s.length == 5 ? s[4] : null );
Artifact artifact = repositorySystem.createArtifactWithClassifier( s[0], s[1], s[2], type, classifier );
try
{
ArtifactResult result = artifactResolver.resolveArtifact(
mavenSession.getProjectBuildingRequest(), artifact );
artifactFile = result.getArtifact().getFile();
}
catch ( ArtifactResolverException e )
{
throw new MojoExecutionException( "Error processing remote resources", e );
}
}
bundleArtifacts.add( artifactFile );
}
return bundleArtifacts;
}
private ClassLoader initalizeClassloader( List<File> artifacts )
throws MojoExecutionException
{
RemoteResourcesClassLoader cl = new RemoteResourcesClassLoader( null );
try
{
for ( File artifact : artifacts )
{
cl.addURL( artifact.toURI().toURL() );
}
return cl;
}
catch ( MalformedURLException e )
{
throw new MojoExecutionException( "Unable to configure resources classloader: " + e.getMessage(), e );
}
}
protected void processResourceBundles( ClassLoader classLoader, VelocityContext context )
throws MojoExecutionException
{
List<Map.Entry<String, RemoteResourcesBundle>> remoteResources =
new ArrayList<>();
int bundleCount = 0;
int resourceCount = 0;
// list remote resources form bundles
try
{
RemoteResourcesBundleXpp3Reader bundleReader = new RemoteResourcesBundleXpp3Reader();
for ( Enumeration<URL> e =
classLoader.getResources( BundleRemoteResourcesMojo.RESOURCES_MANIFEST ); e.hasMoreElements(); )
{
URL url = e.nextElement();
bundleCount++;
getLog().debug( "processResourceBundle on bundle#" + bundleCount + " " + url );
RemoteResourcesBundle bundle;
try ( InputStream in = url.openStream() )
{
bundle = bundleReader.read( in );
}
int n = 0;
for ( String bundleResource : bundle.getRemoteResources() )
{
n++;
resourceCount++;
getLog().debug( "bundle#" + bundleCount + " resource#" + n + " " + bundleResource );
remoteResources.add( new AbstractMap.SimpleEntry<>( bundleResource,
bundle ) );
}
}
}
catch ( IOException ioe )
{
throw new MojoExecutionException( "Error finding remote resources manifests", ioe );
}
catch ( XmlPullParserException xppe )
{
throw new MojoExecutionException( "Error parsing remote resource bundle descriptor.", xppe );
}
getLog().info( "Copying " + resourceCount + " resource" + ( ( resourceCount > 1 ) ? "s" : "" ) + " from "
+ bundleCount + " bundle" + ( ( bundleCount > 1 ) ? "s" : "" ) + "." );
String velocityResource = null;
try
{
for ( Map.Entry<String, RemoteResourcesBundle> entry : remoteResources )
{
String bundleResource = entry.getKey();
RemoteResourcesBundle bundle = entry.getValue();
String projectResource = bundleResource;
boolean doVelocity = false;
if ( projectResource.endsWith( TEMPLATE_SUFFIX ) )
{
projectResource = projectResource.substring( 0, projectResource.length() - 3 );
velocityResource = bundleResource;
doVelocity = true;
}
// Don't overwrite resource that are already being provided.
File f = new File( outputDirectory, projectResource );
FileUtils.mkdir( f.getParentFile().getAbsolutePath() );
if ( !copyResourceIfExists( f, projectResource, context ) )
{
if ( doVelocity )
{
try ( DeferredFileOutputStream os =
new DeferredFileOutputStream( velocityFilterInMemoryThreshold, f ) )
{
try ( Writer writer = bundle.getSourceEncoding() == null ? new OutputStreamWriter( os )
: new OutputStreamWriter( os, bundle.getSourceEncoding() ) )
{
if ( bundle.getSourceEncoding() == null )
{
// TODO: Is this correct? Shouldn't we behave like the rest of maven and fail
// down to JVM default instead ISO-8859-1 ?
velocity.mergeTemplate( bundleResource, "ISO-8859-1", context, writer );
}
else
{
velocity.mergeTemplate( bundleResource, bundle.getSourceEncoding(), context,
writer );
}
}
fileWriteIfDiffers( os );
}
}
else
{
URL resUrl = classLoader.getResource( bundleResource );
if ( resUrl != null )
{
FileUtils.copyURLToFile( resUrl, f );
}
}
File appendedResourceFile = new File( appendedResourcesDirectory, projectResource );
File appendedVmResourceFile = new File( appendedResourcesDirectory, projectResource + ".vm" );
if ( appendedResourceFile.exists() )
{
getLog().info( "Copying appended resource: " + projectResource );
try ( InputStream in = new FileInputStream( appendedResourceFile );
OutputStream out = new FileOutputStream( f, true ) )
{
IOUtil.copy( in, out );
}
}
else if ( appendedVmResourceFile.exists() )
{
getLog().info( "Filtering appended resource: " + projectResource + ".vm" );
try ( Reader reader = new FileReader( appendedVmResourceFile );
Writer writer = getWriter( bundle, f ) )
{
Velocity.init();
Velocity.evaluate( context, writer, "remote-resources", reader );
}
}
}
}
}
catch ( IOException ioe )
{
throw new MojoExecutionException( "Error reading remote resource", ioe );
}
catch ( VelocityException e )
{
throw new MojoExecutionException( "Error rendering Velocity resource '" + velocityResource + "'", e );
}
}
private Writer getWriter( RemoteResourcesBundle bundle, File f )
throws IOException, UnsupportedEncodingException, FileNotFoundException
{
Writer writer;
if ( bundle.getSourceEncoding() == null )
{
writer = new PrintWriter( new FileWriter( f, true ) );
}
else
{
writer = new PrintWriter( new OutputStreamWriter( new FileOutputStream( f, true ),
bundle.getSourceEncoding() ) );
}
return writer;
}
protected Model getSupplement( Xpp3Dom supplementModelXml )
throws MojoExecutionException
{
MavenXpp3Reader modelReader = new MavenXpp3Reader();
Model model = null;
try
{
model = modelReader.read( new StringReader( supplementModelXml.toString() ) );
String groupId = model.getGroupId();
String artifactId = model.getArtifactId();
if ( groupId == null || groupId.trim().equals( "" ) )
{
throw new MojoExecutionException( "Supplemental project XML "
+ "requires that a <groupId> element be present." );
}
if ( artifactId == null || artifactId.trim().equals( "" ) )
{
throw new MojoExecutionException( "Supplemental project XML "
+ "requires that a <artifactId> element be present." );
}
}
catch ( IOException e )
{
getLog().warn( "Unable to read supplemental XML: " + e.getMessage(), e );
}
catch ( XmlPullParserException e )
{
getLog().warn( "Unable to parse supplemental XML: " + e.getMessage(), e );
}
return model;
}
protected Model mergeModels( Model parent, Model child )
{
inheritanceAssembler.assembleModelInheritance( child, parent );
return child;
}
private static String generateSupplementMapKey( String groupId, String artifactId )
{
return groupId.trim() + ":" + artifactId.trim();
}
private Map<String, Model> loadSupplements( String models[] )
throws MojoExecutionException
{
if ( models == null )
{
getLog().debug( "Supplemental data models won't be loaded. " + "No models specified." );
return Collections.emptyMap();
}
List<Supplement> supplements = new ArrayList<>();
for ( String set : models )
{
getLog().debug( "Preparing ruleset: " + set );
try
{
File f = locator.getResourceAsFile( set, getLocationTemp( set ) );
if ( null == f || !f.exists() )
{
throw new MojoExecutionException( "Cold not resolve " + set );
}
if ( !f.canRead() )
{
throw new MojoExecutionException( "Supplemental data models won't be loaded. " + "File "
+ f.getAbsolutePath() + " cannot be read, check permissions on the file." );
}
getLog().debug( "Loading supplemental models from " + f.getAbsolutePath() );
SupplementalDataModelXpp3Reader reader = new SupplementalDataModelXpp3Reader();
SupplementalDataModel supplementalModel = reader.read( new FileReader( f ) );
supplements.addAll( supplementalModel.getSupplement() );
}
catch ( Exception e )
{
String msg = "Error loading supplemental data models: " + e.getMessage();
getLog().error( msg, e );
throw new MojoExecutionException( msg, e );
}
}
getLog().debug( "Loading supplements complete." );
Map<String, Model> supplementMap = new HashMap<>();
for ( Supplement sd : supplements )
{
Xpp3Dom dom = (Xpp3Dom) sd.getProject();
Model m = getSupplement( dom );
supplementMap.put( generateSupplementMapKey( m.getGroupId(), m.getArtifactId() ), m );
}
return supplementMap;
}
/**
* Convenience method to get the location of the specified file name.
*
* @param name the name of the file whose location is to be resolved
* @return a String that contains the absolute file name of the file
*/
private String getLocationTemp( String name )
{
String loc = name;
if ( loc.indexOf( '/' ) != -1 )
{
loc = loc.substring( loc.lastIndexOf( '/' ) + 1 );
}
if ( loc.indexOf( '\\' ) != -1 )
{
loc = loc.substring( loc.lastIndexOf( '\\' ) + 1 );
}
getLog().debug( "Before: " + name + " After: " + loc );
return loc;
}
class OrganizationComparator
implements Comparator<Organization>
{
@Override
public int compare( Organization org1, Organization org2 )
{
int i = compareStrings( org1.getName(), org2.getName() );
if ( i == 0 )
{
i = compareStrings( org1.getUrl(), org2.getUrl() );
}
return i;
}
public boolean equals( Organization o1, Organization o2 )
{
return compare( o1, o2 ) == 0;
}
private int compareStrings( String s1, String s2 )
{
if ( s1 == null && s2 == null )
{
return 0;
}
else if ( s1 == null && s2 != null )
{
return 1;
}
else if ( s1 != null && s2 == null )
{
return -1;
}
return s1.compareToIgnoreCase( s2 );
}
}
class ProjectComparator
implements Comparator<MavenProject>
{
@Override
public int compare( MavenProject p1, MavenProject p2 )
{
return p1.getArtifact().compareTo( p2.getArtifact() );
}
public boolean equals( MavenProject p1, MavenProject p2 )
{
return p1.getArtifact().equals( p2.getArtifact() );
}
}
/* LogChute methods */
@Override
public void init( RuntimeServices rs )
throws Exception
{
}
@Override
public void log( int level, String message )
{
switch ( level )
{
case LogChute.WARN_ID:
getLog().warn( message );
break;
case LogChute.INFO_ID:
// velocity info messages are too verbose, just consider them as debug messages...
getLog().debug( message );
break;
case LogChute.DEBUG_ID:
getLog().debug( message );
break;
case LogChute.ERROR_ID:
getLog().error( message );
break;
default:
getLog().debug( message );
break;
}
}
@Override
public void log( int level, String message, Throwable t )
{
switch ( level )
{
case LogChute.WARN_ID:
getLog().warn( message, t );
break;
case LogChute.INFO_ID:
// velocity info messages are too verbose, just consider them as debug messages...
getLog().debug( message, t );
break;
case LogChute.DEBUG_ID:
getLog().debug( message, t );
break;
case LogChute.ERROR_ID:
getLog().error( message, t );
break;
default:
getLog().debug( message, t );
break;
}
}
@Override
public boolean isLevelEnabled( int level )
{
return false;
}
}