All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.maven.shared.archiver.MavenArchiver Maven / Gradle / Ivy

Go to download

Provides utility methods for creating JARs and other archive files from a Maven project.

The newest version!
/*
 * 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.
 */
package org.apache.maven.shared.archiver;

import javax.lang.model.SourceVersion;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
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.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.jar.Attributes;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.maven.api.Dependency;
import org.apache.maven.api.PathScope;
import org.apache.maven.api.Project;
import org.apache.maven.api.Session;
import org.apache.maven.api.services.DependencyResolver;
import org.apache.maven.api.services.DependencyResolverResult;
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 static org.apache.maven.shared.archiver.ManifestConfiguration.CLASSPATH_LAYOUT_TYPE_CUSTOM;
import static org.apache.maven.shared.archiver.ManifestConfiguration.CLASSPATH_LAYOUT_TYPE_REPOSITORY;
import static org.apache.maven.shared.archiver.ManifestConfiguration.CLASSPATH_LAYOUT_TYPE_SIMPLE;

/**
 * MavenArchiver class.
 */
public class MavenArchiver {

    private static final String CREATED_BY = "Maven Archiver";

    /**
     * The simple 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 ARTIFACT_EXPRESSION_PREFIXES;

    static {
        List 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;

    /**
     * 

getManifest.

* * @param session the Maven Session * @param project the Maven Project * @param config the MavenArchiveConfiguration * @return the {@link org.codehaus.plexus.archiver.jar.Manifest} * @throws MavenArchiverException in case of a failure */ public Manifest getManifest(Session session, Project project, MavenArchiveConfiguration config) throws MavenArchiverException { boolean hasManifestEntries = !config.isManifestEntriesEmpty(); Map entries = hasManifestEntries ? config.getManifestEntries() : Collections.emptyMap(); Manifest manifest = getManifest(session, project, config.getManifest(), entries); try { // any custom manifest entries in the archive configuration manifest? if (hasManifestEntries) { for (Map.Entry 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 sectionEntries = section.getManifestEntries(); for (Map.Entry 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); } } } catch (ManifestException e) { throw new MavenArchiverException("Unable to create manifest", e); } return manifest; } /** * Return a pre-configured manifest. * * @param project {@link org.apache.maven.api.Project} * @param config {@link ManifestConfiguration} * @return {@link org.codehaus.plexus.archiver.jar.Manifest} * @throws MavenArchiverException exception. */ // TODO Add user attributes list and user groups list public Manifest getManifest(Project project, ManifestConfiguration config) throws MavenArchiverException { return getManifest(null, project, config, Collections.emptyMap()); } public Manifest getManifest(Session session, Project project, ManifestConfiguration config) throws MavenArchiverException { return getManifest(session, project, config, Collections.emptyMap()); } private void addManifestAttribute(Manifest manifest, Map 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 (!(value == null || value.isEmpty())) { 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); } } /** *

getManifest.

* * @param session {@link org.apache.maven.api.Session} * @param project {@link org.apache.maven.api.Project} * @param config {@link ManifestConfiguration} * @param entries The entries. * @return {@link org.codehaus.plexus.archiver.jar.Manifest} * @throws MavenArchiverException exception */ protected Manifest getManifest( Session session, Project project, ManifestConfiguration config, Map entries) throws MavenArchiverException { try { return doGetManifest(session, project, config, entries); } catch (ManifestException e) { throw new MavenArchiverException("Unable to create manifest", e); } } protected Manifest doGetManifest( Session session, Project project, ManifestConfiguration config, Map entries) throws ManifestException { // 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); } DependencyResolverResult result; if (config.isAddClasspath() || config.isAddExtensions()) { result = session.getService(DependencyResolver.class).resolve(session, project, PathScope.MAIN_RUNTIME); } else { result = null; } if (config.isAddClasspath()) { StringBuilder classpath = new StringBuilder(); String classpathPrefix = config.getClasspathPrefix(); String layoutType = config.getClasspathLayoutType(); String layout = config.getCustomClasspathLayout(); Interpolator interpolator = new StringSearchInterpolator(); for (Map.Entry entry : result.getDependencies().entrySet()) { Path artifactFile = entry.getValue(); Dependency dependency = entry.getKey(); if (Files.isRegularFile(artifactFile.toAbsolutePath())) { if (!classpath.isEmpty()) { 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 (dependency == null || layoutType == null) { classpath.append(artifactFile.getFileName().toString()); } else { List valueSources = new ArrayList<>(); handleExtraExpression(dependency, 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 " + " element."); } classpath.append(interpolator.interpolate(layout, recursionInterceptor)); break; default: throw new ManifestException("Unknown classpath layout type: '" + layoutType + "'. Check your 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.isEmpty()) { // 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 && !mainClass.isEmpty()) { addManifestAttribute(m, entries, "Main-Class", mainClass); } addCustomEntries(m, entries, config); return m; } private void handleExtraExpression(Dependency dependency, List valueSources) { valueSources.add(new PrefixedObjectValueSource(ARTIFACT_EXPRESSION_PREFIXES, dependency, true)); valueSources.add(new PrefixedObjectValueSource(ARTIFACT_EXPRESSION_PREFIXES, dependency.getType(), true)); Properties extraExpressions = new Properties(); // FIXME: This query method SHOULD NOT affect the internal // state of the artifact version, but it does. if (!dependency.isSnapshot()) { extraExpressions.setProperty("baseVersion", dependency.getVersion().toString()); } extraExpressions.setProperty("groupIdPath", dependency.getGroupId().replace('.', '/')); String classifier = dependency.getClassifier(); if (classifier != null && !classifier.isEmpty()) { extraExpressions.setProperty("dashClassifier", "-" + classifier); extraExpressions.setProperty("dashClassifier?", "-" + classifier); } else { extraExpressions.setProperty("dashClassifier", ""); extraExpressions.setProperty("dashClassifier?", ""); } valueSources.add(new PrefixedPropertiesValueSource(ARTIFACT_EXPRESSION_PREFIXES, extraExpressions, true)); } private void handleImplementationEntries(Project project, Map entries, Manifest m) throws ManifestException { addManifestAttribute( m, entries, "Implementation-Title", project.getModel().getName()); addManifestAttribute(m, entries, "Implementation-Version", project.getVersion()); if (project.getModel().getOrganization() != null) { addManifestAttribute( m, entries, "Implementation-Vendor", project.getModel().getOrganization().getName()); } } private void handleSpecificationEntries(Project project, Map entries, Manifest m) throws ManifestException { addManifestAttribute( m, entries, "Specification-Title", project.getModel().getName()); String version = project.getPomArtifact().getVersion().toString(); Matcher matcher = Pattern.compile("([0-9]+\\.[0-9]+)(.*?)").matcher(version); if (matcher.matches()) { String specVersion = matcher.group(1); addManifestAttribute(m, entries, "Specification-Version", specVersion); } /* TODO: v4: overconstrained try { Version version = project.getArtifact().getVersion(); 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.getModel().getOrganization() != null) { addManifestAttribute( m, entries, "Specification-Vendor", project.getModel().getOrganization().getName()); } } private void addCustomEntries(Manifest m, Map 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()); } } /** *

Getter for the field archiver.

* * @return {@link org.codehaus.plexus.archiver.jar.JarArchiver} */ public JarArchiver getArchiver() { return archiver; } /** *

Setter for the field archiver.

* * @param archiver {@link org.codehaus.plexus.archiver.jar.JarArchiver} */ public void setArchiver(JarArchiver archiver) { this.archiver = archiver; } /** *

setOutputFile.

* * @param outputFile Set output file. */ public void setOutputFile(File outputFile) { archiveFile = outputFile; } /** *

createArchive.

* * @param session {@link org.apache.maven.api.Session} * @param project {@link org.apache.maven.api.Project} * @param archiveConfiguration {@link MavenArchiveConfiguration} * @throws MavenArchiverException Archiver Exception. */ public void createArchive(Session session, Project project, MavenArchiveConfiguration archiveConfiguration) throws MavenArchiverException { try { doCreateArchive(session, project, archiveConfiguration); } catch (ManifestException | IOException e) { throw new MavenArchiverException(e); } } public void doCreateArchive(Session session, Project project, MavenArchiveConfiguration archiveConfiguration) throws ManifestException, IOException { // we have to clone the project instance so we can write out the pom with the deployment version, // without impacting the main project instance... 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. // ---------------------------------------------------------------------- String groupId = project.getGroupId(); String artifactId = project.getArtifactId(); String version; if (project.getPomArtifact().isSnapshot()) { version = project.getPomArtifact().getVersion().toString(); } else { version = project.getVersion(); } archiver.addFile( project.getPomPath().toFile(), "META-INF/maven/" + groupId + "/" + artifactId + "/pom.xml"); // ---------------------------------------------------------------------- // Create pom.properties file // ---------------------------------------------------------------------- Path customPomPropertiesFile = archiveConfiguration.getPomPropertiesFile(); Path dir = Paths.get(project.getBuild().getDirectory(), "maven-archiver"); Path pomPropertiesFile = dir.resolve("pom.properties"); new PomPropertiesUtil() .createPomProperties( groupId, artifactId, version, archiver, customPomPropertiesFile, pomPropertiesFile); } // ---------------------------------------------------------------------- // Create the manifest // ---------------------------------------------------------------------- archiver.setMinimalDefaultManifest(true); Path manifestFile = archiveConfiguration.getManifestFile(); if (manifestFile != null) { archiver.setManifest(manifestFile.toFile()); } Manifest manifest = getManifest(session, project, archiveConfiguration); // Configure the jar archiver.addConfiguredManifest(manifest); archiver.setCompress(archiveConfiguration.isCompress()); archiver.setRecompressAddedZips(archiveConfiguration.isRecompressAddedZips()); archiver.setDestFile(archiveFile); 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 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(Session session, Manifest m, Map entries) throws ManifestException { addManifestAttribute( m, entries, "Build-Tool", session != null ? session.getSystemProperties().get("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 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 as {@link java.time.format.DateTimeFormatter#ISO_OFFSET_DATE_TIME} or as a number representing seconds * since the epoch (like SOURCE_DATE_EPOCH). * * @param outputTimestamp the value of {@code ${project.build.outputTimestamp}} (may be {@code null}) * @return the parsed timestamp as an {@code Optional}, {@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 as defined by * ZIP application note, * section 4.4.6. */ public static Optional parseBuildOutputTimestamp(String outputTimestamp) { // Fail-fast on nulls if (outputTimestamp == null) { return Optional.empty(); } // Number representing seconds since the epoch if (isNumeric(outputTimestamp)) { final Instant date = Instant.ofEpochSecond(Long.parseLong(outputTimestamp)); 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); } // 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); } } private static boolean isNumeric(String str) { if (str.isEmpty()) { return false; } for (char c : str.toCharArray()) { if (!Character.isDigit(c)) { return false; } } return true; } /** * 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)); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy