Skip to content

Instantly share code, notes, and snippets.

@jmini
Created January 31, 2023 06:01
Show Gist options
  • Save jmini/222e9454667ff0da2e099f05aef0dfa2 to your computer and use it in GitHub Desktop.
Save jmini/222e9454667ff0da2e099f05aef0dfa2 to your computer and use it in GitHub Desktop.
Format MANIFEST.MF files
///usr/bin/env jbang "$0" "$@" ; exit $?
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.Manifest;
import java.util.stream.Stream;
public class FormatManifestMain {
private static final Comparator<Name> nameComparator = Comparator.comparing(Name::toString, String.CASE_INSENSITIVE_ORDER);
private static final Name NAME = new Name("Name");
private static final String EOL = "\r\n";
private static final String SEPARATOR = ": ";
private static final int CHAR = 1;
private static final int DELIMITER = 2;
private static final int STARTQUOTE = 4;
private static final int ENDQUOTE = 8;
public static void main(String... args) throws IOException {
if (args.length != 1) {
throw new IllegalStateException("jbang " + FormatManifestMain.class.getSimpleName() + " <manifest file name>");
}
System.out.println("File: " + args[0]);
File file = new File(args[0]);
if (!"MANIFEST.MF".equals(file.getName())) {
throw new IllegalStateException("File name is not expected: " + file.getAbsolutePath());
}
System.out.println();
Manifest manifest = new Manifest(new FileInputStream(file));
String content = humanReadableContent(manifest);
System.out.println(content);
}
// Inspired from aQute.lib.manifest.ManifestUtil.write(..) in the bnd project
// See https://github.com/bndtools/bnd/blob/master/aQute.libg/src/aQute/lib/manifest/ManifestUtil.java#L50
public static String humanReadableContent(Manifest manifest) throws IOException {
StringBuilder sb = new StringBuilder();
Attributes mainAttributes = manifest.getMainAttributes();
Stream<Entry<Name, String>> sortedAttributes = sortedAttributes(mainAttributes);
Name versionName = Name.MANIFEST_VERSION;
String versionValue = mainAttributes.getValue(versionName);
if (versionValue == null) {
versionName = Name.SIGNATURE_VERSION;
versionValue = mainAttributes.getValue(versionName);
}
if (versionValue != null) {
writeEntry(sb, versionName, versionValue);
Name filterName = versionName;
// Name.equals is case-insensitive
sortedAttributes = sortedAttributes.filter(e -> !Objects.equals(e.getKey(), filterName));
}
writeAttributes(sb, sortedAttributes);
sb.append(EOL);
for (Iterator<Entry<String, Attributes>> iterator = manifest.getEntries()
.entrySet()
.stream()
.sorted(Entry.comparingByKey())
.iterator(); iterator.hasNext();) {
Entry<String, Attributes> entry = iterator.next();
writeEntry(sb, NAME, entry.getKey());
writeAttributes(sb, sortedAttributes(entry.getValue()));
sb.append(EOL);
}
return sb.toString();
}
private static void writeEntry(StringBuilder sb, Name name, String value) throws IOException {
sb.append(name.toString());
sb.append(SEPARATOR);
List<String> values = parseDelimitedString(value, ",");
if (values.size() == 1) {
sb.append(value);
} else {
String multilineValue = String.join(",\r\n ", values);
sb.append(multilineValue);
}
sb.append(EOL);
}
private static void writeAttributes(StringBuilder sb, Stream<Entry<Name, String>> attributes) throws IOException {
for (Iterator<Entry<Name, String>> iterator = attributes.iterator(); iterator.hasNext();) {
Entry<Name, String> attribute = iterator.next();
writeEntry(sb, attribute.getKey(), attribute.getValue());
}
}
private static Stream<Entry<Name, String>> sortedAttributes(Attributes attributes) {
return coerce(attributes).entrySet()
.stream()
.sorted(Entry.comparingByKey(nameComparator));
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static Map<Name, String> coerce(Attributes attributes) {
return (Map) attributes;
}
// Inspired from org.apache.felix.utils.resource.ResourceBuilder.parseDelimitedString(..) in the felix project
// See https://github.com/apache/felix-dev/blob/master/utils/src/main/java/org/apache/felix/utils/resource/ResourceBuilder.java
private static List<String> parseDelimitedString(String value, String delim) {
if (value == null) {
return Collections.emptyList();
}
List<String> list = new ArrayList<>();
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 Collections.unmodifiableList(list);
}
private FormatManifestMain() {
}
}
@jmini
Copy link
Author

jmini commented Jan 31, 2023

In the bnd project the method in the ManifestUtil is serializing the content of a java.util.jar.Manifest object according to the spec (72 chars per line…)

This method is serializing the content in an human readable way.

To run the script, you can use jbang:

jbang run FormatManifestMain.java <path to the manifest file>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment