View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.archiver;
20  
21  import javax.lang.model.SourceVersion;
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.nio.file.attribute.FileTime;
27  import java.time.Instant;
28  import java.time.OffsetDateTime;
29  import java.time.ZoneOffset;
30  import java.time.format.DateTimeParseException;
31  import java.time.temporal.ChronoUnit;
32  import java.util.ArrayList;
33  import java.util.Collections;
34  import java.util.Date;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.Optional;
38  import java.util.Properties;
39  import java.util.Set;
40  import java.util.jar.Attributes;
41  
42  import org.apache.maven.artifact.Artifact;
43  import org.apache.maven.artifact.DependencyResolutionRequiredException;
44  import org.apache.maven.artifact.versioning.ArtifactVersion;
45  import org.apache.maven.artifact.versioning.OverConstrainedVersionException;
46  import org.apache.maven.execution.MavenSession;
47  import org.apache.maven.project.MavenProject;
48  import org.codehaus.plexus.archiver.jar.JarArchiver;
49  import org.codehaus.plexus.archiver.jar.Manifest;
50  import org.codehaus.plexus.archiver.jar.ManifestException;
51  import org.codehaus.plexus.interpolation.InterpolationException;
52  import org.codehaus.plexus.interpolation.Interpolator;
53  import org.codehaus.plexus.interpolation.PrefixAwareRecursionInterceptor;
54  import org.codehaus.plexus.interpolation.PrefixedObjectValueSource;
55  import org.codehaus.plexus.interpolation.PrefixedPropertiesValueSource;
56  import org.codehaus.plexus.interpolation.RecursionInterceptor;
57  import org.codehaus.plexus.interpolation.StringSearchInterpolator;
58  import org.codehaus.plexus.interpolation.ValueSource;
59  
60  import static org.apache.maven.archiver.ManifestConfiguration.CLASSPATH_LAYOUT_TYPE_CUSTOM;
61  import static org.apache.maven.archiver.ManifestConfiguration.CLASSPATH_LAYOUT_TYPE_REPOSITORY;
62  import static org.apache.maven.archiver.ManifestConfiguration.CLASSPATH_LAYOUT_TYPE_SIMPLE;
63  
64  /**
65   * <p>MavenArchiver class.</p>
66   *
67   * @author <a href="evenisse@apache.org">Emmanuel Venisse</a>
68   * @author kama
69   */
70  public class MavenArchiver {
71  
72      private static final String CREATED_BY = "Maven Archiver";
73  
74      /**
75       * The simply layout.
76       */
77      public static final String SIMPLE_LAYOUT =
78              "${artifact.artifactId}-${artifact.version}${dashClassifier?}.${artifact.extension}";
79  
80      /**
81       * Repository layout.
82       */
83      public static final String REPOSITORY_LAYOUT =
84              "${artifact.groupIdPath}/${artifact.artifactId}/" + "${artifact.baseVersion}/${artifact.artifactId}-"
85                      + "${artifact.version}${dashClassifier?}.${artifact.extension}";
86  
87      /**
88       * simple layout non unique.
89       */
90      public static final String SIMPLE_LAYOUT_NONUNIQUE =
91              "${artifact.artifactId}-${artifact.baseVersion}${dashClassifier?}.${artifact.extension}";
92  
93      /**
94       * Repository layout non unique.
95       */
96      public static final String REPOSITORY_LAYOUT_NONUNIQUE =
97              "${artifact.groupIdPath}/${artifact.artifactId}/" + "${artifact.baseVersion}/${artifact.artifactId}-"
98                      + "${artifact.baseVersion}${dashClassifier?}.${artifact.extension}";
99  
100     private static final Instant DATE_MIN = Instant.parse("1980-01-01T00:00:02Z");
101 
102     private static final Instant DATE_MAX = Instant.parse("2099-12-31T23:59:59Z");
103 
104     private static final List<String> ARTIFACT_EXPRESSION_PREFIXES;
105 
106     static {
107         List<String> artifactExpressionPrefixes = new ArrayList<>();
108         artifactExpressionPrefixes.add("artifact.");
109 
110         ARTIFACT_EXPRESSION_PREFIXES = artifactExpressionPrefixes;
111     }
112 
113     static boolean isValidModuleName(String name) {
114         return SourceVersion.isName(name);
115     }
116 
117     private JarArchiver archiver;
118 
119     private File archiveFile;
120 
121     private String createdBy;
122 
123     private boolean buildJdkSpecDefaultEntry = true;
124 
125     /**
126      * <p>getManifest.</p>
127      *
128      * @param session the Maven Session
129      * @param project the Maven Project
130      * @param config the MavenArchiveConfiguration
131      * @return the {@link org.codehaus.plexus.archiver.jar.Manifest}
132      * @throws org.codehaus.plexus.archiver.jar.ManifestException in case of a failure
133      * @throws org.apache.maven.artifact.DependencyResolutionRequiredException resolution failure
134      */
135     public Manifest getManifest(MavenSession session, MavenProject project, MavenArchiveConfiguration config)
136             throws ManifestException, DependencyResolutionRequiredException {
137         boolean hasManifestEntries = !config.isManifestEntriesEmpty();
138         Map<String, String> entries = hasManifestEntries ? config.getManifestEntries() : Collections.emptyMap();
139 
140         Manifest manifest = getManifest(session, project, config.getManifest(), entries);
141 
142         // any custom manifest entries in the archive configuration manifest?
143         if (hasManifestEntries) {
144 
145             for (Map.Entry<String, String> entry : entries.entrySet()) {
146                 String key = entry.getKey();
147                 String value = entry.getValue();
148                 Manifest.Attribute attr = manifest.getMainSection().getAttribute(key);
149                 if (key.equals(Attributes.Name.CLASS_PATH.toString()) && attr != null) {
150                     // Merge the user-supplied Class-Path value with the programmatically
151                     // created Class-Path. Note that the user-supplied value goes first
152                     // so that resources there will override any in the standard Class-Path.
153                     attr.setValue(value + " " + attr.getValue());
154                 } else {
155                     addManifestAttribute(manifest, key, value);
156                 }
157             }
158         }
159 
160         // any custom manifest sections in the archive configuration manifest?
161         if (!config.isManifestSectionsEmpty()) {
162             for (ManifestSection section : config.getManifestSections()) {
163                 Manifest.Section theSection = new Manifest.Section();
164                 theSection.setName(section.getName());
165 
166                 if (!section.isManifestEntriesEmpty()) {
167                     Map<String, String> sectionEntries = section.getManifestEntries();
168 
169                     for (Map.Entry<String, String> entry : sectionEntries.entrySet()) {
170                         String key = entry.getKey();
171                         String value = entry.getValue();
172                         Manifest.Attribute attr = new Manifest.Attribute(key, value);
173                         theSection.addConfiguredAttribute(attr);
174                     }
175                 }
176 
177                 manifest.addConfiguredSection(theSection);
178             }
179         }
180 
181         return manifest;
182     }
183 
184     /**
185      * Return a pre-configured manifest.
186      *
187      * @param project {@link org.apache.maven.project.MavenProject}
188      * @param config {@link org.apache.maven.archiver.ManifestConfiguration}
189      * @return {@link org.codehaus.plexus.archiver.jar.Manifest}
190      * @throws org.codehaus.plexus.archiver.jar.ManifestException Manifest exception.
191      * @throws org.apache.maven.artifact.DependencyResolutionRequiredException Dependency resolution exception.
192      */
193     // TODO Add user attributes list and user groups list
194     public Manifest getManifest(MavenProject project, ManifestConfiguration config)
195             throws ManifestException, DependencyResolutionRequiredException {
196         return getManifest(null, project, config, Collections.emptyMap());
197     }
198 
199     /**
200      * <p>getManifest.</p>
201      *
202      * @param mavenSession {@link org.apache.maven.execution.MavenSession}
203      * @param project      {@link org.apache.maven.project.MavenProject}
204      * @param config       {@link org.apache.maven.archiver.ManifestConfiguration}
205      * @return {@link org.codehaus.plexus.archiver.jar.Manifest}
206      * @throws org.codehaus.plexus.archiver.jar.ManifestException              the manifest exception
207      * @throws org.apache.maven.artifact.DependencyResolutionRequiredException the dependency resolution required
208      *                                                                         exception
209      */
210     public Manifest getManifest(MavenSession mavenSession, MavenProject project, ManifestConfiguration config)
211             throws ManifestException, DependencyResolutionRequiredException {
212         return getManifest(mavenSession, project, config, Collections.emptyMap());
213     }
214 
215     private void addManifestAttribute(Manifest manifest, Map<String, String> map, String key, String value)
216             throws ManifestException {
217         if (map.containsKey(key)) {
218             return; // The map value will be added later
219         }
220         addManifestAttribute(manifest, key, value);
221     }
222 
223     private void addManifestAttribute(Manifest manifest, String key, String value) throws ManifestException {
224         if (!(value == null || value.isEmpty())) {
225             Manifest.Attribute attr = new Manifest.Attribute(key, value);
226             manifest.addConfiguredAttribute(attr);
227         } else {
228             // if the value is empty, create an entry with an empty string
229             // to prevent null print in the manifest file
230             Manifest.Attribute attr = new Manifest.Attribute(key, "");
231             manifest.addConfiguredAttribute(attr);
232         }
233     }
234 
235     /**
236      * <p>getManifest.</p>
237      *
238      * @param session {@link org.apache.maven.execution.MavenSession}
239      * @param project {@link org.apache.maven.project.MavenProject}
240      * @param config  {@link org.apache.maven.archiver.ManifestConfiguration}
241      * @param entries The entries.
242      * @return {@link org.codehaus.plexus.archiver.jar.Manifest}
243      * @throws org.codehaus.plexus.archiver.jar.ManifestException              the manifest exception
244      * @throws org.apache.maven.artifact.DependencyResolutionRequiredException the dependency resolution required
245      *                                                                         exception
246      */
247     protected Manifest getManifest(
248             MavenSession session, MavenProject project, ManifestConfiguration config, Map<String, String> entries)
249             throws ManifestException, DependencyResolutionRequiredException {
250         // TODO: Should we replace "map" with a copy? Note, that we modify it!
251 
252         Manifest m = new Manifest();
253 
254         if (config.isAddDefaultEntries()) {
255             handleDefaultEntries(m, entries);
256         }
257 
258         if (config.isAddBuildEnvironmentEntries()) {
259             handleBuildEnvironmentEntries(session, m, entries);
260         }
261 
262         if (config.isAddClasspath()) {
263             StringBuilder classpath = new StringBuilder();
264 
265             List<String> artifacts = project.getRuntimeClasspathElements();
266             String classpathPrefix = config.getClasspathPrefix();
267             String layoutType = config.getClasspathLayoutType();
268             String layout = config.getCustomClasspathLayout();
269 
270             Interpolator interpolator = new StringSearchInterpolator();
271 
272             for (String artifactFile : artifacts) {
273                 File f = new File(artifactFile);
274                 if (f.getAbsoluteFile().isFile()) {
275                     Artifact artifact = findArtifactWithFile(project.getArtifacts(), f);
276 
277                     if (classpath.length() > 0) {
278                         classpath.append(" ");
279                     }
280                     classpath.append(classpathPrefix);
281 
282                     // NOTE: If the artifact or layout type (from config) is null, give up and use the file name by
283                     // itself.
284                     if (artifact == null || layoutType == null) {
285                         classpath.append(f.getName());
286                     } else {
287                         List<ValueSource> valueSources = new ArrayList<>();
288 
289                         handleExtraExpression(artifact, valueSources);
290 
291                         for (ValueSource vs : valueSources) {
292                             interpolator.addValueSource(vs);
293                         }
294 
295                         RecursionInterceptor recursionInterceptor =
296                                 new PrefixAwareRecursionInterceptor(ARTIFACT_EXPRESSION_PREFIXES);
297 
298                         try {
299                             switch (layoutType) {
300                                 case CLASSPATH_LAYOUT_TYPE_SIMPLE:
301                                     if (config.isUseUniqueVersions()) {
302                                         classpath.append(interpolator.interpolate(SIMPLE_LAYOUT, recursionInterceptor));
303                                     } else {
304                                         classpath.append(interpolator.interpolate(
305                                                 SIMPLE_LAYOUT_NONUNIQUE, recursionInterceptor));
306                                     }
307                                     break;
308                                 case CLASSPATH_LAYOUT_TYPE_REPOSITORY:
309                                     // we use layout /$groupId[0]/../${groupId[n]/$artifactId/$version/{fileName}
310                                     // here we must find the Artifact in the project Artifacts
311                                     // to create the maven layout
312                                     if (config.isUseUniqueVersions()) {
313                                         classpath.append(
314                                                 interpolator.interpolate(REPOSITORY_LAYOUT, recursionInterceptor));
315                                     } else {
316                                         classpath.append(interpolator.interpolate(
317                                                 REPOSITORY_LAYOUT_NONUNIQUE, recursionInterceptor));
318                                     }
319                                     break;
320                                 case CLASSPATH_LAYOUT_TYPE_CUSTOM:
321                                     if (layout == null) {
322                                         throw new ManifestException(CLASSPATH_LAYOUT_TYPE_CUSTOM
323                                                 + " layout type was declared, but custom layout expression was not"
324                                                 + " specified. Check your <archive><manifest><customLayout/>"
325                                                 + " element.");
326                                     }
327 
328                                     classpath.append(interpolator.interpolate(layout, recursionInterceptor));
329                                     break;
330                                 default:
331                                     throw new ManifestException("Unknown classpath layout type: '" + layoutType
332                                             + "'. Check your <archive><manifest><layoutType/> element.");
333                             }
334                         } catch (InterpolationException e) {
335                             ManifestException error = new ManifestException(
336                                     "Error interpolating artifact path for classpath entry: " + e.getMessage());
337 
338                             error.initCause(e);
339                             throw error;
340                         } finally {
341                             for (ValueSource vs : valueSources) {
342                                 interpolator.removeValuesSource(vs);
343                             }
344                         }
345                     }
346                 }
347             }
348 
349             if (classpath.length() > 0) {
350                 // Class-Path is special and should be added to manifest even if
351                 // it is specified in the manifestEntries section
352                 addManifestAttribute(m, "Class-Path", classpath.toString());
353             }
354         }
355 
356         if (config.isAddDefaultSpecificationEntries()) {
357             handleSpecificationEntries(project, entries, m);
358         }
359 
360         if (config.isAddDefaultImplementationEntries()) {
361             handleImplementationEntries(project, entries, m);
362         }
363 
364         String mainClass = config.getMainClass();
365         if (mainClass != null && !"".equals(mainClass)) {
366             addManifestAttribute(m, entries, "Main-Class", mainClass);
367         }
368 
369         if (config.isAddExtensions()) {
370             handleExtensions(project, entries, m);
371         }
372 
373         addCustomEntries(m, entries, config);
374 
375         return m;
376     }
377 
378     private void handleExtraExpression(Artifact artifact, List<ValueSource> valueSources) {
379         valueSources.add(new PrefixedObjectValueSource(ARTIFACT_EXPRESSION_PREFIXES, artifact, true));
380         valueSources.add(
381                 new PrefixedObjectValueSource(ARTIFACT_EXPRESSION_PREFIXES, artifact.getArtifactHandler(), true));
382 
383         Properties extraExpressions = new Properties();
384         // FIXME: This query method SHOULD NOT affect the internal
385         // state of the artifact version, but it does.
386         if (!artifact.isSnapshot()) {
387             extraExpressions.setProperty("baseVersion", artifact.getVersion());
388         }
389 
390         extraExpressions.setProperty("groupIdPath", artifact.getGroupId().replace('.', '/'));
391         String classifier = artifact.getClassifier();
392         if (classifier != null && !classifier.isEmpty()) {
393             extraExpressions.setProperty("dashClassifier", "-" + classifier);
394             extraExpressions.setProperty("dashClassifier?", "-" + classifier);
395         } else {
396             extraExpressions.setProperty("dashClassifier", "");
397             extraExpressions.setProperty("dashClassifier?", "");
398         }
399         valueSources.add(new PrefixedPropertiesValueSource(ARTIFACT_EXPRESSION_PREFIXES, extraExpressions, true));
400     }
401 
402     private void handleExtensions(MavenProject project, Map<String, String> entries, Manifest m)
403             throws ManifestException {
404         // TODO: this is only for applets - should we distinguish them as a packaging?
405         StringBuilder extensionsList = new StringBuilder();
406         Set<Artifact> artifacts = project.getArtifacts();
407 
408         for (Artifact artifact : artifacts) {
409             if (!Artifact.SCOPE_TEST.equals(artifact.getScope())) {
410                 if ("jar".equals(artifact.getType())) {
411                     if (extensionsList.length() > 0) {
412                         extensionsList.append(" ");
413                     }
414                     extensionsList.append(artifact.getArtifactId());
415                 }
416             }
417         }
418 
419         if (extensionsList.length() > 0) {
420             addManifestAttribute(m, entries, "Extension-List", extensionsList.toString());
421         }
422 
423         for (Artifact artifact : artifacts) {
424             // TODO: the correct solution here would be to have an extension type, and to read
425             // the real extension values either from the artifact's manifest or some part of the POM
426             if ("jar".equals(artifact.getType())) {
427                 String artifactId = artifact.getArtifactId().replace('.', '_');
428                 String ename = artifactId + "-Extension-Name";
429                 addManifestAttribute(m, entries, ename, artifact.getArtifactId());
430                 String iname = artifactId + "-Implementation-Version";
431                 addManifestAttribute(m, entries, iname, artifact.getVersion());
432 
433                 if (artifact.getRepository() != null) {
434                     iname = artifactId + "-Implementation-URL";
435                     String url = artifact.getRepository().getUrl() + "/" + artifact;
436                     addManifestAttribute(m, entries, iname, url);
437                 }
438             }
439         }
440     }
441 
442     private void handleImplementationEntries(MavenProject project, Map<String, String> entries, Manifest m)
443             throws ManifestException {
444         addManifestAttribute(m, entries, "Implementation-Title", project.getName());
445         addManifestAttribute(m, entries, "Implementation-Version", project.getVersion());
446 
447         if (project.getOrganization() != null) {
448             addManifestAttribute(
449                     m,
450                     entries,
451                     "Implementation-Vendor",
452                     project.getOrganization().getName());
453         }
454     }
455 
456     private void handleSpecificationEntries(MavenProject project, Map<String, String> entries, Manifest m)
457             throws ManifestException {
458         addManifestAttribute(m, entries, "Specification-Title", project.getName());
459 
460         try {
461             ArtifactVersion version = project.getArtifact().getSelectedVersion();
462             String specVersion = String.format("%s.%s", version.getMajorVersion(), version.getMinorVersion());
463             addManifestAttribute(m, entries, "Specification-Version", specVersion);
464         } catch (OverConstrainedVersionException e) {
465             throw new ManifestException("Failed to get selected artifact version to calculate"
466                     + " the specification version: " + e.getMessage());
467         }
468 
469         if (project.getOrganization() != null) {
470             addManifestAttribute(
471                     m,
472                     entries,
473                     "Specification-Vendor",
474                     project.getOrganization().getName());
475         }
476     }
477 
478     private void addCustomEntries(Manifest m, Map<String, String> entries, ManifestConfiguration config)
479             throws ManifestException {
480         /*
481          * TODO: rethink this, it wasn't working Artifact projectArtifact = project.getArtifact(); if (
482          * projectArtifact.isSnapshot() ) { Manifest.Attribute buildNumberAttr = new Manifest.Attribute( "Build-Number",
483          * "" + project.getSnapshotDeploymentBuildNumber() ); m.addConfiguredAttribute( buildNumberAttr ); }
484          */
485         if (config.getPackageName() != null) {
486             addManifestAttribute(m, entries, "Package", config.getPackageName());
487         }
488     }
489 
490     /**
491      * <p>Getter for the field <code>archiver</code>.</p>
492      *
493      * @return {@link org.codehaus.plexus.archiver.jar.JarArchiver}
494      */
495     public JarArchiver getArchiver() {
496         return archiver;
497     }
498 
499     /**
500      * <p>Setter for the field <code>archiver</code>.</p>
501      *
502      * @param archiver {@link org.codehaus.plexus.archiver.jar.JarArchiver}
503      */
504     public void setArchiver(JarArchiver archiver) {
505         this.archiver = archiver;
506     }
507 
508     /**
509      * <p>setOutputFile.</p>
510      *
511      * @param outputFile Set output file.
512      */
513     public void setOutputFile(File outputFile) {
514         archiveFile = outputFile;
515     }
516 
517     /**
518      * <p>createArchive.</p>
519      *
520      * @param session {@link org.apache.maven.execution.MavenSession}
521      * @param project {@link org.apache.maven.project.MavenProject}
522      * @param archiveConfiguration {@link org.apache.maven.archiver.MavenArchiveConfiguration}
523      * @throws org.codehaus.plexus.archiver.ArchiverException Archiver Exception.
524      * @throws org.codehaus.plexus.archiver.jar.ManifestException Manifest Exception.
525      * @throws java.io.IOException IO Exception.
526      * @throws org.apache.maven.artifact.DependencyResolutionRequiredException Dependency resolution exception.
527      */
528     public void createArchive(
529             MavenSession session, MavenProject project, MavenArchiveConfiguration archiveConfiguration)
530             throws ManifestException, IOException, DependencyResolutionRequiredException {
531         // we have to clone the project instance so we can write out the pom with the deployment version,
532         // without impacting the main project instance...
533         MavenProject workingProject = project.clone();
534 
535         boolean forced = archiveConfiguration.isForced();
536         if (archiveConfiguration.isAddMavenDescriptor()) {
537             // ----------------------------------------------------------------------
538             // We want to add the metadata for the project to the JAR in two forms:
539             //
540             // The first form is that of the POM itself. Applications that wish to
541             // access the POM for an artifact using maven tools they can.
542             //
543             // The second form is that of a properties file containing the basic
544             // top-level POM elements so that applications that wish to access
545             // POM information without the use of maven tools can do so.
546             // ----------------------------------------------------------------------
547 
548             if (workingProject.getArtifact().isSnapshot()) {
549                 workingProject.setVersion(workingProject.getArtifact().getVersion());
550             }
551 
552             String groupId = workingProject.getGroupId();
553 
554             String artifactId = workingProject.getArtifactId();
555 
556             archiver.addFile(project.getFile(), "META-INF/maven/" + groupId + "/" + artifactId + "/pom.xml");
557 
558             // ----------------------------------------------------------------------
559             // Create pom.properties file
560             // ----------------------------------------------------------------------
561 
562             File customPomPropertiesFile = archiveConfiguration.getPomPropertiesFile();
563             File dir = new File(workingProject.getBuild().getDirectory(), "maven-archiver");
564             File pomPropertiesFile = new File(dir, "pom.properties");
565 
566             new PomPropertiesUtil()
567                     .createPomProperties(workingProject, archiver, customPomPropertiesFile, pomPropertiesFile, forced);
568         }
569 
570         // ----------------------------------------------------------------------
571         // Create the manifest
572         // ----------------------------------------------------------------------
573 
574         archiver.setMinimalDefaultManifest(true);
575 
576         File manifestFile = archiveConfiguration.getManifestFile();
577 
578         if (manifestFile != null) {
579             archiver.setManifest(manifestFile);
580         }
581 
582         Manifest manifest = getManifest(session, workingProject, archiveConfiguration);
583 
584         // Configure the jar
585         archiver.addConfiguredManifest(manifest);
586         archiver.setCompress(archiveConfiguration.isCompress());
587         archiver.setRecompressAddedZips(archiveConfiguration.isRecompressAddedZips());
588         archiver.setDestFile(archiveFile);
589 
590         archiver.setForced(forced);
591         if (!archiveConfiguration.isForced() && archiver.isSupportingForced()) {
592             // TODO Should issue a warning here, but how do we get a logger?
593             // TODO getLog().warn(
594             // "Forced build is disabled, but disabling the forced mode isn't supported by the archiver." );
595         }
596 
597         String automaticModuleName = manifest.getMainSection().getAttributeValue("Automatic-Module-Name");
598         if (automaticModuleName != null) {
599             if (automaticModuleName.isEmpty()) {
600                 manifest.getMainSection().removeAttribute("Automatic-Module-Name");
601             } else if (!isValidModuleName(automaticModuleName)) {
602                 throw new ManifestException("Invalid automatic module name: '" + automaticModuleName + "'");
603             }
604         }
605 
606         // create archive
607         archiver.createArchive();
608     }
609 
610     private void handleDefaultEntries(Manifest m, Map<String, String> entries) throws ManifestException {
611         String createdBy = this.createdBy;
612         if (createdBy == null) {
613             createdBy = createdBy(CREATED_BY, "org.apache.maven", "maven-archiver");
614         }
615         addManifestAttribute(m, entries, "Created-By", createdBy);
616         if (buildJdkSpecDefaultEntry) {
617             addManifestAttribute(m, entries, "Build-Jdk-Spec", System.getProperty("java.specification.version"));
618         }
619     }
620 
621     private void handleBuildEnvironmentEntries(MavenSession session, Manifest m, Map<String, String> entries)
622             throws ManifestException {
623         addManifestAttribute(
624                 m,
625                 entries,
626                 "Build-Tool",
627                 session != null ? session.getSystemProperties().getProperty("maven.build.version") : "Apache Maven");
628         addManifestAttribute(
629                 m,
630                 entries,
631                 "Build-Jdk",
632                 String.format("%s (%s)", System.getProperty("java.version"), System.getProperty("java.vendor")));
633         addManifestAttribute(
634                 m,
635                 entries,
636                 "Build-Os",
637                 String.format(
638                         "%s (%s; %s)",
639                         System.getProperty("os.name"),
640                         System.getProperty("os.version"),
641                         System.getProperty("os.arch")));
642     }
643 
644     private Artifact findArtifactWithFile(Set<Artifact> artifacts, File file) {
645         for (Artifact artifact : artifacts) {
646             // normally not null but we can check
647             if (artifact.getFile() != null) {
648                 if (artifact.getFile().equals(file)) {
649                     return artifact;
650                 }
651             }
652         }
653         return null;
654     }
655 
656     private static String getCreatedByVersion(String groupId, String artifactId) {
657         final Properties properties = loadOptionalProperties(MavenArchiver.class.getResourceAsStream(
658                 "/META-INF/maven/" + groupId + "/" + artifactId + "/pom.properties"));
659 
660         return properties.getProperty("version");
661     }
662 
663     private static Properties loadOptionalProperties(final InputStream inputStream) {
664         Properties properties = new Properties();
665         if (inputStream != null) {
666             try (InputStream in = inputStream) {
667                 properties.load(in);
668             } catch (IllegalArgumentException | IOException ex) {
669                 // ignore and return empty properties
670             }
671         }
672         return properties;
673     }
674 
675     /**
676      * Define a value for "Created By" entry.
677      *
678      * @param description description of the plugin, like "Maven Source Plugin"
679      * @param groupId groupId where to get version in pom.properties
680      * @param artifactId artifactId where to get version in pom.properties
681      * @since 3.5.0
682      */
683     public void setCreatedBy(String description, String groupId, String artifactId) {
684         createdBy = createdBy(description, groupId, artifactId);
685     }
686 
687     private String createdBy(String description, String groupId, String artifactId) {
688         String createdBy = description;
689         String version = getCreatedByVersion(groupId, artifactId);
690         if (version != null) {
691             createdBy += " " + version;
692         }
693         return createdBy;
694     }
695 
696     /**
697      * Add "Build-Jdk-Spec" entry as part of default manifest entries (true by default).
698      * For plugins whose output is not impacted by JDK release (like maven-source-plugin), adding
699      * Jdk spec adds unnecessary requirement on JDK version used at build to get reproducible result.
700      *
701      * @param buildJdkSpecDefaultEntry the value for "Build-Jdk-Spec" entry
702      * @since 3.5.0
703      */
704     public void setBuildJdkSpecDefaultEntry(boolean buildJdkSpecDefaultEntry) {
705         this.buildJdkSpecDefaultEntry = buildJdkSpecDefaultEntry;
706     }
707 
708     /**
709      * Parse output timestamp configured for Reproducible Builds' archive entries, either formatted as ISO 8601
710      * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
711      * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>.
712      *
713      * @param outputTimestamp the value of <code>${project.build.outputTimestamp}</code> (may be <code>null</code>)
714      * @return the parsed timestamp, may be <code>null</code> if <code>null</code> input or input contains only 1
715      *         character
716      * @since 3.5.0
717      * @throws IllegalArgumentException if the outputTimestamp is neither ISO 8601 nor an integer, or it's not within
718      *             the valid range 1980-01-01T00:00:02Z to 2099-12-31T23:59:59Z
719      * @deprecated Use {@link #parseBuildOutputTimestamp(String)} instead.
720      */
721     @Deprecated
722     public Date parseOutputTimestamp(String outputTimestamp) {
723         return parseBuildOutputTimestamp(outputTimestamp).map(Date::from).orElse(null);
724     }
725 
726     /**
727      * Configure Reproducible Builds archive creation if a timestamp is provided.
728      *
729      * @param outputTimestamp the value of {@code ${project.build.outputTimestamp}} (may be {@code null})
730      * @return the parsed timestamp as {@link java.util.Date}
731      * @since 3.5.0
732      * @see #parseOutputTimestamp
733      * @deprecated Use {@link #configureReproducibleBuild(String)} instead.
734      */
735     @Deprecated
736     public Date configureReproducible(String outputTimestamp) {
737         configureReproducibleBuild(outputTimestamp);
738         return parseOutputTimestamp(outputTimestamp);
739     }
740 
741     /**
742      * Parse output timestamp configured for Reproducible Builds' archive entries.
743      *
744      * <p>Either as {@link java.time.format.DateTimeFormatter#ISO_OFFSET_DATE_TIME} or as a number representing seconds
745      * since the epoch (like <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
746      *
747      * <p>Since 3.6.4, if not configured or disabled, the {@code SOURCE_DATE_EPOCH} environment variable is used as
748      * a fallback value, to ease forcing Reproducible Build externally when the build has not enabled it natively in POM.
749      *
750      * @param outputTimestamp the value of {@code ${project.build.outputTimestamp}} (may be {@code null})
751      * @return the parsed timestamp as an {@code Optional<Instant>}, {@code empty} if input is {@code null} or input
752      *         contains only 1 character (not a number)
753      * @since 3.6.0
754      * @throws IllegalArgumentException if the outputTimestamp is neither ISO 8601 nor an integer, or it's not within
755      *             the valid range 1980-01-01T00:00:02Z to 2099-12-31T23:59:59Z as defined by
756      *             <a href="https://pkwaredownloads.blob.core.windows.net/pem/APPNOTE.txt">ZIP application note</a>,
757      *             section 4.4.6.
758      * @see <a href="https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=74682318">Maven Wiki "Reproducible/Verifiable
759      *      Builds"</a>
760      */
761     public static Optional<Instant> parseBuildOutputTimestamp(String outputTimestamp) {
762         // Fail fast on null and no timestamp configured (1 character configuration is useful to override
763         // a full value during pom inheritance)
764         if (outputTimestamp == null || (outputTimestamp.length() < 2 && !isNumeric(outputTimestamp))) {
765             // Reproducible Builds not configured or disabled => fallback to SOURCE_DATE_EPOCH env
766             outputTimestamp = System.getenv("SOURCE_DATE_EPOCH");
767             if (outputTimestamp == null) {
768                 return Optional.empty();
769             }
770         }
771 
772         // Number representing seconds since the epoch
773         if (isNumeric(outputTimestamp)) {
774             final Instant date = Instant.ofEpochSecond(Long.parseLong(outputTimestamp));
775 
776             if (date.isBefore(DATE_MIN) || date.isAfter(DATE_MAX)) {
777                 throw new IllegalArgumentException(
778                         "'" + date + "' is not within the valid range " + DATE_MIN + " to " + DATE_MAX);
779             }
780             return Optional.of(date);
781         }
782 
783         try {
784             // Parse the date in UTC such as '2011-12-03T10:15:30Z' or with an offset '2019-10-05T20:37:42+06:00'.
785             final Instant date = OffsetDateTime.parse(outputTimestamp)
786                     .withOffsetSameInstant(ZoneOffset.UTC)
787                     .truncatedTo(ChronoUnit.SECONDS)
788                     .toInstant();
789 
790             if (date.isBefore(DATE_MIN) || date.isAfter(DATE_MAX)) {
791                 throw new IllegalArgumentException(
792                         "'" + date + "' is not within the valid range " + DATE_MIN + " to " + DATE_MAX);
793             }
794             return Optional.of(date);
795         } catch (DateTimeParseException pe) {
796             throw new IllegalArgumentException(
797                     "Invalid project.build.outputTimestamp value '" + outputTimestamp + "'", pe);
798         }
799     }
800 
801     private static boolean isNumeric(String str) {
802         if (str.isEmpty()) {
803             return false;
804         }
805 
806         for (char c : str.toCharArray()) {
807             if (!Character.isDigit(c)) {
808                 return false;
809             }
810         }
811 
812         return true;
813     }
814 
815     /**
816      * Configure Reproducible Builds archive creation if a timestamp is provided.
817      *
818      * @param outputTimestamp the value of {@code project.build.outputTimestamp} (may be {@code null})
819      * @since 3.6.0
820      * @see #parseBuildOutputTimestamp(String)
821      */
822     public void configureReproducibleBuild(String outputTimestamp) {
823         parseBuildOutputTimestamp(outputTimestamp).map(FileTime::from).ifPresent(modifiedTime -> getArchiver()
824                 .configureReproducibleBuild(modifiedTime));
825     }
826 }