| /******************************************************************************* |
| * Copyright (c) 2003, 2016 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| |
| package org.eclipse.osgi.util; |
| |
| import java.io.BufferedInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.StringTokenizer; |
| import org.eclipse.osgi.internal.messages.Msg; |
| import org.eclipse.osgi.internal.util.SupplementDebug; |
| import org.eclipse.osgi.internal.util.Tokenizer; |
| import org.osgi.framework.BundleException; |
| |
| /** |
| * This class represents a single manifest element. A manifest element must consist of a single |
| * {@link String} value. The {@link String} value may be split up into component values each |
| * separated by a semi-colon (';'). A manifest element may optionally have a set of |
| * attribute and directive values associated with it. The general syntax of a manifest element is as follows: |
| * <pre> |
| * ManifestElement ::= component (';' component)* (';' parameter)* |
| * component ::= ([^;,:="\#x0D#x0A#x00])+ | quoted-string |
| * quoted-string::= '"' ( [^"\#x0D#x0A#x00] | '\"'| '\\')* '"' |
| * parameter ::= directive | attribute |
| * directive ::= token ':=' argument |
| * attribute ::= token '=' argument |
| * argument ::= extended | quoted-string |
| * token ::= ( alphanum | '_' | '-' )+ |
| * extended ::= ( alphanum | '_' | '-' | '.' )+ |
| * </pre> |
| * <p> |
| * For example, the following is an example of a manifest element to the <code>Export-Package</code> header: |
| * </p> |
| * <pre> |
| * org.osgi.framework; specification-version="1.2"; another-attr="examplevalue" |
| * </pre> |
| * <p> |
| * This manifest element has a value of <code>org.osgi.framework</code> and it has two attributes, |
| * <code>specification-version</code> and <code>another-attr</code>. |
| * </p> |
| * <p> |
| * The following manifest element is an example of a manifest element that has multiple |
| * components to its value: |
| * </p> |
| * <pre> |
| * code1.jar;code2.jar;code3.jar;attr1=value1;attr2=value2;attr3=value3 |
| * </pre> |
| * <p> |
| * This manifest element has a value of <code>code1.jar;code2.jar;code3.jar</code>. |
| * This is an example of a multiple component value. This value has three |
| * components: <code>code1.jar</code>, <code>code2.jar</code>, and <code>code3.jar</code>. |
| * </p> |
| * <p> |
| * If components contain delimiter characters (e.g ';', ',' ':' "=") then it must be |
| * a quoted string. For example, the following is an example of a manifest element |
| * that has multiple components containing delimiter characters: |
| * </p> |
| * <pre> |
| * "component ; 1"; "component , 2"; "component : 3"; attr1=value1; attr2=value2; attr3=value3 |
| * </pre> |
| * <p> |
| * This manifest element has a value of <code>"component ; 1"; "component , 2"; "component : 3"</code>. |
| * This value has three components: <code>"component ; 1"</code>, <code>"component , 2"</code>, <code>"component : 3"</code>. |
| * </p> |
| * <p> |
| * This class is not intended to be subclassed by clients. |
| * </p> |
| * |
| * @since 3.0 |
| * @noextend This class is not intended to be subclassed by clients. |
| */ |
| public class ManifestElement { |
| |
| /** |
| * The value of the manifest element. |
| */ |
| private final String mainValue; |
| |
| /** |
| * The value components of the manifest element. |
| */ |
| private final String[] valueComponents; |
| |
| /** |
| * The table of attributes for the manifest element. |
| */ |
| private HashMap<String, Object> attributes; |
| |
| /** |
| * The table of directives for the manifest element. |
| */ |
| private HashMap<String, Object> directives; |
| |
| /** |
| * Constructs an empty manifest element with no value or attributes. |
| */ |
| private ManifestElement(String value, String[] valueComponents) { |
| this.mainValue = value; |
| this.valueComponents = valueComponents; |
| } |
| |
| /** |
| * Returns the value of the manifest element. The value returned is the |
| * complete value up to the first attribute or directive. For example, the |
| * following manifest element: |
| * <pre> |
| * test1.jar;test2.jar;test3.jar;selection-filter="(os.name=Windows XP)" |
| * </pre> |
| * <p> |
| * This manifest element has a value of <code>test1.jar;test2.jar;test3.jar</code> |
| * </p> |
| * |
| * @return the value of the manifest element. |
| */ |
| public String getValue() { |
| return mainValue; |
| } |
| |
| /** |
| * Returns the value components of the manifest element. The value |
| * components returned are the complete list of value components up to |
| * the first attribute or directive. |
| * For example, the following manifest element: |
| * <pre> |
| * test1.jar;test2.jar;test3.jar;selection-filter="(os.name=Windows XP)" |
| * </pre> |
| * <p> |
| * This manifest element has the value components array |
| * <code>{ "test1.jar", "test2.jar", "test3.jar" }</code> |
| * Each value component is delemited by a semi-colon (<code>';'</code>). |
| * </p> |
| * |
| * @return the String[] of value components |
| */ |
| public String[] getValueComponents() { |
| return valueComponents; |
| } |
| |
| /** |
| * Returns the value for the specified attribute or <code>null</code> if it does |
| * not exist. If the attribute has multiple values specified then the last value |
| * specified is returned. For example the following manifest element: |
| * <pre> |
| * elementvalue; myattr="value1"; myattr="value2" |
| * </pre> |
| * <p> |
| * specifies two values for the attribute key <code>myattr</code>. In this case <code>value2</code> |
| * will be returned because it is the last value specified for the attribute |
| * <code>myattr</code>. |
| * </p> |
| * |
| * @param key the attribute key to return the value for |
| * @return the attribute value or <code>null</code> |
| */ |
| public String getAttribute(String key) { |
| return getTableValue(attributes, key); |
| } |
| |
| /** |
| * Returns an array of values for the specified attribute or |
| * <code>null</code> if the attribute does not exist. |
| * |
| * @param key the attribute key to return the values for |
| * @return the array of attribute values or <code>null</code> |
| * @see #getAttribute(String) |
| */ |
| public String[] getAttributes(String key) { |
| return getTableValues(attributes, key); |
| } |
| |
| /** |
| * Returns an enumeration of attribute keys for this manifest element or |
| * <code>null</code> if none exist. |
| * |
| * @return the enumeration of attribute keys or null if none exist. |
| */ |
| public Enumeration<String> getKeys() { |
| return getTableKeys(attributes); |
| } |
| |
| /** |
| * Add an attribute to this manifest element. |
| * |
| * @param key the key of the attribute |
| * @param value the value of the attribute |
| */ |
| private void addAttribute(String key, String value) { |
| attributes = addTableValue(attributes, key, value); |
| } |
| |
| /** |
| * Returns the value for the specified directive or <code>null</code> if it |
| * does not exist. If the directive has multiple values specified then the |
| * last value specified is returned. For example the following manifest element: |
| * <pre> |
| * elementvalue; mydir:="value1"; mydir:="value2" |
| * </pre> |
| * <p> |
| * specifies two values for the directive key <code>mydir</code>. In this case <code>value2</code> |
| * will be returned because it is the last value specified for the directive <code>mydir</code>. |
| * </p> |
| * |
| * @param key the directive key to return the value for |
| * @return the directive value or <code>null</code> |
| */ |
| public String getDirective(String key) { |
| return getTableValue(directives, key); |
| } |
| |
| /** |
| * Returns an array of string values for the specified directives or |
| * <code>null</code> if it does not exist. |
| * |
| * @param key the directive key to return the values for |
| * @return the array of directive values or <code>null</code> |
| * @see #getDirective(String) |
| */ |
| public String[] getDirectives(String key) { |
| return getTableValues(directives, key); |
| } |
| |
| /** |
| * Return an enumeration of directive keys for this manifest element or |
| * <code>null</code> if there are none. |
| * |
| * @return the enumeration of directive keys or <code>null</code> |
| */ |
| public Enumeration<String> getDirectiveKeys() { |
| return getTableKeys(directives); |
| } |
| |
| /** |
| * Add a directive to this manifest element. |
| * |
| * @param key the key of the attribute |
| * @param value the value of the attribute |
| */ |
| private void addDirective(String key, String value) { |
| directives = addTableValue(directives, key, value); |
| } |
| |
| /* |
| * Return the last value associated with the given key in the specified table. |
| */ |
| private String getTableValue(HashMap<String, Object> table, String key) { |
| if (table == null) { |
| return null; |
| } |
| Object result = table.get(key); |
| if (result == null) { |
| return null; |
| } |
| if (result instanceof String) |
| return (String) result; |
| |
| @SuppressWarnings("unchecked") |
| List<String> valueList = (List<String>) result; |
| //return the last value |
| return valueList.get(valueList.size() - 1); |
| } |
| |
| /* |
| * Return the values associated with the given key in the specified table. |
| */ |
| private String[] getTableValues(HashMap<String, Object> table, String key) { |
| if (table == null) { |
| return null; |
| } |
| Object result = table.get(key); |
| if (result == null) { |
| return null; |
| } |
| if (result instanceof String) |
| return new String[] {(String) result}; |
| @SuppressWarnings("unchecked") |
| List<String> valueList = (List<String>) result; |
| return valueList.toArray(new String[valueList.size()]); |
| } |
| |
| /* |
| * Return an enumeration of table keys for the specified table. |
| */ |
| private Enumeration<String> getTableKeys(HashMap<String, Object> table) { |
| if (table == null) |
| return null; |
| return Collections.enumeration(table.keySet()); |
| } |
| |
| /* |
| * Add the given key/value association to the specified table. If an entry already exists |
| * for this key, then create an array list from the current value (if necessary) and |
| * append the new value to the end of the list. |
| */ |
| @SuppressWarnings("unchecked") |
| private HashMap<String, Object> addTableValue(HashMap<String, Object> table, String key, String value) { |
| if (table == null) { |
| table = new HashMap<>(7); |
| } |
| Object curValue = table.get(key); |
| if (curValue != null) { |
| List<String> newList; |
| // create a list to contain multiple values |
| if (curValue instanceof List) { |
| newList = (List<String>) curValue; |
| } else { |
| newList = new ArrayList<>(5); |
| newList.add((String) curValue); |
| } |
| newList.add(value); |
| table.put(key, newList); |
| } else { |
| table.put(key, value); |
| } |
| return table; |
| } |
| |
| /** |
| * Parses a manifest header value into an array of ManifestElements. Each |
| * ManifestElement returned will have a non-null value returned by getValue(). |
| * |
| * @param header the header name to parse. This is only specified to provide error messages |
| * when the header value is invalid. |
| * @param value the header value to parse. |
| * @return the array of ManifestElements that are represented by the header value; null will be |
| * returned if the value specified is null or if the value does not parse into |
| * one or more ManifestElements. |
| * @throws BundleException if the header value is invalid |
| */ |
| public static ManifestElement[] parseHeader(String header, String value) throws BundleException { |
| if (value == null) |
| return (null); |
| List<ManifestElement> headerElements = new ArrayList<>(10); |
| Tokenizer tokenizer = new Tokenizer(value); |
| parseloop: while (true) { |
| String next = tokenizer.getString(";,"); //$NON-NLS-1$ |
| if (next == null) |
| throw new BundleException(NLS.bind(Msg.MANIFEST_INVALID_HEADER_EXCEPTION, header, value), BundleException.MANIFEST_ERROR); |
| List<String> headerValues = new ArrayList<>(); |
| StringBuilder headerValue = new StringBuilder(next); |
| headerValues.add(next); |
| |
| if (SupplementDebug.STATIC_DEBUG_MANIFEST) |
| System.out.print("parseHeader: " + next); //$NON-NLS-1$ |
| boolean directive = false; |
| char c = tokenizer.getChar(); |
| // Header values may be a list of ';' separated values. Just append them all into one value until the first '=' or ',' |
| while (c == ';') { |
| next = tokenizer.getString(";,=:"); //$NON-NLS-1$ |
| if (next == null) |
| throw new BundleException(NLS.bind(Msg.MANIFEST_INVALID_HEADER_EXCEPTION, header, value), BundleException.MANIFEST_ERROR); |
| c = tokenizer.getChar(); |
| while (c == ':') { // may not really be a := |
| c = tokenizer.getChar(); |
| if (c != '=') { |
| String restOfNext = tokenizer.getToken(";,=:"); //$NON-NLS-1$ |
| if (restOfNext == null) |
| throw new BundleException(NLS.bind(Msg.MANIFEST_INVALID_HEADER_EXCEPTION, header, value), BundleException.MANIFEST_ERROR); |
| next += ":" + c + restOfNext; //$NON-NLS-1$ |
| c = tokenizer.getChar(); |
| } else |
| directive = true; |
| } |
| if (c == ';' || c == ',' || c == '\0') /* more */ { |
| headerValues.add(next); |
| headerValue.append(";").append(next); //$NON-NLS-1$ |
| if (SupplementDebug.STATIC_DEBUG_MANIFEST) |
| System.out.print(";" + next); //$NON-NLS-1$ |
| } |
| } |
| // found the header value create a manifestElement for it. |
| ManifestElement manifestElement = new ManifestElement(headerValue.toString(), headerValues.toArray(new String[headerValues.size()])); |
| |
| // now add any attributes/directives for the manifestElement. |
| while (c == '=' || c == ':') { |
| while (c == ':') { // may not really be a := |
| c = tokenizer.getChar(); |
| if (c != '=') { |
| String restOfNext = tokenizer.getToken("=:"); //$NON-NLS-1$ |
| if (restOfNext == null) |
| throw new BundleException(NLS.bind(Msg.MANIFEST_INVALID_HEADER_EXCEPTION, header, value), BundleException.MANIFEST_ERROR); |
| next += ":" + c + restOfNext; //$NON-NLS-1$ |
| c = tokenizer.getChar(); |
| } else |
| directive = true; |
| } |
| // determine if the attribute is the form attr:List<type> |
| String preserveEscapes = null; |
| if (!directive && next.indexOf("List") > 0) { //$NON-NLS-1$ |
| Tokenizer listTokenizer = new Tokenizer(next); |
| String attrKey = listTokenizer.getToken(":"); //$NON-NLS-1$ |
| if (attrKey != null && listTokenizer.getChar() == ':' && "List".equals(listTokenizer.getToken("<"))) { //$NON-NLS-1$//$NON-NLS-2$ |
| // we assume we must preserve escapes for , and " |
| preserveEscapes = "\\,"; //$NON-NLS-1$ |
| } |
| } |
| String val = tokenizer.getString(";,", preserveEscapes); //$NON-NLS-1$ |
| if (val == null) |
| throw new BundleException(NLS.bind(Msg.MANIFEST_INVALID_HEADER_EXCEPTION, header, value), BundleException.MANIFEST_ERROR); |
| |
| if (SupplementDebug.STATIC_DEBUG_MANIFEST) |
| System.out.print(";" + next + "=" + val); //$NON-NLS-1$ //$NON-NLS-2$ |
| try { |
| if (directive) |
| manifestElement.addDirective(next, val); |
| else |
| manifestElement.addAttribute(next, val); |
| directive = false; |
| } catch (Exception e) { |
| throw new BundleException(NLS.bind(Msg.MANIFEST_INVALID_HEADER_EXCEPTION, header, value), BundleException.MANIFEST_ERROR, e); |
| } |
| c = tokenizer.getChar(); |
| if (c == ';') /* more */ { |
| next = tokenizer.getToken("=:"); //$NON-NLS-1$ |
| if (next == null) |
| throw new BundleException(NLS.bind(Msg.MANIFEST_INVALID_HEADER_EXCEPTION, header, value), BundleException.MANIFEST_ERROR); |
| c = tokenizer.getChar(); |
| } |
| } |
| headerElements.add(manifestElement); |
| if (SupplementDebug.STATIC_DEBUG_MANIFEST) |
| System.out.println(""); //$NON-NLS-1$ |
| if (c == ',') /* another manifest element */ |
| continue parseloop; |
| if (c == '\0') /* end of value */ |
| break parseloop; |
| throw new BundleException(NLS.bind(Msg.MANIFEST_INVALID_HEADER_EXCEPTION, header, value), BundleException.MANIFEST_ERROR); |
| } |
| int size = headerElements.size(); |
| if (size == 0) |
| return (null); |
| |
| ManifestElement[] result = headerElements.toArray(new ManifestElement[size]); |
| return (result); |
| } |
| |
| /** |
| * Returns the result of converting a list of comma-separated tokens into an array. |
| * |
| * @return the array of string tokens or <code>null</code> if there are none |
| * @param stringList the initial comma-separated string |
| */ |
| public static String[] getArrayFromList(String stringList) { |
| String[] result = getArrayFromList(stringList, ","); //$NON-NLS-1$ |
| return result.length == 0 ? null : result; |
| } |
| |
| /** |
| * Returns the result of converting a list of tokens into an array. The tokens |
| * are split using the specified separator. |
| * |
| * @return the array of string tokens. If there are none then an empty array |
| * is returned. |
| * @param stringList the initial string list |
| * @param separator the separator to use to split the list into tokens. |
| * @since 3.2 |
| */ |
| public static String[] getArrayFromList(String stringList, String separator) { |
| if (stringList == null || stringList.trim().length() == 0) |
| return new String[0]; |
| List<String> list = new ArrayList<>(); |
| StringTokenizer tokens = new StringTokenizer(stringList, separator); |
| while (tokens.hasMoreTokens()) { |
| String token = tokens.nextToken().trim(); |
| if (token.length() != 0) |
| list.add(token); |
| } |
| return list.toArray(new String[list.size()]); |
| } |
| |
| /** |
| * Parses a bundle manifest and puts the header/value pairs into the supplied Map. |
| * Only the main section of the manifest is parsed (up to the first blank line). All |
| * other sections are ignored. If a header is duplicated then only the last |
| * value is stored in the map. |
| * <p> |
| * The supplied input stream is consumed by this method and will be closed. |
| * If the supplied Map is null then a Map is created to put the header/value pairs into. |
| * </p> |
| * @param manifest an input stream for a bundle manifest. |
| * @param headers a map used to put the header/value pairs from the bundle manifest. This value may be null. |
| * @throws BundleException if the manifest has an invalid syntax |
| * @throws IOException if an error occurs while reading the manifest |
| * @return the map with the header/value pairs from the bundle manifest |
| */ |
| public static Map<String, String> parseBundleManifest(InputStream manifest, Map<String, String> headers) throws IOException, BundleException { |
| if (headers == null) |
| headers = new HashMap<>(); |
| |
| manifest = new BufferedInputStream(manifest); |
| try { |
| |
| ByteArrayOutputStream buffer = new ByteArrayOutputStream(256); |
| while (true) { |
| String line = readLine(manifest, buffer); |
| /* The java.util.jar classes in JDK 1.3 use the value of the last |
| * encountered manifest header. So we do the same to emulate |
| * this behavior. We no longer throw a BundleException |
| * for duplicate manifest headers. |
| */ |
| |
| if ((line == null) || (line.length() == 0)) /* EOF or empty line */ |
| { |
| break; /* done processing main attributes */ |
| } |
| |
| int colon = line.indexOf(':'); |
| if (colon == -1) /* no colon */ |
| { |
| throw new BundleException(NLS.bind(Msg.MANIFEST_INVALID_LINE_NOCOLON, line), BundleException.MANIFEST_ERROR); |
| } |
| String header = line.substring(0, colon).trim(); |
| String value = line.substring(colon + 1).trim(); |
| // intern the header here because they likely have constants for them anyway |
| headers.put(header.intern(), value); |
| } |
| } finally { |
| try { |
| manifest.close(); |
| } catch (IOException ee) { |
| // do nothing |
| } |
| } |
| return headers; |
| } |
| |
| private static String readLine(InputStream input, ByteArrayOutputStream buffer) throws IOException { |
| // Read a header 'line' |
| // A header line may span multiple lines with line continuations using a beginning space. |
| // This method reads all the line continuations into a single string. |
| // Care must be taken for cases where double byte UTF characters are split |
| // across line continuations. |
| // This is why BufferedReader.readLine is not used here. We must process the |
| // CR LF chars ourselves |
| lineLoop: while (true) { |
| int c = input.read(); |
| if (c == '\n') { // LF |
| // next char is either a continuation (space) char or the first char of the next header |
| input.mark(1); |
| c = input.read(); |
| if (c != ' ') { |
| // This first char of the next header, reset so we don't loose the char |
| input.reset(); |
| break lineLoop; |
| } |
| // This is a continuation, skip the space and read the next char |
| c = input.read(); |
| } else if (c == '\r') { // CR |
| // next char is either a continuation (space) char, LF or the first char of the next header |
| input.mark(1); |
| c = input.read(); |
| if (c == '\n') { // LF |
| // next char is either a continuation (space) char or the first char of the next header |
| input.mark(1); |
| c = input.read(); |
| } |
| if (c != ' ') { |
| // This first char of the next header, reset so we don't loose the char |
| input.reset(); |
| break lineLoop; |
| } |
| c = input.read(); |
| } |
| if (c == -1) { |
| break lineLoop; |
| } |
| buffer.write(c); |
| } |
| String result = buffer.toString("UTF8"); //$NON-NLS-1$ |
| buffer.reset(); |
| return result; |
| } |
| |
| @Override |
| public String toString() { |
| Enumeration<String> attrKeys = getKeys(); |
| Enumeration<String> directiveKeys = getDirectiveKeys(); |
| if (attrKeys == null && directiveKeys == null) |
| return mainValue; |
| StringBuilder result = new StringBuilder(mainValue); |
| if (attrKeys != null) { |
| while (attrKeys.hasMoreElements()) { |
| String key = attrKeys.nextElement(); |
| addValues(false, key, getAttributes(key), result); |
| } |
| } |
| if (directiveKeys != null) { |
| while (directiveKeys.hasMoreElements()) { |
| String key = directiveKeys.nextElement(); |
| addValues(true, key, getDirectives(key), result); |
| } |
| } |
| return result.toString(); |
| } |
| |
| private void addValues(boolean directive, String key, String[] values, StringBuilder result) { |
| if (values == null) |
| return; |
| for (String value : values) { |
| result.append(';').append(key); |
| if (directive) |
| result.append(':'); |
| result.append("=\"").append(value).append('\"'); //$NON-NLS-1$ |
| } |
| } |
| } |