FELIX-514 Updated compendium bundle to R4.1 OSGi API
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@681945 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/org.osgi.compendium/src/main/java/info/dmtree/Uri.java b/org.osgi.compendium/src/main/java/info/dmtree/Uri.java
new file mode 100644
index 0000000..c9ad62c
--- /dev/null
+++ b/org.osgi.compendium/src/main/java/info/dmtree/Uri.java
@@ -0,0 +1,563 @@
+/*
+ * $Header: /cvshome/build/info.dmtree/src/info/dmtree/Uri.java,v 1.12 2006/10/24 17:54:28 hargrave Exp $
+ *
+ * Copyright (c) OSGi Alliance (2004, 2006). All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package info.dmtree;
+
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.*;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class contains static utility methods to manipulate DMT URIs.
+ * <p>
+ * Syntax of valid DMT URIs:
+ * <ul>
+ * <li>A slash (<code>'/'</code> \u002F) is the separator of the node names.
+ * Slashes used in node name must therefore be escaped using a backslash slash
+ * (<code>"\/"</code>). The backslash must be escaped with a double backslash
+ * sequence. A backslash found must be ignored when it is not followed by a
+ * slash or backslash.
+ * <li>The node name can be constructed using full Unicode character set
+ * (except the Supplementary code, not being supported by CLDC/CDC). However,
+ * using the full Unicode character set for node names is discouraged because
+ * the encoding in the underlying storage as well as the encoding needed in
+ * communications can create significant performance and memory usage overhead.
+ * Names that are restricted to the URI set <code>[-a-zA-Z0-9_.!~*'()]</code>
+ * are most efficient.
+ * <li>URIs used in the DMT must be treated and interpreted as case sensitive.
+ * <li>No End Slash: URI must not end with the delimiter slash (<code>'/'</code>
+ * \u002F). This implies that the root node must be denoted as
+ * <code>"."</code> and not <code>"./"</code>.
+ * <li>No parent denotation: URI must not be constructed using the character
+ * sequence <code>"../"</code> to traverse the tree upwards.
+ * <li>Single Root: The character sequence <code>"./"</code> must not be used
+ * anywhere else but in the beginning of a URI.
+ * </ul>
+ */
+public final class Uri {
+ /*
+ * NOTE: An implementor may also choose to replace this class in
+ * their distribution with a class that directly interfaces with the
+ * info.dmtree implementation. This replacement class MUST NOT alter the
+ * public/protected signature of this class.
+ */
+
+ /*
+ * This class will load the class named
+ * by the org.osgi.vendor.dmtree.DigestDelegate property. This class will call
+ * the public static byte[] digest(byte[]) method on that class.
+ */
+
+ private static class ImplHolder implements PrivilegedAction {
+ // the name of the system property containing the digest delegate class name
+ private static final String DIGEST_DELEGATE_PROPERTY =
+ "org.osgi.vendor.dmtree.DigestDelegate";
+
+ // the Method where message digest requests can be delegated
+ static final Method digestMethod;
+
+ static {
+ digestMethod = (Method) AccessController.doPrivileged(new ImplHolder());
+ }
+
+ private ImplHolder() {
+ }
+
+ public Object run() {
+ String className = System
+ .getProperty(DIGEST_DELEGATE_PROPERTY);
+ if (className == null) {
+ throw new NoClassDefFoundError("Digest " +
+ "delegate class property '" +
+ DIGEST_DELEGATE_PROPERTY +
+ "' must be set to a " +
+ "class which implements a " +
+ "public static byte[] digest(byte[]) method.");
+ }
+
+ Class delegateClass;
+ try {
+ delegateClass = Class.forName(className);
+ }
+ catch (ClassNotFoundException e) {
+ throw new NoClassDefFoundError(e.toString());
+ }
+
+ Method result;
+ try {
+ result = delegateClass.getMethod("digest",
+ new Class[] {byte[].class});
+ }
+ catch (NoSuchMethodException e) {
+ throw new NoSuchMethodError(e.toString());
+ }
+
+ if (!Modifier.isStatic(result.getModifiers())) {
+ throw new NoSuchMethodError(
+ "digest method must be static");
+ }
+
+ return result;
+ }
+ }
+
+
+ // the name of the system property containing the URI segment length limit
+ private static final String SEGMENT_LENGTH_LIMIT_PROPERTY =
+ "org.osgi.impl.service.dmt.uri.limits.segmentlength";
+
+ // the smallest valid value for the URI segment length limit
+ private static final int MINIMAL_SEGMENT_LENGTH_LIMIT = 32;
+
+ // contains the maximum length of node names
+ private static final int segmentLengthLimit;
+
+ static {
+ segmentLengthLimit = ((Integer) AccessController
+ .doPrivileged(new PrivilegedAction() {
+ public Object run() {
+ String limitString = System.getProperty(SEGMENT_LENGTH_LIMIT_PROPERTY);
+ int limit = MINIMAL_SEGMENT_LENGTH_LIMIT; // min. used as default
+
+ try {
+ int limitInt = Integer.parseInt(limitString);
+ if(limitInt >= MINIMAL_SEGMENT_LENGTH_LIMIT)
+ limit = limitInt;
+ } catch(NumberFormatException e) {}
+
+ return new Integer(limit);
+ }
+ })).intValue();
+ }
+
+ // base64 encoding table, modified for use in node name mangling
+ private static final char BASE_64_TABLE[] = {
+ 'A','B','C','D','E','F','G','H',
+ 'I','J','K','L','M','N','O','P',
+ 'Q','R','S','T','U','V','W','X',
+ 'Y','Z','a','b','c','d','e','f',
+ 'g','h','i','j','k','l','m','n',
+ 'o','p','q','r','s','t','u','v',
+ 'w','x','y','z','0','1','2','3',
+ '4','5','6','7','8','9','+','_', // !!! this differs from base64
+ };
+
+
+ /**
+ * A private constructor to suppress the default public constructor.
+ */
+ private Uri() {}
+
+ /**
+ * Returns a node name that is valid for the tree operation methods, based
+ * on the given node name. This transformation is not idempotent, so it must
+ * not be called with a parameter that is the result of a previous
+ * <code>mangle</code> method call.
+ * <p>
+ * Node name mangling is needed in the following cases:
+ * <ul>
+ * <li>if the name contains '/' or '\' characters
+ * <li>if the length of the name exceeds the limit defined by the
+ * implementation
+ * </ul>
+ * <p>
+ * A node name that does not suffer from either of these problems is
+ * guaranteed to remain unchanged by this method. Therefore the client may
+ * skip the mangling if the node name is known to be valid (though it is
+ * always safe to call this method).
+ * <p>
+ * The method returns the normalized <code>nodeName</code> as described
+ * below. Invalid node names are normalized in different ways, depending on
+ * the cause. If the length of the name does not exceed the limit, but the
+ * name contains '/' or '\' characters, then these are simply escaped by
+ * inserting an additional '\' before each occurrence. If the length of the
+ * name does exceed the limit, the following mechanism is used to normalize
+ * it:
+ * <ul>
+ * <li>the SHA 1 digest of the name is calculated
+ * <li>the digest is encoded with the base 64 algorithm
+ * <li>all '/' characters in the encoded digest are replaced with '_'
+ * <li>trailing '=' signs are removed
+ * </ul>
+ *
+ * @param nodeName the node name to be mangled (if necessary), must not be
+ * <code>null</code> or empty
+ * @return the normalized node name that is valid for tree operations
+ * @throws NullPointerException if <code>nodeName</code> is
+ * <code>null</code>
+ * @throws IllegalArgumentException if <code>nodeName</code> is empty
+ */
+ public static String mangle(String nodeName) {
+ return mangle(nodeName, getMaxSegmentNameLength());
+ }
+
+ /**
+ * Construct a URI from the specified URI segments. The segments must
+ * already be mangled.
+ * <p>
+ * If the specified path is an empty array then an empty URI
+ * (<code>""</code>) is returned.
+ *
+ * @param path a possibly empty array of URI segments, must not be
+ * <code>null</code>
+ * @return the URI created from the specified segments
+ * @throws NullPointerException if the specified path or any of its
+ * segments are <code>null</code>
+ * @throws IllegalArgumentException if the specified path contains too many
+ * or malformed segments or the resulting URI is too long
+ */
+ public static String toUri(String[] path) {
+ if (0 == path.length) {
+ return "";
+ }
+
+ if (path.length > getMaxUriSegments()) {
+ throw new IllegalArgumentException(
+ "Path contains too many segments.");
+ }
+
+ StringBuffer uri = new StringBuffer();
+ int uriLength = 0;
+ for (int i = 0; i < path.length; ++i) {
+ // getSegmentLength throws exceptions on malformed segments.
+ int segmentLength = getSegmentLength(path[i]);
+ if (segmentLength > getMaxSegmentNameLength()) {
+ throw new IllegalArgumentException("URI segment too long.");
+ }
+ if (i > 0) {
+ uri.append('/');
+ uriLength++;
+ }
+ uriLength += segmentLength;
+ uri.append(path[i]);
+ }
+ if (uriLength > getMaxUriLength()) {
+ throw new IllegalArgumentException("URI too long.");
+ }
+ return uri.toString();
+ }
+
+ /**
+ * This method returns the length of a URI segment. The length of the URI
+ * segment is defined as the number of bytes in the unescaped, UTF-8 encoded
+ * represenation of the segment.
+ * <p>
+ * The method verifies that the URI segment is well-formed.
+ *
+ * @param segment the URI segment
+ * @return URI segment length
+ * @throws NullPointerException if the specified segment is
+ * <code>null</code>
+ * @throws IllegalArgumentException if the specified URI segment is
+ * malformed
+ */
+ private static int getSegmentLength(String segment) {
+ if (segment.length() == 0)
+ throw new IllegalArgumentException("URI segment is empty.");
+
+ StringBuffer newsegment = new StringBuffer(segment);
+ int i = 0;
+ while (i < newsegment.length()) { // length can decrease during the loop!
+ if (newsegment.charAt(i) == '\\') {
+ if (i == newsegment.length() - 1) // last character cannot be a '\'
+ throw new IllegalArgumentException(
+ "URI segment ends with the escape character.");
+
+ newsegment.deleteCharAt(i); // remove the extra '\'
+ } else if (newsegment.charAt(i) == '/')
+ throw new IllegalArgumentException(
+ "URI segment contains an unescaped '/' character.");
+
+ i++;
+ }
+
+ if (newsegment.toString().equals(".."))
+ throw new IllegalArgumentException(
+ "URI segment must not be \"..\".");
+
+ try {
+ return newsegment.toString().getBytes("UTF-8").length;
+ } catch (UnsupportedEncodingException e) {
+ // This should never happen. All implementations must support
+ // UTF-8 encoding;
+ throw new RuntimeException(e.toString());
+ }
+ }
+
+ /**
+ * Split the specified URI along the path separator '/' charaters and return
+ * an array of URI segments. Special characters in the returned segments are
+ * escaped. The returned array may be empty if the specifed URI was empty.
+ *
+ * @param uri the URI to be split, must not be <code>null</code>
+ * @return an array of URI segments created by splitting the specified URI
+ * @throws NullPointerException if the specified URI is <code>null</code>
+ * @throws IllegalArgumentException if the specified URI is malformed
+ */
+ public static String[] toPath(String uri) {
+ if (uri == null)
+ throw new NullPointerException("'uri' parameter is null.");
+
+ if (!isValidUri(uri))
+ throw new IllegalArgumentException("Malformed URI: " + uri);
+
+ if (uri.length() == 0)
+ return new String[] {};
+
+ List segments = new ArrayList();
+ StringBuffer segment = new StringBuffer();
+
+ boolean escape = false;
+ for (int i = 0; i < uri.length(); i++) {
+ char ch = uri.charAt(i);
+
+ if (escape) {
+ if(ch == '/' || ch == '\\')
+ segment.append('\\');
+ segment.append(ch);
+ escape = false;
+ } else if (ch == '/') {
+ segments.add(segment.toString());
+ segment = new StringBuffer();
+ } else if (ch == '\\') {
+ escape = true;
+ } else
+ segment.append(ch);
+ }
+ if (segment.length() > 0) {
+ segments.add(segment.toString());
+ }
+
+ return (String[]) segments.toArray(new String[segments.size()]);
+ }
+
+ /**
+ * Returns the maximum allowed number of URI segments. The returned value is
+ * implementation specific.
+ * <p>
+ * The return value of <code>Integer.MAX_VALUE</code> indicates that there
+ * is no upper limit on the number of URI segments.
+ *
+ * @return maximum number of URI segments supported by the implementation
+ */
+ public static int getMaxUriSegments() {
+ return Integer.MAX_VALUE;
+ }
+
+ /**
+ * Returns the maximum allowed length of a URI. The value is implementation
+ * specific. The length of the URI is defined as the number of bytes in the
+ * unescaped, UTF-8 encoded represenation of the URI.
+ * <p>
+ * The return value of <code>Integer.MAX_VALUE</code> indicates that there
+ * is no upper limit on the length of URIs.
+ *
+ * @return maximum URI length supported by the implementation
+ */
+ public static int getMaxUriLength() {
+ return Integer.MAX_VALUE;
+ }
+
+ /**
+ * Returns the maximum allowed length of a URI segment. The value is
+ * implementation specific. The length of the URI segment is defined as the
+ * number of bytes in the unescaped, UTF-8 encoded represenation of the
+ * segment.
+ * <p>
+ * The return value of <code>Integer.MAX_VALUE</code> indicates that there
+ * is no upper limit on the length of segment names.
+ *
+ * @return maximum URI segment length supported by the implementation
+ */
+ public static int getMaxSegmentNameLength() {
+ return segmentLengthLimit;
+ }
+
+ /**
+ * Checks whether the specified URI is an absolute URI. An absolute URI
+ * contains the complete path to a node in the DMT starting from the DMT
+ * root (".").
+ *
+ * @param uri the URI to be checked, must not be <code>null</code> and must
+ * contain a valid URI
+ * @return whether the specified URI is absolute
+ * @throws NullPointerException if the specified URI is <code>null</code>
+ * @throws IllegalArgumentException if the specified URI is malformed
+ */
+ public static boolean isAbsoluteUri(String uri) {
+ if( null == uri ) {
+ throw new NullPointerException("'uri' parameter is null.");
+ }
+ if( !isValidUri(uri) )
+ throw new IllegalArgumentException("Malformed URI: " + uri);
+ return uri.equals(".") || uri.equals("\\.") || uri.startsWith("./")
+ || uri.startsWith("\\./");
+ }
+
+ /**
+ * Checks whether the specified URI is valid. A URI is considered valid if
+ * it meets the following constraints:
+ * <ul>
+ * <li>the URI is not <code>null</code>;
+ * <li>the URI follows the syntax defined for valid DMT URIs;
+ * <li>the length of the URI is not more than {@link #getMaxUriLength()};
+ * <li>the URI doesn't contain more than {@link #getMaxUriSegments()}
+ * segments;
+ * <li>the length of each segment of the URI is less than or equal to
+ * {@link #getMaxSegmentNameLength()}.
+ * </ul>
+ * The exact definition of the length of a URI and its segments is
+ * given in the descriptions of the <code>getMaxUriLength()</code> and
+ * <code>getMaxSegmentNameLength()</code> methods.
+ *
+ * @param uri the URI to be validated
+ * @return whether the specified URI is valid
+ */
+ public static boolean isValidUri(String uri) {
+ if (null == uri)
+ return false;
+
+ int paramLen = uri.length();
+ if( paramLen == 0 )
+ return true;
+ if( uri.charAt(0) == '/' || uri.charAt(paramLen-1) == '\\' )
+ return false;
+
+ int processedUriLength = 0;
+ int segmentNumber = 0;
+
+ // append a '/' to indicate the end of the last segment (the URI in the
+ // parameter must not end with a '/')
+ uri += '/';
+ paramLen++;
+
+ int start = 0;
+ for(int i = 1; i < paramLen; i++) { // first character is not a '/'
+ if(uri.charAt(i) == '/' && uri.charAt(i-1) != '\\') {
+ segmentNumber++;
+
+ String segment = uri.substring(start, i);
+ if(segmentNumber > 1 && segment.equals("."))
+ return false; // the URI contains the "." node name at a
+ // position other than the beginning of the URI
+
+ int segmentLength;
+ try {
+ // also checks that the segment is valid
+ segmentLength = getSegmentLength(segment);
+ } catch(IllegalArgumentException e) {
+ return false;
+ }
+
+ if(segmentLength > getMaxSegmentNameLength())
+ return false;
+
+ // the extra byte is for the separator '/' (will be deducted
+ // again for the last segment of the URI)
+ processedUriLength += segmentLength + 1;
+ start = i+1;
+ }
+ }
+
+ processedUriLength--; // remove the '/' added to the end of the URI
+
+ return segmentNumber <= getMaxUriSegments() &&
+ processedUriLength <= getMaxUriLength();
+ }
+
+ // Non-public fields and methods
+
+ // package private method for testing purposes
+ static String mangle(String nodeName, int limit) {
+ if(nodeName == null)
+ throw new NullPointerException(
+ "The 'nodeName' parameter must not be null.");
+
+ if(nodeName.equals(""))
+ throw new IllegalArgumentException(
+ "The 'nodeName' parameter must not be empty.");
+
+ if(nodeName.length() > limit)
+ // create node name hash
+ return getHash(nodeName);
+
+ // escape any '/' and '\' characters in the node name
+ StringBuffer nameBuffer = new StringBuffer(nodeName);
+ for(int i = 0; i < nameBuffer.length(); i++) // 'i' can increase in loop
+ if(nameBuffer.charAt(i) == '\\' || nameBuffer.charAt(i) == '/')
+ nameBuffer.insert(i++, '\\');
+
+ return nameBuffer.toString();
+ }
+
+ private static String getHash(String from) {
+ byte[] byteStream;
+ try {
+ byteStream = from.getBytes("UTF-8");
+ }
+ catch (UnsupportedEncodingException e) {
+ // There's no way UTF-8 encoding is not implemented...
+ throw new IllegalStateException("there's no UTF-8 encoder here!");
+ }
+ byte[] digest = digestMessage(byteStream);
+
+ // very dumb base64 encoder code. There is no need for multiple lines
+ // or trailing '='-s....
+ // also, we hardcoded the fact that sha-1 digests are 20 bytes long
+ StringBuffer sb = new StringBuffer(digest.length*2);
+ for(int i=0;i<6;i++) {
+ int d0 = digest[i*3]&0xff;
+ int d1 = digest[i*3+1]&0xff;
+ int d2 = digest[i*3+2]&0xff;
+ sb.append(BASE_64_TABLE[d0>>2]);
+ sb.append(BASE_64_TABLE[(d0<<4|d1>>4)&63]);
+ sb.append(BASE_64_TABLE[(d1<<2|d2>>6)&63]);
+ sb.append(BASE_64_TABLE[d2&63]);
+ }
+ int d0 = digest[18]&0xff;
+ int d1 = digest[19]&0xff;
+ sb.append(BASE_64_TABLE[d0>>2]);
+ sb.append(BASE_64_TABLE[(d0<<4|d1>>4)&63]);
+ sb.append(BASE_64_TABLE[(d1<<2)&63]);
+
+ return sb.toString();
+ }
+
+ private static byte[] digestMessage(byte[] byteStream) {
+ try {
+ try {
+ return (byte[]) ImplHolder.digestMethod.invoke(null, new Object[] {
+ byteStream});
+ }
+ catch (InvocationTargetException e) {
+ throw e.getTargetException();
+ }
+ }
+ catch (Error e) {
+ throw e;
+ }
+ catch (RuntimeException e) {
+ throw e;
+ }
+ catch (Throwable e) {
+ throw new RuntimeException(e.toString());
+ }
+ }
+}