Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nice manifest writer #6323

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 139 additions & 14 deletions aQute.libg/src/aQute/lib/manifest/ManifestUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<String> 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<Entry<Name, String>> sortedAttributes = sortedAttributes(mainAttributes);

Expand All @@ -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<Entry<String, Attributes>> iterator = manifest.getEntries()
Expand All @@ -72,8 +106,8 @@ public static void write(Manifest manifest, OutputStream out) throws IOException
.sorted(Entry.comparingByKey())
.iterator(); iterator.hasNext();) {
Entry<String, Attributes> 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);
}

Expand All @@ -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);
}

}

/**
Expand Down Expand Up @@ -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);
Expand All @@ -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<Entry<Name, String>> attributes) throws IOException {
private static void writeAttributes(OutputStream out, Stream<Entry<Name, String>> attributes, boolean nice)
throws IOException {
for (Iterator<Entry<Name, String>> iterator = attributes.iterator(); iterator.hasNext();) {
Entry<Name, String> attribute = iterator.next();
writeEntry(out, attribute.getKey(), attribute.getValue());
writeEntry(out, attribute.getKey(), attribute.getValue(), nice);
}
}

Expand Down Expand Up @@ -170,4 +236,63 @@ private static Stream<Entry<Name, String>> sortedAttributes(Attributes attribute
private static Map<Name, String> 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<String> 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()]);
}
}
2 changes: 1 addition & 1 deletion aQute.libg/src/aQute/lib/manifest/package-info.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@Version("1.0.0")
@Version("1.1.0")
package aQute.lib.manifest;

import org.osgi.annotation.versioning.Version;