1 /*******************************************************************************
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * which accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
10 * VTT Technical Research Centre of Finland - initial API and implementation
11 *******************************************************************************/
12 /* The following copyright is attached because marked parts of the following code are
13 * copied and modified from Jena 2.4.
16 * (c) Copyright 2001, 2002, 2003, 2004, 2005, 2006 Hewlett-Packard Development Company, LP
17 * All rights reserved.
19 * Redistribution and use in source and binary forms, with or without
20 * modification, are permitted provided that the following conditions
22 * 1. Redistributions of source code must retain the above copyright
23 * notice, this list of conditions and the following disclaimer.
24 * 2. Redistributions in binary form must reproduce the above copyright
25 * notice, this list of conditions and the following disclaimer in the
26 * documentation and/or other materials provided with the distribution.
27 * 3. The name of the author may not be used to endorse or promote products
28 * derived from this software without specific prior written permission.
30 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
31 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
33 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
34 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
35 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
36 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
37 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
38 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
39 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
41 * * Id: URIref.java,v 1.5 2006/03/22 13:52:49 andy_seaborne Exp
43 AUTHOR: Jeremy J. Carroll
46 package org.simantics.databoard.util;
48 import java.util.Arrays;
49 import java.util.List;
53 * Contains utility methods for handling URI Strings in the context of ProCore
54 * and the Simantics platform. This includes URI escaping and unescaping and
55 * namespace/local name separation and joining.
58 * URI's in this context are assumed to be formed as follows:
61 * <namespace part>#<local name part>
65 * The implementation of {@link #escape(String)} and {@link #unescape(String)}
66 * is copied and modified from Jena's com.hp.hpl.jena.util.URIref.
69 * @see <a href="http://en.wikipedia.org/wiki/Percent-encoding">Percent-encoding</a>
71 * @author Tuukka Lehtonen
73 public final class URIStringUtils {
76 * The character '/' is used as a path separator in URI namespace parts in ProCore.
78 public static final char NAMESPACE_PATH_SEPARATOR = '/';
81 * The '#' character is used to separate the local name and namespace parts
82 * of an URI, for example <code>http://www.example.org#localName</code>.
84 public static final char NAMESPACE_LOCAL_SEPARATOR = '#';
87 * Checks that only one separator character ({@link #NAMESPACE_LOCAL_SEPARATOR})
88 * between namespace and localname exists in the specified URI and returns
91 * @param uri the URI to search from
92 * @return the character index of the separator ranging from 0 to uri.length()-1
93 * @throws IllegalArgumentException if no {@link #NAMESPACE_LOCAL_SEPARATOR}
94 * is found in the specified URI
96 private static int assertSingleSeparatorPosition(String uri) {
97 int sharpIndex = uri.indexOf(NAMESPACE_LOCAL_SEPARATOR);
98 if (sharpIndex == -1) {
99 throw new IllegalArgumentException("URI '" + uri + "' does not contain any '" + NAMESPACE_LOCAL_SEPARATOR + "' separator characters");
101 int nextSharpIndex = uri.indexOf(NAMESPACE_LOCAL_SEPARATOR, sharpIndex + 1);
102 if (nextSharpIndex != -1) {
103 throw new IllegalArgumentException("URI '" + uri + "' contains multiple '" + NAMESPACE_LOCAL_SEPARATOR + "' separator characters");
109 * Checks that only one separator character (
110 * {@link #NAMESPACE_LOCAL_SEPARATOR}) between namespace and localname
111 * exists in the specified URI and returns its index. This version does not
112 * throw an exception when the separator is not found.
114 * @param uri the URI to search from
115 * @return the character index of the separator ranging from 0 to
116 * uri.length()-1 or -1 if no separator was found.
118 private static int singleSeparatorPosition(String uri) {
119 int sharpIndex = uri.indexOf(NAMESPACE_LOCAL_SEPARATOR);
120 if (sharpIndex == -1) {
123 int nextSharpIndex = uri.indexOf(NAMESPACE_LOCAL_SEPARATOR, sharpIndex + 1);
124 if (nextSharpIndex != -1) {
131 * Splits the specified URI into a namespace and a local name and returns
135 * Assumes that namespaces are always separated by
136 * {@link #NAMESPACE_LOCAL_SEPARATOR} characters.
139 * @param uri the URI to split, must be non-null
140 * @return the namespace part of the specified URI
141 * @throws IllegalArgumentException for URIs without a
142 * {@link #NAMESPACE_LOCAL_SEPARATOR}
143 * @throws NullPointerException for <code>null</code> URIs
145 public static String getNamespace(String uri) {
147 throw new NullPointerException("null uri");
148 int separatorIndex = assertSingleSeparatorPosition(uri);
149 return uri.substring(0, separatorIndex);
152 public static String getRVIParent(String uri) {
153 int childSeparator = uri.lastIndexOf(URIStringUtils.NAMESPACE_PATH_SEPARATOR);
154 int propertySeparator = uri.lastIndexOf(URIStringUtils.NAMESPACE_LOCAL_SEPARATOR);
155 int separator = Math.max(childSeparator, propertySeparator);
156 return uri.substring(0, separator);
161 * Splits the specified URI into a namespace and a local name and returns
165 * Assumes that namespaces are always separated by
166 * {@link #NAMESPACE_LOCAL_SEPARATOR} characters.
169 * @param uri the URI to split, must be non-null
170 * @return the local name part of the specified URI
171 * @throws IllegalArgumentException for URIs without a
172 * {@link #NAMESPACE_LOCAL_SEPARATOR}
173 * @throws NullPointerException for <code>null</code> URIs
175 public static String getLocalName(String uri) {
177 throw new NullPointerException("null uri");
178 int separatorIndex = assertSingleSeparatorPosition(uri);
179 return uri.substring(separatorIndex + 1);
182 public static String escapeName(String name) {
183 char[] chars = name.toCharArray();
184 boolean modified = false;
185 for(int i=0;i<chars.length;++i)
186 if(!Character.isJavaIdentifierPart(chars[i])) {
191 return new String(chars);
196 final private static String HTTP_PREFIX = "http://";
197 final private static int HTTP_POSITION = HTTP_PREFIX.length();
199 public static String[] splitURI(String uri) {
200 int nextPathSeparator = uri.lastIndexOf(URIStringUtils.NAMESPACE_PATH_SEPARATOR);
201 if (nextPathSeparator == -1) return null;
202 if (nextPathSeparator == HTTP_POSITION - 1) {
203 if(uri.startsWith(HTTP_PREFIX)) return new String[] { HTTP_PREFIX, uri.substring(HTTP_POSITION, uri.length()) };
206 return new String[] {
207 uri.substring(0, nextPathSeparator),
208 uri.substring(nextPathSeparator + 1, uri.length())
212 public static List<String> splitURISCL(String uri) {
213 String[] result = splitURI(uri);
214 return Arrays.asList(result);
218 * Splits the specified URI into a namespace and a local name and returns
219 * them both separately as an array.
221 * @param uri the URI to split, must be non-null
222 * @return [0] = namespace, [1] = local name or <code>null</code> if the URI
224 * @throws NullPointerException for <code>null</code> URIs
226 public static String[] trySplitNamespaceAndLocalName(String uri) {
228 throw new NullPointerException("null uri");
229 int separatorIndex = singleSeparatorPosition(uri);
230 return separatorIndex == -1 ?
232 : new String[] { uri.substring(0, separatorIndex), uri.substring(separatorIndex + 1) };
236 * Splits the specified URI into a namespace and a local name and returns
237 * them both separately as an array.
239 * @param uri the URI to split, must be non-null
240 * @return [0] = namespace, [1] = local name
241 * @throws IllegalArgumentException for URIs without a
242 * {@link #NAMESPACE_LOCAL_SEPARATOR}
243 * @throws NullPointerException for <code>null</code> URIs
245 public static String[] splitNamespaceAndLocalName(String uri) {
247 throw new NullPointerException("null uri");
248 int separatorIndex = assertSingleSeparatorPosition(uri);
249 return new String[] { uri.substring(0, separatorIndex), uri.substring(separatorIndex + 1) };
253 * Converts a unicode string into an RFC 2396 compliant URI, using %NN
254 * escapes where appropriate, including the
255 * {@link #NAMESPACE_PATH_SEPARATOR} character.
257 * @param localName the string to escape
258 * @return the escaped string
259 * @throws NullPointerException for <code>null</code> URIs
261 public static String escapeURI(String localName) {
262 if (localName == null)
263 throw new NullPointerException("null local name");
264 return encode(localName);
268 * Add a suffix path to a namespace string, i.e. join the strings to
269 * together with the {@link #NAMESPACE_PATH_SEPARATOR} character in between.
271 * @param namespace the namespace to append to
272 * @param suffix the suffix to append
273 * @return the joined namespace
275 public static String appendURINamespace(String namespace, String suffix) {
276 return new StringBuilder(namespace.length() + 1 + suffix.length())
278 .append(NAMESPACE_PATH_SEPARATOR)
284 * Join a namespace and a localname to form an URI with
285 * {@link #NAMESPACE_LOCAL_SEPARATOR}.
287 * @param namespace the namespace part to join
288 * @param localName the localname part to join
289 * @return the joined URI
291 public static String makeURI(String namespace, String localName) {
292 String escapedLocalName = escapeURI(localName);
293 return new StringBuilder(namespace.length() + 1 + escapedLocalName.length())
295 .append(NAMESPACE_LOCAL_SEPARATOR)
296 .append(escapedLocalName)
301 * Convert a Unicode string, first to UTF-8 and then to an RFC 2396
302 * compliant URI with optional fragment identifier using %NN escape
303 * mechanism as appropriate. The '%' character is assumed to already
304 * indicated an escape byte. The '%' character must be followed by two
305 * hexadecimal digits.
308 * Meant to be used for encoding URI local name parts if it is desired to
309 * have '/' characters in the local name without creating a new namespace.
310 * For example these two URI's:<br/>
313 * http://foo.bar.com/foo/bar/org%2Fcom<br/>
314 * http://foo.bar.com/foo/bar/net%2Fcom<br/>
317 * have the same namespace <code>http://foo.bar.com/foo/bar/</code> and
318 * different local names <code>org%2Fcom</code> and <code>net%2Fcom</code>
319 * or <code>org/com</code> and <code>net/com</code> in unescaped form.
322 * @param unicode The uri, in characters specified by RFC 2396 + '#'
323 * @return The corresponding Unicode String
325 public static String escape(String unicode) {
326 return encode(unicode);
331 * RFC 3986 section 2.2 Reserved Characters (January 2005)
334 private static boolean[] ESCAPED_US_ASCII_CHARS = new boolean[128];
337 ESCAPED_US_ASCII_CHARS[' '] = true;
338 // IMPORTANT NOTE: every time escape is invoked, all input needs to be escaped,
339 // i.e. escape("%01") should result in "%2501", not "%01".
340 // escape and unescape form a bijection, where neither
341 // of them is an idempotent operation.
342 ESCAPED_US_ASCII_CHARS['%'] = true;
343 // '#' and '/' are URL segment/fragment delimiters, need to be escaped in names.
344 ESCAPED_US_ASCII_CHARS['#'] = true;
345 ESCAPED_US_ASCII_CHARS['/'] = true;
346 // Escape '&' characters to avoid them being interpreted as SGML entities.
347 ESCAPED_US_ASCII_CHARS['&'] = true;
350 private static int needsEscaping(String unicode) {
351 int len = unicode.length();
353 for (int i = 0; i < len; ++i) {
354 char ch = unicode.charAt(i);
355 if (ch < 128 && ESCAPED_US_ASCII_CHARS[ch])
361 private static String encode(String unicode) {
362 int needsEscapes = needsEscaping(unicode);
363 if (needsEscapes == 0)
366 int len = unicode.length();
367 char result[] = new char[(len - needsEscapes) + needsEscapes * 3];
371 char inCh = unicode.charAt(in++);
372 if (inCh >= 128 || !ESCAPED_US_ASCII_CHARS[inCh]) {
373 result[out++] = inCh;
375 // Only selected 7-bit US-ASCII characters are escaped
378 result[out++] = (char) hexEncode(c / 16);
379 result[out++] = (char) hexEncode(c % 16);
382 return new String(result, 0, out);
385 private static boolean needsUnescaping(String unicode) {
386 return unicode.indexOf('%') > -1;
390 * Convert a URI, in UTF-16 with escaped characters taken from US-ASCII, to
391 * the corresponding unescaped Unicode string. On ill-formed input the results are
394 * @param uri the uri, in characters specified by RFC 2396 + '#'.
395 * @return the corresponding unescaped Unicode String.
396 * @exception IllegalArgumentException if a % hex sequence is ill-formed.
398 public static String unescape(String uri) {
400 if (!needsUnescaping(uri))
403 int len = uri.length();
404 String unicode = uri;
405 char result[] = new char[len];
409 char inCh = unicode.charAt(in++);
411 char d1 = unicode.charAt(in);
412 char d2 = unicode.charAt(in+1);
413 if (d1 > 127 || d2 > 127)
414 throw new IllegalArgumentException("Invalid hex digit escape sequence in " + uri + " at " + in);
415 result[out++] = (char) (hexDecode((byte) d1) * 16 | hexDecode((byte) d2));
418 result[out++] = inCh;
421 return new String(result, 0, out);
422 } catch (IllegalArgumentException e) {
423 throw new IllegalArgumentException("Problem while unescaping string: " + uri, e);
424 } catch (IndexOutOfBoundsException ee) {
425 throw new IllegalArgumentException("Incomplete hex digit escape sequence in " + uri);
429 /* Copied from Jena 2.4 com.hp.hpl.jena.util.URIref */
430 private static byte hexEncode(int i) {
432 return (byte) ('0' + i);
434 return (byte)('A' + i - 10);
437 /* Copied from Jena 2.4 com.hp.hpl.jena.util.URIref */
438 private static int hexDecode(byte b) {
440 case (byte)'a': case (byte)'b': case (byte)'c': case (byte)'d': case (byte)'e': case (byte)'f':
441 return ((b) & 255) - 'a' + 10;
442 case (byte)'A': case (byte)'B': case (byte)'C': case (byte)'D': case (byte)'E': case (byte)'F':
443 return b - (byte) 'A' + 10;
444 case (byte)'0': case (byte)'1': case (byte)'2': case (byte)'3': case (byte)'4': case (byte)'5': case (byte)'6': case (byte)'7': case (byte)'8': case (byte)'9':
445 return b - (byte) '0';
447 throw new IllegalArgumentException("Bad Hex escape character: " + ((b)&255) );
455 public static void main(String[] args) {
456 String s = makeURI("http://foo.bar.com/foo/bar", "baz/guuk/org%2Fnet");
457 System.out.println("escapeURI: " + s);
458 System.out.println("getNamespace: " + getNamespace(s));
459 System.out.println("getLocalName: " + getLocalName(s));
461 System.out.println("escapeURI: " + escapeURI("foo/bar/org%2Fnet"));
462 System.out.println("escapeURI('...#...'): " + escapeURI("foo/bar#org%2Fnet"));
464 testEscape("/", "%2F");
465 testEscape("#", "%23");
466 testEscape("%", "%25");
467 testEscape("%01", "%2501");
468 testEscape("%GG", "%25GG");
469 testEscape("säätö venttiili", "säätö%20venttiili");
470 testEscape("säätö", "säätö");
471 testEscape("Something / Else", "Something%20%2F%20Else");
472 testEscape("http://www.vtt.fi%2FSome- %25 Namespace/Something", "http:%2F%2Fwww.vtt.fi%252FSome-%20%2525%20Namespace%2FSomething");
473 testEscape("http://www.vtt.fi/PSK", "http:%2F%2Fwww.vtt.fi%2FPSK");
474 testEscape("http://www.vtt.fi%2FSome-Namespace/Something / Else", "http:%2F%2Fwww.vtt.fi%252FSome-Namespace%2FSomething%20%2F%20Else");
477 private static void testEscape(String unescaped, String expectedEscaped) {
478 String esc = escape(unescaped);
479 String unesc = unescape(esc);
480 System.out.format("escape('%s') -> '%s', unescape('%s') -> '%s'", unescaped, esc, esc, unesc);
481 if (!esc.equals(expectedEscaped))
482 throw new AssertionError("escape('" + unescaped + "') was expected to return '" + expectedEscaped + "' but returned '" + esc + "'");
483 if (!unesc.equals(unescaped))
484 throw new AssertionError("unescape(escape('" + unescaped + "'))=unescape(" + esc + ") was expected to return '" + unescaped + "' but returned '" + unesc + "'");
485 System.out.println(" OK");