X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Forg%2Fjson%2FJSONPointer.java;fp=src%2Forg%2Fjson%2FJSONPointer.java;h=406041173f89ebc829396d36f619e9e7483546b2;hb=5e20c0116864d77705d951e35c41a13197791156;hp=fc0b04b7cc3a753450f271cb6f805ef48a1b1769;hpb=1bad3c3f74b2e204e0d7ba93a745f5ec775c8a3e;p=jalview.git diff --git a/src/org/json/JSONPointer.java b/src/org/json/JSONPointer.java index fc0b04b..4060411 100644 --- a/src/org/json/JSONPointer.java +++ b/src/org/json/JSONPointer.java @@ -39,255 +39,324 @@ SOFTWARE. * * In a nutshell, JSONPointer allows the user to navigate into a JSON document * using strings, and retrieve targeted objects, like a simple form of XPATH. - * Path segments are separated by the '/' char, which signifies the root of - * the document when it appears as the first char of the string. Array - * elements are navigated using ordinals, counting from 0. JSONPointer strings - * may be extended to any arbitrary number of segments. If the navigation - * is successful, the matched item is returned. A matched item may be a - * JSONObject, a JSONArray, or a JSON value. If the JSONPointer string building - * fails, an appropriate exception is thrown. If the navigation fails to find - * a match, a JSONPointerException is thrown. + * Path segments are separated by the '/' char, which signifies the root of the + * document when it appears as the first char of the string. Array elements are + * navigated using ordinals, counting from 0. JSONPointer strings may be + * extended to any arbitrary number of segments. If the navigation is + * successful, the matched item is returned. A matched item may be a JSONObject, + * a JSONArray, or a JSON value. If the JSONPointer string building fails, an + * appropriate exception is thrown. If the navigation fails to find a match, a + * JSONPointerException is thrown. * * @author JSON.org * @version 2016-05-14 */ -public class JSONPointer { +public class JSONPointer +{ - // used for URL encoding and decoding - private static final String ENCODING = "utf-8"; + // used for URL encoding and decoding + private static final String ENCODING = "utf-8"; - /** - * This class allows the user to build a JSONPointer in steps, using - * exactly one segment in each step. - */ - public static class Builder { - - // Segments for the eventual JSONPointer string - private final List refTokens = new ArrayList(); - - /** - * Creates a {@code JSONPointer} instance using the tokens previously set using the - * {@link #append(String)} method calls. - */ - public JSONPointer build() { - return new JSONPointer(this.refTokens); - } + /** + * This class allows the user to build a JSONPointer in steps, using exactly + * one segment in each step. + */ + public static class Builder + { - /** - * Adds an arbitrary token to the list of reference tokens. It can be any non-null value. - * - * Unlike in the case of JSON string or URI fragment representation of JSON pointers, the - * argument of this method MUST NOT be escaped. If you want to query the property called - * {@code "a~b"} then you should simply pass the {@code "a~b"} string as-is, there is no - * need to escape it as {@code "a~0b"}. - * - * @param token the new token to be appended to the list - * @return {@code this} - * @throws NullPointerException if {@code token} is null - */ - public Builder append(String token) { - if (token == null) { - throw new NullPointerException("token cannot be null"); - } - this.refTokens.add(token); - return this; - } + // Segments for the eventual JSONPointer string + private final List refTokens = new ArrayList(); - /** - * Adds an integer to the reference token list. Although not necessarily, mostly this token will - * denote an array index. - * - * @param arrayIndex the array index to be added to the token list - * @return {@code this} - */ - public Builder append(int arrayIndex) { - this.refTokens.add(String.valueOf(arrayIndex)); - return this; - } + /** + * Creates a {@code JSONPointer} instance using the tokens previously set + * using the {@link #append(String)} method calls. + */ + public JSONPointer build() + { + return new JSONPointer(this.refTokens); } /** - * Static factory method for {@link Builder}. Example usage: + * Adds an arbitrary token to the list of reference tokens. It can be any + * non-null value. * - *

-     * JSONPointer pointer = JSONPointer.builder()
-     *       .append("obj")
-     *       .append("other~key").append("another/key")
-     *       .append("\"")
-     *       .append(0)
-     *       .build();
-     * 
+ * Unlike in the case of JSON string or URI fragment representation of JSON + * pointers, the argument of this method MUST NOT be escaped. If you want to + * query the property called {@code "a~b"} then you should simply pass the + * {@code "a~b"} string as-is, there is no need to escape it as + * {@code "a~0b"}. * - * @return a builder instance which can be used to construct a {@code JSONPointer} instance by chained - * {@link Builder#append(String)} calls. + * @param token + * the new token to be appended to the list + * @return {@code this} + * @throws NullPointerException + * if {@code token} is null */ - public static Builder builder() { - return new Builder(); + public Builder append(String token) + { + if (token == null) + { + throw new NullPointerException("token cannot be null"); + } + this.refTokens.add(token); + return this; } - // Segments for the JSONPointer string - private final List refTokens; - /** - * Pre-parses and initializes a new {@code JSONPointer} instance. If you want to - * evaluate the same JSON Pointer on different JSON documents then it is recommended - * to keep the {@code JSONPointer} instances due to performance considerations. + * Adds an integer to the reference token list. Although not necessarily, + * mostly this token will denote an array index. * - * @param pointer the JSON String or URI Fragment representation of the JSON pointer. - * @throws IllegalArgumentException if {@code pointer} is not a valid JSON pointer + * @param arrayIndex + * the array index to be added to the token list + * @return {@code this} */ - public JSONPointer(final String pointer) { - if (pointer == null) { - throw new NullPointerException("pointer cannot be null"); - } - if (pointer.isEmpty() || pointer.equals("#")) { - this.refTokens = Collections.emptyList(); - return; - } - String refs; - if (pointer.startsWith("#/")) { - refs = pointer.substring(2); - try { - refs = URLDecoder.decode(refs, ENCODING); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - } else if (pointer.startsWith("/")) { - refs = pointer.substring(1); - } else { - throw new IllegalArgumentException("a JSON pointer should start with '/' or '#/'"); - } - this.refTokens = new ArrayList(); - int slashIdx = -1; - int prevSlashIdx = 0; - do { - prevSlashIdx = slashIdx + 1; - slashIdx = refs.indexOf('/', prevSlashIdx); - if(prevSlashIdx == slashIdx || prevSlashIdx == refs.length()) { - // found 2 slashes in a row ( obj//next ) - // or single slash at the end of a string ( obj/test/ ) - this.refTokens.add(""); - } else if (slashIdx >= 0) { - final String token = refs.substring(prevSlashIdx, slashIdx); - this.refTokens.add(unescape(token)); - } else { - // last item after separator, or no separator at all. - final String token = refs.substring(prevSlashIdx); - this.refTokens.add(unescape(token)); - } - } while (slashIdx >= 0); - // using split does not take into account consecutive separators or "ending nulls" - //for (String token : refs.split("/")) { - // this.refTokens.add(unescape(token)); - //} + public Builder append(int arrayIndex) + { + this.refTokens.add(String.valueOf(arrayIndex)); + return this; } + } - public JSONPointer(List refTokens) { - this.refTokens = new ArrayList(refTokens); - } + /** + * Static factory method for {@link Builder}. Example usage: + * + *
+   * 
+   * JSONPointer pointer = JSONPointer.builder()
+   *       .append("obj")
+   *       .append("other~key").append("another/key")
+   *       .append("\"")
+   *       .append(0)
+   *       .build();
+   * 
+   * 
+ * + * @return a builder instance which can be used to construct a + * {@code JSONPointer} instance by chained + * {@link Builder#append(String)} calls. + */ + public static Builder builder() + { + return new Builder(); + } - private String unescape(String token) { - return token.replace("~1", "/").replace("~0", "~") - .replace("\\\"", "\"") - .replace("\\\\", "\\"); - } + // Segments for the JSONPointer string + private final List refTokens; - /** - * Evaluates this JSON Pointer on the given {@code document}. The {@code document} - * is usually a {@link JSONObject} or a {@link JSONArray} instance, but the empty - * JSON Pointer ({@code ""}) can be evaluated on any JSON values and in such case the - * returned value will be {@code document} itself. - * - * @param document the JSON document which should be the subject of querying. - * @return the result of the evaluation - * @throws JSONPointerException if an error occurs during evaluation - */ - public Object queryFrom(Object document) throws JSONPointerException { - if (this.refTokens.isEmpty()) { - return document; - } - Object current = document; - for (String token : this.refTokens) { - if (current instanceof JSONObject) { - current = ((JSONObject) current).opt(unescape(token)); - } else if (current instanceof JSONArray) { - current = readByIndexToken(current, token); - } else { - throw new JSONPointerException(format( - "value [%s] is not an array or object therefore its key %s cannot be resolved", current, - token)); - } - } - return current; + /** + * Pre-parses and initializes a new {@code JSONPointer} instance. If you want + * to evaluate the same JSON Pointer on different JSON documents then it is + * recommended to keep the {@code JSONPointer} instances due to performance + * considerations. + * + * @param pointer + * the JSON String or URI Fragment representation of the JSON + * pointer. + * @throws IllegalArgumentException + * if {@code pointer} is not a valid JSON pointer + */ + public JSONPointer(final String pointer) + { + if (pointer == null) + { + throw new NullPointerException("pointer cannot be null"); + } + if (pointer.isEmpty() || pointer.equals("#")) + { + this.refTokens = Collections.emptyList(); + return; + } + String refs; + if (pointer.startsWith("#/")) + { + refs = pointer.substring(2); + try + { + refs = URLDecoder.decode(refs, ENCODING); + } catch (UnsupportedEncodingException e) + { + throw new RuntimeException(e); + } + } + else if (pointer.startsWith("/")) + { + refs = pointer.substring(1); } + else + { + throw new IllegalArgumentException( + "a JSON pointer should start with '/' or '#/'"); + } + this.refTokens = new ArrayList(); + int slashIdx = -1; + int prevSlashIdx = 0; + do + { + prevSlashIdx = slashIdx + 1; + slashIdx = refs.indexOf('/', prevSlashIdx); + if (prevSlashIdx == slashIdx || prevSlashIdx == refs.length()) + { + // found 2 slashes in a row ( obj//next ) + // or single slash at the end of a string ( obj/test/ ) + this.refTokens.add(""); + } + else if (slashIdx >= 0) + { + final String token = refs.substring(prevSlashIdx, slashIdx); + this.refTokens.add(unescape(token)); + } + else + { + // last item after separator, or no separator at all. + final String token = refs.substring(prevSlashIdx); + this.refTokens.add(unescape(token)); + } + } while (slashIdx >= 0); + // using split does not take into account consecutive separators or "ending + // nulls" + // for (String token : refs.split("/")) { + // this.refTokens.add(unescape(token)); + // } + } - /** - * Matches a JSONArray element by ordinal position - * @param current the JSONArray to be evaluated - * @param indexToken the array index in string form - * @return the matched object. If no matching item is found a - * @throws JSONPointerException is thrown if the index is out of bounds - */ - private Object readByIndexToken(Object current, String indexToken) throws JSONPointerException { - try { - int index = Integer.parseInt(indexToken); - JSONArray currentArr = (JSONArray) current; - if (index >= currentArr.length()) { - throw new JSONPointerException(format("index %d is out of bounds - the array has %d elements", index, - currentArr.length())); - } - try { - return currentArr.get(index); - } catch (JSONException e) { - throw new JSONPointerException("Error reading value at index position " + index, e); - } - } catch (NumberFormatException e) { - throw new JSONPointerException(format("%s is not an array index", indexToken), e); - } + public JSONPointer(List refTokens) + { + this.refTokens = new ArrayList(refTokens); + } + + private String unescape(String token) + { + return token.replace("~1", "/").replace("~0", "~").replace("\\\"", "\"") + .replace("\\\\", "\\"); + } + + /** + * Evaluates this JSON Pointer on the given {@code document}. The + * {@code document} is usually a {@link JSONObject} or a {@link JSONArray} + * instance, but the empty JSON Pointer ({@code ""}) can be evaluated on any + * JSON values and in such case the returned value will be {@code document} + * itself. + * + * @param document + * the JSON document which should be the subject of querying. + * @return the result of the evaluation + * @throws JSONPointerException + * if an error occurs during evaluation + */ + public Object queryFrom(Object document) throws JSONPointerException + { + if (this.refTokens.isEmpty()) + { + return document; + } + Object current = document; + for (String token : this.refTokens) + { + if (current instanceof JSONObject) + { + current = ((JSONObject) current).opt(unescape(token)); + } + else if (current instanceof JSONArray) + { + current = readByIndexToken(current, token); + } + else + { + throw new JSONPointerException(format( + "value [%s] is not an array or object therefore its key %s cannot be resolved", + current, token)); + } } + return current; + } - /** - * Returns a string representing the JSONPointer path value using string - * representation - */ - @Override - public String toString() { - StringBuilder rval = new StringBuilder(""); - for (String token: this.refTokens) { - rval.append('/').append(escape(token)); - } - return rval.toString(); + /** + * Matches a JSONArray element by ordinal position + * + * @param current + * the JSONArray to be evaluated + * @param indexToken + * the array index in string form + * @return the matched object. If no matching item is found a + * @throws JSONPointerException + * is thrown if the index is out of bounds + */ + private Object readByIndexToken(Object current, String indexToken) + throws JSONPointerException + { + try + { + int index = Integer.parseInt(indexToken); + JSONArray currentArr = (JSONArray) current; + if (index >= currentArr.length()) + { + throw new JSONPointerException(format( + "index %d is out of bounds - the array has %d elements", + index, currentArr.length())); + } + try + { + return currentArr.get(index); + } catch (JSONException e) + { + throw new JSONPointerException( + "Error reading value at index position " + index, e); + } + } catch (NumberFormatException e) + { + throw new JSONPointerException( + format("%s is not an array index", indexToken), e); } + } - /** - * Escapes path segment values to an unambiguous form. - * The escape char to be inserted is '~'. The chars to be escaped - * are ~, which maps to ~0, and /, which maps to ~1. Backslashes - * and double quote chars are also escaped. - * @param token the JSONPointer segment value to be escaped - * @return the escaped value for the token - */ - private String escape(String token) { - return token.replace("~", "~0") - .replace("/", "~1") - .replace("\\", "\\\\") - .replace("\"", "\\\""); + /** + * Returns a string representing the JSONPointer path value using string + * representation + */ + @Override + public String toString() + { + StringBuilder rval = new StringBuilder(""); + for (String token : this.refTokens) + { + rval.append('/').append(escape(token)); } + return rval.toString(); + } - /** - * Returns a string representing the JSONPointer path value using URI - * fragment identifier representation - */ - public String toURIFragment() { - try { - StringBuilder rval = new StringBuilder("#"); - for (String token : this.refTokens) { - rval.append('/').append(URLEncoder.encode(token, ENCODING)); - } - return rval.toString(); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } + /** + * Escapes path segment values to an unambiguous form. The escape char to be + * inserted is '~'. The chars to be escaped are ~, which maps to ~0, and /, + * which maps to ~1. Backslashes and double quote chars are also escaped. + * + * @param token + * the JSONPointer segment value to be escaped + * @return the escaped value for the token + */ + private String escape(String token) + { + return token.replace("~", "~0").replace("/", "~1").replace("\\", "\\\\") + .replace("\"", "\\\""); + } + + /** + * Returns a string representing the JSONPointer path value using URI fragment + * identifier representation + */ + public String toURIFragment() + { + try + { + StringBuilder rval = new StringBuilder("#"); + for (String token : this.refTokens) + { + rval.append('/').append(URLEncoder.encode(token, ENCODING)); + } + return rval.toString(); + } catch (UnsupportedEncodingException e) + { + throw new RuntimeException(e); } - + } + }