diff --git a/JSONArray.java b/JSONArray.java index 7aaa611..f338a2d 100644 --- a/JSONArray.java +++ b/JSONArray.java @@ -30,7 +30,11 @@ import java.io.Writer; import java.lang.reflect.Array; import java.math.BigDecimal; import java.math.BigInteger; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; /** * A JSONArray is an ordered sequence of values. Its external text form is a @@ -955,6 +959,10 @@ public class JSONArray implements Iterable { } return this; } + + public Object query(String jsonPointer) { + return new JSONPointer(jsonPointer).queryFrom(this); + } /** * Remove an index and close the hole. diff --git a/JSONObject.java b/JSONObject.java index 3cac6e0..f2f0f7d 100644 --- a/JSONObject.java +++ b/JSONObject.java @@ -32,8 +32,15 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.math.BigDecimal; import java.math.BigInteger; -import java.util.*; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; import java.util.Map.Entry; +import java.util.ResourceBundle; +import java.util.Set; /** * A JSONObject is an unordered collection of name/value pairs. Its external @@ -1330,6 +1337,10 @@ public class JSONObject { } return this; } + + public Object query(String jsonPointer) { + return new JSONPointer(jsonPointer).queryFrom(this); + } /** * Produce a string in double quotes with backslash sequences in all the diff --git a/JSONPointer.java b/JSONPointer.java new file mode 100644 index 0000000..820a448 --- /dev/null +++ b/JSONPointer.java @@ -0,0 +1,226 @@ +package org.json; + +import static java.lang.String.format; +import static java.util.Collections.emptyList; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * A JSON Pointer is a simple query language defined for JSON documents by + * RFC 6901. + */ +public class JSONPointer { + + private static final String ENCODING = "utf-8"; + + public static class Builder { + + 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(refTokens); + } + + /** + * Adds an arbitary 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"); + } + refTokens.add(token); + return this; + } + + /** + * 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) { + refTokens.add(String.valueOf(arrayIndex)); + return this; + } + + } + + /** + * 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 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. + * + * @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(String pointer) { + if (pointer == null) { + throw new NullPointerException("pointer cannot be null"); + } + if (pointer.isEmpty()) { + refTokens = emptyList(); + return; + } + if (pointer.startsWith("#/")) { + pointer = pointer.substring(2); + try { + pointer = URLDecoder.decode(pointer, ENCODING); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } else if (pointer.startsWith("/")) { + pointer = pointer.substring(1); + } else { + throw new IllegalArgumentException("a JSON pointer should start with '/' or '#/'"); + } + refTokens = new ArrayList(); + for (String token : pointer.split("/")) { + refTokens.add(unescape(token)); + } + } + + 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) { + if (refTokens.isEmpty()) { + return document; + } + Object current = document; + for (String token : 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; + } + + private Object readByIndexToken(Object current, String indexToken) { + 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())); + } + return currentArr.get(index); + } catch (NumberFormatException e) { + throw new JSONPointerException(format("%s is not an array index", indexToken), e); + } + } + + @Override + public String toString() { + StringBuilder rval = new StringBuilder(""); + for (String token: refTokens) { + rval.append('/').append(escape(token)); + } + return rval.toString(); + } + + private String escape(String token) { + return token.replace("~", "~0") + .replace("/", "~1") + .replace("\\", "\\\\") + .replace("\"", "\\\""); + } + + public String toURIFragment() { + try { + StringBuilder rval = new StringBuilder("#"); + for (String token : refTokens) { + rval.append('/').append(URLEncoder.encode(token, ENCODING)); + } + return rval.toString(); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/JSONPointerException.java b/JSONPointerException.java new file mode 100644 index 0000000..e3d20a9 --- /dev/null +++ b/JSONPointerException.java @@ -0,0 +1,42 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * The JSONPointerException is thrown by {@link JSONPointer} if an error occurs + * during evaluating a pointer. + */ +public class JSONPointerException extends JSONException { + private static final long serialVersionUID = 8872944667561856751L; + + public JSONPointerException(String message) { + super(message); + } + + public JSONPointerException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/README b/README index 8ac6ccd..fb0e9e4 100644 --- a/README +++ b/README @@ -32,6 +32,11 @@ tokens. It can be constructed from a String, Reader, or InputStream. JSONException.java: The JSONException is the standard exception type thrown by this package. +JSONPointer.java: Implementation of +[JSON Pointer (RFC 6901)](https://tools.ietf.org/html/rfc6901). Supports +JSON Pointers both in the form of string representation and URI fragment +representation. + JSONString.java: The JSONString interface requires a toJSONString method, allowing an object to provide its own serialization.