1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.felix.bundleplugin;
20  
21  
22  import java.io.File;
23  import java.io.FilenameFilter;
24  import java.util.Arrays;
25  import java.util.Collection;
26  import java.util.HashSet;
27  import java.util.Iterator;
28  import java.util.LinkedHashMap;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Properties;
32  import java.util.Set;
33  import java.util.jar.Manifest;
34  import java.util.regex.Matcher;
35  import java.util.regex.Pattern;
36  
37  import org.apache.maven.artifact.Artifact;
38  import org.apache.maven.artifact.factory.ArtifactFactory;
39  import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
40  import org.apache.maven.artifact.repository.ArtifactRepository;
41  import org.apache.maven.artifact.resolver.ArtifactCollector;
42  import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
43  import org.apache.maven.artifact.resolver.ArtifactResolutionException;
44  import org.apache.maven.artifact.resolver.ArtifactResolver;
45  import org.apache.maven.artifact.versioning.VersionRange;
46  import org.apache.maven.plugin.MojoExecutionException;
47  import org.apache.maven.project.MavenProject;
48  import org.apache.maven.project.MavenProjectBuilder;
49  import org.apache.maven.project.ProjectBuildingException;
50  import org.apache.maven.project.artifact.InvalidDependencyVersionException;
51  import org.apache.maven.shared.dependency.tree.DependencyNode;
52  import org.apache.maven.shared.dependency.tree.DependencyTreeBuilder;
53  import org.apache.maven.shared.dependency.tree.DependencyTreeBuilderException;
54  import org.codehaus.plexus.util.FileUtils;
55  
56  import aQute.bnd.osgi.Analyzer;
57  import aQute.bnd.osgi.Jar;
58  
59  
60  /**
61   * Create OSGi bundles from all dependencies in the Maven project
62   * 
63   * @goal bundleall
64   * @phase package
65   * @requiresDependencyResolution test
66   * @description build an OSGi bundle jar for all transitive dependencies
67   * @deprecated The bundleall goal is no longer supported and may be removed in a future release
68   */
69  @Deprecated
70  public class BundleAllPlugin extends ManifestPlugin
71  {
72      private static final String LS = System.getProperty( "line.separator" );
73  
74      private static final Pattern SNAPSHOT_VERSION_PATTERN = Pattern.compile( "[0-9]{8}_[0-9]{6}_[0-9]+" );
75  
76      /**
77       * Local repository.
78       *
79       * @parameter expression="${localRepository}"
80       * @required
81       * @readonly
82       */
83      private ArtifactRepository localRepository;
84  
85      /**
86       * Remote repositories.
87       * 
88       * @parameter expression="${project.remoteArtifactRepositories}"
89       * @required
90       * @readonly
91       */
92      private List remoteRepositories;
93  
94      /**
95       * Import-Package to be used when wrapping dependencies.
96       *
97       * @parameter expression="${wrapImportPackage}" default-value="*"
98       */
99      private String wrapImportPackage;
100 
101     /**
102      * @component
103      */
104     private ArtifactFactory m_factory;
105 
106     /**
107      * @component
108      */
109     private ArtifactMetadataSource m_artifactMetadataSource;
110 
111     /**
112      * @component
113      */
114     private ArtifactCollector m_collector;
115 
116     /**
117      * Artifact resolver, needed to download jars.
118      * 
119      * @component
120      */
121     private ArtifactResolver m_artifactResolver;
122 
123     /**
124      * @component
125      */
126     private DependencyTreeBuilder m_dependencyTreeBuilder;
127 
128     /**
129      * @component
130      */
131     private MavenProjectBuilder m_mavenProjectBuilder;
132 
133     /**
134      * Ignore missing artifacts that are not required by current project but are required by the
135      * transitive dependencies.
136      * 
137      * @parameter
138      */
139     private boolean ignoreMissingArtifacts;
140 
141     private Set m_artifactsBeingProcessed = new HashSet();
142 
143     /**
144      * Process up to some depth 
145      * 
146      * @parameter
147      */
148     private int depth = Integer.MAX_VALUE;
149 
150 
151     public void execute() throws MojoExecutionException
152     {
153         getLog().warn( "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" );
154         getLog().warn( "! The bundleall goal is no longer supported and may be removed in a future release !" );
155         getLog().warn( "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" );
156 
157         BundleInfo bundleInfo = bundleAll( getProject() );
158         logDuplicatedPackages( bundleInfo );
159     }
160 
161 
162     /**
163      * Bundle a project and all its dependencies
164      * 
165      * @param project
166      * @throws MojoExecutionException
167      */
168     private BundleInfo bundleAll( MavenProject project ) throws MojoExecutionException
169     {
170         return bundleAll( project, depth );
171     }
172 
173 
174     /**
175      * Bundle a project and its transitive dependencies up to some depth level
176      * 
177      * @param project
178      * @param maxDepth how deep to process the dependency tree
179      * @throws MojoExecutionException
180      */
181     protected BundleInfo bundleAll( MavenProject project, int maxDepth ) throws MojoExecutionException
182     {
183         if ( alreadyBundled( project.getArtifact() ) )
184         {
185             getLog().debug( "Ignoring project already processed " + project.getArtifact() );
186             return null;
187         }
188 
189         if ( m_artifactsBeingProcessed.contains( project.getArtifact() ) )
190         {
191             getLog().warn( "Ignoring artifact due to dependency cycle " + project.getArtifact() );
192             return null;
193         }
194         m_artifactsBeingProcessed.add( project.getArtifact() );
195 
196         DependencyNode dependencyTree;
197 
198         try
199         {
200             dependencyTree = m_dependencyTreeBuilder.buildDependencyTree( project, localRepository, m_factory,
201                 m_artifactMetadataSource, null, m_collector );
202         }
203         catch ( DependencyTreeBuilderException e )
204         {
205             throw new MojoExecutionException( "Unable to build dependency tree", e );
206         }
207 
208         BundleInfo bundleInfo = new BundleInfo();
209 
210         if ( !dependencyTree.hasChildren() )
211         {
212             /* no need to traverse the tree */
213             return bundleRoot( project, bundleInfo );
214         }
215 
216         getLog().debug( "Will bundle the following dependency tree" + LS + dependencyTree );
217 
218         for ( Iterator it = dependencyTree.inverseIterator(); it.hasNext(); )
219         {
220             DependencyNode node = ( DependencyNode ) it.next();
221             if ( !it.hasNext() )
222             {
223                 /* this is the root, current project */
224                 break;
225             }
226 
227             if ( node.getState() != DependencyNode.INCLUDED )
228             {
229                 continue;
230             }
231 
232             if ( Artifact.SCOPE_SYSTEM.equals( node.getArtifact().getScope() ) )
233             {
234                 getLog().debug( "Ignoring system scoped artifact " + node.getArtifact() );
235                 continue;
236             }
237 
238             Artifact artifact;
239             try
240             {
241                 artifact = resolveArtifact( node.getArtifact() );
242             }
243             catch ( ArtifactNotFoundException e )
244             {
245                 if ( ignoreMissingArtifacts )
246                 {
247                     continue;
248                 }
249 
250                 throw new MojoExecutionException( "Artifact was not found in the repo" + node.getArtifact(), e );
251             }
252 
253             node.getArtifact().setFile( artifact.getFile() );
254 
255             int nodeDepth = node.getDepth();
256             if ( nodeDepth > maxDepth )
257             {
258                 /* node is deeper than we want */
259                 getLog().debug(
260                     "Ignoring " + node.getArtifact() + ", depth is " + nodeDepth + ", bigger than " + maxDepth );
261                 continue;
262             }
263 
264             MavenProject childProject;
265             try
266             {
267                 childProject = m_mavenProjectBuilder.buildFromRepository( artifact, remoteRepositories,
268                     localRepository, true );
269                 if ( childProject.getDependencyArtifacts() == null )
270                 {
271                     childProject.setDependencyArtifacts( childProject.createArtifacts( m_factory, null, null ) );
272                 }
273             }
274             catch ( InvalidDependencyVersionException e )
275             {
276                 throw new MojoExecutionException( "Invalid dependency version for artifact " + artifact );
277             }
278             catch ( ProjectBuildingException e )
279             {
280                 throw new MojoExecutionException( "Unable to build project object for artifact " + artifact, e );
281             }
282 
283             childProject.setArtifact( artifact );
284             getLog().debug( "Child project artifact location: " + childProject.getArtifact().getFile() );
285 
286             if ( ( Artifact.SCOPE_COMPILE.equals( artifact.getScope() ) )
287                 || ( Artifact.SCOPE_RUNTIME.equals( artifact.getScope() ) ) )
288             {
289                 BundleInfo subBundleInfo = bundleAll( childProject, maxDepth - 1 );
290                 if ( subBundleInfo != null )
291                 {
292                     bundleInfo.merge( subBundleInfo );
293                 }
294             }
295             else
296             {
297                 getLog().debug(
298                     "Not processing due to scope (" + childProject.getArtifact().getScope() + "): "
299                         + childProject.getArtifact() );
300             }
301         }
302 
303         return bundleRoot( project, bundleInfo );
304     }
305 
306 
307     /**
308      * Bundle the root of a dependency tree after all its children have been bundled
309      * 
310      * @param project
311      * @param bundleInfo
312      * @return
313      * @throws MojoExecutionException
314      */
315     private BundleInfo bundleRoot( MavenProject project, BundleInfo bundleInfo ) throws MojoExecutionException
316     {
317         /* do not bundle the project the mojo was called on */
318         if ( getProject() != project )
319         {
320             getLog().debug( "Project artifact location: " + project.getArtifact().getFile() );
321 
322             BundleInfo subBundleInfo = bundle( project );
323             if ( subBundleInfo != null )
324             {
325                 bundleInfo.merge( subBundleInfo );
326             }
327         }
328         return bundleInfo;
329     }
330 
331 
332     /**
333      * Bundle one project only without building its childre
334      * 
335      * @param project
336      * @throws MojoExecutionException
337      */
338     protected BundleInfo bundle( MavenProject project ) throws MojoExecutionException
339     {
340         Artifact artifact = project.getArtifact();
341         getLog().info( "Bundling " + artifact );
342 
343         try
344         {
345             Map instructions = new LinkedHashMap();
346             instructions.put( Analyzer.IMPORT_PACKAGE, wrapImportPackage );
347 
348             project.getArtifact().setFile( getFile( artifact ) );
349             File outputFile = getOutputFile( artifact );
350 
351             if ( project.getArtifact().getFile().equals( outputFile ) )
352             {
353                 /* TODO find the cause why it's getting here */
354                 return null;
355                 //                getLog().error(
356                 //                                "Trying to read and write " + artifact + " to the same file, try cleaning: "
357                 //                                    + outputFile );
358                 //                throw new IllegalStateException( "Trying to read and write " + artifact
359                 //                    + " to the same file, try cleaning: " + outputFile );
360             }
361 
362             Analyzer analyzer = getAnalyzer( project, instructions, new Properties(), getClasspath( project ) );
363 
364             Jar osgiJar = new Jar( project.getArtifactId(), project.getArtifact().getFile() );
365 
366             outputFile.getAbsoluteFile().getParentFile().mkdirs();
367 
368             Collection exportedPackages;
369             if ( isOsgi( osgiJar ) )
370             {
371                 /* if it is already an OSGi jar copy it as is */
372                 getLog().info(
373                     "Using existing OSGi bundle for " + project.getGroupId() + ":" + project.getArtifactId() + ":"
374                         + project.getVersion() );
375                 String exportHeader = osgiJar.getManifest().getMainAttributes().getValue( Analyzer.EXPORT_PACKAGE );
376                 exportedPackages = analyzer.parseHeader( exportHeader ).keySet();
377                 FileUtils.copyFile( project.getArtifact().getFile(), outputFile );
378             }
379             else
380             {
381                 /* else generate the manifest from the packages */
382                 exportedPackages = analyzer.getExports().keySet();
383                 Manifest manifest = analyzer.getJar().getManifest();
384                 osgiJar.setManifest( manifest );
385                 osgiJar.write( outputFile );
386             }
387 
388             BundleInfo bundleInfo = addExportedPackages( project, exportedPackages );
389 
390             // cleanup...
391             analyzer.close();
392             osgiJar.close();
393 
394             return bundleInfo;
395         }
396         /* too bad Jar.write throws Exception */
397         catch ( Exception e )
398         {
399             throw new MojoExecutionException( "Error generating OSGi bundle for project "
400                 + getArtifactKey( project.getArtifact() ), e );
401         }
402     }
403 
404 
405     private boolean isOsgi( Jar jar ) throws Exception
406     {
407         if ( jar.getManifest() != null )
408         {
409             return jar.getManifest().getMainAttributes().getValue( Analyzer.BUNDLE_NAME ) != null;
410         }
411         return false;
412     }
413 
414 
415     private BundleInfo addExportedPackages( MavenProject project, Collection packages )
416     {
417         BundleInfo bundleInfo = new BundleInfo();
418         for ( Iterator it = packages.iterator(); it.hasNext(); )
419         {
420             String packageName = ( String ) it.next();
421             bundleInfo.addExportedPackage( packageName, project.getArtifact() );
422         }
423         return bundleInfo;
424     }
425 
426 
427     private String getArtifactKey( Artifact artifact )
428     {
429         return artifact.getGroupId() + ":" + artifact.getArtifactId();
430     }
431 
432 
433     private String getBundleName( Artifact artifact )
434     {
435         return getMaven2OsgiConverter().getBundleFileName( artifact );
436     }
437 
438 
439     private boolean alreadyBundled( Artifact artifact )
440     {
441         return getBuiltFile( artifact ) != null;
442     }
443 
444 
445     /**
446      * Use previously built bundles when available.
447      * 
448      * @param artifact
449      */
450     protected File getFile( final Artifact artifact )
451     {
452         File bundle = getBuiltFile( artifact );
453 
454         if ( bundle != null )
455         {
456             getLog().debug( "Using previously built OSGi bundle for " + artifact + " in " + bundle );
457             return bundle;
458         }
459         return super.getFile( artifact );
460     }
461 
462 
463     private File getBuiltFile( final Artifact artifact )
464     {
465         File bundle = null;
466 
467         /* if bundle was already built use it instead of jar from repo */
468         File outputFile = getOutputFile( artifact );
469         if ( outputFile.exists() )
470         {
471             bundle = outputFile;
472         }
473 
474         /*
475          * Find snapshots in output folder, eg. 2.1-SNAPSHOT will match 2.1.0.20070207_193904_2
476          * TODO there has to be another way to do this using Maven libs 
477          */
478         if ( ( bundle == null ) && artifact.isSnapshot() )
479         {
480             final File buildDirectory = new File( getBuildDirectory() );
481             if ( !buildDirectory.exists() )
482             {
483                 buildDirectory.mkdirs();
484             }
485             File[] files = buildDirectory.listFiles( new FilenameFilter()
486             {
487                 public boolean accept( File dir, String name )
488                 {
489                     if ( dir.equals( buildDirectory ) && snapshotMatch( artifact, name ) )
490                     {
491                         return true;
492                     }
493                     return false;
494                 }
495             } );
496             if ( files.length > 1 )
497             {
498                 throw new RuntimeException( "More than one previously built bundle matches for artifact " + artifact
499                     + " : " + Arrays.asList( files ) );
500             }
501             if ( files.length == 1 )
502             {
503                 bundle = files[0];
504             }
505         }
506 
507         return bundle;
508     }
509 
510 
511     /**
512      * Check that the bundleName provided correspond to the artifact provided.
513      * Used to determine when the bundle name is a timestamped snapshot and the artifact is a snapshot not timestamped.
514      * 
515      * @param artifact artifact with snapshot version
516      * @param bundleName bundle file name 
517      * @return if both represent the same artifact and version, forgetting about the snapshot timestamp
518      */
519     protected boolean snapshotMatch( Artifact artifact, String bundleName )
520     {
521         String artifactBundleName = getBundleName( artifact );
522         int i = artifactBundleName.indexOf( "SNAPSHOT" );
523         if ( i < 0 )
524         {
525             return false;
526         }
527         artifactBundleName = artifactBundleName.substring( 0, i );
528 
529         if ( bundleName.startsWith( artifactBundleName ) )
530         {
531             /* it's the same artifact groupId and artifactId */
532             String timestamp = bundleName.substring( artifactBundleName.length(), bundleName.lastIndexOf( ".jar" ) );
533             Matcher m = SNAPSHOT_VERSION_PATTERN.matcher( timestamp );
534             return m.matches();
535         }
536         return false;
537     }
538 
539 
540     protected File getOutputFile( Artifact artifact )
541     {
542         return new File( getOutputDirectory(), getBundleName( artifact ) );
543     }
544 
545 
546     private Artifact resolveArtifact( Artifact artifact ) throws MojoExecutionException, ArtifactNotFoundException
547     {
548         VersionRange versionRange;
549         if ( artifact.getVersion() != null )
550         {
551             versionRange = VersionRange.createFromVersion( artifact.getVersion() );
552         }
553         else
554         {
555             versionRange = artifact.getVersionRange();
556         }
557 
558         /*
559          * there's a bug with ArtifactFactory#createDependencyArtifact(String, String, VersionRange,
560          * String, String, String) that ignores the scope parameter, that's why we use the one with
561          * the extra null parameter
562          */
563         Artifact resolvedArtifact = m_factory.createDependencyArtifact( artifact.getGroupId(),
564             artifact.getArtifactId(), versionRange, artifact.getType(), artifact.getClassifier(), artifact.getScope(),
565             null );
566 
567         try
568         {
569             m_artifactResolver.resolve( resolvedArtifact, remoteRepositories, localRepository );
570         }
571         catch ( ArtifactResolutionException e )
572         {
573             throw new MojoExecutionException( "Error resolving artifact " + resolvedArtifact, e );
574         }
575 
576         return resolvedArtifact;
577     }
578 
579 
580     /**
581      * Log what packages are exported in more than one bundle
582      */
583     protected void logDuplicatedPackages( BundleInfo bundleInfo )
584     {
585         Map duplicatedExports = bundleInfo.getDuplicatedExports();
586 
587         for ( Iterator it = duplicatedExports.entrySet().iterator(); it.hasNext(); )
588         {
589             Map.Entry entry = ( Map.Entry ) it.next();
590             String packageName = ( String ) entry.getKey();
591             Collection artifacts = ( Collection ) entry.getValue();
592 
593             getLog().warn( "Package " + packageName + " is exported in more than a bundle: " );
594             for ( Iterator it2 = artifacts.iterator(); it2.hasNext(); )
595             {
596                 Artifact artifact = ( Artifact ) it2.next();
597                 getLog().warn( "  " + artifact );
598             }
599 
600         }
601     }
602 }