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(
569                             session, workingProject, archiver, customPomPropertiesFile, pomPropertiesFile, forced);
570         }
571 
572         // ----------------------------------------------------------------------
573         // Create the manifest
574         // ----------------------------------------------------------------------
575 
576         archiver.setMinimalDefaultManifest(true);
577 
578         File manifestFile = archiveConfiguration.getManifestFile();
579 
580         if (manifestFile != null) {
581             archiver.setManifest(manifestFile);
582         }
583 
584         Manifest manifest = getManifest(session, workingProject, archiveConfiguration);
585 
586         // Configure the jar
587         archiver.addConfiguredManifest(manifest);
588 
589         archiver.setCompress(archiveConfiguration.isCompress());
590 
591         archiver.setRecompressAddedZips(archiveConfiguration.isRecompressAddedZips());
592 
593         archiver.setIndex(archiveConfiguration.isIndex());
594 
595         archiver.setDestFile(archiveFile);
596 
597         // make the archiver index the jars on the classpath, if we are adding that to the manifest
598         if (archiveConfiguration.getManifest().isAddClasspath()) {
599             List<String> artifacts = project.getRuntimeClasspathElements();
600             for (String artifact : artifacts) {
601                 File f = new File(artifact);
602                 archiver.addConfiguredIndexJars(f);
603             }
604         }
605 
606         archiver.setForced(forced);
607         if (!archiveConfiguration.isForced() && archiver.isSupportingForced()) {
608             // TODO Should issue a warning here, but how do we get a logger?
609             // TODO getLog().warn(
610             // "Forced build is disabled, but disabling the forced mode isn't supported by the archiver." );
611         }
612 
613         String automaticModuleName = manifest.getMainSection().getAttributeValue("Automatic-Module-Name");
614         if (automaticModuleName != null) {
615             if (!isValidModuleName(automaticModuleName)) {
616                 throw new ManifestException("Invalid automatic module name: '" + automaticModuleName + "'");
617             }
618         }
619 
620         // create archive
621         archiver.createArchive();
622     }
623 
624     private void handleDefaultEntries(Manifest m, Map<String, String> entries) throws ManifestException {
625         String createdBy = this.createdBy;
626         if (createdBy == null) {
627             createdBy = createdBy(CREATED_BY, "org.apache.maven", "maven-archiver");
628         }
629         addManifestAttribute(m, entries, "Created-By", createdBy);
630         if (buildJdkSpecDefaultEntry) {
631             addManifestAttribute(m, entries, "Build-Jdk-Spec", System.getProperty("java.specification.version"));
632         }
633     }
634 
635     private void handleBuildEnvironmentEntries(MavenSession session, Manifest m, Map<String, String> entries)
636             throws ManifestException {
637         addManifestAttribute(
638                 m,
639                 entries,
640                 "Build-Tool",
641                 session != null ? session.getSystemProperties().getProperty("maven.build.version") : "Apache Maven");
642         addManifestAttribute(
643                 m,
644                 entries,
645                 "Build-Jdk",
646                 String.format("%s (%s)", System.getProperty("java.version"), System.getProperty("java.vendor")));
647         addManifestAttribute(
648                 m,
649                 entries,
650                 "Build-Os",
651                 String.format(
652                         "%s (%s; %s)",
653                         System.getProperty("os.name"),
654                         System.getProperty("os.version"),
655                         System.getProperty("os.arch")));
656     }
657 
658     private Artifact findArtifactWithFile(Set<Artifact> artifacts, File file) {
659         for (Artifact artifact : artifacts) {
660             // normally not null but we can check
661             if (artifact.getFile() != null) {
662                 if (artifact.getFile().equals(file)) {
663                     return artifact;
664                 }
665             }
666         }
667         return null;
668     }
669 
670     private static String getCreatedByVersion(String groupId, String artifactId) {
671         final Properties properties = loadOptionalProperties(MavenArchiver.class.getResourceAsStream(
672                 "/META-INF/maven/" + groupId + "/" + artifactId + "/pom.properties"));
673 
674         return properties.getProperty("version");
675     }
676 
677     private static Properties loadOptionalProperties(final InputStream inputStream) {
678         Properties properties = new Properties();
679         if (inputStream != null) {
680             try (InputStream in = inputStream) {
681                 properties.load(in);
682             } catch (IllegalArgumentException | IOException ex) {
683                 // ignore and return empty properties
684             }
685         }
686         return properties;
687     }
688 
689     /**
690      * Define a value for "Created By" entry.
691      *
692      * @param description description of the plugin, like "Maven Source Plugin"
693      * @param groupId groupId where to get version in pom.properties
694      * @param artifactId artifactId where to get version in pom.properties
695      * @since 3.5.0
696      */
697     public void setCreatedBy(String description, String groupId, String artifactId) {
698         createdBy = createdBy(description, groupId, artifactId);
699     }
700 
701     private String createdBy(String description, String groupId, String artifactId) {
702         String createdBy = description;
703         String version = getCreatedByVersion(groupId, artifactId);
704         if (version != null) {
705             createdBy += " " + version;
706         }
707         return createdBy;
708     }
709 
710     /**
711      * Add "Build-Jdk-Spec" entry as part of default manifest entries (true by default).
712      * For plugins whose output is not impacted by JDK release (like maven-source-plugin), adding
713      * Jdk spec adds unnecessary requirement on JDK version used at build to get reproducible result.
714      *
715      * @param buildJdkSpecDefaultEntry the value for "Build-Jdk-Spec" entry
716      * @since 3.5.0
717      */
718     public void setBuildJdkSpecDefaultEntry(boolean buildJdkSpecDefaultEntry) {
719         this.buildJdkSpecDefaultEntry = buildJdkSpecDefaultEntry;
720     }
721 
722     /**
723      * Parse output timestamp configured for Reproducible Builds' archive entries, either formatted as ISO 8601
724      * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
725      * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>.
726      *
727      * @param outputTimestamp the value of <code>${project.build.outputTimestamp}</code> (may be <code>null</code>)
728      * @return the parsed timestamp, may be <code>null</code> if <code>null</code> input or input contains only 1
729      *         character
730      * @since 3.5.0
731      * @throws IllegalArgumentException if the outputTimestamp is neither ISO 8601 nor an integer, or it's not within
732      *             the valid range 1980-01-01T00:00:02Z to 2099-12-31T23:59:59Z
733      * @deprecated Use {@link #parseBuildOutputTimestamp(String)} instead.
734      */
735     @Deprecated
736     public Date parseOutputTimestamp(String outputTimestamp) {
737         return parseBuildOutputTimestamp(outputTimestamp).map(Date::from).orElse(null);
738     }
739 
740     /**
741      * Configure Reproducible Builds archive creation if a timestamp is provided.
742      *
743      * @param outputTimestamp the value of {@code ${project.build.outputTimestamp}} (may be {@code null})
744      * @return the parsed timestamp as {@link java.util.Date}
745      * @since 3.5.0
746      * @see #parseOutputTimestamp
747      * @deprecated Use {@link #configureReproducibleBuild(String)} instead.
748      */
749     @Deprecated
750     public Date configureReproducible(String outputTimestamp) {
751         configureReproducibleBuild(outputTimestamp);
752         return parseOutputTimestamp(outputTimestamp);
753     }
754 
755     /**
756      * Parse output timestamp configured for Reproducible Builds' archive entries.
757      *
758      * <p>Either as {@link java.time.format.DateTimeFormatter#ISO_OFFSET_DATE_TIME} or as a number representing seconds
759      * since the epoch (like <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
760      *
761      * @param outputTimestamp the value of {@code ${project.build.outputTimestamp}} (may be {@code null})
762      * @return the parsed timestamp as an {@code Optional<Instant>}, {@code empty} if input is {@code null} or input
763      *         contains only 1 character (not a number)
764      * @since 3.6.0
765      * @throws IllegalArgumentException if the outputTimestamp is neither ISO 8601 nor an integer, or it's not within
766      *             the valid range 1980-01-01T00:00:02Z to 2099-12-31T23:59:59Z
767      */
768     public static Optional<Instant> parseBuildOutputTimestamp(String outputTimestamp) {
769         // Fail-fast on nulls
770         if (outputTimestamp == null) {
771             return Optional.empty();
772         }
773 
774         // Number representing seconds since the epoch
775         if (isNumeric(outputTimestamp)) {
776             return Optional.of(Instant.ofEpochSecond(Long.parseLong(outputTimestamp)));
777         }
778 
779         // no timestamp configured (1 character configuration is useful to override a full value during pom
780         // inheritance)
781         if (outputTimestamp.length() < 2) {
782             return Optional.empty();
783         }
784 
785         try {
786             // Parse the date in UTC such as '2011-12-03T10:15:30Z' or with an offset '2019-10-05T20:37:42+06:00'.
787             final Instant date = OffsetDateTime.parse(outputTimestamp)
788                     .withOffsetSameInstant(ZoneOffset.UTC)
789                     .truncatedTo(ChronoUnit.SECONDS)
790                     .toInstant();
791 
792             if (date.isBefore(DATE_MIN) || date.isAfter(DATE_MAX)) {
793                 throw new IllegalArgumentException(
794                         "'" + date + "' is not within the valid range " + DATE_MIN + " to " + DATE_MAX);
795             }
796             return Optional.of(date);
797         } catch (DateTimeParseException pe) {
798             throw new IllegalArgumentException(
799                     "Invalid project.build.outputTimestamp value '" + outputTimestamp + "'", pe);
800         }
801     }
802 
803     private static boolean isNumeric(String str) {
804 
805         if (str.isEmpty()) {
806             return false;
807         }
808 
809         for (char c : str.toCharArray()) {
810             if (!Character.isDigit(c)) {
811                 return false;
812             }
813         }
814 
815         return true;
816     }
817 
818     /**
819      * Configure Reproducible Builds archive creation if a timestamp is provided.
820      *
821      * @param outputTimestamp the value of {@code project.build.outputTimestamp} (may be {@code null})
822      * @since 3.6.0
823      * @see #parseBuildOutputTimestamp(String)
824      */
825     public void configureReproducibleBuild(String outputTimestamp) {
826         parseBuildOutputTimestamp(outputTimestamp).map(FileTime::from).ifPresent(modifiedTime -> getArchiver()
827                 .configureReproducibleBuild(modifiedTime));
828     }
829 }