package org.archstudio.releng.pde.actions; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.StringReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Arrays; import java.util.Collections; import java.util.Collections; import java.util.Comparator; import java.util.Comparator; import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.List; import java.util.Set; import org.archstudio.sysutils.SystemUtils; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.osgi.framework.util.Headers; import org.eclipse.osgi.util.ManifestElement; import org.osgi.framework.BundleException; /** * Utility to sort the contents of a MANIFEST.MF file. The MANIFEST.MF specification is located at: * http://docs.oracle.com/javase/1.5.0/docs/guide/jar/jar.html#Manifest% 20Specification * <p> * Of note, Eclipse does not adhere to the maximum line length requirement, so neither do we. (It * does when creating the binary version of a plugin, however.) */ @SuppressWarnings("all") public class SortManifests extends AbstractProjectHandler { private static final String MANIFEST_ELEMENT_SEPARATOR = ","; private static final String ATTRIBUTE_ASSIGNMENT = "="; private static final String DIRECTIVE_ASSIGNMENT = ":="; private static final String ATTRIBUTE_AND_DIRECTIVE_SEPARATOR = ";"; private static final String MANIFEST_VERSION_HEADER = "Manifest-Version"; private static final String MANIFEST_HEADER_VALUE_SEPARATOR = ": "; private static final String MANIFEST_CONTINUE_LINE_PREFIX = " "; private static final String EOL = System.getProperty("line.separator"); private static final Set<String> HEADERS_TO_SORT = new HashSet<>(); static { HEADERS_TO_SORT.add("Bundle-ClassPath"); HEADERS_TO_SORT.add("Export-Package"); HEADERS_TO_SORT.add("Import-Package"); HEADERS_TO_SORT.add("Require-Bundle"); } @Override protected void execute(IProject project) { sortManifest(project); } public static void sortManifest(IProject project) { try { // Sort MANIFEST.MF file. if (project.getFile("META-INF/MANIFEST.MF").exists()) { IFile manifestFile = project.getFile("META-INF/MANIFEST.MF"); String manifest = new String(SystemUtils.blt(manifestFile.getContents()), "UTF-8"); String sortedManifest = sortManifest(manifest); manifestFile.setContents(new ByteArrayInputStream(sortedManifest.getBytes("UTF-8")), true, true, null); manifestFile.refreshLocal(IResource.DEPTH_ZERO, null); } } catch (Exception e) { e.printStackTrace(); } } public static String sortManifest(String manifestInput) throws BundleException { // Parse the manifest input. byte[] inputByteArray = manifestInput.getBytes(StandardCharsets.UTF_8); Headers<String, String> manifestHeaders = Headers.parseManifest(new ByteArrayInputStream(inputByteArray)); // Convert the headers to an array and sort by header name. Enumeration<String> headerEnumeration = manifestHeaders.keys(); List<String> sortedHeaders = new ArrayList<>(); while (headerEnumeration.hasMoreElements()) { sortedHeaders.add(headerEnumeration.nextElement()); } Collections.sort(sortedHeaders); // Treat the manifest version specially and place it on the first line. String manifestVersion = manifestHeaders.get(MANIFEST_VERSION_HEADER); // Some older MANIFEST.MF files lack this property. So, add it. if (manifestVersion == null) { manifestVersion = "1.0"; } sortedHeaders.remove(MANIFEST_VERSION_HEADER); // Capture each line of manifest output. Note: we allow these lines to exceed the allowable line // length for MANIFEST.MF files. List<String> manifestLineOutput = new ArrayList<>(); manifestLineOutput .add(MANIFEST_VERSION_HEADER + MANIFEST_HEADER_VALUE_SEPARATOR + manifestVersion); // Combine headers into a new, sorted manifest file. for (String name : sortedHeaders) { String value = manifestHeaders.get(name); // This will hold the sorted elements of the header, or the value itself it cannot be split up // into elements to sort List<String> reassembledElements = new ArrayList<>(); // Sort the elements only if the header is tagged in HEADERS_TO_SORT. We restrict ourselves to // only these headers because the parser will treat any ',' in header value as an element // delimiter. This doesn't make sense for some header values, such as the name specified by // Bundle-Name (which may contain commas). if (HEADERS_TO_SORT.contains(name)) { // Break the header value into elements as delimited by a ','. ManifestElement[] elements = ManifestElement.parseHeader(name, value); Arrays.sort(elements, new Comparator<ManifestElement>() { @Override public int compare(ManifestElement o1, ManifestElement o2) { return o1.getValue().compareTo(o2.getValue()); } }); // Sort the elements themselves (they are accessed as a key in a map, so presumably this is // okay). for (ManifestElement element : elements) { List<String> attributes = new ArrayList<>(); List<String> directives = new ArrayList<>(); // Sort and process attributes. Enumeration<String> attributeKeysEnumeration = element.getKeys(); if (attributeKeysEnumeration != null) { Set<String> attributeKeysSet = new HashSet<>(); while (attributeKeysEnumeration.hasMoreElements()) { attributeKeysSet.add(attributeKeysEnumeration.nextElement()); } List<String> attributeKeysList = new ArrayList<>(attributeKeysSet); Collections.sort(attributeKeysList, new Comparator<String>() { @Override public int compare(String o1, String o2) { return o1.compareTo(o2); } }); // Attributes may have multiple, unsorted, values. We leave them in the same order in // case it is semantically meaningful. for (String attributeKey : attributeKeysList) { for (String attributeValue : element.getAttributes(attributeKey)) { // Eclipse wraps attribute values in Quotation marks. attributes.add(attributeKey + ATTRIBUTE_ASSIGNMENT + "\"" + attributeValue + "\""); } } } // Sort and process directives. Enumeration<String> directiveKeysEnumeration = element.getDirectiveKeys(); if (directiveKeysEnumeration != null) { Set<String> directiveKeysSet = new HashSet<>(); while (directiveKeysEnumeration.hasMoreElements()) { directiveKeysSet.add(directiveKeysEnumeration.nextElement()); } List<String> directiveKeysList = new ArrayList<>(directiveKeysSet); Collections.sort(directiveKeysList, new Comparator<String>() { @Override public int compare(String o1, String o2) { return o1.compareTo(o2); } }); // Directives may have multiple, unsorted, values. We leave them in the same order in // case it is semantically meaningful. for (String directiveKey : directiveKeysList) { for (String directiveValue : element.getDirectives(directiveKey)) { directives.add(directiveKey + DIRECTIVE_ASSIGNMENT + directiveValue); } } } // Reconstruct the manifest element. Eclipse places attributes before directives. StringBuffer reassembledElement = new StringBuffer(); reassembledElement.append(element.getValue()); for (String attribute : attributes) { reassembledElement.append(ATTRIBUTE_AND_DIRECTIVE_SEPARATOR); reassembledElement.append(attribute); } for (String directive : directives) { reassembledElement.append(ATTRIBUTE_AND_DIRECTIVE_SEPARATOR); reassembledElement.append(directive); } reassembledElements.add(reassembledElement.toString()); } } // if (HEADERS_TO_SORT.contains(name)) else { // The header is not marked for sorting of its elements. reassembledElements.add(value); } // Reconstruct the header. StringBuffer manifestLine = new StringBuffer(); for (int i = 0; i < reassembledElements.size(); i++) { manifestLine.delete(0, manifestLine.length()); if (i == 0) { // The first line has the header name. manifestLine.append(name).append(MANIFEST_HEADER_VALUE_SEPARATOR); } else if (i > 0) { // Subsequent lines begin with a space. manifestLine.append(MANIFEST_CONTINUE_LINE_PREFIX); } manifestLine.append(reassembledElements.get(i)); // Lines that continue on are followed with a comma. if (i < reassembledElements.size() - 1) { manifestLine.append(MANIFEST_ELEMENT_SEPARATOR); } manifestLineOutput.add(manifestLine.toString()); } } // We don't bother wrapping lines since Eclipse doesn't either. StringBuffer manifestOutput = new StringBuffer(); for (String line : manifestLineOutput) { manifestOutput.append(line); // Each line, including the terminal line, ends in a EOL. manifestOutput.append(EOL); } // Finally, Headers.parseManifest stops at the first empty line. So, we append the remaining // part of the file, if present. try { BufferedReader lineReader = new BufferedReader(new StringReader(manifestInput)); String line; boolean hitEmptyLine = false; while ((line = lineReader.readLine()) != null) { if (line.trim().length() == 0) { hitEmptyLine = true; } if (hitEmptyLine) { manifestOutput.append(line).append(EOL); } } } catch (IOException ignored) { // This shouldn't happen since we're reading a string. throw new RuntimeException(ignored); } return manifestOutput.toString(); } }