blob: a3c4d92244aaea89b167cd44fbc24d61679ca996 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.felix.bundleplugin;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import org.apache.maven.archiver.ManifestSection;
import org.apache.maven.archiver.MavenArchiveConfiguration;
import org.apache.maven.archiver.MavenArchiver;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.License;
import org.apache.maven.model.Model;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.maven.shared.osgi.DefaultMaven2OsgiConverter;
import org.apache.maven.shared.osgi.Maven2OsgiConverter;
import org.codehaus.plexus.archiver.UnArchiver;
import org.codehaus.plexus.archiver.manager.ArchiverManager;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.PropertyUtils;
import org.codehaus.plexus.util.StringUtils;
import aQute.bnd.header.Attrs;
import aQute.bnd.osgi.Analyzer;
import aQute.bnd.osgi.Builder;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.Descriptors.PackageRef;
import aQute.bnd.osgi.EmbeddedResource;
import aQute.bnd.osgi.FileResource;
import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.Packages;
import aQute.bnd.osgi.Processor;
import aQute.lib.spring.SpringXMLType;
/**
* Create an OSGi bundle from Maven project
*
* @goal bundle
* @phase package
* @requiresDependencyResolution test
* @description build an OSGi bundle jar
* @threadSafe
*/
public class BundlePlugin extends AbstractMojo
{
/**
* Directory where the manifest will be written
*
* @parameter expression="${manifestLocation}" default-value="${project.build.outputDirectory}/META-INF"
*/
protected File manifestLocation;
/**
* File where the BND instructions will be dumped
*
* @parameter expression="${dumpInstructions}"
*/
protected File dumpInstructions;
/**
* File where the BND class-path will be dumped
*
* @parameter expression="${dumpClasspath}"
*/
protected File dumpClasspath;
/**
* When true, unpack the bundle contents to the outputDirectory
*
* @parameter expression="${unpackBundle}"
*/
protected boolean unpackBundle;
/**
* Comma separated list of artifactIds to exclude from the dependency classpath passed to BND (use "true" to exclude everything)
*
* @parameter expression="${excludeDependencies}"
*/
protected String excludeDependencies;
/**
* Final name of the bundle (without classifier or extension)
*
* @parameter expression="${project.build.finalName}"
*/
private String finalName;
/**
* Classifier type of the bundle to be installed. For example, "jdk14".
* Defaults to none which means this is the project's main bundle.
*
* @parameter
*/
protected String classifier;
/**
* Packaging type of the bundle to be installed. For example, "jar".
* Defaults to none which means use the same packaging as the project.
*
* @parameter
*/
protected String packaging;
/**
* @component
*/
private MavenProjectHelper m_projectHelper;
/**
* @component
*/
private ArchiverManager m_archiverManager;
/**
* @component
*/
private ArtifactHandlerManager m_artifactHandlerManager;
/**
* Project types which this plugin supports.
*
* @parameter
*/
protected List supportedProjectTypes = Arrays.asList( new String[]
{ "jar", "bundle" } );
/**
* The directory for the generated bundles.
*
* @parameter expression="${project.build.outputDirectory}"
* @required
*/
private File outputDirectory;
/**
* The directory for the generated JAR.
*
* @parameter expression="${project.build.directory}"
* @required
*/
private String buildDirectory;
/**
* The Maven project.
*
* @parameter expression="${project}"
* @required
* @readonly
*/
private MavenProject project;
/**
* The BND instructions for the bundle.
*
* @parameter
*/
private Map instructions = new LinkedHashMap();
/**
* Use locally patched version for now.
*/
private Maven2OsgiConverter m_maven2OsgiConverter = new DefaultMaven2OsgiConverter();
/**
* The archive configuration to use.
*
* @parameter
*/
private MavenArchiveConfiguration archive; // accessed indirectly in JarPluginConfiguration
/**
* @parameter default-value="${session}"
* @required
* @readonly
*/
private MavenSession m_mavenSession;
/**
* @parameter
*/
private boolean niceManifest = false;
private static final String MAVEN_SYMBOLICNAME = "maven-symbolicname";
private static final String MAVEN_RESOURCES = "{maven-resources}";
private static final String LOCAL_PACKAGES = "{local-packages}";
private static final String MAVEN_SOURCES = "{maven-sources}";
private static final String[] EMPTY_STRING_ARRAY =
{};
private static final String[] DEFAULT_INCLUDES =
{ "**/**" };
private static final String NL = System.getProperty( "line.separator" );
protected Maven2OsgiConverter getMaven2OsgiConverter()
{
return m_maven2OsgiConverter;
}
protected void setMaven2OsgiConverter( Maven2OsgiConverter maven2OsgiConverter )
{
m_maven2OsgiConverter = maven2OsgiConverter;
}
protected MavenProject getProject()
{
return project;
}
/**
* @see org.apache.maven.plugin.AbstractMojo#execute()
*/
public void execute() throws MojoExecutionException
{
Properties properties = new Properties();
String projectType = getProject().getArtifact().getType();
// ignore unsupported project types, useful when bundleplugin is configured in parent pom
if ( !supportedProjectTypes.contains( projectType ) )
{
getLog().warn(
"Ignoring project type " + projectType + " - supportedProjectTypes = " + supportedProjectTypes );
return;
}
execute( getProject(), instructions, properties );
}
protected void execute( MavenProject currentProject, Map originalInstructions, Properties properties )
throws MojoExecutionException
{
try
{
execute( currentProject, originalInstructions, properties, getClasspath( currentProject ) );
}
catch ( IOException e )
{
throw new MojoExecutionException( "Error calculating classpath for project " + currentProject, e );
}
}
/* transform directives from their XML form to the expected BND syntax (eg. _include becomes -include) */
protected static Map transformDirectives( Map originalInstructions )
{
Map transformedInstructions = new LinkedHashMap();
for ( Iterator i = originalInstructions.entrySet().iterator(); i.hasNext(); )
{
Map.Entry e = ( Map.Entry ) i.next();
String key = ( String ) e.getKey();
if ( key.startsWith( "_" ) )
{
key = "-" + key.substring( 1 );
}
String value = ( String ) e.getValue();
if ( null == value )
{
value = "";
}
else
{
value = value.replaceAll( "\\p{Blank}*[\r\n]\\p{Blank}*", "" );
}
if ( Analyzer.WAB.equals( key ) && value.length() == 0 )
{
// provide useful default
value = "src/main/webapp/";
}
transformedInstructions.put( key, value );
}
return transformedInstructions;
}
protected boolean reportErrors( String prefix, Analyzer analyzer )
{
List errors = analyzer.getErrors();
List warnings = analyzer.getWarnings();
for ( Iterator w = warnings.iterator(); w.hasNext(); )
{
String msg = ( String ) w.next();
getLog().warn( prefix + " : " + msg );
}
boolean hasErrors = false;
String fileNotFound = "Input file does not exist: ";
for ( Iterator e = errors.iterator(); e.hasNext(); )
{
String msg = ( String ) e.next();
if ( msg.startsWith( fileNotFound ) && msg.endsWith( "~" ) )
{
// treat as warning; this error happens when you have duplicate entries in Include-Resource
String duplicate = Processor.removeDuplicateMarker( msg.substring( fileNotFound.length() ) );
getLog().warn( prefix + " : Duplicate path '" + duplicate + "' in Include-Resource" );
}
else
{
getLog().error( prefix + " : " + msg );
hasErrors = true;
}
}
return hasErrors;
}
protected void execute( MavenProject currentProject, Map originalInstructions, Properties properties,
Jar[] classpath ) throws MojoExecutionException
{
try
{
File jarFile = new File( getBuildDirectory(), getBundleName( currentProject ) );
Builder builder = buildOSGiBundle( currentProject, originalInstructions, properties, classpath );
boolean hasErrors = reportErrors( "Bundle " + currentProject.getArtifact(), builder );
if ( hasErrors )
{
String failok = builder.getProperty( "-failok" );
if ( null == failok || "false".equalsIgnoreCase( failok ) )
{
jarFile.delete();
throw new MojoFailureException( "Error(s) found in bundle configuration" );
}
}
// attach bundle to maven project
jarFile.getParentFile().mkdirs();
builder.getJar().write( jarFile );
Artifact mainArtifact = currentProject.getArtifact();
if ( "bundle".equals( mainArtifact.getType() ) )
{
// workaround for MNG-1682: force maven to install artifact using the "jar" handler
mainArtifact.setArtifactHandler( m_artifactHandlerManager.getArtifactHandler( "jar" ) );
}
boolean customClassifier = null != classifier && classifier.trim().length() > 0;
boolean customPackaging = null != packaging && packaging.trim().length() > 0;
if ( customClassifier && customPackaging )
{
m_projectHelper.attachArtifact( currentProject, packaging, classifier, jarFile );
}
else if ( customClassifier )
{
m_projectHelper.attachArtifact( currentProject, jarFile, classifier );
}
else if ( customPackaging )
{
m_projectHelper.attachArtifact( currentProject, packaging, jarFile );
}
else
{
mainArtifact.setFile( jarFile );
}
if ( unpackBundle )
{
unpackBundle( jarFile );
}
if ( manifestLocation != null )
{
File outputFile = new File( manifestLocation, "MANIFEST.MF" );
try
{
Manifest manifest = builder.getJar().getManifest();
FileOutputStream fos = new FileOutputStream( outputFile );
try
{
ManifestWriter.outputManifest( manifest, fos, niceManifest );
}
finally
{
fos.close();
}
}
catch ( IOException e )
{
getLog().error( "Error trying to write Manifest to file " + outputFile, e );
}
}
// cleanup...
builder.close();
}
catch ( MojoFailureException e )
{
getLog().error( e.getLocalizedMessage() );
throw new MojoExecutionException( "Error(s) found in bundle configuration", e );
}
catch ( Exception e )
{
getLog().error( "An internal error occurred", e );
throw new MojoExecutionException( "Internal error in maven-bundle-plugin", e );
}
}
protected Builder getOSGiBuilder( MavenProject currentProject, Map originalInstructions, Properties properties,
Jar[] classpath ) throws Exception
{
properties.putAll( getDefaultProperties( currentProject ) );
properties.putAll( transformDirectives( originalInstructions ) );
if (properties.getProperty("Bundle-Activator") != null
&& properties.getProperty("Bundle-Activator").isEmpty())
{
properties.remove("Bundle-Activator");
}
if (properties.containsKey("-disable-plugin"))
{
String[] disabled = properties.remove("-disable-plugin").toString().replaceAll(" ", "").split(",");
String[] enabled = properties.getProperty(Analyzer.PLUGIN, "").replaceAll(" ", "").split(",");
Set<String> plugin = new LinkedHashSet<String>();
plugin.addAll(Arrays.asList(enabled));
plugin.removeAll(Arrays.asList(disabled));
StringBuilder sb = new StringBuilder();
for (String s : plugin)
{
if (sb.length() > 0)
{
sb.append(",");
}
sb.append(sb);
}
properties.setProperty(Analyzer.PLUGIN, sb.toString());
}
Builder builder = new Builder();
synchronized ( BundlePlugin.class ) // protect setBase...getBndLastModified which uses static DateFormat
{
builder.setBase( getBase( currentProject ) );
}
builder.setProperties( sanitize( properties ) );
if ( classpath != null )
{
builder.setClasspath( classpath );
}
return builder;
}
protected static Properties sanitize( Properties properties )
{
// convert any non-String keys/values to Strings
Properties sanitizedEntries = new Properties();
for ( Iterator itr = properties.entrySet().iterator(); itr.hasNext(); )
{
Map.Entry entry = ( Map.Entry ) itr.next();
if ( entry.getKey() instanceof String == false )
{
String key = sanitize( entry.getKey() );
if ( !properties.containsKey( key ) )
{
sanitizedEntries.setProperty( key, sanitize( entry.getValue() ) );
}
itr.remove();
}
else if ( entry.getValue() instanceof String == false )
{
entry.setValue( sanitize( entry.getValue() ) );
}
}
properties.putAll( sanitizedEntries );
return properties;
}
protected static String sanitize( Object value )
{
if ( value instanceof String )
{
return ( String ) value;
}
else if ( value instanceof Iterable )
{
String delim = "";
StringBuilder buf = new StringBuilder();
for ( Object i : ( Iterable<?> ) value )
{
buf.append( delim ).append( i );
delim = ", ";
}
return buf.toString();
}
else if ( value.getClass().isArray() )
{
String delim = "";
StringBuilder buf = new StringBuilder();
for ( int i = 0, len = Array.getLength( value ); i < len; i++ )
{
buf.append( delim ).append( Array.get( value, i ) );
delim = ", ";
}
return buf.toString();
}
else
{
return String.valueOf( value );
}
}
protected void addMavenInstructions( MavenProject currentProject, Builder builder ) throws Exception
{
if ( currentProject.getBasedir() != null )
{
// update BND instructions to add included Maven resources
includeMavenResources( currentProject, builder, getLog() );
// calculate default export/private settings based on sources
addLocalPackages( outputDirectory, builder );
// tell BND where the current project source resides
addMavenSourcePath( currentProject, builder, getLog() );
}
// update BND instructions to embed selected Maven dependencies
Collection embeddableArtifacts = getEmbeddableArtifacts( currentProject, builder );
new DependencyEmbedder( getLog(), embeddableArtifacts ).processHeaders( builder );
if ( dumpInstructions != null || getLog().isDebugEnabled() )
{
StringBuilder buf = new StringBuilder();
getLog().debug( "BND Instructions:" + NL + dumpInstructions( builder.getProperties(), buf ) );
if ( dumpInstructions != null )
{
getLog().info( "Writing BND instructions to " + dumpInstructions );
dumpInstructions.getParentFile().mkdirs();
FileUtils.fileWrite( dumpInstructions, "# BND instructions" + NL + buf );
}
}
if ( dumpClasspath != null || getLog().isDebugEnabled() )
{
StringBuilder buf = new StringBuilder();
getLog().debug( "BND Classpath:" + NL + dumpClasspath( builder.getClasspath(), buf ) );
if ( dumpClasspath != null )
{
getLog().info( "Writing BND classpath to " + dumpClasspath );
dumpClasspath.getParentFile().mkdirs();
FileUtils.fileWrite( dumpClasspath, "# BND classpath" + NL + buf );
}
}
}
protected Builder buildOSGiBundle( MavenProject currentProject, Map originalInstructions, Properties properties,
Jar[] classpath ) throws Exception
{
Builder builder = getOSGiBuilder( currentProject, originalInstructions, properties, classpath );
addMavenInstructions( currentProject, builder );
builder.build();
mergeMavenManifest( currentProject, builder );
return builder;
}
protected static StringBuilder dumpInstructions( Properties properties, StringBuilder buf )
{
try
{
buf.append( "#-----------------------------------------------------------------------" + NL );
Properties stringProperties = new Properties();
for ( Enumeration e = properties.propertyNames(); e.hasMoreElements(); )
{
// we can only store String properties
String key = ( String ) e.nextElement();
String value = properties.getProperty( key );
if ( value != null )
{
stringProperties.setProperty( key, value );
}
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
stringProperties.store( out, null ); // properties encoding is 8859_1
buf.append( out.toString( "8859_1" ) );
buf.append( "#-----------------------------------------------------------------------" + NL );
}
catch ( Throwable e )
{
// ignore...
}
return buf;
}
protected static StringBuilder dumpClasspath( List classpath, StringBuilder buf )
{
try
{
buf.append( "#-----------------------------------------------------------------------" + NL );
buf.append( "-classpath:\\" + NL );
for ( Iterator i = classpath.iterator(); i.hasNext(); )
{
File path = ( ( Jar ) i.next() ).getSource();
if ( path != null )
{
buf.append( ' ' + path.toString() + ( i.hasNext() ? ",\\" : "" ) + NL );
}
}
buf.append( "#-----------------------------------------------------------------------" + NL );
}
catch ( Throwable e )
{
// ignore...
}
return buf;
}
protected StringBuilder dumpManifest( Manifest manifest, StringBuilder buf )
{
try
{
buf.append( "#-----------------------------------------------------------------------" + NL );
ByteArrayOutputStream out = new ByteArrayOutputStream();
ManifestWriter.outputManifest( manifest, out, false ); // manifest encoding is UTF8
buf.append( out.toString( "UTF8" ) );
buf.append( "#-----------------------------------------------------------------------" + NL );
}
catch ( Throwable e )
{
// ignore...
}
return buf;
}
protected static void includeMavenResources( MavenProject currentProject, Analyzer analyzer, Log log )
{
// pass maven resource paths onto BND analyzer
final String mavenResourcePaths = getMavenResourcePaths( currentProject );
final String includeResource = ( String ) analyzer.getProperty( Analyzer.INCLUDE_RESOURCE );
if ( includeResource != null )
{
if ( includeResource.indexOf( MAVEN_RESOURCES ) >= 0 )
{
// if there is no maven resource path, we do a special treatment and replace
// every occurance of MAVEN_RESOURCES and a following comma with an empty string
if ( mavenResourcePaths.length() == 0 )
{
String cleanedResource = removeTagFromInstruction( includeResource, MAVEN_RESOURCES );
if ( cleanedResource.length() > 0 )
{
analyzer.setProperty( Analyzer.INCLUDE_RESOURCE, cleanedResource );
}
else
{
analyzer.unsetProperty( Analyzer.INCLUDE_RESOURCE );
}
}
else
{
String combinedResource = StringUtils
.replace( includeResource, MAVEN_RESOURCES, mavenResourcePaths );
analyzer.setProperty( Analyzer.INCLUDE_RESOURCE, combinedResource );
}
}
else if ( mavenResourcePaths.length() > 0 )
{
log.warn( Analyzer.INCLUDE_RESOURCE + ": overriding " + mavenResourcePaths + " with " + includeResource
+ " (add " + MAVEN_RESOURCES + " if you want to include the maven resources)" );
}
}
else if ( mavenResourcePaths.length() > 0 )
{
analyzer.setProperty( Analyzer.INCLUDE_RESOURCE, mavenResourcePaths );
}
}
protected void mergeMavenManifest( MavenProject currentProject, Builder builder ) throws Exception
{
Jar jar = builder.getJar();
if ( getLog().isDebugEnabled() )
{
getLog().debug( "BND Manifest:" + NL + dumpManifest( jar.getManifest(), new StringBuilder() ) );
}
boolean addMavenDescriptor = currentProject.getBasedir() != null;
try
{
/*
* Grab customized manifest entries from the maven-jar-plugin configuration
*/
MavenArchiveConfiguration archiveConfig = JarPluginConfiguration.getArchiveConfiguration( currentProject );
String mavenManifestText = new MavenArchiver().getManifest( currentProject, archiveConfig ).toString();
addMavenDescriptor = addMavenDescriptor && archiveConfig.isAddMavenDescriptor();
Manifest mavenManifest = new Manifest();
// First grab the external manifest file (if specified and different to target location)
File externalManifestFile = archiveConfig.getManifestFile();
if ( null != externalManifestFile )
{
if ( !externalManifestFile.isAbsolute() )
{
externalManifestFile = new File( currentProject.getBasedir(), externalManifestFile.getPath() );
}
if ( externalManifestFile.exists() && !externalManifestFile.equals( new File( manifestLocation, "MANIFEST.MF" ) ) )
{
InputStream mis = new FileInputStream( externalManifestFile );
mavenManifest.read( mis );
mis.close();
}
}
// Then apply customized entries from the jar plugin; note: manifest encoding is UTF8
mavenManifest.read( new ByteArrayInputStream( mavenManifestText.getBytes( "UTF8" ) ) );
if ( !archiveConfig.isManifestSectionsEmpty() )
{
/*
* Add customized manifest sections (for some reason MavenArchiver doesn't do this for us)
*/
List sections = archiveConfig.getManifestSections();
for ( Iterator i = sections.iterator(); i.hasNext(); )
{
ManifestSection section = ( ManifestSection ) i.next();
Attributes attributes = new Attributes();
if ( !section.isManifestEntriesEmpty() )
{
Map entries = section.getManifestEntries();
for ( Iterator j = entries.entrySet().iterator(); j.hasNext(); )
{
Map.Entry entry = ( Map.Entry ) j.next();
attributes.putValue( ( String ) entry.getKey(), ( String ) entry.getValue() );
}
}
mavenManifest.getEntries().put( section.getName(), attributes );
}
}
Attributes mainMavenAttributes = mavenManifest.getMainAttributes();
mainMavenAttributes.putValue( "Created-By", "Apache Maven Bundle Plugin" );
String[] removeHeaders = builder.getProperty( Constants.REMOVEHEADERS, "" ).split( "," );
// apply -removeheaders to the custom manifest
for ( int i = 0; i < removeHeaders.length; i++ )
{
for ( Iterator j = mainMavenAttributes.keySet().iterator(); j.hasNext(); )
{
if ( j.next().toString().matches( removeHeaders[i].trim() ) )
{
j.remove();
}
}
}
/*
* Overlay generated bundle manifest with customized entries
*/
Manifest bundleManifest = jar.getManifest();
bundleManifest.getMainAttributes().putAll( mainMavenAttributes );
bundleManifest.getEntries().putAll( mavenManifest.getEntries() );
// adjust the import package attributes so that optional dependencies use
// optional resolution.
String importPackages = bundleManifest.getMainAttributes().getValue( "Import-Package" );
if ( importPackages != null )
{
Set optionalPackages = getOptionalPackages( currentProject );
Map<String, ? extends Map<String, String>> values = new Analyzer().parseHeader( importPackages );
for ( Map.Entry<String, ? extends Map<String, String>> entry : values.entrySet() )
{
String pkg = entry.getKey();
Map<String, String> options = entry.getValue();
if ( !options.containsKey( "resolution:" ) && optionalPackages.contains( pkg ) )
{
options.put( "resolution:", "optional" );
}
}
String result = Processor.printClauses( values );
bundleManifest.getMainAttributes().putValue( "Import-Package", result );
}
jar.setManifest( bundleManifest );
}
catch ( Exception e )
{
getLog().warn( "Unable to merge Maven manifest: " + e.getLocalizedMessage() );
}
if ( addMavenDescriptor )
{
doMavenMetadata( currentProject, jar );
}
if ( getLog().isDebugEnabled() )
{
getLog().debug( "Final Manifest:" + NL + dumpManifest( jar.getManifest(), new StringBuilder() ) );
}
builder.setJar( jar );
}
protected Set getOptionalPackages( MavenProject currentProject ) throws IOException, MojoExecutionException
{
ArrayList inscope = new ArrayList();
final Collection artifacts = getSelectedDependencies( currentProject.getArtifacts() );
for ( Iterator it = artifacts.iterator(); it.hasNext(); )
{
Artifact artifact = ( Artifact ) it.next();
if ( artifact.getArtifactHandler().isAddedToClasspath() )
{
if ( !Artifact.SCOPE_TEST.equals( artifact.getScope() ) )
{
inscope.add( artifact );
}
}
}
HashSet optionalArtifactIds = new HashSet();
for ( Iterator it = inscope.iterator(); it.hasNext(); )
{
Artifact artifact = ( Artifact ) it.next();
if ( artifact.isOptional() )
{
String id = artifact.toString();
if ( artifact.getScope() != null )
{
// strip the scope...
id = id.replaceFirst( ":[^:]*$", "" );
}
optionalArtifactIds.add( id );
}
}
HashSet required = new HashSet();
HashSet optional = new HashSet();
for ( Iterator it = inscope.iterator(); it.hasNext(); )
{
Artifact artifact = ( Artifact ) it.next();
File file = getFile( artifact );
if ( file == null )
{
continue;
}
Jar jar = new Jar( artifact.getArtifactId(), file );
if ( isTransitivelyOptional( optionalArtifactIds, artifact ) )
{
optional.addAll( jar.getPackages() );
}
else
{
required.addAll( jar.getPackages() );
}
jar.close();
}
optional.removeAll( required );
return optional;
}
/**
* Check to see if any dependency along the dependency trail of
* the artifact is optional.
*
* @param artifact
*/
protected boolean isTransitivelyOptional( HashSet optionalArtifactIds, Artifact artifact )
{
List trail = artifact.getDependencyTrail();
for ( Iterator iterator = trail.iterator(); iterator.hasNext(); )
{
String next = ( String ) iterator.next();
if ( optionalArtifactIds.contains( next ) )
{
return true;
}
}
return false;
}
private void unpackBundle( File jarFile )
{
File outputDir = getOutputDirectory();
if ( null == outputDir )
{
outputDir = new File( getBuildDirectory(), "classes" );
}
try
{
/*
* this directory must exist before unpacking, otherwise the plexus
* unarchiver decides to use the current working directory instead!
*/
if ( !outputDir.exists() )
{
outputDir.mkdirs();
}
UnArchiver unArchiver = m_archiverManager.getUnArchiver( "jar" );
unArchiver.setDestDirectory( outputDir );
unArchiver.setSourceFile( jarFile );
unArchiver.extract();
}
catch ( Exception e )
{
getLog().error( "Problem unpacking " + jarFile + " to " + outputDir, e );
}
}
protected static String removeTagFromInstruction( String instruction, String tag )
{
StringBuffer buf = new StringBuffer();
String[] clauses = instruction.split( "," );
for ( int i = 0; i < clauses.length; i++ )
{
String clause = clauses[i].trim();
if ( !tag.equals( clause ) )
{
if ( buf.length() > 0 )
{
buf.append( ',' );
}
buf.append( clause );
}
}
return buf.toString();
}
private static Map getProperties( Model projectModel, String prefix )
{
Map properties = new LinkedHashMap();
Method methods[] = Model.class.getDeclaredMethods();
for ( int i = 0; i < methods.length; i++ )
{
String name = methods[i].getName();
if ( name.startsWith( "get" ) )
{
try
{
Object v = methods[i].invoke( projectModel, null );
if ( v != null )
{
name = prefix + Character.toLowerCase( name.charAt( 3 ) ) + name.substring( 4 );
if ( v.getClass().isArray() )
properties.put( name, Arrays.asList( ( Object[] ) v ).toString() );
else
properties.put( name, v );
}
}
catch ( Exception e )
{
// too bad
}
}
}
return properties;
}
private static StringBuffer printLicenses( List licenses )
{
if ( licenses == null || licenses.size() == 0 )
return null;
StringBuffer sb = new StringBuffer();
String del = "";
for ( Iterator i = licenses.iterator(); i.hasNext(); )
{
License l = ( License ) i.next();
String url = l.getUrl();
if ( url == null )
continue;
sb.append( del );
sb.append( url );
del = ", ";
}
if ( sb.length() == 0 )
return null;
return sb;
}
/**
* @param jar
* @throws IOException
*/
private void doMavenMetadata( MavenProject currentProject, Jar jar ) throws IOException
{
String path = "META-INF/maven/" + currentProject.getGroupId() + "/" + currentProject.getArtifactId();
File pomFile = new File( currentProject.getBasedir(), "pom.xml" );
jar.putResource( path + "/pom.xml", new FileResource( pomFile ) );
Properties p = new Properties();
p.put( "version", currentProject.getVersion() );
p.put( "groupId", currentProject.getGroupId() );
p.put( "artifactId", currentProject.getArtifactId() );
ByteArrayOutputStream out = new ByteArrayOutputStream();
p.store( out, "Generated by org.apache.felix.bundleplugin" );
jar.putResource( path + "/pom.properties", new EmbeddedResource( out.toByteArray(), System.currentTimeMillis() ) );
}
protected Jar[] getClasspath( MavenProject currentProject ) throws IOException, MojoExecutionException
{
List list = new ArrayList();
if ( getOutputDirectory() != null && getOutputDirectory().exists() )
{
list.add( new Jar( ".", getOutputDirectory() ) );
}
final Collection artifacts = getSelectedDependencies( currentProject.getArtifacts() );
for ( Iterator it = artifacts.iterator(); it.hasNext(); )
{
Artifact artifact = ( Artifact ) it.next();
if ( artifact.getArtifactHandler().isAddedToClasspath() )
{
if ( !Artifact.SCOPE_TEST.equals( artifact.getScope() ) )
{
File file = getFile( artifact );
if ( file == null )
{
getLog().warn(
"File is not available for artifact " + artifact + " in project "
+ currentProject.getArtifact() );
continue;
}
Jar jar = new Jar( artifact.getArtifactId(), file );
list.add( jar );
}
}
}
Jar[] cp = new Jar[list.size()];
list.toArray( cp );
return cp;
}
private Collection getSelectedDependencies( Collection artifacts ) throws MojoExecutionException
{
if ( null == excludeDependencies || excludeDependencies.length() == 0 )
{
return artifacts;
}
else if ( "true".equalsIgnoreCase( excludeDependencies ) )
{
return Collections.EMPTY_LIST;
}
Collection selectedDependencies = new LinkedHashSet( artifacts );
DependencyExcluder excluder = new DependencyExcluder( artifacts );
excluder.processHeaders( excludeDependencies );
selectedDependencies.removeAll( excluder.getExcludedArtifacts() );
return selectedDependencies;
}
/**
* Get the file for an Artifact
*
* @param artifact
*/
protected File getFile( Artifact artifact )
{
return artifact.getFile();
}
private static void header( Properties properties, String key, Object value )
{
if ( value == null )
return;
if ( value instanceof Collection && ( ( Collection ) value ).isEmpty() )
return;
properties.put( key, value.toString().replaceAll( "[\r\n]", "" ) );
}
/**
* Convert a Maven version into an OSGi compliant version
*
* @param version Maven version
* @return the OSGi version
*/
protected String convertVersionToOsgi( String version )
{
return getMaven2OsgiConverter().getVersion( version );
}
/**
* TODO this should return getMaven2Osgi().getBundleFileName( project.getArtifact() )
*/
protected String getBundleName( MavenProject currentProject )
{
String extension;
try
{
extension = currentProject.getArtifact().getArtifactHandler().getExtension();
}
catch ( Throwable e )
{
extension = currentProject.getArtifact().getType();
}
if ( StringUtils.isEmpty( extension ) || "bundle".equals( extension ) || "pom".equals( extension ) )
{
extension = "jar"; // just in case maven gets confused
}
if ( null != classifier && classifier.trim().length() > 0 )
{
return finalName + '-' + classifier + '.' + extension;
}
return finalName + '.' + extension;
}
protected String getBuildDirectory()
{
return buildDirectory;
}
protected void setBuildDirectory( String _buildirectory )
{
buildDirectory = _buildirectory;
}
protected Properties getDefaultProperties( MavenProject currentProject )
{
Properties properties = new Properties();
String bsn;
try
{
bsn = getMaven2OsgiConverter().getBundleSymbolicName( currentProject.getArtifact() );
}
catch ( Exception e )
{
bsn = currentProject.getGroupId() + "." + currentProject.getArtifactId();
}
// Setup defaults
properties.put( MAVEN_SYMBOLICNAME, bsn );
properties.put( Analyzer.BUNDLE_SYMBOLICNAME, bsn );
properties.put( Analyzer.IMPORT_PACKAGE, "*" );
properties.put( Analyzer.BUNDLE_VERSION, getMaven2OsgiConverter().getVersion( currentProject.getVersion() ) );
// remove the extraneous Include-Resource and Private-Package entries from generated manifest
properties.put( Constants.REMOVEHEADERS, Analyzer.INCLUDE_RESOURCE + ',' + Analyzer.PRIVATE_PACKAGE );
header( properties, Analyzer.BUNDLE_DESCRIPTION, currentProject.getDescription() );
StringBuffer licenseText = printLicenses( currentProject.getLicenses() );
if ( licenseText != null )
{
header( properties, Analyzer.BUNDLE_LICENSE, licenseText );
}
header( properties, Analyzer.BUNDLE_NAME, currentProject.getName() );
if ( currentProject.getOrganization() != null )
{
if ( currentProject.getOrganization().getName() != null )
{
String organizationName = currentProject.getOrganization().getName();
header( properties, Analyzer.BUNDLE_VENDOR, organizationName );
properties.put( "project.organization.name", organizationName );
properties.put( "pom.organization.name", organizationName );
}
if ( currentProject.getOrganization().getUrl() != null )
{
String organizationUrl = currentProject.getOrganization().getUrl();
header( properties, Analyzer.BUNDLE_DOCURL, organizationUrl );
properties.put( "project.organization.url", organizationUrl );
properties.put( "pom.organization.url", organizationUrl );
}
}
properties.putAll( currentProject.getProperties() );
properties.putAll( currentProject.getModel().getProperties() );
for ( Iterator i = currentProject.getFilters().iterator(); i.hasNext(); )
{
File filterFile = new File( ( String ) i.next() );
if ( filterFile.isFile() )
{
properties.putAll( PropertyUtils.loadProperties( filterFile ) );
}
}
if ( m_mavenSession != null )
{
try
{
// don't pass upper-case session settings to bnd as they end up in the manifest
Properties sessionProperties = m_mavenSession.getExecutionProperties();
for ( Enumeration e = sessionProperties.propertyNames(); e.hasMoreElements(); )
{
String key = ( String ) e.nextElement();
if ( key.length() > 0 && !Character.isUpperCase( key.charAt( 0 ) ) )
{
properties.put( key, sessionProperties.getProperty( key ) );
}
}
}
catch ( Exception e )
{
getLog().warn( "Problem with Maven session properties: " + e.getLocalizedMessage() );
}
}
properties.putAll( getProperties( currentProject.getModel(), "project.build." ) );
properties.putAll( getProperties( currentProject.getModel(), "pom." ) );
properties.putAll( getProperties( currentProject.getModel(), "project." ) );
properties.put( "project.baseDir", getBase( currentProject ) );
properties.put( "project.build.directory", getBuildDirectory() );
properties.put( "project.build.outputdirectory", getOutputDirectory() );
properties.put( "classifier", classifier == null ? "" : classifier );
// Add default plugins
header( properties, Analyzer.PLUGIN, ScrPlugin.class.getName() + ","
+ BlueprintPlugin.class.getName() + ","
+ SpringXMLType.class.getName() );
return properties;
}
protected static File getBase( MavenProject currentProject )
{
return currentProject.getBasedir() != null ? currentProject.getBasedir() : new File( "" );
}
protected File getOutputDirectory()
{
return outputDirectory;
}
protected void setOutputDirectory( File _outputDirectory )
{
outputDirectory = _outputDirectory;
}
private static void addLocalPackages( File outputDirectory, Analyzer analyzer ) throws IOException
{
Packages packages = new Packages();
if ( outputDirectory != null && outputDirectory.isDirectory() )
{
// scan classes directory for potential packages
DirectoryScanner scanner = new DirectoryScanner();
scanner.setBasedir( outputDirectory );
scanner.setIncludes( new String[]
{ "**/*.class" } );
scanner.addDefaultExcludes();
scanner.scan();
String[] paths = scanner.getIncludedFiles();
for ( int i = 0; i < paths.length; i++ )
{
packages.put( analyzer.getPackageRef( getPackageName( paths[i] ) ) );
}
}
Packages exportedPkgs = new Packages();
Packages privatePkgs = new Packages();
boolean noprivatePackages = "!*".equals( analyzer.getProperty( Analyzer.PRIVATE_PACKAGE ) );
for ( PackageRef pkg : packages.keySet() )
{
// mark all source packages as private by default (can be overridden by export list)
privatePkgs.put( pkg );
// we can't export the default package (".") and we shouldn't export internal packages
String fqn = pkg.getFQN();
if ( noprivatePackages || !( ".".equals( fqn ) || fqn.contains( ".internal" ) || fqn.contains( ".impl" ) ) )
{
exportedPkgs.put( pkg );
}
}
Properties properties = analyzer.getProperties();
String exported = properties.getProperty( Analyzer.EXPORT_PACKAGE );
if ( exported == null )
{
if ( !properties.containsKey( Analyzer.EXPORT_CONTENTS ) )
{
// no -exportcontents overriding the exports, so use our computed list
for ( Attrs attrs : exportedPkgs.values() )
{
attrs.put( Constants.SPLIT_PACKAGE_DIRECTIVE, "merge-first" );
}
properties.setProperty( Analyzer.EXPORT_PACKAGE, Processor.printClauses( exportedPkgs ) );
}
else
{
// leave Export-Package empty (but non-null) as we have -exportcontents
properties.setProperty( Analyzer.EXPORT_PACKAGE, "" );
}
}
else if ( exported.indexOf( LOCAL_PACKAGES ) >= 0 )
{
String newExported = StringUtils.replace( exported, LOCAL_PACKAGES, Processor.printClauses( exportedPkgs ) );
properties.setProperty( Analyzer.EXPORT_PACKAGE, newExported );
}
String internal = properties.getProperty( Analyzer.PRIVATE_PACKAGE );
if ( internal == null )
{
if ( !privatePkgs.isEmpty() )
{
for ( Attrs attrs : privatePkgs.values() )
{
attrs.put( Constants.SPLIT_PACKAGE_DIRECTIVE, "merge-first" );
}
properties.setProperty( Analyzer.PRIVATE_PACKAGE, Processor.printClauses( privatePkgs ) );
}
else
{
// if there are really no private packages then use "!*" as this will keep the Bnd Tool happy
properties.setProperty( Analyzer.PRIVATE_PACKAGE, "!*" );
}
}
else if ( internal.indexOf( LOCAL_PACKAGES ) >= 0 )
{
String newInternal = StringUtils.replace( internal, LOCAL_PACKAGES, Processor.printClauses( privatePkgs ) );
properties.setProperty( Analyzer.PRIVATE_PACKAGE, newInternal );
}
}
private static String getPackageName( String filename )
{
int n = filename.lastIndexOf( File.separatorChar );
return n < 0 ? "." : filename.substring( 0, n ).replace( File.separatorChar, '.' );
}
private static List getMavenResources( MavenProject currentProject )
{
List resources = new ArrayList( currentProject.getResources() );
if ( currentProject.getCompileSourceRoots() != null )
{
// also scan for any "packageinfo" files lurking in the source folders
List packageInfoIncludes = Collections.singletonList( "**/packageinfo" );
for ( Iterator i = currentProject.getCompileSourceRoots().iterator(); i.hasNext(); )
{
String sourceRoot = ( String ) i.next();
Resource packageInfoResource = new Resource();
packageInfoResource.setDirectory( sourceRoot );
packageInfoResource.setIncludes( packageInfoIncludes );
resources.add( packageInfoResource );
}
}
return resources;
}
protected static String getMavenResourcePaths( MavenProject currentProject )
{
final String basePath = currentProject.getBasedir().getAbsolutePath();
Set pathSet = new LinkedHashSet();
for ( Iterator i = getMavenResources( currentProject ).iterator(); i.hasNext(); )
{
Resource resource = ( Resource ) i.next();
final String sourcePath = resource.getDirectory();
final String targetPath = resource.getTargetPath();
// ignore empty or non-local resources
if ( new File( sourcePath ).exists() && ( ( targetPath == null ) || ( targetPath.indexOf( ".." ) < 0 ) ) )
{
DirectoryScanner scanner = new DirectoryScanner();
scanner.setBasedir( sourcePath );
if ( resource.getIncludes() != null && !resource.getIncludes().isEmpty() )
{
scanner.setIncludes( ( String[] ) resource.getIncludes().toArray( EMPTY_STRING_ARRAY ) );
}
else
{
scanner.setIncludes( DEFAULT_INCLUDES );
}
if ( resource.getExcludes() != null && !resource.getExcludes().isEmpty() )
{
scanner.setExcludes( ( String[] ) resource.getExcludes().toArray( EMPTY_STRING_ARRAY ) );
}
scanner.addDefaultExcludes();
scanner.scan();
List includedFiles = Arrays.asList( scanner.getIncludedFiles() );
for ( Iterator j = includedFiles.iterator(); j.hasNext(); )
{
String name = ( String ) j.next();
String path = sourcePath + '/' + name;
// make relative to project
if ( path.startsWith( basePath ) )
{
if ( path.length() == basePath.length() )
{
path = ".";
}
else
{
path = path.substring( basePath.length() + 1 );
}
}
// replace windows backslash with a slash
// this is a workaround for a problem with bnd 0.0.189
if ( File.separatorChar != '/' )
{
name = name.replace( File.separatorChar, '/' );
path = path.replace( File.separatorChar, '/' );
}
// copy to correct place
path = name + '=' + path;
if ( targetPath != null )
{
path = targetPath + '/' + path;
}
// use Bnd filtering?
if ( resource.isFiltering() )
{
path = '{' + path + '}';
}
pathSet.add( path );
}
}
}
StringBuffer resourcePaths = new StringBuffer();
for ( Iterator i = pathSet.iterator(); i.hasNext(); )
{
resourcePaths.append( i.next() );
if ( i.hasNext() )
{
resourcePaths.append( ',' );
}
}
return resourcePaths.toString();
}
protected Collection getEmbeddableArtifacts( MavenProject currentProject, Analyzer analyzer )
throws MojoExecutionException
{
final Collection artifacts;
String embedTransitive = analyzer.getProperty( DependencyEmbedder.EMBED_TRANSITIVE );
if ( Boolean.valueOf( embedTransitive ).booleanValue() )
{
// includes transitive dependencies
artifacts = currentProject.getArtifacts();
}
else
{
// only includes direct dependencies
artifacts = currentProject.getDependencyArtifacts();
}
return getSelectedDependencies( artifacts );
}
protected static void addMavenSourcePath( MavenProject currentProject, Analyzer analyzer, Log log )
{
// pass maven source paths onto BND analyzer
StringBuilder mavenSourcePaths = new StringBuilder();
if ( currentProject.getCompileSourceRoots() != null )
{
for ( Iterator i = currentProject.getCompileSourceRoots().iterator(); i.hasNext(); )
{
if ( mavenSourcePaths.length() > 0 )
{
mavenSourcePaths.append( ',' );
}
mavenSourcePaths.append( ( String ) i.next() );
}
}
final String sourcePath = ( String ) analyzer.getProperty( Analyzer.SOURCEPATH );
if ( sourcePath != null )
{
if ( sourcePath.indexOf( MAVEN_SOURCES ) >= 0 )
{
// if there is no maven source path, we do a special treatment and replace
// every occurance of MAVEN_SOURCES and a following comma with an empty string
if ( mavenSourcePaths.length() == 0 )
{
String cleanedSource = removeTagFromInstruction( sourcePath, MAVEN_SOURCES );
if ( cleanedSource.length() > 0 )
{
analyzer.setProperty( Analyzer.SOURCEPATH, cleanedSource );
}
else
{
analyzer.unsetProperty( Analyzer.SOURCEPATH );
}
}
else
{
String combinedSource = StringUtils
.replace( sourcePath, MAVEN_SOURCES, mavenSourcePaths.toString() );
analyzer.setProperty( Analyzer.SOURCEPATH, combinedSource );
}
}
else if ( mavenSourcePaths.length() > 0 )
{
log.warn( Analyzer.SOURCEPATH + ": overriding " + mavenSourcePaths + " with " + sourcePath + " (add "
+ MAVEN_SOURCES + " if you want to include the maven sources)" );
}
}
else if ( mavenSourcePaths.length() > 0 )
{
analyzer.setProperty( Analyzer.SOURCEPATH, mavenSourcePaths.toString() );
}
}
}