[FELIX-2872] Provide a way to have an easier to read manifest

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1602577 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 742052b..a3c4d92 100644
--- a/bundleplugin/src/main/java/org/apache/felix/bundleplugin/BundlePlugin.java
+++ b/bundleplugin/src/main/java/org/apache/felix/bundleplugin/BundlePlugin.java
@@ -23,6 +23,7 @@
 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;
@@ -223,6 +224,11 @@
      */
     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}";
@@ -419,7 +425,15 @@
                 try
                 {
                     Manifest manifest = builder.getJar().getManifest();
-                    ManifestPlugin.writeManifest( manifest, outputFile );
+                    FileOutputStream fos = new FileOutputStream( outputFile );
+                    try
+                    {
+                        ManifestWriter.outputManifest( manifest, fos, niceManifest );
+                    }
+                    finally
+                    {
+                        fos.close();
+                    }
                 }
                 catch ( IOException e )
                 {
@@ -660,13 +674,13 @@
     }
 
 
-    protected static StringBuilder dumpManifest( Manifest manifest, StringBuilder buf )
+    protected StringBuilder dumpManifest( Manifest manifest, StringBuilder buf )
     {
         try
         {
             buf.append( "#-----------------------------------------------------------------------" + NL );
             ByteArrayOutputStream out = new ByteArrayOutputStream();
-            Jar.writeManifest( manifest, out ); // manifest encoding is UTF8
+            ManifestWriter.outputManifest( manifest, out, false ); // manifest encoding is UTF8
             buf.append( out.toString( "UTF8" ) );
             buf.append( "#-----------------------------------------------------------------------" + NL );
         }
diff --git a/bundleplugin/src/main/java/org/apache/felix/bundleplugin/ManifestWriter.java b/bundleplugin/src/main/java/org/apache/felix/bundleplugin/ManifestWriter.java
new file mode 100644
index 0000000..d90dfda9
--- /dev/null
+++ b/bundleplugin/src/main/java/org/apache/felix/bundleplugin/ManifestWriter.java
@@ -0,0 +1,250 @@
+/*
+ * 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.IOException;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+
+import org.apache.felix.utils.manifest.Parser;
+import org.osgi.framework.Constants;
+
+public class ManifestWriter {
+
+    /**
+     * Unfortunately we have to write our own manifest :-( because of a stupid
+     * bug in the manifest code. It tries to handle UTF-8 but the way it does it
+     * it makes the bytes platform dependent. So the following code outputs the
+     * manifest. A Manifest consists of
+     *
+     * <pre>
+     *   'Manifest-Version: 1.0\r\n'
+     *   main-attributes *
+     *   \r\n
+     *   name-section
+     *
+     *   main-attributes ::= attributes
+     *   attributes      ::= key ': ' value '\r\n'
+     *   name-section    ::= 'Name: ' name '\r\n' attributes
+     * </pre>
+     *
+     * Lines in the manifest should not exceed 72 bytes (! this is where the
+     * manifest screwed up as well when 16 bit unicodes were used).
+     * <p>
+     * As a bonus, we can now sort the manifest!
+     */
+    static byte[]	CONTINUE	= new byte[] {
+            '\r', '\n', ' '
+    };
+
+    static Set<String> NICE_HEADERS = new HashSet<String>(
+            Arrays.asList(
+                    Constants.IMPORT_PACKAGE,
+                    Constants.DYNAMICIMPORT_PACKAGE,
+                    Constants.IMPORT_SERVICE,
+                    Constants.REQUIRE_CAPABILITY,
+                    Constants.EXPORT_PACKAGE,
+                    Constants.EXPORT_SERVICE,
+                    Constants.PROVIDE_CAPABILITY
+            )
+    );
+
+    /**
+     * Main function to output a manifest properly in UTF-8.
+     *
+     * @param manifest
+     *            The manifest to output
+     * @param out
+     *            The output stream
+     * @throws IOException
+     *             when something fails
+     */
+    public static void outputManifest(Manifest manifest, OutputStream out, boolean nice) throws IOException {
+        writeEntry(out, "Manifest-Version", "1.0", nice);
+        attributes(manifest.getMainAttributes(), out, nice);
+
+        TreeSet<String> keys = new TreeSet<String>();
+        for (Object o : manifest.getEntries().keySet())
+            keys.add(o.toString());
+
+        for (String key : keys) {
+            write(out, 0, "\r\n");
+            writeEntry(out, "Name", key, nice);
+            attributes(manifest.getAttributes(key), out, nice);
+        }
+        out.flush();
+    }
+
+    /**
+     * Write out an entry, handling proper unicode and line length constraints
+     */
+    private static void writeEntry(OutputStream out, String name, String value, boolean nice) throws IOException {
+        if (nice && NICE_HEADERS.contains(name)) {
+            int n = write(out, 0, name + ": ");
+            String[] parts = Parser.parseDelimitedString(value, ",");
+            if (parts.length > 1) {
+                write(out, 0, "\r\n ");
+                n = 1;
+            }
+            for (int i = 0; i < parts.length; i++) {
+                if (i < parts.length - 1) {
+                    write(out, n, parts[i] + ",");
+                    write(out, 0, "\r\n ");
+                } else {
+                    write(out, n, parts[i]);
+                    write(out, 0, "\r\n");
+                }
+                n = 1;
+            }
+        } else {
+            int n = write(out, 0, name + ": ");
+            write(out, n, value);
+            write(out, 0, "\r\n");
+        }
+    }
+
+    /**
+     * Convert a string to bytes with UTF8 and then output in max 72 bytes
+     *
+     * @param out
+     *            the output string
+     * @param i
+     *            the current width
+     * @param s
+     *            the string to output
+     * @return the new width
+     * @throws IOException
+     *             when something fails
+     */
+    private static int write(OutputStream out, int i, String s) throws IOException {
+        byte[] bytes = s.getBytes("UTF8");
+        return write(out, i, bytes);
+    }
+
+    /**
+     * Write the bytes but ensure that the line length does not exceed 72
+     * characters. If it is more than 70 characters, we just put a cr/lf +
+     * space.
+     *
+     * @param out
+     *            The output stream
+     * @param width
+     *            The nr of characters output in a line before this method
+     *            started
+     * @param bytes
+     *            the bytes to output
+     * @return the nr of characters in the last line
+     * @throws IOException
+     *             if something fails
+     */
+    private static int write(OutputStream out, int width, byte[] bytes) throws IOException {
+        int w = width;
+        for (int i = 0; i < bytes.length; i++) {
+            if (w >= 72) { // we need to add the \n\r!
+                out.write(CONTINUE);
+                w = 1;
+            }
+            out.write(bytes[i]);
+            w++;
+        }
+        return w;
+    }
+
+    /**
+     * Output an Attributes map. We will sort this map before outputing.
+     *
+     * @param value
+     *            the attrbutes
+     * @param out
+     *            the output stream
+     * @throws IOException
+     *             when something fails
+     */
+    private static void attributes(Attributes value, OutputStream out, boolean nice) throws IOException {
+        TreeMap<String,String> map = new TreeMap<String,String>(String.CASE_INSENSITIVE_ORDER);
+        for (Map.Entry<Object,Object> entry : value.entrySet()) {
+            map.put(entry.getKey().toString(), entry.getValue().toString());
+        }
+
+        map.remove("Manifest-Version"); // get rid of
+        // manifest
+        // version
+        for (Map.Entry<String,String> entry : map.entrySet()) {
+            writeEntry(out, entry.getKey(), entry.getValue(), nice);
+        }
+    }
+
+    private static Manifest clean(Manifest org) {
+
+        Manifest result = new Manifest();
+        for (Map.Entry< ? , ? > entry : org.getMainAttributes().entrySet()) {
+            String nice = clean((String) entry.getValue());
+            result.getMainAttributes().put(entry.getKey(), nice);
+        }
+        for (String name : org.getEntries().keySet()) {
+            Attributes attrs = result.getAttributes(name);
+            if (attrs == null) {
+                attrs = new Attributes();
+                result.getEntries().put(name, attrs);
+            }
+
+            for (Map.Entry< ? , ? > entry : org.getAttributes(name).entrySet()) {
+                String nice = clean((String) entry.getValue());
+                attrs.put(entry.getKey(), nice);
+            }
+        }
+        return result;
+    }
+
+    private static String clean(String s) {
+        StringBuilder sb = new StringBuilder(s);
+        boolean changed = false;
+        boolean replacedPrev = false;
+        for ( int i=0; i<sb.length(); i++) {
+            char c = s.charAt(i);
+            switch(c) {
+            case 0:
+            case '\n':
+            case '\r':
+                changed = true;
+                if ( !replacedPrev ) {
+                    sb.replace(i, i+1, " ");
+                    replacedPrev= true;
+                } else
+                    sb.delete(i, i+1);
+                break;
+            default:
+                replacedPrev = false;
+                break;
+            }
+        }
+        if ( changed)
+            return sb.toString();
+        else
+            return s;
+    }
+
+}
diff --git a/bundleplugin/src/test/java/org/apache/felix/bundleplugin/ManifestWriterTest.java b/bundleplugin/src/test/java/org/apache/felix/bundleplugin/ManifestWriterTest.java
new file mode 100644
index 0000000..1ae2ebe
--- /dev/null
+++ b/bundleplugin/src/test/java/org/apache/felix/bundleplugin/ManifestWriterTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.util.jar.Manifest;
+
+import junit.framework.TestCase;
+
+public class ManifestWriterTest extends TestCase
+{
+
+    public void testNiceManifest() throws Exception
+    {
+        // This manifest has an export clause ending on char 73
+        Manifest manifest = new Manifest();
+        manifest.read(getClass().getResourceAsStream("/test2.mf"));
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        ManifestWriter.outputManifest( manifest, baos, true );
+
+        Manifest manifest2 = new Manifest();
+        manifest2.read(new ByteArrayInputStream(baos.toByteArray()));
+
+        assertEquals( toString(manifest, false), toString(manifest2, false) );
+
+    }
+
+    String toString(Manifest manifest, boolean nice) throws Exception
+    {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        ManifestWriter.outputManifest( manifest, baos, nice );
+        return baos.toString();
+    }
+
+}
diff --git a/bundleplugin/src/test/resources/test2.mf b/bundleplugin/src/test/resources/test2.mf
new file mode 100644
index 0000000..a7f9abd
--- /dev/null
+++ b/bundleplugin/src/test/resources/test2.mf
@@ -0,0 +1,44 @@
+Manifest-Version: 1.0
+Bundle-Activator: org.apache.karaf.features.internal.osgi.Activator
+Bundle-Description: This bundle is the core implementation of the Karaf
+ features support.
+Bundle-DocURL: http://karaf.apache.org/
+Bundle-License: http://www.apache.org/licenses/LICENSE-2.0.txt
+Bundle-ManifestVersion: 2
+Bundle-Name: Apache Karaf :: Features :: Core
+Bundle-SymbolicName: org.apache.karaf.features.core
+Bundle-Vendor: The Apache Software Foundation
+Bundle-Version: 4.0.0.SNAPSHOT
+Export-Package: org.apache.karaf.features;uses:="javax.xml.namespace";ve
+ rsion="4.0.0",org.apache.karaf.features.management;uses:="javax.managem
+ ent.openmbean";version="4.0.0",org.apache.karaf.features.management.cod
+ ec;uses:="javax.management.openmbean,org.apache.karaf.features";version
+ ="4.0.0",org.osgi.service.resolver;version="1.0.1";uses:="org.osgi.reso
+ urce",org.osgi.service.repository;version="1.0";uses:="org.osgi.resourc
+ e",org.eclipse.equinox.region;version="1.1.0";uses:="org.osgi.framework
+ ,org.osgi.framework.hooks.bundle,org.osgi.framework.hooks.resolver,org.
+ osgi.framework.hooks.service,org.osgi.framework.wiring",org.eclipse.equ
+ inox.region.management;uses:="javax.management";version="4.0.0"
+Import-Package: javax.management,javax.management.openmbean,javax.xml.bi
+ nd,javax.xml.bind.annotation,javax.xml.namespace,javax.xml.parsers,java
+ x.xml.stream,javax.xml.transform,javax.xml.transform.dom,javax.xml.tran
+ sform.sax,javax.xml.transform.stream,javax.xml.validation,org.eclipse.e
+ quinox.region;version="[1.1,2)",org.eclipse.equinox.region.management,o
+ rg.osgi.framework;version="[1.7,2)",org.osgi.framework.hooks.bundle;ver
+ sion="[1.1,2)",org.osgi.framework.hooks.resolver;version="[1.0,2)",org.
+ osgi.framework.hooks.service;version="[1.1,2)",org.osgi.framework.start
+ level;version="[1.0,2)",org.osgi.framework.wiring;version="[1.1,2)",org
+ .osgi.resource;version="[1.0,2)",org.osgi.service.cm;version="[1.5,2)",
+ org.osgi.service.event;version="[1.3,2)",org.osgi.service.repository;ve
+ rsion="[1.0,2)",org.osgi.service.resolver;version="[1.0,2)",org.osgi.se
+ rvice.url;version="[1.0,2)",org.osgi.util.tracker;version="[1.5,2)",org
+ .slf4j;version="[1.7,2)",org.w3c.dom,org.xml.sax,org.xml.sax.helpers
+Provide-Capability: osgi.service;effective:=active;objectClass="org.apac
+ he.karaf.features.FeaturesService",osgi.service;effective:=active;objec
+ tClass="org.eclipse.equinox.region.RegionDigraph",osgi.service;effectiv
+ e:=active;objectClass="org.osgi.service.resolver.Resolver"
+Require-Capability: osgi.service;effective:=active;filter:="(objectClass
+ =org.osgi.service.cm.ConfigurationAdmin)",osgi.service;effective:=activ
+ e;filter:="(&(objectClass=org.osgi.service.url.URLStreamHandlerService)
+ (url.handler.protocol=mvn))",osgi.ee;filter:="(&(osgi.ee=JavaSE)(versio
+ n=1.7))"