From 612e41950ca5f70df58ed07499003e092a27b325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Er=C5=91s?= Date: Sun, 17 Apr 2016 23:21:38 +0200 Subject: [PATCH 01/11] initial implementation of JSONPointer --- JSONPointer.java | 61 +++++++++++++++++++++++++++++++++++++++ JSONPointerException.java | 21 ++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 JSONPointer.java create mode 100644 JSONPointerException.java diff --git a/JSONPointer.java b/JSONPointer.java new file mode 100644 index 0000000..a05db98 --- /dev/null +++ b/JSONPointer.java @@ -0,0 +1,61 @@ +package org.json; + +import static java.lang.String.format; +import static java.util.Collections.emptyList; + +import java.util.ArrayList; +import java.util.List; + +public class JSONPointer { + + private List refTokens; + + 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); + } 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)); + } + } + + private String unescape(String token) { + return token.replace("~1", "/").replace("~0", "~"); + } + + public Object queryFrom(JSONObject 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, unescape(token)); + } + } + return current; + } + + private Object readByIndexToken(Object current, String indexToken) { + try { + return ((JSONArray) current).opt(Integer.parseInt(unescape(indexToken))); + } catch (NumberFormatException e) { + throw new JSONPointerException(format("%s is not an array index", unescape(indexToken)), e); + } + } + +} diff --git a/JSONPointerException.java b/JSONPointerException.java new file mode 100644 index 0000000..599f0bb --- /dev/null +++ b/JSONPointerException.java @@ -0,0 +1,21 @@ +package org.json; + +/** + * The JSONPointerException is thrown by {@link JSONPointer} if an error occurs + * during evaluating a pointer. + * + * @author erosb + * + */ +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); + } + +} From 5bee7e3b45984b7119b97f9cc48c879466c18064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Er=C5=91s?= Date: Tue, 26 Apr 2016 23:00:16 +0200 Subject: [PATCH 02/11] escape handling improvements & URL fragment notation handling --- JSONPointer.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/JSONPointer.java b/JSONPointer.java index a05db98..85451e6 100644 --- a/JSONPointer.java +++ b/JSONPointer.java @@ -3,6 +3,8 @@ 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.util.ArrayList; import java.util.List; @@ -20,6 +22,11 @@ public class JSONPointer { } if (pointer.startsWith("#/")) { pointer = pointer.substring(2); + try { + pointer = URLDecoder.decode(pointer, "utf-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } } else if (pointer.startsWith("/")) { pointer = pointer.substring(1); } else { @@ -32,7 +39,9 @@ public class JSONPointer { } private String unescape(String token) { - return token.replace("~1", "/").replace("~0", "~"); + return token.replace("~1", "/").replace("~0", "~") + .replace("\\\"", "\"") + .replace("\\\\", "\\"); } public Object queryFrom(JSONObject document) { From 792c6f6a9c423a58f4d5e812999e7c69da157f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Er=C5=91s?= Date: Tue, 26 Apr 2016 23:32:15 +0200 Subject: [PATCH 03/11] improved failure handling --- JSONPointer.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/JSONPointer.java b/JSONPointer.java index 85451e6..275c036 100644 --- a/JSONPointer.java +++ b/JSONPointer.java @@ -53,7 +53,11 @@ public class JSONPointer { if (current instanceof JSONObject) { current = ((JSONObject) current).opt(unescape(token)); } else if (current instanceof JSONArray) { - current = readByIndexToken(current, unescape(token)); + 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; @@ -61,10 +65,15 @@ public class JSONPointer { private Object readByIndexToken(Object current, String indexToken) { try { - return ((JSONArray) current).opt(Integer.parseInt(unescape(indexToken))); + 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", unescape(indexToken)), e); + throw new JSONPointerException(format("%s is not an array index", indexToken), e); } } - } From 45bd72c15df1bdeae561ff25f0fd1827e405431a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Er=C5=91s?= Date: Tue, 26 Apr 2016 23:48:14 +0200 Subject: [PATCH 04/11] added JSONObject#query() and JSONPointer#query() methods --- JSONArray.java | 10 +++++++++- JSONObject.java | 13 ++++++++++++- JSONPointer.java | 2 +- 3 files changed, 22 insertions(+), 3 deletions(-) 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 852358b..ab113f3 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 index 275c036..e5bc125 100644 --- a/JSONPointer.java +++ b/JSONPointer.java @@ -44,7 +44,7 @@ public class JSONPointer { .replace("\\\\", "\\"); } - public Object queryFrom(JSONObject document) { + public Object queryFrom(Object document) { if (refTokens.isEmpty()) { return document; } From 5223aadd67df246b4347731bd025dbbb0d4d53a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Er=C5=91s?= Date: Wed, 27 Apr 2016 00:10:06 +0200 Subject: [PATCH 05/11] added some javadoc to JSONPointer --- JSONPointer.java | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/JSONPointer.java b/JSONPointer.java index e5bc125..2b8bcd3 100644 --- a/JSONPointer.java +++ b/JSONPointer.java @@ -8,10 +8,22 @@ import java.net.URLDecoder; import java.util.ArrayList; import java.util.List; +/** + * A JSON Pointer is a simple query language defined for JSON documents by + * RFC 6901. + */ public class JSONPointer { private 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"); @@ -44,6 +56,16 @@ public class JSONPointer { .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; From bff352791c4753dfe871a1bf6093f60fb6213447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Er=C5=91s?= Date: Wed, 27 Apr 2016 00:10:45 +0200 Subject: [PATCH 06/11] removed @author tag from JSONPointerException --- JSONPointerException.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/JSONPointerException.java b/JSONPointerException.java index 599f0bb..a7840cb 100644 --- a/JSONPointerException.java +++ b/JSONPointerException.java @@ -3,9 +3,6 @@ package org.json; /** * The JSONPointerException is thrown by {@link JSONPointer} if an error occurs * during evaluating a pointer. - * - * @author erosb - * */ public class JSONPointerException extends JSONException { private static final long serialVersionUID = 8872944667561856751L; From c1a789a70c1a9d0a055a9d1756597beeeab95a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Er=C5=91s?= Date: Wed, 27 Apr 2016 00:11:47 +0200 Subject: [PATCH 07/11] adding missing license headers --- JSONPointer.java | 24 ++++++++++++++++++++++++ JSONPointerException.java | 24 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/JSONPointer.java b/JSONPointer.java index 2b8bcd3..b6d1307 100644 --- a/JSONPointer.java +++ b/JSONPointer.java @@ -8,6 +8,30 @@ import java.net.URLDecoder; 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. diff --git a/JSONPointerException.java b/JSONPointerException.java index a7840cb..e3d20a9 100644 --- a/JSONPointerException.java +++ b/JSONPointerException.java @@ -1,5 +1,29 @@ 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. From cbb1546c530d73d55c51b903b9c14962f4f40671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Er=C5=91s?= Date: Thu, 28 Apr 2016 16:45:17 +0200 Subject: [PATCH 08/11] README improvements for stleary/JSON-Java#218 --- README | 5 +++++ 1 file changed, 5 insertions(+) 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. From d833c2d8de86be697bbc1e9a416736cefafcf320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Er=C5=91s?= Date: Tue, 3 May 2016 23:18:05 +0200 Subject: [PATCH 09/11] added builder class for JSONPointer, and implemented toString() and toURIFragment() --- JSONPointer.java | 64 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/JSONPointer.java b/JSONPointer.java index b6d1307..eb0b3e9 100644 --- a/JSONPointer.java +++ b/JSONPointer.java @@ -5,6 +5,7 @@ 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; @@ -37,8 +38,34 @@ SOFTWARE. * RFC 6901. */ public class JSONPointer { + + private static final String ENCODING = "utf-8"; - private List refTokens; + public static class Builder { + + private final List refTokens = new ArrayList(); + + public Builder append(String token) { + refTokens.add(token); + return this; + } + + public JSONPointer build() { + return new JSONPointer(refTokens); + } + + public Builder append(int arrayIndex) { + refTokens.add(String.valueOf(arrayIndex)); + return this; + } + + } + + public static Builder builder() { + return new Builder(); + } + + private final List refTokens; /** * Pre-parses and initializes a new {@code JSONPointer} instance. If you want to @@ -59,7 +86,7 @@ public class JSONPointer { if (pointer.startsWith("#/")) { pointer = pointer.substring(2); try { - pointer = URLDecoder.decode(pointer, "utf-8"); + pointer = URLDecoder.decode(pointer, ENCODING); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } @@ -74,6 +101,10 @@ public class JSONPointer { } } + public JSONPointer(List refTokens) { + this.refTokens = refTokens; + } + private String unescape(String token) { return token.replace("~1", "/").replace("~0", "~") .replace("\\\"", "\"") @@ -122,4 +153,33 @@ public class JSONPointer { 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); + } + } + } From 5ae6a66e38518ea97a81bd475df6ebadaf982095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Er=C5=91s?= Date: Tue, 3 May 2016 23:41:51 +0200 Subject: [PATCH 10/11] added javadoc & null check in Builder#append(String) --- JSONPointer.java | 53 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/JSONPointer.java b/JSONPointer.java index eb0b3e9..ddfe40b 100644 --- a/JSONPointer.java +++ b/JSONPointer.java @@ -44,16 +44,42 @@ public class JSONPointer { public static class Builder { private final List refTokens = new ArrayList(); - - public Builder append(String token) { - refTokens.add(token); - 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(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; @@ -61,6 +87,21 @@ public class JSONPointer { } + /** + * 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(); } From c044eb14dd388da8f9a570d8b1f79d1952d42e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Er=C5=91s?= Date: Thu, 5 May 2016 15:59:43 +0200 Subject: [PATCH 11/11] added copying to JSONPointer constructor --- JSONPointer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSONPointer.java b/JSONPointer.java index ddfe40b..820a448 100644 --- a/JSONPointer.java +++ b/JSONPointer.java @@ -143,7 +143,7 @@ public class JSONPointer { } public JSONPointer(List refTokens) { - this.refTokens = refTokens; + this.refTokens = new ArrayList(refTokens); } private String unescape(String token) {