]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.databoard/src/org/simantics/databoard/util/URIStringUtils.java
Fixed multiple issues causing dangling references to discarded queries
[simantics/platform.git] / bundles / org.simantics.databoard / src / org / simantics / databoard / util / URIStringUtils.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2010 Association for Decentralized Information Management
3  * in Industry THTH ry.
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
8  *
9  * Contributors:
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.
14  */
15 /*
16  *  (c) Copyright 2001, 2002, 2003, 2004, 2005, 2006 Hewlett-Packard Development Company, LP
17  *  All rights reserved.
18  *
19  * Redistribution and use in source and binary forms, with or without
20  * modification, are permitted provided that the following conditions
21  * are met:
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.
29
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.
40
41  * * Id: URIref.java,v 1.5 2006/03/22 13:52:49 andy_seaborne Exp
42
43    AUTHOR:  Jeremy J. Carroll
44  */
45
46 package org.simantics.databoard.util;
47
48 import java.util.Arrays;
49 import java.util.List;
50
51
52 /**
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.
56  * 
57  * <p>
58  * URI's in this context are assumed to be formed as follows:
59  * 
60  * <pre>
61  * &lt;namespace part&gt;#&lt;local name part&gt;
62  * </pre>
63  * 
64  * <p>
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.
67  * </p>
68  * 
69  * @see <a href="http://en.wikipedia.org/wiki/Percent-encoding">Percent-encoding</a>
70  * 
71  * @author Tuukka Lehtonen
72  */
73 public final class URIStringUtils {
74
75     /**
76      * The character '/' is used as a path separator in URI namespace parts in ProCore.
77      */
78     public static final char NAMESPACE_PATH_SEPARATOR  = '/';
79
80     /**
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>.
83      */
84     public static final char NAMESPACE_LOCAL_SEPARATOR = '#';
85
86     /**
87      * Checks that only one separator character ({@link #NAMESPACE_LOCAL_SEPARATOR})
88      * between namespace and localname exists in the specified URI and returns
89      * its index.
90      * 
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
95      */
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");
100         }
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");
104         }
105         return sharpIndex;
106     }
107
108     /**
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.
113      * 
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.
117      */
118     private static int singleSeparatorPosition(String uri) {
119         int sharpIndex = uri.indexOf(NAMESPACE_LOCAL_SEPARATOR);
120         if (sharpIndex == -1) {
121             return -1;
122         }
123         int nextSharpIndex = uri.indexOf(NAMESPACE_LOCAL_SEPARATOR, sharpIndex + 1);
124         if (nextSharpIndex != -1) {
125             return -1;
126         }
127         return sharpIndex;
128     }
129
130     /**
131      * Splits the specified URI into a namespace and a local name and returns
132      * the namespace.
133      * 
134      * <p>
135      * Assumes that namespaces are always separated by
136      * {@link #NAMESPACE_LOCAL_SEPARATOR} characters.
137      * </p>
138      * 
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
144      */
145     public static String getNamespace(String uri) {
146         if (uri == null)
147             throw new NullPointerException("null uri");
148         int separatorIndex = assertSingleSeparatorPosition(uri);
149         return uri.substring(0, separatorIndex);
150     }
151     
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);
157     }
158     
159
160     /**
161      * Splits the specified URI into a namespace and a local name and returns
162      * the local name.
163      * 
164      * <p>
165      * Assumes that namespaces are always separated by
166      * {@link #NAMESPACE_LOCAL_SEPARATOR} characters.
167      * </p>
168      * 
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
174      */
175     public static String getLocalName(String uri) {
176         if (uri == null)
177             throw new NullPointerException("null uri");
178         int separatorIndex = assertSingleSeparatorPosition(uri);
179         return uri.substring(separatorIndex + 1);
180     }
181
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])) {
187                 chars[i] = '_';
188                 modified = true;
189             }
190         if(modified)
191             return new String(chars);
192         else
193             return name;
194     }
195
196     final private static String HTTP_PREFIX = "http://";
197     final private static int HTTP_POSITION = HTTP_PREFIX.length();
198
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()) };
204             else return null;
205         }
206         return new String[] {
207                 uri.substring(0, nextPathSeparator),
208                 uri.substring(nextPathSeparator + 1, uri.length())
209         };
210     }
211
212     public static List<String> splitURISCL(String uri) {
213         String[] result = splitURI(uri);
214         return Arrays.asList(result);
215     }
216
217     /**
218      * Splits the specified URI into a namespace and a local name and returns
219      * them both separately as an array.
220      * 
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
223      *         cannot be split.
224      * @throws NullPointerException for <code>null</code> URIs
225      */
226     public static String[] trySplitNamespaceAndLocalName(String uri) {
227         if (uri == null)
228             throw new NullPointerException("null uri");
229         int separatorIndex = singleSeparatorPosition(uri);
230         return separatorIndex == -1 ?
231                 null
232                 : new String[] { uri.substring(0, separatorIndex), uri.substring(separatorIndex + 1) };
233     }
234
235     /**
236      * Splits the specified URI into a namespace and a local name and returns
237      * them both separately as an array.
238      * 
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
244      */
245     public static String[] splitNamespaceAndLocalName(String uri) {
246         if (uri == null)
247             throw new NullPointerException("null uri");
248         int separatorIndex = assertSingleSeparatorPosition(uri);
249         return new String[] { uri.substring(0, separatorIndex), uri.substring(separatorIndex + 1) };
250     }
251
252     /**
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.
256      * 
257      * @param localName the string to escape
258      * @return the escaped string
259      * @throws NullPointerException for <code>null</code> URIs
260      */
261     public static String escapeURI(String localName) {
262         if (localName == null)
263             throw new NullPointerException("null local name");
264         return encode(localName);
265     }
266
267     /**
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.
270      * 
271      * @param namespace the namespace to append to
272      * @param suffix the suffix to append
273      * @return the joined namespace
274      */
275     public static String appendURINamespace(String namespace, String suffix) {
276         return new StringBuilder(namespace.length() + 1 + suffix.length())
277         .append(namespace)
278         .append(NAMESPACE_PATH_SEPARATOR)
279         .append(suffix)
280         .toString();
281     }
282
283     /**
284      * Join a namespace and a localname to form an URI with
285      * {@link #NAMESPACE_LOCAL_SEPARATOR}.
286      * 
287      * @param namespace the namespace part to join
288      * @param localName the localname part to join
289      * @return the joined URI
290      */
291     public static String makeURI(String namespace, String localName) {
292         String escapedLocalName = escapeURI(localName);
293         return new StringBuilder(namespace.length() + 1 + escapedLocalName.length())
294         .append(namespace)
295         .append(NAMESPACE_LOCAL_SEPARATOR)
296         .append(escapedLocalName)
297         .toString();
298     }
299
300     /**
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.
306      * 
307      * <p>
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/>
311      * 
312      * <code>
313      * http://foo.bar.com/foo/bar/org%2Fcom<br/>
314      * http://foo.bar.com/foo/bar/net%2Fcom<br/>
315      * </code>
316      * 
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.
320      * </p>
321      * 
322      * @param unicode The uri, in characters specified by RFC 2396 + '#'
323      * @return The corresponding Unicode String
324      */
325     public static String escape(String unicode) {
326         return encode(unicode);
327     }
328
329
330     /*
331      * RFC 3986 section 2.2 Reserved Characters (January 2005)
332      * !*'();:@&=+$,/?#[]
333      */
334     private static boolean[] ESCAPED_US_ASCII_CHARS = new boolean[128];
335
336     static {
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;
348     }
349
350     private static int needsEscaping(String unicode) {
351         int len = unicode.length();
352         int escapeCount = 0;
353         for (int i = 0; i < len; ++i) {
354             char ch = unicode.charAt(i);
355             if (ch < 128 && ESCAPED_US_ASCII_CHARS[ch])
356                 ++escapeCount;
357         }
358         return escapeCount;
359     }
360
361     private static String encode(String unicode) {
362         int needsEscapes = needsEscaping(unicode);
363         if (needsEscapes == 0)
364             return unicode;
365
366         int len = unicode.length();
367         char result[] = new char[(len - needsEscapes) + needsEscapes * 3];
368         int in = 0;
369         int out = 0;
370         while (in < len) {
371             char inCh = unicode.charAt(in++);
372             if (inCh >= 128 || !ESCAPED_US_ASCII_CHARS[inCh]) {
373                 result[out++] = inCh;
374             } else {
375                 // Only selected 7-bit US-ASCII characters are escaped
376                 int c = inCh & 255;
377                 result[out++] = '%';
378                 result[out++] = (char) hexEncode(c / 16);
379                 result[out++] = (char) hexEncode(c % 16);
380             }
381         }
382         return new String(result, 0, out);
383     }
384
385     private static boolean needsUnescaping(String unicode) {
386         return unicode.indexOf('%') > -1;
387     }
388
389     /**
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
392      * undefined.
393      * 
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.
397      */
398     public static String unescape(String uri) {
399         try {
400             if (!needsUnescaping(uri))
401                 return uri;
402
403             int len = uri.length();
404             String unicode = uri;
405             char result[] = new char[len];
406             int in = 0;
407             int out = 0;
408             while (in < len) {
409                 char inCh = unicode.charAt(in++);
410                 if (inCh == '%') {
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));
416                     in += 2;
417                 } else {
418                     result[out++] = inCh;
419                 }
420             }
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);
426         }
427     }
428
429     /* Copied from Jena 2.4 com.hp.hpl.jena.util.URIref */
430     private static byte hexEncode(int i) {
431         if (i < 10)
432             return (byte) ('0' + i);
433         else
434             return (byte)('A' + i - 10);
435     }
436
437     /* Copied from Jena 2.4 com.hp.hpl.jena.util.URIref */
438     private static int hexDecode(byte b) {
439         switch (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';
446             default:
447                 throw new IllegalArgumentException("Bad Hex escape character: " + ((b)&255) );
448         }
449     }
450
451     /**
452      * Some simple tests.
453      * @param args
454      */
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));
460
461         System.out.println("escapeURI: " + escapeURI("foo/bar/org%2Fnet"));
462         System.out.println("escapeURI('...#...'): " + escapeURI("foo/bar#org%2Fnet"));
463
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");
475     }
476
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");
486     }
487
488 }