blob: 5242da71b15c06e95a16c9cd417028468651a152 [file] [log] [blame]
package org.apache.maven.archiver;
/*
* 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 javax.lang.model.SourceVersion;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.jar.Attributes;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.apache.maven.artifact.versioning.OverConstrainedVersionException;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.archiver.jar.JarArchiver;
import org.codehaus.plexus.archiver.jar.Manifest;
import org.codehaus.plexus.archiver.jar.ManifestException;
import org.codehaus.plexus.interpolation.InterpolationException;
import org.codehaus.plexus.interpolation.Interpolator;
import org.codehaus.plexus.interpolation.PrefixAwareRecursionInterceptor;
import org.codehaus.plexus.interpolation.PrefixedObjectValueSource;
import org.codehaus.plexus.interpolation.PrefixedPropertiesValueSource;
import org.codehaus.plexus.interpolation.RecursionInterceptor;
import org.codehaus.plexus.interpolation.StringSearchInterpolator;
import org.codehaus.plexus.interpolation.ValueSource;
import org.codehaus.plexus.util.StringUtils;
import static org.apache.maven.archiver.ManifestConfiguration.CLASSPATH_LAYOUT_TYPE_CUSTOM;
import static org.apache.maven.archiver.ManifestConfiguration.CLASSPATH_LAYOUT_TYPE_REPOSITORY;
import static org.apache.maven.archiver.ManifestConfiguration.CLASSPATH_LAYOUT_TYPE_SIMPLE;
/**
* <p>MavenArchiver class.</p>
*
* @author <a href="evenisse@apache.org">Emmanuel Venisse</a>
* @author kama
* @version $Id: $Id
*/
public class MavenArchiver
{
private static final String CREATED_BY = "Maven Archiver";
/**
* The simply layout.
*/
public static final String SIMPLE_LAYOUT =
"${artifact.artifactId}-${artifact.version}${dashClassifier?}.${artifact.extension}";
/**
* Repository layout.
*/
public static final String REPOSITORY_LAYOUT =
"${artifact.groupIdPath}/${artifact.artifactId}/" + "${artifact.baseVersion}/${artifact.artifactId}-"
+ "${artifact.version}${dashClassifier?}.${artifact.extension}";
/**
* simple layout non unique.
*/
public static final String SIMPLE_LAYOUT_NONUNIQUE =
"${artifact.artifactId}-${artifact.baseVersion}${dashClassifier?}.${artifact.extension}";
/**
* Repository layout non unique.
*/
public static final String REPOSITORY_LAYOUT_NONUNIQUE =
"${artifact.groupIdPath}/${artifact.artifactId}/" + "${artifact.baseVersion}/${artifact.artifactId}-"
+ "${artifact.baseVersion}${dashClassifier?}.${artifact.extension}";
private static final Instant DATE_MIN = Instant.parse( "1980-01-01T00:00:02Z" );
private static final Instant DATE_MAX = Instant.parse( "2099-12-31T23:59:59Z" );
private static final List<String> ARTIFACT_EXPRESSION_PREFIXES;
static
{
List<String> artifactExpressionPrefixes = new ArrayList<>();
artifactExpressionPrefixes.add( "artifact." );
ARTIFACT_EXPRESSION_PREFIXES = artifactExpressionPrefixes;
}
static boolean isValidModuleName( String name )
{
return SourceVersion.isName( name );
}
private JarArchiver archiver;
private File archiveFile;
private String createdBy;
private boolean buildJdkSpecDefaultEntry = true;
/**
* <p>getManifest.</p>
*
* @param session the Maven Session
* @param project the Maven Project
* @param config the MavenArchiveConfiguration
* @return the {@link org.codehaus.plexus.archiver.jar.Manifest}
* @throws org.codehaus.plexus.archiver.jar.ManifestException in case of a failure
* @throws org.apache.maven.artifact.DependencyResolutionRequiredException resolution failure
*/
public Manifest getManifest( MavenSession session, MavenProject project, MavenArchiveConfiguration config )
throws ManifestException, DependencyResolutionRequiredException
{
boolean hasManifestEntries = !config.isManifestEntriesEmpty();
Map<String, String> entries =
hasManifestEntries ? config.getManifestEntries() : Collections.emptyMap();
Manifest manifest = getManifest( session, project, config.getManifest(), entries );
// any custom manifest entries in the archive configuration manifest?
if ( hasManifestEntries )
{
for ( Map.Entry<String, String> entry : entries.entrySet() )
{
String key = entry.getKey();
String value = entry.getValue();
Manifest.Attribute attr = manifest.getMainSection().getAttribute( key );
if ( key.equals( Attributes.Name.CLASS_PATH.toString() ) && attr != null )
{
// Merge the user-supplied Class-Path value with the programmatically
// created Class-Path. Note that the user-supplied value goes first
// so that resources there will override any in the standard Class-Path.
attr.setValue( value + " " + attr.getValue() );
}
else
{
addManifestAttribute( manifest, key, value );
}
}
}
// any custom manifest sections in the archive configuration manifest?
if ( !config.isManifestSectionsEmpty() )
{
for ( ManifestSection section : config.getManifestSections() )
{
Manifest.Section theSection = new Manifest.Section();
theSection.setName( section.getName() );
if ( !section.isManifestEntriesEmpty() )
{
Map<String, String> sectionEntries = section.getManifestEntries();
for ( Map.Entry<String, String> entry : sectionEntries.entrySet() )
{
String key = entry.getKey();
String value = entry.getValue();
Manifest.Attribute attr = new Manifest.Attribute( key, value );
theSection.addConfiguredAttribute( attr );
}
}
manifest.addConfiguredSection( theSection );
}
}
return manifest;
}
/**
* Return a pre-configured manifest.
*
* @param project {@link org.apache.maven.project.MavenProject}
* @param config {@link org.apache.maven.archiver.ManifestConfiguration}
* @return {@link org.codehaus.plexus.archiver.jar.Manifest}
* @throws org.codehaus.plexus.archiver.jar.ManifestException Manifest exception.
* @throws org.apache.maven.artifact.DependencyResolutionRequiredException Dependency resolution exception.
*/
// TODO Add user attributes list and user groups list
public Manifest getManifest( MavenProject project, ManifestConfiguration config )
throws ManifestException, DependencyResolutionRequiredException
{
return getManifest( null, project, config, Collections.emptyMap() );
}
/**
* <p>getManifest.</p>
*
* @param mavenSession {@link org.apache.maven.execution.MavenSession}
* @param project {@link org.apache.maven.project.MavenProject}
* @param config {@link org.apache.maven.archiver.ManifestConfiguration}
* @return {@link org.codehaus.plexus.archiver.jar.Manifest}
* @throws org.codehaus.plexus.archiver.jar.ManifestException the manifest exception
* @throws org.apache.maven.artifact.DependencyResolutionRequiredException the dependency resolution required
* exception
*/
public Manifest getManifest( MavenSession mavenSession, MavenProject project, ManifestConfiguration config )
throws ManifestException, DependencyResolutionRequiredException
{
return getManifest( mavenSession, project, config, Collections.emptyMap() );
}
private void addManifestAttribute( Manifest manifest, Map<String, String> map, String key, String value )
throws ManifestException
{
if ( map.containsKey( key ) )
{
return; // The map value will be added later
}
addManifestAttribute( manifest, key, value );
}
private void addManifestAttribute( Manifest manifest, String key, String value )
throws ManifestException
{
if ( !StringUtils.isEmpty( value ) )
{
Manifest.Attribute attr = new Manifest.Attribute( key, value );
manifest.addConfiguredAttribute( attr );
}
else
{
// if the value is empty, create an entry with an empty string
// to prevent null print in the manifest file
Manifest.Attribute attr = new Manifest.Attribute( key, "" );
manifest.addConfiguredAttribute( attr );
}
}
/**
* <p>getManifest.</p>
*
* @param session {@link org.apache.maven.execution.MavenSession}
* @param project {@link org.apache.maven.project.MavenProject}
* @param config {@link org.apache.maven.archiver.ManifestConfiguration}
* @param entries The entries.
* @return {@link org.codehaus.plexus.archiver.jar.Manifest}
* @throws org.codehaus.plexus.archiver.jar.ManifestException the manifest exception
* @throws org.apache.maven.artifact.DependencyResolutionRequiredException the dependency resolution required
* exception
*/
protected Manifest getManifest( MavenSession session, MavenProject project, ManifestConfiguration config,
Map<String, String> entries )
throws ManifestException, DependencyResolutionRequiredException
{
// TODO: Should we replace "map" with a copy? Note, that we modify it!
Manifest m = new Manifest();
if ( config.isAddDefaultEntries() )
{
handleDefaultEntries( m, entries );
}
if ( config.isAddBuildEnvironmentEntries() )
{
handleBuildEnvironmentEntries( session, m, entries );
}
if ( config.isAddClasspath() )
{
StringBuilder classpath = new StringBuilder();
List<String> artifacts = project.getRuntimeClasspathElements();
String classpathPrefix = config.getClasspathPrefix();
String layoutType = config.getClasspathLayoutType();
String layout = config.getCustomClasspathLayout();
Interpolator interpolator = new StringSearchInterpolator();
for ( String artifactFile : artifacts )
{
File f = new File( artifactFile );
if ( f.getAbsoluteFile().isFile() )
{
Artifact artifact = findArtifactWithFile( project.getArtifacts(), f );
if ( classpath.length() > 0 )
{
classpath.append( " " );
}
classpath.append( classpathPrefix );
// NOTE: If the artifact or layout type (from config) is null, give up and use the file name by
// itself.
if ( artifact == null || layoutType == null )
{
classpath.append( f.getName() );
}
else
{
List<ValueSource> valueSources = new ArrayList<>();
handleExtraExpression( artifact, valueSources );
for ( ValueSource vs : valueSources )
{
interpolator.addValueSource( vs );
}
RecursionInterceptor recursionInterceptor =
new PrefixAwareRecursionInterceptor( ARTIFACT_EXPRESSION_PREFIXES );
try
{
switch ( layoutType )
{
case CLASSPATH_LAYOUT_TYPE_SIMPLE:
if ( config.isUseUniqueVersions() )
{
classpath.append( interpolator.interpolate( SIMPLE_LAYOUT,
recursionInterceptor ) );
}
else
{
classpath.append( interpolator.interpolate( SIMPLE_LAYOUT_NONUNIQUE,
recursionInterceptor ) );
}
break;
case CLASSPATH_LAYOUT_TYPE_REPOSITORY:
// we use layout /$groupId[0]/../${groupId[n]/$artifactId/$version/{fileName}
// here we must find the Artifact in the project Artifacts
// to create the maven layout
if ( config.isUseUniqueVersions() )
{
classpath.append( interpolator.interpolate( REPOSITORY_LAYOUT,
recursionInterceptor ) );
}
else
{
classpath.append( interpolator.interpolate( REPOSITORY_LAYOUT_NONUNIQUE,
recursionInterceptor ) );
}
break;
case CLASSPATH_LAYOUT_TYPE_CUSTOM:
if ( layout == null )
{
throw new ManifestException( CLASSPATH_LAYOUT_TYPE_CUSTOM
+ " layout type was declared, but custom layout expression was not"
+ " specified. Check your <archive><manifest><customLayout/>"
+ " element." );
}
classpath.append( interpolator.interpolate( layout, recursionInterceptor ) );
break;
default:
throw new ManifestException( "Unknown classpath layout type: '" + layoutType
+ "'. Check your <archive><manifest><layoutType/> element." );
}
}
catch ( InterpolationException e )
{
ManifestException error =
new ManifestException( "Error interpolating artifact path for classpath entry: "
+ e.getMessage() );
error.initCause( e );
throw error;
}
finally
{
for ( ValueSource vs : valueSources )
{
interpolator.removeValuesSource( vs );
}
}
}
}
}
if ( classpath.length() > 0 )
{
// Class-Path is special and should be added to manifest even if
// it is specified in the manifestEntries section
addManifestAttribute( m, "Class-Path", classpath.toString() );
}
}
if ( config.isAddDefaultSpecificationEntries() )
{
handleSpecificationEntries( project, entries, m );
}
if ( config.isAddDefaultImplementationEntries() )
{
handleImplementationEntries( project, entries, m );
}
String mainClass = config.getMainClass();
if ( mainClass != null && !"".equals( mainClass ) )
{
addManifestAttribute( m, entries, "Main-Class", mainClass );
}
if ( config.isAddExtensions() )
{
handleExtensions( project, entries, m );
}
addCustomEntries( m, entries, config );
return m;
}
private void handleExtraExpression( Artifact artifact, List<ValueSource> valueSources )
{
valueSources.add( new PrefixedObjectValueSource( ARTIFACT_EXPRESSION_PREFIXES, artifact,
true ) );
valueSources.add( new PrefixedObjectValueSource( ARTIFACT_EXPRESSION_PREFIXES,
artifact.getArtifactHandler(), true ) );
Properties extraExpressions = new Properties();
// FIXME: This query method SHOULD NOT affect the internal
// state of the artifact version, but it does.
if ( !artifact.isSnapshot() )
{
extraExpressions.setProperty( "baseVersion", artifact.getVersion() );
}
extraExpressions.setProperty( "groupIdPath", artifact.getGroupId().replace( '.', '/' ) );
if ( StringUtils.isNotEmpty( artifact.getClassifier() ) )
{
extraExpressions.setProperty( "dashClassifier", "-" + artifact.getClassifier() );
extraExpressions.setProperty( "dashClassifier?", "-" + artifact.getClassifier() );
}
else
{
extraExpressions.setProperty( "dashClassifier", "" );
extraExpressions.setProperty( "dashClassifier?", "" );
}
valueSources.add( new PrefixedPropertiesValueSource( ARTIFACT_EXPRESSION_PREFIXES,
extraExpressions, true ) );
}
private void handleExtensions( MavenProject project, Map<String, String> entries, Manifest m )
throws ManifestException
{
// TODO: this is only for applets - should we distinguish them as a packaging?
StringBuilder extensionsList = new StringBuilder();
Set<Artifact> artifacts = project.getArtifacts();
for ( Artifact artifact : artifacts )
{
if ( !Artifact.SCOPE_TEST.equals( artifact.getScope() ) )
{
if ( "jar".equals( artifact.getType() ) )
{
if ( extensionsList.length() > 0 )
{
extensionsList.append( " " );
}
extensionsList.append( artifact.getArtifactId() );
}
}
}
if ( extensionsList.length() > 0 )
{
addManifestAttribute( m, entries, "Extension-List", extensionsList.toString() );
}
for ( Artifact artifact : artifacts )
{
// TODO: the correct solution here would be to have an extension type, and to read
// the real extension values either from the artifact's manifest or some part of the POM
if ( "jar".equals( artifact.getType() ) )
{
String artifactId = artifact.getArtifactId().replace( '.', '_' );
String ename = artifactId + "-Extension-Name";
addManifestAttribute( m, entries, ename, artifact.getArtifactId() );
String iname = artifactId + "-Implementation-Version";
addManifestAttribute( m, entries, iname, artifact.getVersion() );
if ( artifact.getRepository() != null )
{
iname = artifactId + "-Implementation-URL";
String url = artifact.getRepository().getUrl() + "/" + artifact;
addManifestAttribute( m, entries, iname, url );
}
}
}
}
private void handleImplementationEntries( MavenProject project, Map<String, String> entries, Manifest m )
throws ManifestException
{
addManifestAttribute( m, entries, "Implementation-Title", project.getName() );
addManifestAttribute( m, entries, "Implementation-Version", project.getVersion() );
if ( project.getOrganization() != null )
{
addManifestAttribute( m, entries, "Implementation-Vendor", project.getOrganization().getName() );
}
}
private void handleSpecificationEntries( MavenProject project, Map<String, String> entries, Manifest m )
throws ManifestException
{
addManifestAttribute( m, entries, "Specification-Title", project.getName() );
try
{
ArtifactVersion version = project.getArtifact().getSelectedVersion();
String specVersion = String.format( "%s.%s", version.getMajorVersion(), version.getMinorVersion() );
addManifestAttribute( m, entries, "Specification-Version", specVersion );
}
catch ( OverConstrainedVersionException e )
{
throw new ManifestException( "Failed to get selected artifact version to calculate"
+ " the specification version: " + e.getMessage() );
}
if ( project.getOrganization() != null )
{
addManifestAttribute( m, entries, "Specification-Vendor", project.getOrganization().getName() );
}
}
private void addCustomEntries( Manifest m, Map<String, String> entries, ManifestConfiguration config )
throws ManifestException
{
/*
* TODO: rethink this, it wasn't working Artifact projectArtifact = project.getArtifact(); if (
* projectArtifact.isSnapshot() ) { Manifest.Attribute buildNumberAttr = new Manifest.Attribute( "Build-Number",
* "" + project.getSnapshotDeploymentBuildNumber() ); m.addConfiguredAttribute( buildNumberAttr ); }
*/
if ( config.getPackageName() != null )
{
addManifestAttribute( m, entries, "Package", config.getPackageName() );
}
}
/**
* <p>Getter for the field <code>archiver</code>.</p>
*
* @return {@link org.codehaus.plexus.archiver.jar.JarArchiver}
*/
public JarArchiver getArchiver()
{
return archiver;
}
/**
* <p>Setter for the field <code>archiver</code>.</p>
*
* @param archiver {@link org.codehaus.plexus.archiver.jar.JarArchiver}
*/
public void setArchiver( JarArchiver archiver )
{
this.archiver = archiver;
}
/**
* <p>setOutputFile.</p>
*
* @param outputFile Set output file.
*/
public void setOutputFile( File outputFile )
{
archiveFile = outputFile;
}
/**
* <p>createArchive.</p>
*
* @param session {@link org.apache.maven.execution.MavenSession}
* @param project {@link org.apache.maven.project.MavenProject}
* @param archiveConfiguration {@link org.apache.maven.archiver.MavenArchiveConfiguration}
* @throws org.codehaus.plexus.archiver.ArchiverException Archiver Exception.
* @throws org.codehaus.plexus.archiver.jar.ManifestException Manifest Exception.
* @throws java.io.IOException IO Exception.
* @throws org.apache.maven.artifact.DependencyResolutionRequiredException Dependency resolution exception.
*/
public void createArchive( MavenSession session, MavenProject project,
MavenArchiveConfiguration archiveConfiguration )
throws ManifestException, IOException,
DependencyResolutionRequiredException
{
// we have to clone the project instance so we can write out the pom with the deployment version,
// without impacting the main project instance...
MavenProject workingProject = project.clone();
boolean forced = archiveConfiguration.isForced();
if ( archiveConfiguration.isAddMavenDescriptor() )
{
// ----------------------------------------------------------------------
// We want to add the metadata for the project to the JAR in two forms:
//
// The first form is that of the POM itself. Applications that wish to
// access the POM for an artifact using maven tools they can.
//
// The second form is that of a properties file containing the basic
// top-level POM elements so that applications that wish to access
// POM information without the use of maven tools can do so.
// ----------------------------------------------------------------------
if ( workingProject.getArtifact().isSnapshot() )
{
workingProject.setVersion( workingProject.getArtifact().getVersion() );
}
String groupId = workingProject.getGroupId();
String artifactId = workingProject.getArtifactId();
archiver.addFile( project.getFile(), "META-INF/maven/" + groupId + "/" + artifactId + "/pom.xml" );
// ----------------------------------------------------------------------
// Create pom.properties file
// ----------------------------------------------------------------------
File customPomPropertiesFile = archiveConfiguration.getPomPropertiesFile();
File dir = new File( workingProject.getBuild().getDirectory(), "maven-archiver" );
File pomPropertiesFile = new File( dir, "pom.properties" );
new PomPropertiesUtil().createPomProperties( session, workingProject, archiver,
customPomPropertiesFile, pomPropertiesFile, forced );
}
// ----------------------------------------------------------------------
// Create the manifest
// ----------------------------------------------------------------------
archiver.setMinimalDefaultManifest( true );
File manifestFile = archiveConfiguration.getManifestFile();
if ( manifestFile != null )
{
archiver.setManifest( manifestFile );
}
Manifest manifest = getManifest( session, workingProject, archiveConfiguration );
// Configure the jar
archiver.addConfiguredManifest( manifest );
archiver.setCompress( archiveConfiguration.isCompress() );
archiver.setRecompressAddedZips( archiveConfiguration.isRecompressAddedZips() );
archiver.setIndex( archiveConfiguration.isIndex() );
archiver.setDestFile( archiveFile );
// make the archiver index the jars on the classpath, if we are adding that to the manifest
if ( archiveConfiguration.getManifest().isAddClasspath() )
{
List<String> artifacts = project.getRuntimeClasspathElements();
for ( String artifact : artifacts )
{
File f = new File( artifact );
archiver.addConfiguredIndexJars( f );
}
}
archiver.setForced( forced );
if ( !archiveConfiguration.isForced() && archiver.isSupportingForced() )
{
// TODO Should issue a warning here, but how do we get a logger?
// TODO getLog().warn(
// "Forced build is disabled, but disabling the forced mode isn't supported by the archiver." );
}
String automaticModuleName = manifest.getMainSection().getAttributeValue( "Automatic-Module-Name" );
if ( automaticModuleName != null )
{
if ( !isValidModuleName( automaticModuleName ) )
{
throw new ManifestException( "Invalid automatic module name: '" + automaticModuleName + "'" );
}
}
// create archive
archiver.createArchive();
}
private void handleDefaultEntries( Manifest m, Map<String, String> entries )
throws ManifestException
{
String createdBy = this.createdBy;
if ( createdBy == null )
{
createdBy = createdBy( CREATED_BY, "org.apache.maven", "maven-archiver" );
}
addManifestAttribute( m, entries, "Created-By", createdBy );
if ( buildJdkSpecDefaultEntry )
{
addManifestAttribute( m, entries, "Build-Jdk-Spec", System.getProperty( "java.specification.version" ) );
}
}
private void handleBuildEnvironmentEntries( MavenSession session, Manifest m, Map<String, String> entries )
throws ManifestException
{
addManifestAttribute( m, entries, "Build-Tool",
session != null ? session.getSystemProperties().getProperty( "maven.build.version" ) : "Apache Maven" );
addManifestAttribute( m, entries, "Build-Jdk", String.format( "%s (%s)", System.getProperty( "java.version" ),
System.getProperty( "java.vendor" ) ) );
addManifestAttribute( m, entries, "Build-Os", String.format( "%s (%s; %s)", System.getProperty( "os.name" ),
System.getProperty( "os.version" ), System.getProperty( "os.arch" ) ) );
}
private Artifact findArtifactWithFile( Set<Artifact> artifacts, File file )
{
for ( Artifact artifact : artifacts )
{
// normally not null but we can check
if ( artifact.getFile() != null )
{
if ( artifact.getFile().equals( file ) )
{
return artifact;
}
}
}
return null;
}
private static String getCreatedByVersion( String groupId, String artifactId )
{
final Properties properties = loadOptionalProperties( MavenArchiver.class.getResourceAsStream(
"/META-INF/maven/" + groupId + "/" + artifactId + "/pom.properties" ) );
return properties.getProperty( "version" );
}
private static Properties loadOptionalProperties( final InputStream inputStream )
{
Properties properties = new Properties();
if ( inputStream != null )
{
try ( InputStream in = inputStream )
{
properties.load( in );
}
catch ( IllegalArgumentException | IOException ex )
{
// ignore and return empty properties
}
}
return properties;
}
/**
* Define a value for "Created By" entry.
*
* @param description description of the plugin, like "Maven Source Plugin"
* @param groupId groupId where to get version in pom.properties
* @param artifactId artifactId where to get version in pom.properties
* @since 3.5.0
*/
public void setCreatedBy( String description, String groupId, String artifactId )
{
createdBy = createdBy( description, groupId, artifactId );
}
private String createdBy( String description, String groupId, String artifactId )
{
String createdBy = description;
String version = getCreatedByVersion( groupId, artifactId );
if ( version != null )
{
createdBy += " " + version;
}
return createdBy;
}
/**
* Add "Build-Jdk-Spec" entry as part of default manifest entries (true by default).
* For plugins whose output is not impacted by JDK release (like maven-source-plugin), adding
* Jdk spec adds unnecessary requirement on JDK version used at build to get reproducible result.
*
* @param buildJdkSpecDefaultEntry the value for "Build-Jdk-Spec" entry
* @since 3.5.0
*/
public void setBuildJdkSpecDefaultEntry( boolean buildJdkSpecDefaultEntry )
{
this.buildJdkSpecDefaultEntry = buildJdkSpecDefaultEntry;
}
/**
* Parse output timestamp configured for Reproducible Builds' 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>.
*
* @param outputTimestamp the value of <code>${project.build.outputTimestamp}</code> (may be <code>null</code>)
* @return the parsed timestamp, may be <code>null</code> if <code>null</code> input or input contains only 1
* character
* @since 3.5.0
* @throws IllegalArgumentException if the outputTimestamp is neither ISO 8601 nor an integer, or it's not within
* the valid range 1980-01-01T00:00:02Z to 2099-12-31T23:59:59Z
* @deprecated Use {@link #parseBuildOutputTimestamp(String)} instead.
*/
@Deprecated
public Date parseOutputTimestamp( String outputTimestamp )
{
return parseBuildOutputTimestamp( outputTimestamp ).map( Date::from ).orElse( null );
}
/**
* Configure Reproducible Builds archive creation if a timestamp is provided.
*
* @param outputTimestamp the value of {@code ${project.build.outputTimestamp}} (may be {@code null})
* @return the parsed timestamp as {@link java.util.Date}
* @since 3.5.0
* @see #parseOutputTimestamp
* @deprecated Use {@link #configureReproducibleBuild(String)} instead.
*/
@Deprecated
public Date configureReproducible( String outputTimestamp )
{
configureReproducibleBuild( outputTimestamp );
return parseOutputTimestamp( outputTimestamp );
}
/**
* Parse output timestamp configured for Reproducible Builds' archive entries.
*
* <p>Either as {@link java.time.format.DateTimeFormatter#ISO_OFFSET_DATE_TIME} or as a number representing seconds
* since the epoch (like <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
*
* @param outputTimestamp the value of {@code ${project.build.outputTimestamp}} (may be {@code null})
* @return the parsed timestamp as an {@code Optional<Instant>}, {@code empty} if input is {@code null} or input
* contains only 1 character (not a number)
* @since 3.6.0
* @throws IllegalArgumentException if the outputTimestamp is neither ISO 8601 nor an integer, or it's not within
* the valid range 1980-01-01T00:00:02Z to 2099-12-31T23:59:59Z
*/
public static Optional<Instant> parseBuildOutputTimestamp( String outputTimestamp )
{
// Fail-fast on nulls
if ( outputTimestamp == null )
{
return Optional.empty();
}
// Number representing seconds since the epoch
if ( StringUtils.isNotEmpty( outputTimestamp ) && StringUtils.isNumeric( outputTimestamp ) )
{
return Optional.of( Instant.ofEpochSecond( Long.parseLong( outputTimestamp ) ) );
}
// no timestamp configured (1 character configuration is useful to override a full value during pom
// inheritance)
if ( outputTimestamp.length() < 2 )
{
return Optional.empty();
}
try
{
// Parse the date in UTC such as '2011-12-03T10:15:30Z' or with an offset '2019-10-05T20:37:42+06:00'.
final Instant date = OffsetDateTime.parse( outputTimestamp )
.withOffsetSameInstant( ZoneOffset.UTC ).truncatedTo( ChronoUnit.SECONDS ).toInstant();
if ( date.isBefore( DATE_MIN ) || date.isAfter( DATE_MAX ) )
{
throw new IllegalArgumentException( "'" + date + "' is not within the valid range "
+ DATE_MIN + " to " + DATE_MAX );
}
return Optional.of( date );
}
catch ( DateTimeParseException pe )
{
throw new IllegalArgumentException( "Invalid project.build.outputTimestamp value '" + outputTimestamp + "'",
pe );
}
}
/**
* Configure Reproducible Builds archive creation if a timestamp is provided.
*
* @param outputTimestamp the value of {@code project.build.outputTimestamp} (may be {@code null})
* @since 3.6.0
* @see #parseBuildOutputTimestamp(String)
*/
public void configureReproducibleBuild( String outputTimestamp )
{
parseBuildOutputTimestamp( outputTimestamp )
.map( FileTime::from )
.ifPresent( modifiedTime -> getArchiver().configureReproducibleBuild( modifiedTime ) );
}
}