[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();