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