blob: ab5b8a8b4769677924cb585192ef583254c5c3d2 [file] [log] [blame]
Christian van Spaandonk63814412008-08-02 09:56:01 +00001/*
Richard S. Hall8df9ab12009-07-24 17:06:37 +00002 * Copyright (c) OSGi Alliance (2004, 2008). All Rights Reserved.
Christian van Spaandonk63814412008-08-02 09:56:01 +00003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package info.dmtree;
17
18import java.io.UnsupportedEncodingException;
Richard S. Hall8df9ab12009-07-24 17:06:37 +000019import java.lang.reflect.InvocationTargetException;
20import java.lang.reflect.Method;
21import java.lang.reflect.Modifier;
Christian van Spaandonk63814412008-08-02 09:56:01 +000022import java.security.AccessController;
23import java.security.PrivilegedAction;
24import java.util.ArrayList;
25import java.util.List;
26
27/**
28 * This class contains static utility methods to manipulate DMT URIs.
29 * <p>
30 * Syntax of valid DMT URIs:
31 * <ul>
32 * <li>A slash (<code>'/'</code> &#92;u002F) is the separator of the node names.
Richard S. Hall8df9ab12009-07-24 17:06:37 +000033 * Slashes used in node name must therefore be escaped using a backslash slash (
34 * <code>"\/"</code>). The backslash must be escaped with a double backslash sequence. A
35 * backslash found must be ignored when it is not followed by a slash or
36 * backslash.
37 * <li>The node name can be constructed using full Unicode character set (except
38 * the Supplementary code, not being supported by CLDC/CDC). However, using the
39 * full Unicode character set for node names is discouraged because the encoding
40 * in the underlying storage as well as the encoding needed in communications
41 * can create significant performance and memory usage overhead. Names that are
42 * restricted to the URI set <code>[-a-zA-Z0-9_.!~*'()]</code> are most efficient.
Christian van Spaandonk63814412008-08-02 09:56:01 +000043 * <li>URIs used in the DMT must be treated and interpreted as case sensitive.
44 * <li>No End Slash: URI must not end with the delimiter slash (<code>'/'</code>
Richard S. Hall8df9ab12009-07-24 17:06:37 +000045 * &#92;u002F). This implies that the root node must be denoted as
Christian van Spaandonk63814412008-08-02 09:56:01 +000046 * <code>"."</code> and not <code>"./"</code>.
47 * <li>No parent denotation: URI must not be constructed using the character
48 * sequence <code>"../"</code> to traverse the tree upwards.
49 * <li>Single Root: The character sequence <code>"./"</code> must not be used
50 * anywhere else but in the beginning of a URI.
51 * </ul>
Richard S. Hall8df9ab12009-07-24 17:06:37 +000052 *
53 * @version $Revision: 5673 $
Christian van Spaandonk63814412008-08-02 09:56:01 +000054 */
55public final class Uri {
56 /*
57 * NOTE: An implementor may also choose to replace this class in
58 * their distribution with a class that directly interfaces with the
59 * info.dmtree implementation. This replacement class MUST NOT alter the
60 * public/protected signature of this class.
61 */
62
63 /*
64 * This class will load the class named
65 * by the org.osgi.vendor.dmtree.DigestDelegate property. This class will call
66 * the public static byte[] digest(byte[]) method on that class.
67 */
68
69 private static class ImplHolder implements PrivilegedAction {
70 // the name of the system property containing the digest delegate class name
71 private static final String DIGEST_DELEGATE_PROPERTY =
72 "org.osgi.vendor.dmtree.DigestDelegate";
73
74 // the Method where message digest requests can be delegated
75 static final Method digestMethod;
76
77 static {
78 digestMethod = (Method) AccessController.doPrivileged(new ImplHolder());
79 }
80
81 private ImplHolder() {
82 }
83
84 public Object run() {
85 String className = System
86 .getProperty(DIGEST_DELEGATE_PROPERTY);
87 if (className == null) {
88 throw new NoClassDefFoundError("Digest " +
89 "delegate class property '" +
90 DIGEST_DELEGATE_PROPERTY +
91 "' must be set to a " +
92 "class which implements a " +
93 "public static byte[] digest(byte[]) method.");
94 }
95
96 Class delegateClass;
97 try {
98 delegateClass = Class.forName(className);
99 }
100 catch (ClassNotFoundException e) {
101 throw new NoClassDefFoundError(e.toString());
102 }
103
104 Method result;
105 try {
106 result = delegateClass.getMethod("digest",
107 new Class[] {byte[].class});
108 }
109 catch (NoSuchMethodException e) {
110 throw new NoSuchMethodError(e.toString());
111 }
112
113 if (!Modifier.isStatic(result.getModifiers())) {
114 throw new NoSuchMethodError(
115 "digest method must be static");
116 }
117
118 return result;
119 }
120 }
121
122
123 // the name of the system property containing the URI segment length limit
124 private static final String SEGMENT_LENGTH_LIMIT_PROPERTY =
125 "org.osgi.impl.service.dmt.uri.limits.segmentlength";
126
127 // the smallest valid value for the URI segment length limit
128 private static final int MINIMAL_SEGMENT_LENGTH_LIMIT = 32;
129
130 // contains the maximum length of node names
131 private static final int segmentLengthLimit;
132
133 static {
134 segmentLengthLimit = ((Integer) AccessController
135 .doPrivileged(new PrivilegedAction() {
136 public Object run() {
137 String limitString = System.getProperty(SEGMENT_LENGTH_LIMIT_PROPERTY);
138 int limit = MINIMAL_SEGMENT_LENGTH_LIMIT; // min. used as default
139
140 try {
141 int limitInt = Integer.parseInt(limitString);
142 if(limitInt >= MINIMAL_SEGMENT_LENGTH_LIMIT)
143 limit = limitInt;
144 } catch(NumberFormatException e) {}
145
146 return new Integer(limit);
147 }
148 })).intValue();
149 }
150
151 // base64 encoding table, modified for use in node name mangling
152 private static final char BASE_64_TABLE[] = {
153 'A','B','C','D','E','F','G','H',
154 'I','J','K','L','M','N','O','P',
155 'Q','R','S','T','U','V','W','X',
156 'Y','Z','a','b','c','d','e','f',
157 'g','h','i','j','k','l','m','n',
158 'o','p','q','r','s','t','u','v',
159 'w','x','y','z','0','1','2','3',
160 '4','5','6','7','8','9','+','_', // !!! this differs from base64
161 };
162
163
164 /**
165 * A private constructor to suppress the default public constructor.
166 */
167 private Uri() {}
168
169 /**
170 * Returns a node name that is valid for the tree operation methods, based
171 * on the given node name. This transformation is not idempotent, so it must
172 * not be called with a parameter that is the result of a previous
173 * <code>mangle</code> method call.
174 * <p>
175 * Node name mangling is needed in the following cases:
176 * <ul>
177 * <li>if the name contains '/' or '\' characters
178 * <li>if the length of the name exceeds the limit defined by the
179 * implementation
180 * </ul>
181 * <p>
182 * A node name that does not suffer from either of these problems is
183 * guaranteed to remain unchanged by this method. Therefore the client may
184 * skip the mangling if the node name is known to be valid (though it is
185 * always safe to call this method).
186 * <p>
187 * The method returns the normalized <code>nodeName</code> as described
188 * below. Invalid node names are normalized in different ways, depending on
189 * the cause. If the length of the name does not exceed the limit, but the
190 * name contains '/' or '\' characters, then these are simply escaped by
191 * inserting an additional '\' before each occurrence. If the length of the
192 * name does exceed the limit, the following mechanism is used to normalize
193 * it:
194 * <ul>
195 * <li>the SHA 1 digest of the name is calculated
196 * <li>the digest is encoded with the base 64 algorithm
197 * <li>all '/' characters in the encoded digest are replaced with '_'
198 * <li>trailing '=' signs are removed
199 * </ul>
200 *
201 * @param nodeName the node name to be mangled (if necessary), must not be
202 * <code>null</code> or empty
203 * @return the normalized node name that is valid for tree operations
204 * @throws NullPointerException if <code>nodeName</code> is
205 * <code>null</code>
206 * @throws IllegalArgumentException if <code>nodeName</code> is empty
207 */
208 public static String mangle(String nodeName) {
209 return mangle(nodeName, getMaxSegmentNameLength());
210 }
211
212 /**
213 * Construct a URI from the specified URI segments. The segments must
214 * already be mangled.
215 * <p>
216 * If the specified path is an empty array then an empty URI
217 * (<code>""</code>) is returned.
218 *
219 * @param path a possibly empty array of URI segments, must not be
220 * <code>null</code>
221 * @return the URI created from the specified segments
222 * @throws NullPointerException if the specified path or any of its
223 * segments are <code>null</code>
224 * @throws IllegalArgumentException if the specified path contains too many
225 * or malformed segments or the resulting URI is too long
226 */
227 public static String toUri(String[] path) {
228 if (0 == path.length) {
229 return "";
230 }
231
232 if (path.length > getMaxUriSegments()) {
233 throw new IllegalArgumentException(
234 "Path contains too many segments.");
235 }
236
237 StringBuffer uri = new StringBuffer();
238 int uriLength = 0;
239 for (int i = 0; i < path.length; ++i) {
240 // getSegmentLength throws exceptions on malformed segments.
241 int segmentLength = getSegmentLength(path[i]);
242 if (segmentLength > getMaxSegmentNameLength()) {
243 throw new IllegalArgumentException("URI segment too long.");
244 }
245 if (i > 0) {
246 uri.append('/');
247 uriLength++;
248 }
249 uriLength += segmentLength;
250 uri.append(path[i]);
251 }
252 if (uriLength > getMaxUriLength()) {
253 throw new IllegalArgumentException("URI too long.");
254 }
255 return uri.toString();
256 }
257
258 /**
259 * This method returns the length of a URI segment. The length of the URI
260 * segment is defined as the number of bytes in the unescaped, UTF-8 encoded
261 * represenation of the segment.
262 * <p>
263 * The method verifies that the URI segment is well-formed.
264 *
265 * @param segment the URI segment
266 * @return URI segment length
267 * @throws NullPointerException if the specified segment is
268 * <code>null</code>
269 * @throws IllegalArgumentException if the specified URI segment is
270 * malformed
271 */
272 private static int getSegmentLength(String segment) {
273 if (segment.length() == 0)
274 throw new IllegalArgumentException("URI segment is empty.");
275
276 StringBuffer newsegment = new StringBuffer(segment);
277 int i = 0;
278 while (i < newsegment.length()) { // length can decrease during the loop!
279 if (newsegment.charAt(i) == '\\') {
280 if (i == newsegment.length() - 1) // last character cannot be a '\'
281 throw new IllegalArgumentException(
282 "URI segment ends with the escape character.");
283
284 newsegment.deleteCharAt(i); // remove the extra '\'
285 } else if (newsegment.charAt(i) == '/')
286 throw new IllegalArgumentException(
287 "URI segment contains an unescaped '/' character.");
288
289 i++;
290 }
291
292 if (newsegment.toString().equals(".."))
293 throw new IllegalArgumentException(
294 "URI segment must not be \"..\".");
295
296 try {
297 return newsegment.toString().getBytes("UTF-8").length;
298 } catch (UnsupportedEncodingException e) {
299 // This should never happen. All implementations must support
300 // UTF-8 encoding;
301 throw new RuntimeException(e.toString());
302 }
303 }
304
305 /**
306 * Split the specified URI along the path separator '/' charaters and return
307 * an array of URI segments. Special characters in the returned segments are
308 * escaped. The returned array may be empty if the specifed URI was empty.
309 *
310 * @param uri the URI to be split, must not be <code>null</code>
311 * @return an array of URI segments created by splitting the specified URI
312 * @throws NullPointerException if the specified URI is <code>null</code>
313 * @throws IllegalArgumentException if the specified URI is malformed
314 */
315 public static String[] toPath(String uri) {
316 if (uri == null)
317 throw new NullPointerException("'uri' parameter is null.");
318
319 if (!isValidUri(uri))
320 throw new IllegalArgumentException("Malformed URI: " + uri);
321
322 if (uri.length() == 0)
323 return new String[] {};
324
325 List segments = new ArrayList();
326 StringBuffer segment = new StringBuffer();
327
328 boolean escape = false;
329 for (int i = 0; i < uri.length(); i++) {
330 char ch = uri.charAt(i);
331
332 if (escape) {
333 if(ch == '/' || ch == '\\')
334 segment.append('\\');
335 segment.append(ch);
336 escape = false;
337 } else if (ch == '/') {
338 segments.add(segment.toString());
339 segment = new StringBuffer();
340 } else if (ch == '\\') {
341 escape = true;
342 } else
343 segment.append(ch);
344 }
345 if (segment.length() > 0) {
346 segments.add(segment.toString());
347 }
348
349 return (String[]) segments.toArray(new String[segments.size()]);
350 }
351
352 /**
353 * Returns the maximum allowed number of URI segments. The returned value is
354 * implementation specific.
355 * <p>
356 * The return value of <code>Integer.MAX_VALUE</code> indicates that there
357 * is no upper limit on the number of URI segments.
358 *
359 * @return maximum number of URI segments supported by the implementation
360 */
361 public static int getMaxUriSegments() {
362 return Integer.MAX_VALUE;
363 }
364
365 /**
366 * Returns the maximum allowed length of a URI. The value is implementation
367 * specific. The length of the URI is defined as the number of bytes in the
368 * unescaped, UTF-8 encoded represenation of the URI.
369 * <p>
370 * The return value of <code>Integer.MAX_VALUE</code> indicates that there
371 * is no upper limit on the length of URIs.
372 *
373 * @return maximum URI length supported by the implementation
374 */
375 public static int getMaxUriLength() {
376 return Integer.MAX_VALUE;
377 }
378
379 /**
380 * Returns the maximum allowed length of a URI segment. The value is
381 * implementation specific. The length of the URI segment is defined as the
382 * number of bytes in the unescaped, UTF-8 encoded represenation of the
383 * segment.
384 * <p>
385 * The return value of <code>Integer.MAX_VALUE</code> indicates that there
386 * is no upper limit on the length of segment names.
387 *
388 * @return maximum URI segment length supported by the implementation
389 */
390 public static int getMaxSegmentNameLength() {
391 return segmentLengthLimit;
392 }
393
394 /**
395 * Checks whether the specified URI is an absolute URI. An absolute URI
396 * contains the complete path to a node in the DMT starting from the DMT
397 * root (".").
398 *
399 * @param uri the URI to be checked, must not be <code>null</code> and must
400 * contain a valid URI
401 * @return whether the specified URI is absolute
402 * @throws NullPointerException if the specified URI is <code>null</code>
403 * @throws IllegalArgumentException if the specified URI is malformed
404 */
405 public static boolean isAbsoluteUri(String uri) {
406 if( null == uri ) {
407 throw new NullPointerException("'uri' parameter is null.");
408 }
409 if( !isValidUri(uri) )
410 throw new IllegalArgumentException("Malformed URI: " + uri);
411 return uri.equals(".") || uri.equals("\\.") || uri.startsWith("./")
412 || uri.startsWith("\\./");
413 }
414
415 /**
416 * Checks whether the specified URI is valid. A URI is considered valid if
417 * it meets the following constraints:
418 * <ul>
419 * <li>the URI is not <code>null</code>;
420 * <li>the URI follows the syntax defined for valid DMT URIs;
421 * <li>the length of the URI is not more than {@link #getMaxUriLength()};
422 * <li>the URI doesn't contain more than {@link #getMaxUriSegments()}
423 * segments;
424 * <li>the length of each segment of the URI is less than or equal to
425 * {@link #getMaxSegmentNameLength()}.
426 * </ul>
427 * The exact definition of the length of a URI and its segments is
428 * given in the descriptions of the <code>getMaxUriLength()</code> and
429 * <code>getMaxSegmentNameLength()</code> methods.
430 *
431 * @param uri the URI to be validated
432 * @return whether the specified URI is valid
433 */
434 public static boolean isValidUri(String uri) {
435 if (null == uri)
436 return false;
437
438 int paramLen = uri.length();
439 if( paramLen == 0 )
440 return true;
441 if( uri.charAt(0) == '/' || uri.charAt(paramLen-1) == '\\' )
442 return false;
443
444 int processedUriLength = 0;
445 int segmentNumber = 0;
446
447 // append a '/' to indicate the end of the last segment (the URI in the
448 // parameter must not end with a '/')
449 uri += '/';
450 paramLen++;
451
452 int start = 0;
453 for(int i = 1; i < paramLen; i++) { // first character is not a '/'
454 if(uri.charAt(i) == '/' && uri.charAt(i-1) != '\\') {
455 segmentNumber++;
456
457 String segment = uri.substring(start, i);
458 if(segmentNumber > 1 && segment.equals("."))
459 return false; // the URI contains the "." node name at a
460 // position other than the beginning of the URI
461
462 int segmentLength;
463 try {
464 // also checks that the segment is valid
465 segmentLength = getSegmentLength(segment);
466 } catch(IllegalArgumentException e) {
467 return false;
468 }
469
470 if(segmentLength > getMaxSegmentNameLength())
471 return false;
472
473 // the extra byte is for the separator '/' (will be deducted
474 // again for the last segment of the URI)
475 processedUriLength += segmentLength + 1;
476 start = i+1;
477 }
478 }
479
480 processedUriLength--; // remove the '/' added to the end of the URI
481
482 return segmentNumber <= getMaxUriSegments() &&
483 processedUriLength <= getMaxUriLength();
484 }
485
486 // Non-public fields and methods
487
488 // package private method for testing purposes
489 static String mangle(String nodeName, int limit) {
490 if(nodeName == null)
491 throw new NullPointerException(
492 "The 'nodeName' parameter must not be null.");
493
494 if(nodeName.equals(""))
495 throw new IllegalArgumentException(
496 "The 'nodeName' parameter must not be empty.");
497
498 if(nodeName.length() > limit)
499 // create node name hash
500 return getHash(nodeName);
501
502 // escape any '/' and '\' characters in the node name
503 StringBuffer nameBuffer = new StringBuffer(nodeName);
504 for(int i = 0; i < nameBuffer.length(); i++) // 'i' can increase in loop
505 if(nameBuffer.charAt(i) == '\\' || nameBuffer.charAt(i) == '/')
506 nameBuffer.insert(i++, '\\');
507
508 return nameBuffer.toString();
509 }
510
511 private static String getHash(String from) {
512 byte[] byteStream;
513 try {
514 byteStream = from.getBytes("UTF-8");
515 }
516 catch (UnsupportedEncodingException e) {
517 // There's no way UTF-8 encoding is not implemented...
518 throw new IllegalStateException("there's no UTF-8 encoder here!");
519 }
520 byte[] digest = digestMessage(byteStream);
521
522 // very dumb base64 encoder code. There is no need for multiple lines
523 // or trailing '='-s....
524 // also, we hardcoded the fact that sha-1 digests are 20 bytes long
525 StringBuffer sb = new StringBuffer(digest.length*2);
526 for(int i=0;i<6;i++) {
527 int d0 = digest[i*3]&0xff;
528 int d1 = digest[i*3+1]&0xff;
529 int d2 = digest[i*3+2]&0xff;
530 sb.append(BASE_64_TABLE[d0>>2]);
531 sb.append(BASE_64_TABLE[(d0<<4|d1>>4)&63]);
532 sb.append(BASE_64_TABLE[(d1<<2|d2>>6)&63]);
533 sb.append(BASE_64_TABLE[d2&63]);
534 }
535 int d0 = digest[18]&0xff;
536 int d1 = digest[19]&0xff;
537 sb.append(BASE_64_TABLE[d0>>2]);
538 sb.append(BASE_64_TABLE[(d0<<4|d1>>4)&63]);
539 sb.append(BASE_64_TABLE[(d1<<2)&63]);
540
541 return sb.toString();
542 }
543
544 private static byte[] digestMessage(byte[] byteStream) {
545 try {
546 try {
547 return (byte[]) ImplHolder.digestMethod.invoke(null, new Object[] {
548 byteStream});
549 }
550 catch (InvocationTargetException e) {
551 throw e.getTargetException();
552 }
553 }
554 catch (Error e) {
555 throw e;
556 }
557 catch (RuntimeException e) {
558 throw e;
559 }
560 catch (Throwable e) {
561 throw new RuntimeException(e.toString());
562 }
563 }
564}