]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.databoard/src/org/simantics/databoard/util/URIStringUtils.java
Merge commit 'b809a171b6dfb81ed9ef9e84870dcbcbc5912f0e'
[simantics/platform.git] / bundles / org.simantics.databoard / src / org / simantics / databoard / util / URIStringUtils.java
1 /*******************************************************************************\r
2  * Copyright (c) 2007, 2010 Association for Decentralized Information Management\r
3  * in Industry THTH ry.\r
4  * All rights reserved. This program and the accompanying materials\r
5  * are made available under the terms of the Eclipse Public License v1.0\r
6  * which accompanies this distribution, and is available at\r
7  * http://www.eclipse.org/legal/epl-v10.html\r
8  *\r
9  * Contributors:\r
10  *     VTT Technical Research Centre of Finland - initial API and implementation\r
11  *******************************************************************************/\r
12 /* The following copyright is attached because marked parts of the following code are\r
13  * copied and modified from Jena 2.4.\r
14  */\r
15 /*\r
16  *  (c) Copyright 2001, 2002, 2003, 2004, 2005, 2006 Hewlett-Packard Development Company, LP\r
17  *  All rights reserved.\r
18  *\r
19  * Redistribution and use in source and binary forms, with or without\r
20  * modification, are permitted provided that the following conditions\r
21  * are met:\r
22  * 1. Redistributions of source code must retain the above copyright\r
23  *    notice, this list of conditions and the following disclaimer.\r
24  * 2. Redistributions in binary form must reproduce the above copyright\r
25  *    notice, this list of conditions and the following disclaimer in the\r
26  *    documentation and/or other materials provided with the distribution.\r
27  * 3. The name of the author may not be used to endorse or promote products\r
28  *    derived from this software without specific prior written permission.\r
29 \r
30  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\r
31  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\r
32  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\r
33  * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\r
34  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\r
35  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\r
36  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\r
37  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\r
38  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\r
39  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r
40 \r
41  * * Id: URIref.java,v 1.5 2006/03/22 13:52:49 andy_seaborne Exp\r
42 \r
43    AUTHOR:  Jeremy J. Carroll\r
44  */\r
45 \r
46 package org.simantics.databoard.util;\r
47 \r
48 import java.util.Arrays;\r
49 import java.util.List;\r
50 \r
51 \r
52 /**\r
53  * Contains utility methods for handling URI Strings in the context of ProCore\r
54  * and the Simantics platform. This includes URI escaping and unescaping and\r
55  * namespace/local name separation and joining.\r
56  * \r
57  * <p>\r
58  * URI's in this context are assumed to be formed as follows:\r
59  * \r
60  * <pre>\r
61  * &lt;namespace part&gt;#&lt;local name part&gt;\r
62  * </pre>\r
63  * \r
64  * <p>\r
65  * The implementation of {@link #escape(String)} and {@link #unescape(String)}\r
66  * is copied and modified from Jena's com.hp.hpl.jena.util.URIref.\r
67  * </p>\r
68  * \r
69  * @see <a href="http://en.wikipedia.org/wiki/Percent-encoding">Percent-encoding</a>\r
70  * \r
71  * @author Tuukka Lehtonen\r
72  */\r
73 public final class URIStringUtils {\r
74 \r
75     /**\r
76      * The character '/' is used as a path separator in URI namespace parts in ProCore.\r
77      */\r
78     public static final char NAMESPACE_PATH_SEPARATOR  = '/';\r
79 \r
80     /**\r
81      * The '#' character is used to separate the local name and namespace parts\r
82      * of an URI, for example <code>http://www.example.org#localName</code>.\r
83      */\r
84     public static final char NAMESPACE_LOCAL_SEPARATOR = '#';\r
85 \r
86     /**\r
87      * Checks that only one separator character ({@link #NAMESPACE_LOCAL_SEPARATOR})\r
88      * between namespace and localname exists in the specified URI and returns\r
89      * its index.\r
90      * \r
91      * @param uri the URI to search from\r
92      * @return the character index of the separator ranging from 0 to uri.length()-1\r
93      * @throws IllegalArgumentException if no {@link #NAMESPACE_LOCAL_SEPARATOR}\r
94      *         is found in the specified URI\r
95      */\r
96     private static int assertSingleSeparatorPosition(String uri) {\r
97         int sharpIndex = uri.indexOf(NAMESPACE_LOCAL_SEPARATOR);\r
98         if (sharpIndex == -1) {\r
99             throw new IllegalArgumentException("URI '" + uri + "' does not contain any '" + NAMESPACE_LOCAL_SEPARATOR + "' separator characters");\r
100         }\r
101         int nextSharpIndex = uri.indexOf(NAMESPACE_LOCAL_SEPARATOR, sharpIndex + 1);\r
102         if (nextSharpIndex != -1) {\r
103             throw new IllegalArgumentException("URI '" + uri + "' contains multiple '" + NAMESPACE_LOCAL_SEPARATOR + "' separator characters");\r
104         }\r
105         return sharpIndex;\r
106     }\r
107 \r
108     /**\r
109      * Checks that only one separator character (\r
110      * {@link #NAMESPACE_LOCAL_SEPARATOR}) between namespace and localname\r
111      * exists in the specified URI and returns its index. This version does not\r
112      * throw an exception when the separator is not found.\r
113      * \r
114      * @param uri the URI to search from\r
115      * @return the character index of the separator ranging from 0 to\r
116      *         uri.length()-1 or -1 if no separator was found.\r
117      */\r
118     private static int singleSeparatorPosition(String uri) {\r
119         int sharpIndex = uri.indexOf(NAMESPACE_LOCAL_SEPARATOR);\r
120         if (sharpIndex == -1) {\r
121             return -1;\r
122         }\r
123         int nextSharpIndex = uri.indexOf(NAMESPACE_LOCAL_SEPARATOR, sharpIndex + 1);\r
124         if (nextSharpIndex != -1) {\r
125             return -1;\r
126         }\r
127         return sharpIndex;\r
128     }\r
129 \r
130     /**\r
131      * Splits the specified URI into a namespace and a local name and returns\r
132      * the namespace.\r
133      * \r
134      * <p>\r
135      * Assumes that namespaces are always separated by\r
136      * {@link #NAMESPACE_LOCAL_SEPARATOR} characters.\r
137      * </p>\r
138      * \r
139      * @param uri the URI to split, must be non-null\r
140      * @return the namespace part of the specified URI\r
141      * @throws IllegalArgumentException for URIs without a\r
142      *         {@link #NAMESPACE_LOCAL_SEPARATOR}\r
143      * @throws NullPointerException for <code>null</code> URIs\r
144      */\r
145     public static String getNamespace(String uri) {\r
146         if (uri == null)\r
147             throw new NullPointerException("null uri");\r
148         int separatorIndex = assertSingleSeparatorPosition(uri);\r
149         return uri.substring(0, separatorIndex);\r
150     }\r
151     \r
152     public static String getRVIParent(String uri) {\r
153         int childSeparator = uri.lastIndexOf(URIStringUtils.NAMESPACE_PATH_SEPARATOR);\r
154         int propertySeparator = uri.lastIndexOf(URIStringUtils.NAMESPACE_LOCAL_SEPARATOR);\r
155         int separator = Math.max(childSeparator, propertySeparator);\r
156         return uri.substring(0, separator);\r
157     }\r
158     \r
159 \r
160     /**\r
161      * Splits the specified URI into a namespace and a local name and returns\r
162      * the local name.\r
163      * \r
164      * <p>\r
165      * Assumes that namespaces are always separated by\r
166      * {@link #NAMESPACE_LOCAL_SEPARATOR} characters.\r
167      * </p>\r
168      * \r
169      * @param uri the URI to split, must be non-null\r
170      * @return the local name part of the specified URI\r
171      * @throws IllegalArgumentException for URIs without a\r
172      *         {@link #NAMESPACE_LOCAL_SEPARATOR}\r
173      * @throws NullPointerException for <code>null</code> URIs\r
174      */\r
175     public static String getLocalName(String uri) {\r
176         if (uri == null)\r
177             throw new NullPointerException("null uri");\r
178         int separatorIndex = assertSingleSeparatorPosition(uri);\r
179         return uri.substring(separatorIndex + 1);\r
180     }\r
181 \r
182     public static String escapeName(String name) {\r
183         char[] chars = name.toCharArray();\r
184         boolean modified = false;\r
185         for(int i=0;i<chars.length;++i)\r
186             if(!Character.isJavaIdentifierPart(chars[i])) {\r
187                 chars[i] = '_';\r
188                 modified = true;\r
189             }\r
190         if(modified)\r
191             return new String(chars);\r
192         else\r
193             return name;\r
194     }\r
195 \r
196     final private static String HTTP_PREFIX = "http://";\r
197     final private static int HTTP_POSITION = HTTP_PREFIX.length();\r
198 \r
199     public static String[] splitURI(String uri) {\r
200         int nextPathSeparator = uri.lastIndexOf(URIStringUtils.NAMESPACE_PATH_SEPARATOR);\r
201         if (nextPathSeparator == -1) return null;\r
202         if (nextPathSeparator == HTTP_POSITION - 1) {\r
203             if(uri.startsWith(HTTP_PREFIX)) return new String[] { HTTP_PREFIX, uri.substring(HTTP_POSITION, uri.length()) };\r
204             else return null;\r
205         }\r
206         return new String[] {\r
207                 uri.substring(0, nextPathSeparator),\r
208                 uri.substring(nextPathSeparator + 1, uri.length())\r
209         };\r
210     }\r
211 \r
212     public static List<String> splitURISCL(String uri) {\r
213         String[] result = splitURI(uri);\r
214         return Arrays.asList(result);\r
215     }\r
216 \r
217     /**\r
218      * Splits the specified URI into a namespace and a local name and returns\r
219      * them both separately as an array.\r
220      * \r
221      * @param uri the URI to split, must be non-null\r
222      * @return [0] = namespace, [1] = local name or <code>null</code> if the URI\r
223      *         cannot be split.\r
224      * @throws NullPointerException for <code>null</code> URIs\r
225      */\r
226     public static String[] trySplitNamespaceAndLocalName(String uri) {\r
227         if (uri == null)\r
228             throw new NullPointerException("null uri");\r
229         int separatorIndex = singleSeparatorPosition(uri);\r
230         return separatorIndex == -1 ?\r
231                 null\r
232                 : new String[] { uri.substring(0, separatorIndex), uri.substring(separatorIndex + 1) };\r
233     }\r
234 \r
235     /**\r
236      * Splits the specified URI into a namespace and a local name and returns\r
237      * them both separately as an array.\r
238      * \r
239      * @param uri the URI to split, must be non-null\r
240      * @return [0] = namespace, [1] = local name\r
241      * @throws IllegalArgumentException for URIs without a\r
242      *         {@link #NAMESPACE_LOCAL_SEPARATOR}\r
243      * @throws NullPointerException for <code>null</code> URIs\r
244      */\r
245     public static String[] splitNamespaceAndLocalName(String uri) {\r
246         if (uri == null)\r
247             throw new NullPointerException("null uri");\r
248         int separatorIndex = assertSingleSeparatorPosition(uri);\r
249         return new String[] { uri.substring(0, separatorIndex), uri.substring(separatorIndex + 1) };\r
250     }\r
251 \r
252     /**\r
253      * Converts a unicode string into an RFC 2396 compliant URI, using %NN\r
254      * escapes where appropriate, including the\r
255      * {@link #NAMESPACE_PATH_SEPARATOR} character.\r
256      * \r
257      * @param localName the string to escape\r
258      * @return the escaped string\r
259      * @throws NullPointerException for <code>null</code> URIs\r
260      */\r
261     public static String escapeURI(String localName) {\r
262         if (localName == null)\r
263             throw new NullPointerException("null local name");\r
264         return encode(localName);\r
265     }\r
266 \r
267     /**\r
268      * Add a suffix path to a namespace string, i.e. join the strings to\r
269      * together with the {@link #NAMESPACE_PATH_SEPARATOR} character in between.\r
270      * \r
271      * @param namespace the namespace to append to\r
272      * @param suffix the suffix to append\r
273      * @return the joined namespace\r
274      */\r
275     public static String appendURINamespace(String namespace, String suffix) {\r
276         return new StringBuilder(namespace.length() + 1 + suffix.length())\r
277         .append(namespace)\r
278         .append(NAMESPACE_PATH_SEPARATOR)\r
279         .append(suffix)\r
280         .toString();\r
281     }\r
282 \r
283     /**\r
284      * Join a namespace and a localname to form an URI with\r
285      * {@link #NAMESPACE_LOCAL_SEPARATOR}.\r
286      * \r
287      * @param namespace the namespace part to join\r
288      * @param localName the localname part to join\r
289      * @return the joined URI\r
290      */\r
291     public static String makeURI(String namespace, String localName) {\r
292         String escapedLocalName = escapeURI(localName);\r
293         return new StringBuilder(namespace.length() + 1 + escapedLocalName.length())\r
294         .append(namespace)\r
295         .append(NAMESPACE_LOCAL_SEPARATOR)\r
296         .append(escapedLocalName)\r
297         .toString();\r
298     }\r
299 \r
300     /**\r
301      * Convert a Unicode string, first to UTF-8 and then to an RFC 2396\r
302      * compliant URI with optional fragment identifier using %NN escape\r
303      * mechanism as appropriate. The '%' character is assumed to already\r
304      * indicated an escape byte. The '%' character must be followed by two\r
305      * hexadecimal digits.\r
306      * \r
307      * <p>\r
308      * Meant to be used for encoding URI local name parts if it is desired to\r
309      * have '/' characters in the local name without creating a new namespace.\r
310      * For example these two URI's:<br/>\r
311      * \r
312      * <code>\r
313      * http://foo.bar.com/foo/bar/org%2Fcom<br/>\r
314      * http://foo.bar.com/foo/bar/net%2Fcom<br/>\r
315      * </code>\r
316      * \r
317      * have the same namespace <code>http://foo.bar.com/foo/bar/</code> and\r
318      * different local names <code>org%2Fcom</code> and <code>net%2Fcom</code>\r
319      * or <code>org/com</code> and <code>net/com</code> in unescaped form.\r
320      * </p>\r
321      * \r
322      * @param unicode The uri, in characters specified by RFC 2396 + '#'\r
323      * @return The corresponding Unicode String\r
324      */\r
325     public static String escape(String unicode) {\r
326         return encode(unicode);\r
327     }\r
328 \r
329 \r
330     /*\r
331      * RFC 3986 section 2.2 Reserved Characters (January 2005)\r
332      * !*'();:@&=+$,/?#[]\r
333      */\r
334     private static boolean[] ESCAPED_US_ASCII_CHARS = new boolean[128];\r
335 \r
336     static {\r
337         ESCAPED_US_ASCII_CHARS[' '] = true;\r
338         // IMPORTANT NOTE: every time escape is invoked, all input needs to be escaped,\r
339         // i.e. escape("%01") should result in "%2501", not "%01".\r
340         // escape and unescape form a bijection, where neither\r
341         // of them is an idempotent operation. \r
342         ESCAPED_US_ASCII_CHARS['%'] = true;\r
343         // '#' and '/' are URL segment/fragment delimiters, need to be escaped in names.\r
344         ESCAPED_US_ASCII_CHARS['#'] = true;\r
345         ESCAPED_US_ASCII_CHARS['/'] = true;\r
346         // Escape '&' characters to avoid them being interpreted as SGML entities.\r
347         ESCAPED_US_ASCII_CHARS['&'] = true;\r
348     }\r
349 \r
350     private static int needsEscaping(String unicode) {\r
351         int len = unicode.length();\r
352         int escapeCount = 0;\r
353         for (int i = 0; i < len; ++i) {\r
354             char ch = unicode.charAt(i);\r
355             if (ch < 128 && ESCAPED_US_ASCII_CHARS[ch])\r
356                 ++escapeCount;\r
357         }\r
358         return escapeCount;\r
359     }\r
360 \r
361     private static String encode(String unicode) {\r
362         int needsEscapes = needsEscaping(unicode);\r
363         if (needsEscapes == 0)\r
364             return unicode;\r
365 \r
366         int len = unicode.length();\r
367         char result[] = new char[(len - needsEscapes) + needsEscapes * 3];\r
368         int in = 0;\r
369         int out = 0;\r
370         while (in < len) {\r
371             char inCh = unicode.charAt(in++);\r
372             if (inCh >= 128 || !ESCAPED_US_ASCII_CHARS[inCh]) {\r
373                 result[out++] = inCh;\r
374             } else {\r
375                 // Only selected 7-bit US-ASCII characters are escaped\r
376                 int c = inCh & 255;\r
377                 result[out++] = '%';\r
378                 result[out++] = (char) hexEncode(c / 16);\r
379                 result[out++] = (char) hexEncode(c % 16);\r
380             }\r
381         }\r
382         return new String(result, 0, out);\r
383     }\r
384 \r
385     private static boolean needsUnescaping(String unicode) {\r
386         return unicode.indexOf('%') > -1;\r
387     }\r
388 \r
389     /**\r
390      * Convert a URI, in UTF-16 with escaped characters taken from US-ASCII, to\r
391      * the corresponding unescaped Unicode string. On ill-formed input the results are\r
392      * undefined.\r
393      * \r
394      * @param uri the uri, in characters specified by RFC 2396 + '#'.\r
395      * @return the corresponding unescaped Unicode String.\r
396      * @exception IllegalArgumentException if a % hex sequence is ill-formed.\r
397      */\r
398     public static String unescape(String uri) {\r
399         try {\r
400             if (!needsUnescaping(uri))\r
401                 return uri;\r
402 \r
403             int len = uri.length();\r
404             String unicode = uri;\r
405             char result[] = new char[len];\r
406             int in = 0;\r
407             int out = 0;\r
408             while (in < len) {\r
409                 char inCh = unicode.charAt(in++);\r
410                 if (inCh == '%') {\r
411                     char d1 = unicode.charAt(in);\r
412                     char d2 = unicode.charAt(in+1);\r
413                     if (d1 > 127 || d2 > 127)\r
414                         throw new IllegalArgumentException("Invalid hex digit escape sequence in " + uri + " at " + in);\r
415                     result[out++] = (char) (hexDecode((byte) d1) * 16 | hexDecode((byte) d2));\r
416                     in += 2;\r
417                 } else {\r
418                     result[out++] = inCh;\r
419                 }\r
420             }\r
421             return new String(result, 0, out);\r
422         } catch (IllegalArgumentException e) {\r
423             throw new IllegalArgumentException("Problem while unescaping string: " + uri, e);\r
424         } catch (IndexOutOfBoundsException ee) {\r
425             throw new IllegalArgumentException("Incomplete hex digit escape sequence in " + uri);\r
426         }\r
427     }\r
428 \r
429     /* Copied from Jena 2.4 com.hp.hpl.jena.util.URIref */\r
430     private static byte hexEncode(int i) {\r
431         if (i < 10)\r
432             return (byte) ('0' + i);\r
433         else\r
434             return (byte)('A' + i - 10);\r
435     }\r
436 \r
437     /* Copied from Jena 2.4 com.hp.hpl.jena.util.URIref */\r
438     private static int hexDecode(byte b) {\r
439         switch (b) {\r
440             case (byte)'a': case (byte)'b': case (byte)'c': case (byte)'d': case (byte)'e': case (byte)'f':\r
441                 return ((b) & 255) - 'a' + 10;\r
442             case (byte)'A': case (byte)'B': case (byte)'C': case (byte)'D': case (byte)'E': case (byte)'F':\r
443                 return b - (byte) 'A' + 10;\r
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':\r
445                 return b - (byte) '0';\r
446             default:\r
447                 throw new IllegalArgumentException("Bad Hex escape character: " + ((b)&255) );\r
448         }\r
449     }\r
450 \r
451     /**\r
452      * Some simple tests.\r
453      * @param args\r
454      */\r
455     public static void main(String[] args) {\r
456         String s = makeURI("http://foo.bar.com/foo/bar", "baz/guuk/org%2Fnet");\r
457         System.out.println("escapeURI: " + s);\r
458         System.out.println("getNamespace: " + getNamespace(s));\r
459         System.out.println("getLocalName: " + getLocalName(s));\r
460 \r
461         System.out.println("escapeURI: " + escapeURI("foo/bar/org%2Fnet"));\r
462         System.out.println("escapeURI('...#...'): " + escapeURI("foo/bar#org%2Fnet"));\r
463 \r
464         testEscape("/", "%2F");\r
465         testEscape("#", "%23");\r
466         testEscape("%", "%25");\r
467         testEscape("%01", "%2501");\r
468         testEscape("%GG", "%25GG");\r
469         testEscape("säätö venttiili", "säätö%20venttiili");\r
470         testEscape("säätö", "säätö");\r
471         testEscape("Something / Else", "Something%20%2F%20Else");\r
472         testEscape("http://www.vtt.fi%2FSome- %25 Namespace/Something", "http:%2F%2Fwww.vtt.fi%252FSome-%20%2525%20Namespace%2FSomething");\r
473         testEscape("http://www.vtt.fi/PSK", "http:%2F%2Fwww.vtt.fi%2FPSK");\r
474         testEscape("http://www.vtt.fi%2FSome-Namespace/Something / Else", "http:%2F%2Fwww.vtt.fi%252FSome-Namespace%2FSomething%20%2F%20Else");\r
475     }\r
476 \r
477     private static void testEscape(String unescaped, String expectedEscaped) {\r
478         String esc = escape(unescaped);\r
479         String unesc = unescape(esc);\r
480         System.out.format("escape('%s') -> '%s', unescape('%s') -> '%s'", unescaped, esc, esc, unesc);\r
481         if (!esc.equals(expectedEscaped))\r
482             throw new AssertionError("escape('" + unescaped + "') was expected to return '" + expectedEscaped + "' but returned '" + esc + "'");\r
483         if (!unesc.equals(unescaped))\r
484             throw new AssertionError("unescape(escape('" + unescaped + "'))=unescape(" + esc + ") was expected to return '" + unescaped + "' but returned '" + unesc + "'");\r
485         System.out.println(" OK");\r
486     }\r
487 \r
488 }\r