diff --git a/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java b/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java index 46b15e9cc0..467487dad0 100644 --- a/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java +++ b/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java @@ -4,16 +4,23 @@ import java.io.IOException; import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Comparator; +import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; +import java.util.Set; import java.util.jar.Attributes; import java.util.jar.Attributes.Name; import java.util.jar.Manifest; import java.util.stream.Stream; +import org.osgi.framework.Constants; + /** * 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 @@ -40,14 +47,41 @@ public final class ManifestUtil { private static final byte[] EOL = new byte[] { '\r', '\n' }; + private static final byte[] EOL_INDENT = new byte[] { + '\r', '\n', ' ' + }; private static final byte[] SEPARATOR = new byte[] { ':', ' ' }; private static final int MAX_LENGTH = 72 - EOL.length; + @SuppressWarnings("deprecation") + private static final Set NICE_HEADERS = new HashSet<>( + Arrays.asList( + Constants.IMPORT_PACKAGE, + Constants.DYNAMICIMPORT_PACKAGE, + Constants.IMPORT_SERVICE, + Constants.REQUIRE_CAPABILITY, + Constants.EXPORT_PACKAGE, + Constants.EXPORT_SERVICE, + Constants.PROVIDE_CAPABILITY, + Constants.REQUIRE_BUNDLE, + Constants.BUNDLE_CLASSPATH, "Private-Package" + ) +); + private ManifestUtil() {} + /** + * Writes out the manifest, nicely formatted. Use + * {@link #write(Manifest, OutputStream, boolean)} to control 'nice' + * formatting. + */ public static void write(Manifest manifest, OutputStream out) throws IOException { + write(manifest, out, true); + } + + public static void write(Manifest manifest, OutputStream out, boolean nice) throws IOException { Attributes mainAttributes = manifest.getMainAttributes(); Stream> sortedAttributes = sortedAttributes(mainAttributes); @@ -58,12 +92,12 @@ public static void write(Manifest manifest, OutputStream out) throws IOException versionValue = mainAttributes.getValue(versionName); } if (versionValue != null) { - writeEntry(out, versionName, versionValue); + writeEntry(out, versionName, versionValue, nice); Name filterName = versionName; // Name.equals is case-insensitive sortedAttributes = sortedAttributes.filter(e -> !Objects.equals(e.getKey(), filterName)); } - writeAttributes(out, sortedAttributes); + writeAttributes(out, sortedAttributes, nice); out.write(EOL); for (Iterator> iterator = manifest.getEntries() @@ -72,8 +106,8 @@ public static void write(Manifest manifest, OutputStream out) throws IOException .sorted(Entry.comparingByKey()) .iterator(); iterator.hasNext();) { Entry entry = iterator.next(); - writeEntry(out, NAME, entry.getKey()); - writeAttributes(out, sortedAttributes(entry.getValue())); + writeEntry(out, NAME, entry.getKey(), nice); + writeAttributes(out, sortedAttributes(entry.getValue()), nice); out.write(EOL); } @@ -83,11 +117,42 @@ public static void write(Manifest manifest, OutputStream out) throws IOException /** * Write out an entry, handling proper unicode and line length constraints */ - private static void writeEntry(OutputStream out, Name name, String value) throws IOException { - int width = write(out, 0, name.toString()); - width = write(out, width, SEPARATOR); - write(out, width, value); - out.write(EOL); + private static void writeEntry(OutputStream out, Name name, String value, boolean nice) throws IOException { + + if (nice && NICE_HEADERS.contains(name.toString())) { + int width = write(out, 0, name.toString()); + width = write(out, width, SEPARATOR); + + if (value == null || value.isEmpty()) { + // could be a Multi-Release Jar + write(out, 0, EOL); + return; + } + + String[] parts = parseDelimitedString(value, ","); + if (parts.length > 1) { + write(out, 0, EOL_INDENT); + width = 1; + } + + for (int i = 0; i < parts.length; i++) { + if (i < parts.length - 1) { + width = write(out, width, parts[i] + ","); + write(out, 0, EOL_INDENT); + } else { + width = write(out, width, parts[i]); + write(out, 0, EOL); + } + width = 1; + } + } + else { + int width = write(out, 0, name.toString()); + width = write(out, width, SEPARATOR); + write(out, width, value); + write(out, 0, EOL); + } + } /** @@ -119,8 +184,7 @@ private static int write(OutputStream out, int width, String s) throws IOExcepti private static int write(OutputStream out, int width, byte[] bytes) throws IOException { for (int position = 0, limit = bytes.length, remaining; (remaining = limit - position) > 0;) { if (width >= MAX_LENGTH) { - out.write(EOL); - out.write(' '); + out.write(EOL_INDENT); width = 1; } int count = Math.min(MAX_LENGTH - width, remaining); @@ -134,14 +198,16 @@ private static int write(OutputStream out, int width, byte[] bytes) throws IOExc /** * Output an Attributes map. We sort the map keys. * - * @param value the attributes * @param out the output stream + * @param attributes the attributes + * @param nice nice formatting or not * @throws IOException when something fails */ - private static void writeAttributes(OutputStream out, Stream> attributes) throws IOException { + private static void writeAttributes(OutputStream out, Stream> attributes, boolean nice) + throws IOException { for (Iterator> iterator = attributes.iterator(); iterator.hasNext();) { Entry attribute = iterator.next(); - writeEntry(out, attribute.getKey(), attribute.getValue()); + writeEntry(out, attribute.getKey(), attribute.getValue(), nice); } } @@ -170,4 +236,63 @@ private static Stream> sortedAttributes(Attributes attribute private static Map coerce(Attributes attributes) { return (Map) attributes; } + + /** + * Parses delimited string and returns an array containing the tokens. This + * parser obeys quotes, so the delimiter character will be ignored if it is + * inside of a quote. This method assumes that the quote character is not + * included in the set of delimiter characters. + * + * @param value the delimited string to parse. + * @param delim the characters delimiting the tokens. + * @return an array of string tokens or null if there were no tokens. + **/ + private static String[] parseDelimitedString(String value, String delim) { + if (value == null) { + value = ""; + } + + List list = new ArrayList<>(); + + int CHAR = 1; + int DELIMITER = 2; + int STARTQUOTE = 4; + int ENDQUOTE = 8; + + StringBuilder sb = new StringBuilder(); + + int expecting = (CHAR | DELIMITER | STARTQUOTE); + + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + + boolean isDelimiter = (delim.indexOf(c) >= 0); + boolean isQuote = (c == '"'); + + if (isDelimiter && ((expecting & DELIMITER) > 0)) { + list.add(sb.toString() + .trim()); + sb.delete(0, sb.length()); + expecting = (CHAR | DELIMITER | STARTQUOTE); + } else if (isQuote && ((expecting & STARTQUOTE) > 0)) { + sb.append(c); + expecting = CHAR | ENDQUOTE; + } else if (isQuote && ((expecting & ENDQUOTE) > 0)) { + sb.append(c); + expecting = (CHAR | STARTQUOTE | DELIMITER); + } else if ((expecting & CHAR) > 0) { + sb.append(c); + } else { + throw new IllegalArgumentException("Invalid delimited string: " + value); + } + } + + String s = sb.toString() + .trim(); + if (s.length() > 0) { + list.add(s); + } + + return list.toArray(new String[list.size()]); + } } diff --git a/aQute.libg/src/aQute/lib/manifest/package-info.java b/aQute.libg/src/aQute/lib/manifest/package-info.java index 7912793ebb..ef56e5d036 100644 --- a/aQute.libg/src/aQute/lib/manifest/package-info.java +++ b/aQute.libg/src/aQute/lib/manifest/package-info.java @@ -1,4 +1,4 @@ -@Version("1.0.0") +@Version("1.1.0") package aQute.lib.manifest; import org.osgi.annotation.versioning.Version;