blob: 439efc4f65a3ea4a541dc120ff44325029e3b293 [file] [log] [blame]
/*******************************************************************************
* 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$
}
}
}