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