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.execution.MavenSession;
25  import org.apache.maven.project.MavenProject;
26  import org.codehaus.plexus.archiver.ArchiverException;
27  import org.codehaus.plexus.archiver.jar.JarArchiver;
28  import org.codehaus.plexus.archiver.jar.Manifest;
29  import org.codehaus.plexus.archiver.jar.ManifestException;
30  import org.codehaus.plexus.interpolation.InterpolationException;
31  import org.codehaus.plexus.interpolation.Interpolator;
32  import org.codehaus.plexus.interpolation.PrefixAwareRecursionInterceptor;
33  import org.codehaus.plexus.interpolation.PrefixedObjectValueSource;
34  import org.codehaus.plexus.interpolation.PrefixedPropertiesValueSource;
35  import org.codehaus.plexus.interpolation.RecursionInterceptor;
36  import org.codehaus.plexus.interpolation.StringSearchInterpolator;
37  import org.codehaus.plexus.interpolation.ValueSource;
38  import org.apache.maven.shared.utils.StringUtils;
39  
40  import java.io.File;
41  import java.io.IOException;
42  import java.util.ArrayList;
43  import java.util.Collections;
44  import java.util.List;
45  import java.util.Map;
46  import java.util.Properties;
47  import java.util.Set;
48  
49  /**
50   * @author <a href="evenisse@apache.org">Emmanuel Venisse</a>
51   * @version $Revision: 1634380 $ $Date: 2014-10-26 20:01:23 +0100 (So, 26 Okt 2014) $
52   */
53  public class MavenArchiver
54  {
55  
56      public static final String SIMPLE_LAYOUT =
57          "${artifact.artifactId}-${artifact.version}${dashClassifier?}.${artifact.extension}";
58  
59      public static final String REPOSITORY_LAYOUT = "${artifact.groupIdPath}/${artifact.artifactId}/" +
60          "${artifact.baseVersion}/${artifact.artifactId}-" +
61          "${artifact.version}${dashClassifier?}.${artifact.extension}";
62  
63      public static final String SIMPLE_LAYOUT_NONUNIQUE =
64          "${artifact.artifactId}-${artifact.baseVersion}${dashClassifier?}.${artifact.extension}";
65  
66      public static final String REPOSITORY_LAYOUT_NONUNIQUE = "${artifact.groupIdPath}/${artifact.artifactId}/" +
67          "${artifact.baseVersion}/${artifact.artifactId}-" +
68          "${artifact.baseVersion}${dashClassifier?}.${artifact.extension}";
69  
70      private static final List<String> ARTIFACT_EXPRESSION_PREFIXES;
71  
72      static
73      {
74          List<String> artifactExpressionPrefixes = new ArrayList<String>();
75          artifactExpressionPrefixes.add( "artifact." );
76  
77          ARTIFACT_EXPRESSION_PREFIXES = artifactExpressionPrefixes;
78      }
79  
80      private JarArchiver archiver;
81  
82      private File archiveFile;
83  
84      /**
85       * Return a pre-configured manifest
86       *
87       * @param project the project
88       * @param config  the configuration to use
89       * @return a manifest, clients are recommended to use java.util.jar.Manifest datatype.
90       * @throws org.apache.maven.artifact.DependencyResolutionRequiredException
91       *          .
92       * @throws org.codehaus.plexus.archiver.jar.ManifestException
93       *          .
94       * @todo Add user attributes list and user groups list
95       * @deprecated
96       */
97      @SuppressWarnings ("UnusedDeclaration")
98      public Manifest getManifest( MavenProject project, MavenArchiveConfiguration config )
99          throws ManifestException, DependencyResolutionRequiredException
100     {
101         return getManifest( null, project, config );
102     }
103 
104     public Manifest getManifest( MavenSession session, MavenProject project, MavenArchiveConfiguration config )
105         throws ManifestException, DependencyResolutionRequiredException
106     {
107         boolean hasManifestEntries = !config.isManifestEntriesEmpty();
108         Map<String, String> entries =
109             hasManifestEntries ? config.getManifestEntries() : Collections.<String, String>emptyMap();
110 
111         Manifest manifest = getManifest( session, project, config.getManifest(), entries );
112 
113         // any custom manifest entries in the archive configuration manifest?
114         if ( hasManifestEntries )
115         {
116 
117             for ( Map.Entry<String, String> entry : entries.entrySet() )
118             {
119                 String key = entry.getKey();
120                 String value = entry.getValue();
121                 Manifest.Attribute attr = manifest.getMainSection().getAttribute( key );
122                 if ( key.equals( "Class-Path" ) && attr != null )
123                 {
124                     // Merge the user-supplied Class-Path value with the programmatically
125                     // generated Class-Path.  Note that the user-supplied value goes first
126                     // so that resources there will override any in the standard Class-Path.
127                     attr.setValue( value + " " + attr.getValue() );
128                 }
129                 else
130                 {
131                     addManifestAttribute( manifest, key, value );
132                 }
133             }
134         }
135 
136         // any custom manifest sections in the archive configuration manifest?
137         if ( !config.isManifestSectionsEmpty() )
138         {
139             for ( ManifestSection section : config.getManifestSections() )
140             {
141                 Manifest.Section theSection = new Manifest.Section();
142                 theSection.setName( section.getName() );
143 
144                 if ( !section.isManifestEntriesEmpty() )
145                 {
146                     Map<String, String> sectionEntries = section.getManifestEntries();
147 
148                     for ( Map.Entry<String, String> entry : sectionEntries.entrySet() )
149                     {
150                         String key = entry.getKey();
151                         String value = entry.getValue();
152                         Manifest.Attribute attr = new Manifest.Attribute( key, value );
153                         theSection.addConfiguredAttribute( attr );
154                     }
155                 }
156 
157                 manifest.addConfiguredSection( theSection );
158             }
159         }
160 
161         return manifest;
162     }
163 
164     /**
165      * Return a pre-configured manifest
166      *
167      * @todo Add user attributes list and user groups list
168      */
169     public Manifest getManifest( MavenProject project, ManifestConfiguration config )
170         throws ManifestException, DependencyResolutionRequiredException
171     {
172         return getManifest( null, project, config, Collections.<String, String>emptyMap() );
173     }
174 
175     public Manifest getManifest( MavenSession mavenSession, MavenProject project, ManifestConfiguration config )
176         throws ManifestException, DependencyResolutionRequiredException
177     {
178         return getManifest( mavenSession, project, config, Collections.<String, String>emptyMap() );
179     }
180 
181     private void addManifestAttribute( Manifest manifest, Map<String, String> map, String key, String value )
182         throws ManifestException
183     {
184         if ( map.containsKey( key ) )
185         {
186             return;  // The map value will be added later
187         }
188         addManifestAttribute( manifest, key, value );
189     }
190 
191     private void addManifestAttribute( Manifest manifest, String key, String value )
192         throws ManifestException
193     {
194         if ( !StringUtils.isEmpty( value ) )
195         {
196             Manifest.Attribute attr = new Manifest.Attribute( key, value );
197             manifest.addConfiguredAttribute( attr );
198         }
199         else
200         {
201             // if the value is empty we have create an entry with an empty string
202             // to prevent null print in the manifest file
203             Manifest.Attribute attr = new Manifest.Attribute( key, "" );
204             manifest.addConfiguredAttribute( attr );
205         }
206     }
207 
208     protected Manifest getManifest( MavenSession session, MavenProject project, ManifestConfiguration config,
209                                     Map<String, String> entries )
210         throws ManifestException, DependencyResolutionRequiredException
211     {
212         // TODO: Should we replace "map" with a copy? Note, that we modify it!
213 
214         // Added basic entries
215         Manifest m = new Manifest();
216         addCreatedByEntry( session, m, entries );
217 
218         addCustomEntries( m, entries, config );
219 
220         if ( config.isAddClasspath() )
221         {
222             StringBuilder classpath = new StringBuilder();
223 
224             @SuppressWarnings ("unchecked") List<String> artifacts = project.getRuntimeClasspathElements();
225             String classpathPrefix = config.getClasspathPrefix();
226             String layoutType = config.getClasspathLayoutType();
227             String layout = config.getCustomClasspathLayout();
228 
229             Interpolator interpolator = new StringSearchInterpolator();
230 
231             for ( String artifactFile : artifacts )
232             {
233                 File f = new File( artifactFile );
234                 if ( f.getAbsoluteFile().isFile() )
235                 {
236                     @SuppressWarnings ("unchecked") Artifact artifact =
237                         findArtifactWithFile( project.getArtifacts(), f );
238 
239                     if ( classpath.length() > 0 )
240                     {
241                         classpath.append( " " );
242                     }
243                     classpath.append( classpathPrefix );
244 
245                     // NOTE: If the artifact or layout type (from config) is null, give up and use the file name by itself.
246                     if ( artifact == null || layoutType == null )
247                     {
248                         classpath.append( f.getName() );
249                     }
250                     else
251                     {
252                         List<ValueSource> valueSources = new ArrayList<ValueSource>();
253                         valueSources.add(
254                             new PrefixedObjectValueSource( ARTIFACT_EXPRESSION_PREFIXES, artifact, true ) );
255                         valueSources.add( new PrefixedObjectValueSource( ARTIFACT_EXPRESSION_PREFIXES, artifact == null
256                             ? null
257                             : artifact.getArtifactHandler(), true ) );
258 
259                         Properties extraExpressions = new Properties();
260                         if ( artifact != null )
261                         {
262                             // FIXME: This query method SHOULD NOT affect the internal
263                             // state of the artifact version, but it does.
264                             if ( !artifact.isSnapshot() )
265                             {
266                                 extraExpressions.setProperty( "baseVersion", artifact.getVersion() );
267                             }
268 
269                             extraExpressions.setProperty( "groupIdPath", artifact.getGroupId().replace( '.', '/' ) );
270                             if ( StringUtils.isNotEmpty( artifact.getClassifier() ) )
271                             {
272                                 extraExpressions.setProperty( "dashClassifier", "-" + artifact.getClassifier() );
273                                 extraExpressions.setProperty( "dashClassifier?", "-" + artifact.getClassifier() );
274                             }
275                             else
276                             {
277                                 extraExpressions.setProperty( "dashClassifier", "" );
278                                 extraExpressions.setProperty( "dashClassifier?", "" );
279                             }
280                         }
281                         valueSources.add(
282                             new PrefixedPropertiesValueSource( ARTIFACT_EXPRESSION_PREFIXES, extraExpressions, true ) );
283 
284                         for ( ValueSource vs : valueSources )
285                         {
286                             interpolator.addValueSource( vs );
287                         }
288 
289                         RecursionInterceptor recursionInterceptor =
290                             new PrefixAwareRecursionInterceptor( ARTIFACT_EXPRESSION_PREFIXES );
291 
292                         try
293                         {
294                             if ( ManifestConfiguration.CLASSPATH_LAYOUT_TYPE_SIMPLE.equals( layoutType ) )
295                             {
296                                 if ( config.isUseUniqueVersions() )
297                                 {
298                                     classpath.append( interpolator.interpolate( SIMPLE_LAYOUT, recursionInterceptor ) );
299                                 }
300                                 else
301                                 {
302                                     classpath.append(
303                                         interpolator.interpolate( SIMPLE_LAYOUT_NONUNIQUE, recursionInterceptor ) );
304                                 }
305                             }
306                             else if ( ManifestConfiguration.CLASSPATH_LAYOUT_TYPE_REPOSITORY.equals( layoutType ) )
307                             {
308                                 // we use layout /$groupId[0]/../${groupId[n]/$artifactId/$version/{fileName}
309                                 // here we must find the Artifact in the project Artifacts to generate the maven layout
310                                 if ( config.isUseUniqueVersions() )
311                                 {
312                                     classpath.append(
313                                         interpolator.interpolate( REPOSITORY_LAYOUT, recursionInterceptor ) );
314                                 }
315                                 else
316                                 {
317                                     classpath.append(
318                                         interpolator.interpolate( REPOSITORY_LAYOUT_NONUNIQUE, recursionInterceptor ) );
319                                 }
320                             }
321                             else if ( ManifestConfiguration.CLASSPATH_LAYOUT_TYPE_CUSTOM.equals( layoutType ) )
322                             {
323                                 if ( layout == null )
324                                 {
325                                     throw new ManifestException( ManifestConfiguration.CLASSPATH_LAYOUT_TYPE_CUSTOM
326                                                                      + " layout type was declared, but custom layout expression was not specified. Check your <archive><manifest><customLayout/> element." );
327                                 }
328 
329                                 classpath.append( interpolator.interpolate( layout, recursionInterceptor ) );
330                             }
331                             else
332                             {
333                                 throw new ManifestException( "Unknown classpath layout type: '" + layoutType
334                                                                  + "'. Check your <archive><manifest><layoutType/> element." );
335                             }
336                         }
337                         catch ( InterpolationException e )
338                         {
339                             ManifestException error = new ManifestException(
340                                 "Error interpolating artifact path for classpath entry: " + e.getMessage() );
341 
342                             error.initCause( e );
343                             throw error;
344                         }
345                         finally
346                         {
347                             for ( ValueSource vs : valueSources )
348                             {
349                                 interpolator.removeValuesSource( vs );
350                             }
351                         }
352                     }
353                 }
354             }
355 
356             if ( classpath.length() > 0 )
357             {
358                 // Class-Path is special and should be added to manifest even if
359                 // it is specified in the manifestEntries section
360                 addManifestAttribute( m, "Class-Path", classpath.toString() );
361             }
362         }
363 
364         if ( config.isAddDefaultSpecificationEntries() )
365         {
366             addManifestAttribute( m, entries, "Specification-Title", project.getName() );
367             addManifestAttribute( m, entries, "Specification-Version", project.getVersion() );
368 
369             if ( project.getOrganization() != null )
370             {
371                 addManifestAttribute( m, entries, "Specification-Vendor", project.getOrganization().getName() );
372             }
373         }
374 
375         if ( config.isAddDefaultImplementationEntries() )
376         {
377             addManifestAttribute( m, entries, "Implementation-Title", project.getName() );
378             addManifestAttribute( m, entries, "Implementation-Version", project.getVersion() );
379             // MJAR-5
380             addManifestAttribute( m, entries, "Implementation-Vendor-Id", project.getGroupId() );
381 
382             if ( project.getOrganization() != null )
383             {
384                 addManifestAttribute( m, entries, "Implementation-Vendor", project.getOrganization().getName() );
385             }
386 
387             if ( project.getUrl() != null )
388             {
389                 addManifestAttribute( m, entries, "Implementation-URL", project.getUrl() );
390             }
391         }
392 
393         String mainClass = config.getMainClass();
394         if ( mainClass != null && !"".equals( mainClass ) )
395         {
396             addManifestAttribute( m, entries, "Main-Class", mainClass );
397         }
398 
399         // Added extensions
400         if ( config.isAddExtensions() )
401         {
402             // TODO: this is only for applets - should we distinguish them as a packaging?
403             StringBuilder extensionsList = new StringBuilder();
404             @SuppressWarnings ("unchecked") Set<Artifact> artifacts = (Set<Artifact>) project.getArtifacts();
405 
406             for ( Artifact artifact : artifacts )
407             {
408                 if ( !Artifact.SCOPE_TEST.equals( artifact.getScope() ) )
409                 {
410                     if ( "jar".equals( artifact.getType() ) )
411                     {
412                         if ( extensionsList.length() > 0 )
413                         {
414                             extensionsList.append( " " );
415                         }
416                         extensionsList.append( artifact.getArtifactId() );
417                     }
418                 }
419             }
420 
421             if ( extensionsList.length() > 0 )
422             {
423                 addManifestAttribute( m, entries, "Extension-List", extensionsList.toString() );
424             }
425 
426             for ( Object artifact1 : artifacts )
427             {
428                 // TODO: the correct solution here would be to have an extension type, and to read
429                 // the real extension values either from the artifact's manifest or some part of the POM
430                 Artifact artifact = (Artifact) artifact1;
431                 if ( "jar".equals( artifact.getType() ) )
432                 {
433                     String artifactId = artifact.getArtifactId().replace( '.', '_' );
434                     String ename = artifactId + "-Extension-Name";
435                     addManifestAttribute( m, entries, ename, artifact.getArtifactId() );
436                     String iname = artifactId + "-Implementation-Version";
437                     addManifestAttribute( m, entries, iname, artifact.getVersion() );
438 
439                     if ( artifact.getRepository() != null )
440                     {
441                         iname = artifactId + "-Implementation-URL";
442                         String url = artifact.getRepository().getUrl() + "/" + artifact.toString();
443                         addManifestAttribute( m, entries, iname, url );
444                     }
445                 }
446             }
447         }
448 
449         return m;
450     }
451 
452     private void addCustomEntries( Manifest m, Map<String, String> entries, ManifestConfiguration config )
453         throws ManifestException
454     {
455         addManifestAttribute( m, entries, "Built-By", System.getProperty( "user.name" ) );
456         addManifestAttribute( m, entries, "Build-Jdk", System.getProperty( "java.version" ) );
457 
458 /* TODO: rethink this, it wasn't working
459         Artifact projectArtifact = project.getArtifact();
460 
461         if ( projectArtifact.isSnapshot() )
462         {
463             Manifest.Attribute buildNumberAttr = new Manifest.Attribute( "Build-Number", "" +
464                 project.getSnapshotDeploymentBuildNumber() );
465             m.addConfiguredAttribute( buildNumberAttr );
466         }
467 
468 */
469         if ( config.getPackageName() != null )
470         {
471             addManifestAttribute( m, entries, "Package", config.getPackageName() );
472         }
473     }
474 
475     public JarArchiver getArchiver()
476     {
477         return archiver;
478     }
479 
480     public void setArchiver( JarArchiver archiver )
481     {
482         this.archiver = archiver;
483     }
484 
485     public void setOutputFile( File outputFile )
486     {
487         archiveFile = outputFile;
488     }
489 
490     /**
491      * @deprecated
492      */
493     @SuppressWarnings ("JavaDoc")
494     public void createArchive( MavenProject project, MavenArchiveConfiguration archiveConfiguration )
495         throws ArchiverException, ManifestException, IOException, DependencyResolutionRequiredException
496     {
497         createArchive( null, project, archiveConfiguration );
498     }
499 
500     public void createArchive( MavenSession session, MavenProject project,
501                                MavenArchiveConfiguration archiveConfiguration )
502         throws ArchiverException, ManifestException, IOException, DependencyResolutionRequiredException
503     {
504         // we have to clone the project instance so we can write out the pom with the deployment version,
505         // without impacting the main project instance...
506         MavenProject workingProject = null;
507         try
508         {
509             workingProject = (MavenProject) project.clone();
510         }
511         catch (CloneNotSupportedException e)
512         {
513             // Should never happen
514             throw new ArchiverException( "Failed to clone Maven project", e );
515         }
516 
517         boolean forced = archiveConfiguration.isForced();
518         if ( archiveConfiguration.isAddMavenDescriptor() )
519         {
520             // ----------------------------------------------------------------------
521             // We want to add the metadata for the project to the JAR in two forms:
522             //
523             // The first form is that of the POM itself. Applications that wish to
524             // access the POM for an artifact using maven tools they can.
525             //
526             // The second form is that of a properties file containing the basic
527             // top-level POM elements so that applications that wish to access
528             // POM information without the use of maven tools can do so.
529             // ----------------------------------------------------------------------
530 
531             if ( workingProject.getArtifact().isSnapshot() )
532             {
533                 workingProject.setVersion( workingProject.getArtifact().getVersion() );
534             }
535 
536             String groupId = workingProject.getGroupId();
537 
538             String artifactId = workingProject.getArtifactId();
539 
540             archiver.addFile( project.getFile(), "META-INF/maven/" + groupId + "/" + artifactId + "/pom.xml" );
541 
542             // ----------------------------------------------------------------------
543             // Create pom.properties file
544             // ----------------------------------------------------------------------
545 
546             File pomPropertiesFile = archiveConfiguration.getPomPropertiesFile();
547             if ( pomPropertiesFile == null )
548             {
549                 File dir = new File( workingProject.getBuild().getDirectory(), "maven-archiver" );
550                 pomPropertiesFile = new File( dir, "pom.properties" );
551             }
552             new PomPropertiesUtil().createPomProperties( workingProject, archiver, pomPropertiesFile, forced );
553         }
554 
555         // ----------------------------------------------------------------------
556         // Create the manifest
557         // ----------------------------------------------------------------------
558 
559         File manifestFile = archiveConfiguration.getManifestFile();
560 
561         if ( manifestFile != null )
562         {
563             archiver.setManifest( manifestFile );
564         }
565 
566         Manifest manifest = getManifest( session, workingProject, archiveConfiguration );
567 
568         // Configure the jar
569         archiver.addConfiguredManifest( manifest );
570 
571         archiver.setCompress( archiveConfiguration.isCompress() );
572 
573         archiver.setRecompressAddedZips( archiveConfiguration.isRecompressAddedZips() );
574 
575         archiver.setIndex( archiveConfiguration.isIndex() );
576 
577         archiver.setDestFile( archiveFile );
578 
579         // make the archiver index the jars on the classpath, if we are adding that to the manifest
580         if ( archiveConfiguration.getManifest().isAddClasspath() )
581         {
582             @SuppressWarnings ("unchecked") List<String> artifacts = project.getRuntimeClasspathElements();
583             for ( String artifact : artifacts )
584             {
585                 File f = new File( artifact );
586                 archiver.addConfiguredIndexJars( f );
587             }
588         }
589 
590         archiver.setForced( forced );
591         if ( !archiveConfiguration.isForced() && archiver.isSupportingForced() )
592         {
593             // TODO Should issue a warning here, but how do we get a logger?
594             // TODO getLog().warn( "Forced build is disabled, but disabling the forced mode isn't supported by the archiver." );
595         }
596 
597         // create archive
598         archiver.createArchive();
599     }
600 
601     private void addCreatedByEntry( MavenSession session, Manifest m, Map<String, String> entries )
602         throws ManifestException
603     {
604         String createdBy = "Apache Maven";
605         if ( session != null ) // can be null due to API backwards compatibility
606         {
607             String mavenVersion = session.getExecutionProperties().getProperty( "maven.version" );
608             if ( mavenVersion != null )
609             {
610                 createdBy += " " + mavenVersion;
611             }
612         }
613         addManifestAttribute( m, entries, "Created-By", createdBy );
614     }
615 
616 
617     private Artifact findArtifactWithFile( Set<Artifact> artifacts, File file )
618     {
619         for ( Artifact artifact : artifacts )
620         {
621             // normally not null but we can check
622             if ( artifact.getFile() != null )
623             {
624                 if ( artifact.getFile().equals( file ) )
625                 {
626                     return artifact;
627                 }
628             }
629         }
630         return null;
631     }
632 }