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