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