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