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