[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))"