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