/*
 * 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.archiver;

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 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 (!(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);
        }
    }

    /**
     * <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('.', '/'));
        String classifier = artifact.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 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(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 (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);
        }
    }

    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));
    }
}
