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.ByteArrayInputStream;
23  import java.io.ByteArrayOutputStream;
24  import java.io.File;
25  import java.io.FileInputStream;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.lang.reflect.Method;
29  import java.util.ArrayList;
30  import java.util.Arrays;
31  import java.util.Collection;
32  import java.util.Collections;
33  import java.util.Enumeration;
34  import java.util.HashSet;
35  import java.util.Iterator;
36  import java.util.LinkedHashMap;
37  import java.util.LinkedHashSet;
38  import java.util.List;
39  import java.util.Map;
40  import java.util.Properties;
41  import java.util.Set;
42  import java.util.jar.Attributes;
43  import java.util.jar.Manifest;
44  
45  import org.apache.maven.archiver.ManifestSection;
46  import org.apache.maven.archiver.MavenArchiveConfiguration;
47  import org.apache.maven.archiver.MavenArchiver;
48  import org.apache.maven.artifact.Artifact;
49  import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
50  import org.apache.maven.execution.MavenSession;
51  import org.apache.maven.model.License;
52  import org.apache.maven.model.Model;
53  import org.apache.maven.model.Resource;
54  import org.apache.maven.plugin.AbstractMojo;
55  import org.apache.maven.plugin.MojoExecutionException;
56  import org.apache.maven.plugin.MojoFailureException;
57  import org.apache.maven.plugin.logging.Log;
58  import org.apache.maven.project.MavenProject;
59  import org.apache.maven.project.MavenProjectHelper;
60  import org.apache.maven.shared.osgi.DefaultMaven2OsgiConverter;
61  import org.apache.maven.shared.osgi.Maven2OsgiConverter;
62  import org.codehaus.plexus.archiver.UnArchiver;
63  import org.codehaus.plexus.archiver.manager.ArchiverManager;
64  import org.codehaus.plexus.util.DirectoryScanner;
65  import org.codehaus.plexus.util.FileUtils;
66  import org.codehaus.plexus.util.StringUtils;
67  
68  import aQute.lib.osgi.Analyzer;
69  import aQute.lib.osgi.Builder;
70  import aQute.lib.osgi.Constants;
71  import aQute.lib.osgi.EmbeddedResource;
72  import aQute.lib.osgi.FileResource;
73  import aQute.lib.osgi.Jar;
74  import aQute.lib.osgi.Processor;
75  import aQute.lib.spring.SpringXMLType;
76  
77  
78  /**
79   * Create an OSGi bundle from Maven project
80   *
81   * @goal bundle
82   * @phase package
83   * @requiresDependencyResolution test
84   * @description build an OSGi bundle jar
85   * @threadSafe
86   */
87  public class BundlePlugin extends AbstractMojo
88  {
89      /**
90       * Directory where the manifest will be written
91       *
92       * @parameter expression="${manifestLocation}" default-value="${project.build.outputDirectory}/META-INF"
93       */
94      protected File manifestLocation;
95  
96      /**
97       * File where the BND instructions will be dumped
98       *
99       * @parameter expression="${dumpInstructions}"
100      */
101     protected File dumpInstructions;
102 
103     /**
104      * File where the BND class-path will be dumped
105      *
106      * @parameter expression="${dumpClasspath}"
107      */
108     protected File dumpClasspath;
109 
110     /**
111      * When true, unpack the bundle contents to the outputDirectory
112      *
113      * @parameter expression="${unpackBundle}"
114      */
115     protected boolean unpackBundle;
116 
117     /**
118      * Comma separated list of artifactIds to exclude from the dependency classpath passed to BND (use "true" to exclude everything)
119      *
120      * @parameter expression="${excludeDependencies}"
121      */
122     protected String excludeDependencies;
123 
124     /**
125      * Classifier type of the bundle to be installed.  For example, "jdk14".
126      * Defaults to none which means this is the project's main bundle.
127      *
128      * @parameter
129      */
130     protected String classifier;
131 
132     /**
133      * @component
134      */
135     private MavenProjectHelper m_projectHelper;
136 
137     /**
138      * @component
139      */
140     private ArchiverManager m_archiverManager;
141 
142     /**
143      * @component
144      */
145     private ArtifactHandlerManager m_artifactHandlerManager;
146 
147     /**
148      * Project types which this plugin supports.
149      *
150      * @parameter
151      */
152     protected List supportedProjectTypes = Arrays.asList( new String[]
153         { "jar", "bundle" } );
154 
155     /**
156      * The directory for the generated bundles.
157      *
158      * @parameter expression="${project.build.outputDirectory}"
159      * @required
160      */
161     private File outputDirectory;
162 
163     /**
164      * The directory for the generated JAR.
165      *
166      * @parameter expression="${project.build.directory}"
167      * @required
168      */
169     private String buildDirectory;
170 
171     /**
172      * The Maven project.
173      *
174      * @parameter expression="${project}"
175      * @required
176      * @readonly
177      */
178     private MavenProject project;
179 
180     /**
181      * The BND instructions for the bundle.
182      *
183      * @parameter
184      */
185     private Map instructions = new LinkedHashMap();
186 
187     /**
188      * Use locally patched version for now.
189      */
190     private Maven2OsgiConverter m_maven2OsgiConverter = new DefaultMaven2OsgiConverter();
191 
192     /**
193      * The archive configuration to use.
194      *
195      * @parameter
196      */
197     private MavenArchiveConfiguration archive; // accessed indirectly in JarPluginConfiguration
198 
199     /**
200      * @parameter default-value="${session}"
201      * @required
202      * @readonly
203      */
204     private MavenSession m_mavenSession;
205 
206     private static final String MAVEN_SYMBOLICNAME = "maven-symbolicname";
207     private static final String MAVEN_RESOURCES = "{maven-resources}";
208     private static final String LOCAL_PACKAGES = "{local-packages}";
209 
210     private static final String[] EMPTY_STRING_ARRAY =
211         {};
212     private static final String[] DEFAULT_INCLUDES =
213         { "**/**" };
214 
215     private static final String NL = System.getProperty( "line.separator" );
216 
217 
218     protected Maven2OsgiConverter getMaven2OsgiConverter()
219     {
220         return m_maven2OsgiConverter;
221     }
222 
223 
224     protected void setMaven2OsgiConverter( Maven2OsgiConverter maven2OsgiConverter )
225     {
226         m_maven2OsgiConverter = maven2OsgiConverter;
227     }
228 
229 
230     protected MavenProject getProject()
231     {
232         return project;
233     }
234 
235 
236     /**
237      * @see org.apache.maven.plugin.AbstractMojo#execute()
238      */
239     public void execute() throws MojoExecutionException
240     {
241         Properties properties = new Properties();
242         String projectType = getProject().getArtifact().getType();
243 
244         // ignore unsupported project types, useful when bundleplugin is configured in parent pom
245         if ( !supportedProjectTypes.contains( projectType ) )
246         {
247             getLog().warn(
248                 "Ignoring project type " + projectType + " - supportedProjectTypes = " + supportedProjectTypes );
249             return;
250         }
251 
252         execute( getProject(), instructions, properties );
253     }
254 
255 
256     protected void execute( MavenProject currentProject, Map originalInstructions, Properties properties )
257         throws MojoExecutionException
258     {
259         try
260         {
261             execute( currentProject, originalInstructions, properties, getClasspath( currentProject ) );
262         }
263         catch ( IOException e )
264         {
265             throw new MojoExecutionException( "Error calculating classpath for project " + currentProject, e );
266         }
267     }
268 
269 
270     /* transform directives from their XML form to the expected BND syntax (eg. _include becomes -include) */
271     protected static Map transformDirectives( Map originalInstructions )
272     {
273         Map transformedInstructions = new LinkedHashMap();
274         for ( Iterator i = originalInstructions.entrySet().iterator(); i.hasNext(); )
275         {
276             Map.Entry e = ( Map.Entry ) i.next();
277 
278             String key = ( String ) e.getKey();
279             if ( key.startsWith( "_" ) )
280             {
281                 key = "-" + key.substring( 1 );
282             }
283 
284             String value = ( String ) e.getValue();
285             if ( null == value )
286             {
287                 value = "";
288             }
289             else
290             {
291                 value = value.replaceAll( "\\p{Blank}*[\r\n]\\p{Blank}*", "" );
292             }
293 
294             if ( Analyzer.WAB.equals( key ) && value.length() == 0 )
295             {
296                 // provide useful default
297                 value = "src/main/webapp/";
298             }
299 
300             transformedInstructions.put( key, value );
301         }
302         return transformedInstructions;
303     }
304 
305 
306     protected boolean reportErrors( String prefix, Analyzer analyzer )
307     {
308         List errors = analyzer.getErrors();
309         List warnings = analyzer.getWarnings();
310 
311         for ( Iterator w = warnings.iterator(); w.hasNext(); )
312         {
313             String msg = ( String ) w.next();
314             getLog().warn( prefix + " : " + msg );
315         }
316 
317         boolean hasErrors = false;
318         String fileNotFound = "Input file does not exist: ";
319         for ( Iterator e = errors.iterator(); e.hasNext(); )
320         {
321             String msg = ( String ) e.next();
322             if ( msg.startsWith( fileNotFound ) && msg.endsWith( "~" ) )
323             {
324                 // treat as warning; this error happens when you have duplicate entries in Include-Resource
325                 String duplicate = Processor.removeDuplicateMarker( msg.substring( fileNotFound.length() ) );
326                 getLog().warn( prefix + " : Duplicate path '" + duplicate + "' in Include-Resource" );
327             }
328             else
329             {
330                 getLog().error( prefix + " : " + msg );
331                 hasErrors = true;
332             }
333         }
334         return hasErrors;
335     }
336 
337 
338     protected void execute( MavenProject currentProject, Map originalInstructions, Properties properties,
339         Jar[] classpath ) throws MojoExecutionException
340     {
341         try
342         {
343             File jarFile = new File( getBuildDirectory(), getBundleName( currentProject ) );
344             Builder builder = buildOSGiBundle( currentProject, originalInstructions, properties, classpath );
345             boolean hasErrors = reportErrors( "Bundle " + currentProject.getArtifact(), builder );
346             if ( hasErrors )
347             {
348                 String failok = builder.getProperty( "-failok" );
349                 if ( null == failok || "false".equalsIgnoreCase( failok ) )
350                 {
351                     jarFile.delete();
352 
353                     throw new MojoFailureException( "Error(s) found in bundle configuration" );
354                 }
355             }
356 
357             // attach bundle to maven project
358             jarFile.getParentFile().mkdirs();
359             builder.getJar().write( jarFile );
360 
361             Artifact mainArtifact = currentProject.getArtifact();
362 
363             if ( "bundle".equals( mainArtifact.getType() ) )
364             {
365                 // workaround for MNG-1682: force maven to install artifact using the "jar" handler
366                 mainArtifact.setArtifactHandler( m_artifactHandlerManager.getArtifactHandler( "jar" ) );
367             }
368 
369             if ( null == classifier || classifier.trim().length() == 0 )
370             {
371                 mainArtifact.setFile( jarFile );
372             }
373             else
374             {
375                 m_projectHelper.attachArtifact( currentProject, jarFile, classifier );
376             }
377 
378             if ( unpackBundle )
379             {
380                 unpackBundle( jarFile );
381             }
382 
383             if ( manifestLocation != null )
384             {
385                 File outputFile = new File( manifestLocation, "MANIFEST.MF" );
386 
387                 try
388                 {
389                     Manifest manifest = builder.getJar().getManifest();
390                     ManifestPlugin.writeManifest( manifest, outputFile );
391                 }
392                 catch ( IOException e )
393                 {
394                     getLog().error( "Error trying to write Manifest to file " + outputFile, e );
395                 }
396             }
397 
398             // cleanup...
399             builder.close();
400         }
401         catch ( MojoFailureException e )
402         {
403             getLog().error( e.getLocalizedMessage() );
404             throw new MojoExecutionException( "Error(s) found in bundle configuration", e );
405         }
406         catch ( Exception e )
407         {
408             getLog().error( "An internal error occurred", e );
409             throw new MojoExecutionException( "Internal error in maven-bundle-plugin", e );
410         }
411     }
412 
413 
414     protected Builder getOSGiBuilder( MavenProject currentProject, Map originalInstructions, Properties properties,
415         Jar[] classpath ) throws Exception
416     {
417         properties.putAll( getDefaultProperties( currentProject ) );
418         properties.putAll( transformDirectives( originalInstructions ) );
419 
420         Builder builder = new Builder();
421         builder.setBase( getBase( currentProject ) );
422         builder.setProperties( properties );
423         if ( classpath != null )
424         {
425             builder.setClasspath( classpath );
426         }
427 
428         return builder;
429     }
430 
431 
432     protected void addMavenInstructions( MavenProject currentProject, Builder builder ) throws Exception
433     {
434         if ( currentProject.getBasedir() != null )
435         {
436             // update BND instructions to add included Maven resources
437             includeMavenResources( currentProject, builder, getLog() );
438 
439             // calculate default export/private settings based on sources
440             addLocalPackages( outputDirectory, builder );
441         }
442 
443         // update BND instructions to embed selected Maven dependencies
444         Collection embeddableArtifacts = getEmbeddableArtifacts( currentProject, builder );
445         new DependencyEmbedder( getLog(), embeddableArtifacts ).processHeaders( builder );
446 
447         if ( dumpInstructions != null || getLog().isDebugEnabled() )
448         {
449             StringBuilder buf = new StringBuilder();
450             getLog().debug( "BND Instructions:" + NL + dumpInstructions( builder.getProperties(), buf ) );
451             if ( dumpInstructions != null )
452             {
453                 getLog().info( "Writing BND instructions to " + dumpInstructions );
454                 dumpInstructions.getParentFile().mkdirs();
455                 FileUtils.fileWrite( dumpInstructions, "# BND instructions" + NL + buf );
456             }
457         }
458 
459         if ( dumpClasspath != null || getLog().isDebugEnabled() )
460         {
461             StringBuilder buf = new StringBuilder();
462             getLog().debug( "BND Classpath:" + NL + dumpClasspath( builder.getClasspath(), buf ) );
463             if ( dumpClasspath != null )
464             {
465                 getLog().info( "Writing BND classpath to " + dumpClasspath );
466                 dumpClasspath.getParentFile().mkdirs();
467                 FileUtils.fileWrite( dumpClasspath, "# BND classpath" + NL + buf );
468             }
469         }
470     }
471 
472 
473     protected Builder buildOSGiBundle( MavenProject currentProject, Map originalInstructions, Properties properties,
474         Jar[] classpath ) throws Exception
475     {
476         Builder builder = getOSGiBuilder( currentProject, originalInstructions, properties, classpath );
477 
478         addMavenInstructions( currentProject, builder );
479 
480         builder.build();
481 
482         mergeMavenManifest( currentProject, builder );
483 
484         return builder;
485     }
486 
487 
488     protected static StringBuilder dumpInstructions( Properties properties, StringBuilder buf )
489     {
490         try
491         {
492             buf.append( "#-----------------------------------------------------------------------" + NL );
493             Properties stringProperties = new Properties();
494             for ( Enumeration e = properties.propertyNames(); e.hasMoreElements(); )
495             {
496                 // we can only store String properties
497                 String key = ( String ) e.nextElement();
498                 String value = properties.getProperty( key );
499                 if ( value != null )
500                 {
501                     stringProperties.setProperty( key, value );
502                 }
503             }
504             ByteArrayOutputStream out = new ByteArrayOutputStream();
505             stringProperties.store( out, null ); // properties encoding is 8859_1
506             buf.append( out.toString( "8859_1" ) );
507             buf.append( "#-----------------------------------------------------------------------" + NL );
508         }
509         catch ( Throwable e )
510         {
511             // ignore...
512         }
513         return buf;
514     }
515 
516 
517     protected static StringBuilder dumpClasspath( List classpath, StringBuilder buf )
518     {
519         try
520         {
521             buf.append( "#-----------------------------------------------------------------------" + NL );
522             buf.append( "-classpath:\\" + NL );
523             for ( Iterator i = classpath.iterator(); i.hasNext(); )
524             {
525                 File path = ( ( Jar ) i.next() ).getSource();
526                 if ( path != null )
527                 {
528                     buf.append( ' ' + path.toString() + ( i.hasNext() ? ",\\" : "" ) + NL );
529                 }
530             }
531             buf.append( "#-----------------------------------------------------------------------" + NL );
532         }
533         catch ( Throwable e )
534         {
535             // ignore...
536         }
537         return buf;
538     }
539 
540 
541     protected static StringBuilder dumpManifest( Manifest manifest, StringBuilder buf )
542     {
543         try
544         {
545             buf.append( "#-----------------------------------------------------------------------" + NL );
546             ByteArrayOutputStream out = new ByteArrayOutputStream();
547             manifest.write( out ); // manifest encoding is UTF8
548             buf.append( out.toString( "UTF8" ) );
549             buf.append( "#-----------------------------------------------------------------------" + NL );
550         }
551         catch ( Throwable e )
552         {
553             // ignore...
554         }
555         return buf;
556     }
557 
558 
559     protected static void includeMavenResources( MavenProject currentProject, Analyzer analyzer, Log log )
560     {
561         // pass maven resource paths onto BND analyzer
562         final String mavenResourcePaths = getMavenResourcePaths( currentProject );
563         final String includeResource = ( String ) analyzer.getProperty( Analyzer.INCLUDE_RESOURCE );
564         if ( includeResource != null )
565         {
566             if ( includeResource.indexOf( MAVEN_RESOURCES ) >= 0 )
567             {
568                 // if there is no maven resource path, we do a special treatment and replace
569                 // every occurance of MAVEN_RESOURCES and a following comma with an empty string
570                 if ( mavenResourcePaths.length() == 0 )
571                 {
572                     String cleanedResource = removeTagFromInstruction( includeResource, MAVEN_RESOURCES );
573                     if ( cleanedResource.length() > 0 )
574                     {
575                         analyzer.setProperty( Analyzer.INCLUDE_RESOURCE, cleanedResource );
576                     }
577                     else
578                     {
579                         analyzer.unsetProperty( Analyzer.INCLUDE_RESOURCE );
580                     }
581                 }
582                 else
583                 {
584                     String combinedResource = StringUtils
585                         .replace( includeResource, MAVEN_RESOURCES, mavenResourcePaths );
586                     analyzer.setProperty( Analyzer.INCLUDE_RESOURCE, combinedResource );
587                 }
588             }
589             else if ( mavenResourcePaths.length() > 0 )
590             {
591                 log.warn( Analyzer.INCLUDE_RESOURCE + ": overriding " + mavenResourcePaths + " with " + includeResource
592                     + " (add " + MAVEN_RESOURCES + " if you want to include the maven resources)" );
593             }
594         }
595         else if ( mavenResourcePaths.length() > 0 )
596         {
597             analyzer.setProperty( Analyzer.INCLUDE_RESOURCE, mavenResourcePaths );
598         }
599     }
600 
601 
602     protected void mergeMavenManifest( MavenProject currentProject, Builder builder ) throws Exception
603     {
604         Jar jar = builder.getJar();
605 
606         if ( getLog().isDebugEnabled() )
607         {
608             getLog().debug( "BND Manifest:" + NL + dumpManifest( jar.getManifest(), new StringBuilder() ) );
609         }
610 
611         boolean addMavenDescriptor = currentProject.getBasedir() != null;
612 
613         try
614         {
615             /*
616              * Grab customized manifest entries from the maven-jar-plugin configuration
617              */
618             MavenArchiveConfiguration archiveConfig = JarPluginConfiguration.getArchiveConfiguration( currentProject );
619             String mavenManifestText = new MavenArchiver().getManifest( currentProject, archiveConfig ).toString();
620             addMavenDescriptor = addMavenDescriptor && archiveConfig.isAddMavenDescriptor();
621 
622             Manifest mavenManifest = new Manifest();
623 
624             // First grab the external manifest file (if specified)
625             File externalManifestFile = archiveConfig.getManifestFile();
626             if ( null != externalManifestFile && externalManifestFile.exists() )
627             {
628                 InputStream mis = new FileInputStream( externalManifestFile );
629                 mavenManifest.read( mis );
630                 mis.close();
631             }
632 
633             // Then apply customized entries from the jar plugin; note: manifest encoding is UTF8
634             mavenManifest.read( new ByteArrayInputStream( mavenManifestText.getBytes( "UTF8" ) ) );
635 
636             if ( !archiveConfig.isManifestSectionsEmpty() )
637             {
638                 /*
639                  * Add customized manifest sections (for some reason MavenArchiver doesn't do this for us)
640                  */
641                 List sections = archiveConfig.getManifestSections();
642                 for ( Iterator i = sections.iterator(); i.hasNext(); )
643                 {
644                     ManifestSection section = ( ManifestSection ) i.next();
645                     Attributes attributes = new Attributes();
646 
647                     if ( !section.isManifestEntriesEmpty() )
648                     {
649                         Map entries = section.getManifestEntries();
650                         for ( Iterator j = entries.entrySet().iterator(); j.hasNext(); )
651                         {
652                             Map.Entry entry = ( Map.Entry ) j.next();
653                             attributes.putValue( ( String ) entry.getKey(), ( String ) entry.getValue() );
654                         }
655                     }
656 
657                     mavenManifest.getEntries().put( section.getName(), attributes );
658                 }
659             }
660 
661             Attributes mainMavenAttributes = mavenManifest.getMainAttributes();
662             mainMavenAttributes.putValue( "Created-By", "Apache Maven Bundle Plugin" );
663 
664             String[] removeHeaders = builder.getProperty( Constants.REMOVEHEADERS, "" ).split( "," );
665 
666             // apply -removeheaders to the custom manifest
667             for ( int i = 0; i < removeHeaders.length; i++ )
668             {
669                 for ( Iterator j = mainMavenAttributes.keySet().iterator(); j.hasNext(); )
670                 {
671                     if ( j.next().toString().matches( removeHeaders[i].trim() ) )
672                     {
673                         j.remove();
674                     }
675                 }
676             }
677 
678             /*
679              * Overlay generated bundle manifest with customized entries
680              */
681             Manifest bundleManifest = jar.getManifest();
682             bundleManifest.getMainAttributes().putAll( mainMavenAttributes );
683             bundleManifest.getEntries().putAll( mavenManifest.getEntries() );
684 
685             // adjust the import package attributes so that optional dependencies use
686             // optional resolution.
687             String importPackages = bundleManifest.getMainAttributes().getValue( "Import-Package" );
688             if ( importPackages != null )
689             {
690                 Set optionalPackages = getOptionalPackages( currentProject );
691 
692                 Map<String, Map<String, String>> values = new Analyzer().parseHeader( importPackages );
693                 for ( Map.Entry<String, Map<String, String>> entry : values.entrySet() )
694                 {
695                     String pkg = entry.getKey();
696                     Map<String, String> options = entry.getValue();
697                     if ( !options.containsKey( "resolution:" ) && optionalPackages.contains( pkg ) )
698                     {
699                         options.put( "resolution:", "optional" );
700                     }
701                 }
702                 String result = Processor.printClauses( values, "resolution:" );
703                 bundleManifest.getMainAttributes().putValue( "Import-Package", result );
704             }
705 
706             jar.setManifest( bundleManifest );
707         }
708         catch ( Exception e )
709         {
710             getLog().warn( "Unable to merge Maven manifest: " + e.getLocalizedMessage() );
711         }
712 
713         if ( addMavenDescriptor )
714         {
715             doMavenMetadata( currentProject, jar );
716         }
717 
718         if ( getLog().isDebugEnabled() )
719         {
720             getLog().debug( "Final Manifest:" + NL + dumpManifest( jar.getManifest(), new StringBuilder() ) );
721         }
722 
723         builder.setJar( jar );
724     }
725 
726 
727     protected Set getOptionalPackages( MavenProject currentProject ) throws IOException, MojoExecutionException
728     {
729         ArrayList inscope = new ArrayList();
730         final Collection artifacts = getSelectedDependencies( currentProject.getArtifacts() );
731         for ( Iterator it = artifacts.iterator(); it.hasNext(); )
732         {
733             Artifact artifact = ( Artifact ) it.next();
734             if ( artifact.getArtifactHandler().isAddedToClasspath() )
735             {
736                 if ( !Artifact.SCOPE_TEST.equals( artifact.getScope() ) )
737                 {
738                     inscope.add( artifact );
739                 }
740             }
741         }
742 
743         HashSet optionalArtifactIds = new HashSet();
744         for ( Iterator it = inscope.iterator(); it.hasNext(); )
745         {
746             Artifact artifact = ( Artifact ) it.next();
747             if ( artifact.isOptional() )
748             {
749                 String id = artifact.toString();
750                 if ( artifact.getScope() != null )
751                 {
752                     // strip the scope...
753                     id = id.replaceFirst( ":[^:]*$", "" );
754                 }
755                 optionalArtifactIds.add( id );
756             }
757 
758         }
759 
760         HashSet required = new HashSet();
761         HashSet optional = new HashSet();
762         for ( Iterator it = inscope.iterator(); it.hasNext(); )
763         {
764             Artifact artifact = ( Artifact ) it.next();
765             File file = getFile( artifact );
766             if ( file == null )
767             {
768                 continue;
769             }
770 
771             Jar jar = new Jar( artifact.getArtifactId(), file );
772             if ( isTransitivelyOptional( optionalArtifactIds, artifact ) )
773             {
774                 optional.addAll( jar.getPackages() );
775             }
776             else
777             {
778                 required.addAll( jar.getPackages() );
779             }
780             jar.close();
781         }
782 
783         optional.removeAll( required );
784         return optional;
785     }
786 
787 
788     /**
789      * Check to see if any dependency along the dependency trail of
790      * the artifact is optional.
791      *
792      * @param artifact
793      */
794     protected boolean isTransitivelyOptional( HashSet optionalArtifactIds, Artifact artifact )
795     {
796         List trail = artifact.getDependencyTrail();
797         for ( Iterator iterator = trail.iterator(); iterator.hasNext(); )
798         {
799             String next = ( String ) iterator.next();
800             if ( optionalArtifactIds.contains( next ) )
801             {
802                 return true;
803             }
804         }
805         return false;
806     }
807 
808 
809     private void unpackBundle( File jarFile )
810     {
811         File outputDir = getOutputDirectory();
812         if ( null == outputDir )
813         {
814             outputDir = new File( getBuildDirectory(), "classes" );
815         }
816 
817         try
818         {
819             /*
820              * this directory must exist before unpacking, otherwise the plexus
821              * unarchiver decides to use the current working directory instead!
822              */
823             if ( !outputDir.exists() )
824             {
825                 outputDir.mkdirs();
826             }
827 
828             UnArchiver unArchiver = m_archiverManager.getUnArchiver( "jar" );
829             unArchiver.setDestDirectory( outputDir );
830             unArchiver.setSourceFile( jarFile );
831             unArchiver.extract();
832         }
833         catch ( Exception e )
834         {
835             getLog().error( "Problem unpacking " + jarFile + " to " + outputDir, e );
836         }
837     }
838 
839 
840     protected static String removeTagFromInstruction( String instruction, String tag )
841     {
842         StringBuffer buf = new StringBuffer();
843 
844         String[] clauses = instruction.split( "," );
845         for ( int i = 0; i < clauses.length; i++ )
846         {
847             String clause = clauses[i].trim();
848             if ( !tag.equals( clause ) )
849             {
850                 if ( buf.length() > 0 )
851                 {
852                     buf.append( ',' );
853                 }
854                 buf.append( clause );
855             }
856         }
857 
858         return buf.toString();
859     }
860 
861 
862     private static Map getProperties( Model projectModel, String prefix )
863     {
864         Map properties = new LinkedHashMap();
865         Method methods[] = Model.class.getDeclaredMethods();
866         for ( int i = 0; i < methods.length; i++ )
867         {
868             String name = methods[i].getName();
869             if ( name.startsWith( "get" ) )
870             {
871                 try
872                 {
873                     Object v = methods[i].invoke( projectModel, null );
874                     if ( v != null )
875                     {
876                         name = prefix + Character.toLowerCase( name.charAt( 3 ) ) + name.substring( 4 );
877                         if ( v.getClass().isArray() )
878                             properties.put( name, Arrays.asList( ( Object[] ) v ).toString() );
879                         else
880                             properties.put( name, v );
881 
882                     }
883                 }
884                 catch ( Exception e )
885                 {
886                     // too bad
887                 }
888             }
889         }
890         return properties;
891     }
892 
893 
894     private static StringBuffer printLicenses( List licenses )
895     {
896         if ( licenses == null || licenses.size() == 0 )
897             return null;
898         StringBuffer sb = new StringBuffer();
899         String del = "";
900         for ( Iterator i = licenses.iterator(); i.hasNext(); )
901         {
902             License l = ( License ) i.next();
903             String url = l.getUrl();
904             if ( url == null )
905                 continue;
906             sb.append( del );
907             sb.append( url );
908             del = ", ";
909         }
910         if ( sb.length() == 0 )
911             return null;
912         return sb;
913     }
914 
915 
916     /**
917      * @param jar
918      * @throws IOException
919      */
920     private void doMavenMetadata( MavenProject currentProject, Jar jar ) throws IOException
921     {
922         String path = "META-INF/maven/" + currentProject.getGroupId() + "/" + currentProject.getArtifactId();
923         File pomFile = new File( currentProject.getBasedir(), "pom.xml" );
924         jar.putResource( path + "/pom.xml", new FileResource( pomFile ) );
925 
926         Properties p = new Properties();
927         p.put( "version", currentProject.getVersion() );
928         p.put( "groupId", currentProject.getGroupId() );
929         p.put( "artifactId", currentProject.getArtifactId() );
930         ByteArrayOutputStream out = new ByteArrayOutputStream();
931         p.store( out, "Generated by org.apache.felix.bundleplugin" );
932         jar.putResource( path + "/pom.properties", new EmbeddedResource( out.toByteArray(), System.currentTimeMillis() ) );
933     }
934 
935 
936     protected Jar[] getClasspath( MavenProject currentProject ) throws IOException, MojoExecutionException
937     {
938         List list = new ArrayList();
939 
940         if ( getOutputDirectory() != null && getOutputDirectory().exists() )
941         {
942             list.add( new Jar( ".", getOutputDirectory() ) );
943         }
944 
945         final Collection artifacts = getSelectedDependencies( currentProject.getArtifacts() );
946         for ( Iterator it = artifacts.iterator(); it.hasNext(); )
947         {
948             Artifact artifact = ( Artifact ) it.next();
949             if ( artifact.getArtifactHandler().isAddedToClasspath() )
950             {
951                 if ( !Artifact.SCOPE_TEST.equals( artifact.getScope() ) )
952                 {
953                     File file = getFile( artifact );
954                     if ( file == null )
955                     {
956                         getLog().warn(
957                             "File is not available for artifact " + artifact + " in project "
958                                 + currentProject.getArtifact() );
959                         continue;
960                     }
961                     Jar jar = new Jar( artifact.getArtifactId(), file );
962                     list.add( jar );
963                 }
964             }
965         }
966         Jar[] cp = new Jar[list.size()];
967         list.toArray( cp );
968         return cp;
969     }
970 
971 
972     private Collection getSelectedDependencies( Collection artifacts ) throws MojoExecutionException
973     {
974         if ( null == excludeDependencies || excludeDependencies.length() == 0 )
975         {
976             return artifacts;
977         }
978         else if ( "true".equalsIgnoreCase( excludeDependencies ) )
979         {
980             return Collections.EMPTY_LIST;
981         }
982 
983         Collection selectedDependencies = new LinkedHashSet( artifacts );
984         DependencyExcluder excluder = new DependencyExcluder( artifacts );
985         excluder.processHeaders( excludeDependencies );
986         selectedDependencies.removeAll( excluder.getExcludedArtifacts() );
987 
988         return selectedDependencies;
989     }
990 
991 
992     /**
993      * Get the file for an Artifact
994      *
995      * @param artifact
996      */
997     protected File getFile( Artifact artifact )
998     {
999         return artifact.getFile();
1000     }
1001 
1002 
1003     private static void header( Properties properties, String key, Object value )
1004     {
1005         if ( value == null )
1006             return;
1007 
1008         if ( value instanceof Collection && ( ( Collection ) value ).isEmpty() )
1009             return;
1010 
1011         properties.put( key, value.toString().replaceAll( "[\r\n]", "" ) );
1012     }
1013 
1014 
1015     /**
1016      * Convert a Maven version into an OSGi compliant version
1017      *
1018      * @param version Maven version
1019      * @return the OSGi version
1020      */
1021     protected String convertVersionToOsgi( String version )
1022     {
1023         return getMaven2OsgiConverter().getVersion( version );
1024     }
1025 
1026 
1027     /**
1028      * TODO this should return getMaven2Osgi().getBundleFileName( project.getArtifact() )
1029      */
1030     protected String getBundleName( MavenProject currentProject )
1031     {
1032         String extension;
1033         try
1034         {
1035             extension = currentProject.getArtifact().getArtifactHandler().getExtension();
1036         }
1037         catch ( Throwable e )
1038         {
1039             extension = currentProject.getArtifact().getType();
1040         }
1041         if ( StringUtils.isEmpty( extension ) || "bundle".equals( extension ) || "pom".equals( extension ) )
1042         {
1043             extension = "jar"; // just in case maven gets confused
1044         }
1045         String finalName = currentProject.getBuild().getFinalName();
1046         if ( null != classifier && classifier.trim().length() > 0 )
1047         {
1048             return finalName + '-' + classifier + '.' + extension;
1049         }
1050         return finalName + '.' + extension;
1051     }
1052 
1053 
1054     protected String getBuildDirectory()
1055     {
1056         return buildDirectory;
1057     }
1058 
1059 
1060     protected void setBuildDirectory( String _buildirectory )
1061     {
1062         buildDirectory = _buildirectory;
1063     }
1064 
1065 
1066     protected Properties getDefaultProperties( MavenProject currentProject )
1067     {
1068         Properties properties = new Properties();
1069 
1070         String bsn;
1071         try
1072         {
1073             bsn = getMaven2OsgiConverter().getBundleSymbolicName( currentProject.getArtifact() );
1074         }
1075         catch ( Exception e )
1076         {
1077             bsn = currentProject.getGroupId() + "." + currentProject.getArtifactId();
1078         }
1079 
1080         // Setup defaults
1081         properties.put( MAVEN_SYMBOLICNAME, bsn );
1082         properties.put( Analyzer.BUNDLE_SYMBOLICNAME, bsn );
1083         properties.put( Analyzer.IMPORT_PACKAGE, "*" );
1084         properties.put( Analyzer.BUNDLE_VERSION, getMaven2OsgiConverter().getVersion( currentProject.getVersion() ) );
1085 
1086         // remove the extraneous Include-Resource and Private-Package entries from generated manifest
1087         properties.put( Constants.REMOVEHEADERS, Analyzer.INCLUDE_RESOURCE + ',' + Analyzer.PRIVATE_PACKAGE );
1088 
1089         header( properties, Analyzer.BUNDLE_DESCRIPTION, currentProject.getDescription() );
1090         StringBuffer licenseText = printLicenses( currentProject.getLicenses() );
1091         if ( licenseText != null )
1092         {
1093             header( properties, Analyzer.BUNDLE_LICENSE, licenseText );
1094         }
1095         header( properties, Analyzer.BUNDLE_NAME, currentProject.getName() );
1096 
1097         if ( currentProject.getOrganization() != null )
1098         {
1099             if ( currentProject.getOrganization().getName() != null )
1100             {
1101                 String organizationName = currentProject.getOrganization().getName();
1102                 header( properties, Analyzer.BUNDLE_VENDOR, organizationName );
1103                 properties.put( "project.organization.name", organizationName );
1104                 properties.put( "pom.organization.name", organizationName );
1105             }
1106             if ( currentProject.getOrganization().getUrl() != null )
1107             {
1108                 String organizationUrl = currentProject.getOrganization().getUrl();
1109                 header( properties, Analyzer.BUNDLE_DOCURL, organizationUrl );
1110                 properties.put( "project.organization.url", organizationUrl );
1111                 properties.put( "pom.organization.url", organizationUrl );
1112             }
1113         }
1114 
1115         properties.putAll( currentProject.getProperties() );
1116         properties.putAll( currentProject.getModel().getProperties() );
1117         if ( m_mavenSession != null )
1118         {
1119             properties.putAll( m_mavenSession.getExecutionProperties() );
1120         }
1121 
1122         properties.putAll( getProperties( currentProject.getModel(), "project.build." ) );
1123         properties.putAll( getProperties( currentProject.getModel(), "pom." ) );
1124         properties.putAll( getProperties( currentProject.getModel(), "project." ) );
1125 
1126         properties.put( "project.baseDir", getBase( currentProject ) );
1127         properties.put( "project.build.directory", getBuildDirectory() );
1128         properties.put( "project.build.outputdirectory", getOutputDirectory() );
1129 
1130         properties.put( "classifier", classifier == null ? "" : classifier );
1131 
1132         // Add default plugins
1133         header( properties, Analyzer.PLUGIN, BlueprintPlugin.class.getName() + "," + SpringXMLType.class.getName() );
1134 
1135         return properties;
1136     }
1137 
1138 
1139     protected static File getBase( MavenProject currentProject )
1140     {
1141         return currentProject.getBasedir() != null ? currentProject.getBasedir() : new File( "" );
1142     }
1143 
1144 
1145     protected File getOutputDirectory()
1146     {
1147         return outputDirectory;
1148     }
1149 
1150 
1151     protected void setOutputDirectory( File _outputDirectory )
1152     {
1153         outputDirectory = _outputDirectory;
1154     }
1155 
1156 
1157     private static void addLocalPackages( File outputDirectory, Analyzer analyzer )
1158     {
1159         Collection packages = new LinkedHashSet();
1160 
1161         if ( outputDirectory != null && outputDirectory.isDirectory() )
1162         {
1163             // scan classes directory for potential packages
1164             DirectoryScanner scanner = new DirectoryScanner();
1165             scanner.setBasedir( outputDirectory );
1166             scanner.setIncludes( new String[]
1167                 { "**/*.class" } );
1168 
1169             scanner.addDefaultExcludes();
1170             scanner.scan();
1171 
1172             String[] paths = scanner.getIncludedFiles();
1173             for ( int i = 0; i < paths.length; i++ )
1174             {
1175                 packages.add( getPackageName( paths[i] ) );
1176             }
1177         }
1178 
1179         StringBuffer exportedPkgs = new StringBuffer();
1180         StringBuffer privatePkgs = new StringBuffer();
1181 
1182         boolean noprivatePackages = "!*".equals( analyzer.getProperty( Analyzer.PRIVATE_PACKAGE ) );
1183 
1184         for ( Iterator i = packages.iterator(); i.hasNext(); )
1185         {
1186             String pkg = ( String ) i.next();
1187 
1188             // mark all source packages as private by default (can be overridden by export list)
1189             privatePkgs.append( pkg ).append( ";-split-package:=merge-first," );
1190 
1191             // we can't export the default package (".") and we shouldn't export internal packages 
1192             if ( noprivatePackages || !( ".".equals( pkg ) || pkg.contains( ".internal" ) || pkg.contains( ".impl" ) ) )
1193             {
1194                 if ( exportedPkgs.length() > 0 )
1195                 {
1196                     exportedPkgs.append( ';' );
1197                 }
1198                 exportedPkgs.append( pkg );
1199             }
1200         }
1201 
1202         if ( analyzer.getProperty( Analyzer.EXPORT_PACKAGE ) == null )
1203         {
1204             if ( analyzer.getProperty( Analyzer.EXPORT_CONTENTS ) == null )
1205             {
1206                 // no -exportcontents overriding the exports, so use our computed list
1207                 analyzer.setProperty( Analyzer.EXPORT_PACKAGE, exportedPkgs.toString() );
1208             }
1209             else
1210             {
1211                 // leave Export-Package empty (but non-null) as we have -exportcontents
1212                 analyzer.setProperty( Analyzer.EXPORT_PACKAGE, "" );
1213             }
1214         }
1215         else
1216         {
1217             String exported = analyzer.getProperty( Analyzer.EXPORT_PACKAGE );
1218             if ( exported.indexOf( LOCAL_PACKAGES ) >= 0 )
1219             {
1220                 String newExported = StringUtils.replace( exported, LOCAL_PACKAGES, exportedPkgs.toString() );
1221                 analyzer.setProperty( Analyzer.EXPORT_PACKAGE, newExported );
1222 
1223             }
1224         }
1225 
1226         if ( analyzer.getProperty( Analyzer.PRIVATE_PACKAGE ) == null )
1227         {
1228             // if there are really no private packages then use "!*" as this will keep the Bnd Tool happy
1229             analyzer.setProperty( Analyzer.PRIVATE_PACKAGE, privatePkgs.length() == 0 ? "!*" : privatePkgs.toString() );
1230         }
1231     }
1232 
1233 
1234     private static String getPackageName( String filename )
1235     {
1236         int n = filename.lastIndexOf( File.separatorChar );
1237         return n < 0 ? "." : filename.substring( 0, n ).replace( File.separatorChar, '.' );
1238     }
1239 
1240 
1241     private static List getMavenResources( MavenProject currentProject )
1242     {
1243         List resources = new ArrayList( currentProject.getResources() );
1244 
1245         if ( currentProject.getCompileSourceRoots() != null )
1246         {
1247             // also scan for any "packageinfo" files lurking in the source folders
1248             List packageInfoIncludes = Collections.singletonList( "**/packageinfo" );
1249             for ( Iterator i = currentProject.getCompileSourceRoots().iterator(); i.hasNext(); )
1250             {
1251                 String sourceRoot = ( String ) i.next();
1252                 Resource packageInfoResource = new Resource();
1253                 packageInfoResource.setDirectory( sourceRoot );
1254                 packageInfoResource.setIncludes( packageInfoIncludes );
1255                 resources.add( packageInfoResource );
1256             }
1257         }
1258 
1259         return resources;
1260     }
1261 
1262 
1263     protected static String getMavenResourcePaths( MavenProject currentProject )
1264     {
1265         final String basePath = currentProject.getBasedir().getAbsolutePath();
1266 
1267         Set pathSet = new LinkedHashSet();
1268         for ( Iterator i = getMavenResources( currentProject ).iterator(); i.hasNext(); )
1269         {
1270             Resource resource = ( Resource ) i.next();
1271 
1272             final String sourcePath = resource.getDirectory();
1273             final String targetPath = resource.getTargetPath();
1274 
1275             // ignore empty or non-local resources
1276             if ( new File( sourcePath ).exists() && ( ( targetPath == null ) || ( targetPath.indexOf( ".." ) < 0 ) ) )
1277             {
1278                 DirectoryScanner scanner = new DirectoryScanner();
1279 
1280                 scanner.setBasedir( sourcePath );
1281                 if ( resource.getIncludes() != null && !resource.getIncludes().isEmpty() )
1282                 {
1283                     scanner.setIncludes( ( String[] ) resource.getIncludes().toArray( EMPTY_STRING_ARRAY ) );
1284                 }
1285                 else
1286                 {
1287                     scanner.setIncludes( DEFAULT_INCLUDES );
1288                 }
1289 
1290                 if ( resource.getExcludes() != null && !resource.getExcludes().isEmpty() )
1291                 {
1292                     scanner.setExcludes( ( String[] ) resource.getExcludes().toArray( EMPTY_STRING_ARRAY ) );
1293                 }
1294 
1295                 scanner.addDefaultExcludes();
1296                 scanner.scan();
1297 
1298                 List includedFiles = Arrays.asList( scanner.getIncludedFiles() );
1299 
1300                 for ( Iterator j = includedFiles.iterator(); j.hasNext(); )
1301                 {
1302                     String name = ( String ) j.next();
1303                     String path = sourcePath + '/' + name;
1304 
1305                     // make relative to project
1306                     if ( path.startsWith( basePath ) )
1307                     {
1308                         if ( path.length() == basePath.length() )
1309                         {
1310                             path = ".";
1311                         }
1312                         else
1313                         {
1314                             path = path.substring( basePath.length() + 1 );
1315                         }
1316                     }
1317 
1318                     // replace windows backslash with a slash
1319                     // this is a workaround for a problem with bnd 0.0.189
1320                     if ( File.separatorChar != '/' )
1321                     {
1322                         name = name.replace( File.separatorChar, '/' );
1323                         path = path.replace( File.separatorChar, '/' );
1324                     }
1325 
1326                     // copy to correct place
1327                     path = name + '=' + path;
1328                     if ( targetPath != null )
1329                     {
1330                         path = targetPath + '/' + path;
1331                     }
1332 
1333                     // use Bnd filtering?
1334                     if ( resource.isFiltering() )
1335                     {
1336                         path = '{' + path + '}';
1337                     }
1338 
1339                     pathSet.add( path );
1340                 }
1341             }
1342         }
1343 
1344         StringBuffer resourcePaths = new StringBuffer();
1345         for ( Iterator i = pathSet.iterator(); i.hasNext(); )
1346         {
1347             resourcePaths.append( i.next() );
1348             if ( i.hasNext() )
1349             {
1350                 resourcePaths.append( ',' );
1351             }
1352         }
1353 
1354         return resourcePaths.toString();
1355     }
1356 
1357 
1358     protected Collection getEmbeddableArtifacts( MavenProject currentProject, Analyzer analyzer )
1359         throws MojoExecutionException
1360     {
1361         final Collection artifacts;
1362 
1363         String embedTransitive = analyzer.getProperty( DependencyEmbedder.EMBED_TRANSITIVE );
1364         if ( Boolean.valueOf( embedTransitive ).booleanValue() )
1365         {
1366             // includes transitive dependencies
1367             artifacts = currentProject.getArtifacts();
1368         }
1369         else
1370         {
1371             // only includes direct dependencies
1372             artifacts = currentProject.getDependencyArtifacts();
1373         }
1374 
1375         return getSelectedDependencies( artifacts );
1376     }
1377 }