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