[FELIX-3994] Optional merging of duplicate manifest headers
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1690154 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/bundleplugin/src/main/java/org/apache/felix/bundleplugin/BundlePlugin.java b/bundleplugin/src/main/java/org/apache/felix/bundleplugin/BundlePlugin.java
index 20f4b35..1937cbb 100644
--- a/bundleplugin/src/main/java/org/apache/felix/bundleplugin/BundlePlugin.java
+++ b/bundleplugin/src/main/java/org/apache/felix/bundleplugin/BundlePlugin.java
@@ -27,23 +27,16 @@
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.HashMap;
-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.*;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
+import aQute.bnd.header.OSGiHeader;
+import aQute.bnd.header.Parameters;
+import aQute.bnd.osgi.Instruction;
+import aQute.bnd.osgi.Instructions;
+import aQute.lib.collections.ExtList;
+import aQute.libg.generics.Create;
import org.apache.maven.archiver.ManifestSection;
import org.apache.maven.archiver.MavenArchiveConfiguration;
import org.apache.maven.archiver.MavenArchiver;
@@ -339,7 +332,7 @@
for ( Iterator<String> e = errors.iterator(); e.hasNext(); )
{
String msg = e.next();
- if ( msg.startsWith( fileNotFound ) && msg.endsWith( "~" ) )
+ 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() ) );
@@ -417,8 +410,7 @@
try
{
- Manifest manifest = builder.getJar().getManifest();
- ManifestPlugin.writeManifest( manifest, outputFile, niceManifest );
+ ManifestPlugin.writeManifest( builder, outputFile, niceManifest );
}
catch ( IOException e )
{
@@ -539,7 +531,7 @@
Map.Entry<Object,Object> entry = itr.next();
if ( entry.getKey() instanceof String == false )
{
- String key = sanitize( entry.getKey() );
+ String key = sanitize(entry.getKey());
if ( !properties.containsKey( key ) )
{
sanitizedEntries.setProperty( key, sanitize( entry.getValue() ) );
@@ -596,18 +588,18 @@
if ( currentProject.getBasedir() != null )
{
// update BND instructions to add included Maven resources
- includeMavenResources( currentProject, builder, getLog() );
+ includeMavenResources(currentProject, builder, getLog());
// calculate default export/private settings based on sources
- addLocalPackages( outputDirectory, builder );
+ addLocalPackages(outputDirectory, builder);
// tell BND where the current project source resides
- addMavenSourcePath( currentProject, builder, getLog() );
+ addMavenSourcePath(currentProject, builder, getLog());
}
// update BND instructions to embed selected Maven dependencies
Collection<Artifact> embeddableArtifacts = getEmbeddableArtifacts( currentProject, dependencyGraph, builder );
- new DependencyEmbedder( getLog(), dependencyGraph, embeddableArtifacts ).processHeaders( builder );
+ new DependencyEmbedder( getLog(), dependencyGraph, embeddableArtifacts ).processHeaders(builder);
if ( dumpInstructions != null || getLog().isDebugEnabled() )
{
@@ -624,7 +616,7 @@
if ( dumpClasspath != null || getLog().isDebugEnabled() )
{
StringBuilder buf = new StringBuilder();
- getLog().debug( "BND Classpath:" + NL + dumpClasspath( builder.getClasspath(), buf ) );
+ getLog().debug("BND Classpath:" + NL + dumpClasspath(builder.getClasspath(), buf));
if ( dumpClasspath != null )
{
getLog().info( "Writing BND classpath to " + dumpClasspath );
@@ -644,7 +636,7 @@
builder.build();
- mergeMavenManifest( currentProject, dependencyGraph, builder );
+ mergeMavenManifest(currentProject, dependencyGraph, builder);
return builder;
}
@@ -683,7 +675,7 @@
{
try
{
- buf.append( "#-----------------------------------------------------------------------" + NL );
+ buf.append("#-----------------------------------------------------------------------" + NL);
buf.append( "-classpath:\\" + NL );
for ( Iterator<Jar> i = classpath.iterator(); i.hasNext(); )
{
@@ -709,7 +701,7 @@
{
buf.append( "#-----------------------------------------------------------------------" + NL );
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ManifestWriter.outputManifest( manifest, out, true ); // manifest encoding is UTF8
+ ManifestWriter.outputManifest(manifest, out, true); // manifest encoding is UTF8
buf.append( out.toString( "UTF8" ) );
buf.append( "#-----------------------------------------------------------------------" + NL );
}
@@ -841,9 +833,18 @@
/*
* Overlay generated bundle manifest with customized entries
*/
+ Properties properties = builder.getProperties();
Manifest bundleManifest = jar.getManifest();
- bundleManifest.getMainAttributes().putAll( mainMavenAttributes );
- bundleManifest.getEntries().putAll( mavenManifest.getEntries() );
+ if ( properties.containsKey( "Merge-Headers" ) )
+ {
+ Instructions instructions = new Instructions( ExtList.from(builder.getProperty("Merge-Headers")) );
+ mergeManifest( instructions, bundleManifest, mavenManifest );
+ }
+ else
+ {
+ bundleManifest.getMainAttributes().putAll( mainMavenAttributes );
+ bundleManifest.getEntries().putAll( mavenManifest.getEntries() );
+ }
// adjust the import package attributes so that optional dependencies use
// optional resolution.
@@ -887,6 +888,187 @@
}
+ protected static void mergeManifest( Instructions instructions, Manifest... manifests ) throws IOException
+ {
+ for ( int i = manifests.length - 2; i >= 0; i-- )
+ {
+ Manifest mergedManifest = manifests[i];
+ Manifest manifest = manifests[i + 1];
+ Attributes mergedMainAttributes = mergedManifest.getMainAttributes();
+ Attributes mainAttributes = manifest.getMainAttributes();
+ Attributes filteredMainAttributes = filterAttributes( instructions, mainAttributes, null );
+ if ( !filteredMainAttributes.isEmpty() )
+ {
+ mergeAttributes( mergedMainAttributes, filteredMainAttributes );
+ }
+ Map<String, Attributes> mergedEntries = mergedManifest.getEntries();
+ Map<String, Attributes> entries = manifest.getEntries();
+ for ( Map.Entry<String, Attributes> entry : entries.entrySet() )
+ {
+ String name = entry.getKey();
+ Attributes attributes = entry.getValue();
+ Attributes filteredAttributes = filterAttributes( instructions, attributes, null );
+ if ( !filteredAttributes.isEmpty() )
+ {
+ Attributes mergedAttributes = mergedManifest.getAttributes( name );
+ if ( mergedAttributes != null)
+ {
+ mergeAttributes(mergedAttributes, filteredAttributes);
+ }
+ else
+ {
+ mergedEntries.put(name, filteredAttributes);
+ }
+ }
+ }
+ }
+ }
+
+
+ /**
+ * @see Analyzer#filter
+ */
+ private static Attributes filterAttributes(Instructions instructions, Attributes source, Set<Instruction> nomatch) {
+ Attributes result = new Attributes();
+ Map<String, Object> keys = new TreeMap<String, Object>();
+ for ( Object key : source.keySet() )
+ {
+ keys.put( key.toString(), key );
+ }
+
+ List<Instruction> filters = new ArrayList<Instruction>( instructions.keySet() );
+ if (nomatch == null)
+ {
+ nomatch = Create.set();
+ }
+ for ( Instruction instruction : filters ) {
+ boolean match = false;
+ for (Iterator<Map.Entry<String, Object>> i = keys.entrySet().iterator(); i.hasNext();)
+ {
+ Map.Entry<String, Object> entry = i.next();
+ String key = entry.getKey();
+ if ( instruction.matches( key ) )
+ {
+ match = true;
+ if (!instruction.isNegated()) {
+ Object name = entry.getValue();
+ Object value = source.get( name );
+ result.put( name, value );
+ }
+ i.remove(); // Can never match again for another pattern
+ }
+ }
+ if (!match && !instruction.isAny())
+ nomatch.add(instruction);
+ }
+
+ /*
+ * Tricky. If we have umatched instructions they might indicate that we
+ * want to have multiple decorators for the same package. So we check
+ * the unmatched against the result list. If then then match and have
+ * actually interesting properties then we merge them
+ */
+
+ for (Iterator<Instruction> i = nomatch.iterator(); i.hasNext();) {
+ Instruction instruction = i.next();
+
+ // We assume the user knows what he is
+ // doing and inserted a literal. So
+ // we ignore any not matched literals
+ // #252, we should not be negated to make it a constant
+ if (instruction.isLiteral() && !instruction.isNegated()) {
+ Object key = keys.get( instruction.getLiteral() );
+ if ( key != null )
+ {
+ Object value = source.get( key );
+ result.put( key, value );
+ }
+ i.remove();
+ continue;
+ }
+
+ // Not matching a negated instruction looks
+ // like an error ... Though so, but
+ // in the second phase of Export-Package
+ // the !package will never match anymore.
+ if (instruction.isNegated()) {
+ i.remove();
+ continue;
+ }
+
+ // An optional instruction should not generate
+ // an error
+ if (instruction.isOptional()) {
+ i.remove();
+ continue;
+ }
+ }
+ return result;
+ }
+
+
+ private static void mergeAttributes( Attributes... attributesArray ) throws IOException
+ {
+ for ( int i = attributesArray.length - 2; i >= 0; i-- )
+ {
+ Attributes mergedAttributes = attributesArray[i];
+ Attributes attributes = attributesArray[i + 1];
+ for ( Map.Entry<Object, Object> entry : attributes.entrySet() )
+ {
+ Object name = entry.getKey();
+ String value = (String) entry.getValue();
+ String oldValue = (String) mergedAttributes.put( name, value );
+ if ( oldValue != null )
+ {
+ Parameters mergedClauses = OSGiHeader.parseHeader(oldValue);
+ Parameters clauses = OSGiHeader.parseHeader( value );
+ if ( !mergedClauses.isEqual( clauses) )
+ {
+ for ( Map.Entry<String, Attrs> clauseEntry : clauses.entrySet() )
+ {
+ String clause = clauseEntry.getKey();
+ Attrs attrs = clauseEntry.getValue();
+ Attrs mergedAttrs = mergedClauses.get( clause );
+ if ( mergedAttrs == null)
+ {
+ mergedClauses.put( clause, attrs );
+ }
+ else if ( !mergedAttrs.isEqual(attrs) )
+ {
+ for ( Map.Entry<String,String> adentry : attrs.entrySet() )
+ {
+ String adname = adentry.getKey();
+ String ad = adentry.getValue();
+ if ( mergedAttrs.containsKey( adname ) )
+ {
+ Attrs.Type type = attrs.getType( adname );
+ switch (type)
+ {
+ case VERSIONS:
+ case STRINGS:
+ case LONGS:
+ case DOUBLES:
+ ExtList<String> mergedAd = ExtList.from( mergedAttrs.get( adname ) );
+ ExtList.from( ad ).addAll( ExtList.from( ad ) );
+ mergedAttrs.put(adname, mergedAd.join() );
+ break;
+ }
+ }
+ else
+ {
+ mergedAttrs.put( adname, ad );
+ }
+ }
+ }
+ }
+ mergedAttributes.put( name, Processor.printClauses( mergedClauses ) );
+ }
+ }
+ }
+ }
+ }
+
+
protected Set<String> getOptionalPackages( MavenProject currentProject, DependencyNode dependencyGraph ) throws IOException, MojoExecutionException
{
ArrayList<Artifact> inscope = new ArrayList<Artifact>();
diff --git a/bundleplugin/src/main/java/org/apache/felix/bundleplugin/ManifestPlugin.java b/bundleplugin/src/main/java/org/apache/felix/bundleplugin/ManifestPlugin.java
index 10e3f9c..2e6519e 100644
--- a/bundleplugin/src/main/java/org/apache/felix/bundleplugin/ManifestPlugin.java
+++ b/bundleplugin/src/main/java/org/apache/felix/bundleplugin/ManifestPlugin.java
@@ -20,9 +20,11 @@
import java.io.File;
+import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.LinkedHashMap;
@@ -31,6 +33,8 @@
import java.util.Properties;
import java.util.jar.Manifest;
+import aQute.bnd.osgi.Instructions;
+import aQute.lib.collections.ExtList;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
@@ -65,10 +69,10 @@
protected void execute( MavenProject project, DependencyNode dependencyGraph, Map<String, String> instructions, Properties properties, Jar[] classpath )
throws MojoExecutionException
{
- Manifest manifest;
+ Analyzer analyzer;
try
{
- manifest = getManifest( project, dependencyGraph, instructions, properties, classpath );
+ analyzer = getAnalyzer(project, dependencyGraph, instructions, properties, classpath);
}
catch ( FileNotFoundException e )
{
@@ -94,12 +98,16 @@
try
{
- writeManifest( manifest, outputFile, niceManifest );
+ writeManifest( analyzer, outputFile, niceManifest );
}
- catch ( IOException e )
+ catch ( Exception e )
{
throw new MojoExecutionException( "Error trying to write Manifest to file " + outputFile, e );
}
+ finally
+ {
+ analyzer.close();
+ }
}
@@ -113,36 +121,9 @@
public Manifest getManifest( MavenProject project, DependencyNode dependencyGraph, Map<String, String> instructions, Properties properties, Jar[] classpath )
throws IOException, MojoFailureException, MojoExecutionException, Exception
{
- Analyzer analyzer = getAnalyzer( project, dependencyGraph, instructions, properties, classpath );
- boolean hasErrors = reportErrors( "Manifest " + project.getArtifact(), analyzer );
- if ( hasErrors )
- {
- String failok = analyzer.getProperty( "-failok" );
- if ( null == failok || "false".equalsIgnoreCase( failok ) )
- {
- throw new MojoFailureException( "Error(s) found in manifest configuration" );
- }
- }
+ Analyzer analyzer = getAnalyzer(project, dependencyGraph, instructions, properties, classpath);
- Jar jar = analyzer.getJar();
-
- if ( unpackBundle )
- {
- File outputFile = getOutputDirectory();
- for ( Entry<String, Resource> entry : jar.getResources().entrySet() )
- {
- File entryFile = new File( outputFile, entry.getKey() );
- if ( !entryFile.exists() || entry.getValue().lastModified() == 0 )
- {
- entryFile.getParentFile().mkdirs();
- OutputStream os = new FileOutputStream( entryFile );
- entry.getValue().write( os );
- os.close();
- }
- }
- }
-
- Manifest manifest = jar.getManifest();
+ Manifest manifest = analyzer.getJar().getManifest();
// cleanup...
analyzer.close();
@@ -217,10 +198,67 @@
mergeMavenManifest( project, dependencyGraph, analyzer );
+ boolean hasErrors = reportErrors( "Manifest " + project.getArtifact(), analyzer );
+ if ( hasErrors )
+ {
+ String failok = analyzer.getProperty( "-failok" );
+ if ( null == failok || "false".equalsIgnoreCase( failok ) )
+ {
+ throw new MojoFailureException( "Error(s) found in manifest configuration" );
+ }
+ }
+
+ Jar jar = analyzer.getJar();
+
+ if ( unpackBundle )
+ {
+ File outputFile = getOutputDirectory();
+ for ( Entry<String, Resource> entry : jar.getResources().entrySet() )
+ {
+ File entryFile = new File( outputFile, entry.getKey() );
+ if ( !entryFile.exists() || entry.getValue().lastModified() == 0 )
+ {
+ entryFile.getParentFile().mkdirs();
+ OutputStream os = new FileOutputStream( entryFile );
+ entry.getValue().write( os );
+ os.close();
+ }
+ }
+ }
+
return analyzer;
}
+ public static void writeManifest( Analyzer analyzer, File outputFile, boolean niceManifest ) throws Exception
+ {
+ Properties properties = analyzer.getProperties();
+ Manifest manifest = analyzer.getJar().getManifest();
+ if ( outputFile.exists() && properties.containsKey( "Merge-Headers" ) )
+ {
+ Manifest analyzerManifest = manifest;
+ manifest = new Manifest();
+ InputStream inputStream = new FileInputStream( outputFile );
+ try
+ {
+ manifest.read( inputStream );
+ }
+ finally
+ {
+ inputStream.close();
+ }
+ Instructions instructions = new Instructions( ExtList.from( analyzer.getProperty("Merge-Headers") ) );
+ mergeManifest( instructions, manifest, analyzerManifest );
+ }
+ else
+ {
+ File parentFile = outputFile.getParentFile();
+ parentFile.mkdirs();
+ }
+ writeManifest( manifest, outputFile, niceManifest );
+ }
+
+
public static void writeManifest( Manifest manifest, File outputFile, boolean niceManifest ) throws IOException
{
outputFile.getParentFile().mkdirs();