BundlePlugin xref

    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 }