Skip to content

Commit 8a72509

Browse files
committed
Merge pull request stleary#222 from erosb/master
JSON Pointer implementation
2 parents f21ffbe + c044eb1 commit 8a72509

File tree

5 files changed

+294
-2
lines changed

5 files changed

+294
-2
lines changed

JSONArray.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ of this software and associated documentation files (the "Software"), to deal
3030
import java.lang.reflect.Array;
3131
import java.math.BigDecimal;
3232
import java.math.BigInteger;
33-
import java.util.*;
33+
import java.util.ArrayList;
34+
import java.util.Collection;
35+
import java.util.Iterator;
36+
import java.util.List;
37+
import java.util.Map;
3438

3539
/**
3640
* A JSONArray is an ordered sequence of values. Its external text form is a
@@ -955,6 +959,10 @@ public JSONArray put(int index, Object value) throws JSONException {
955959
}
956960
return this;
957961
}
962+
963+
public Object query(String jsonPointer) {
964+
return new JSONPointer(jsonPointer).queryFrom(this);
965+
}
958966

959967
/**
960968
* Remove an index and close the hole.

JSONObject.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,15 @@ of this software and associated documentation files (the "Software"), to deal
3232
import java.lang.reflect.Modifier;
3333
import java.math.BigDecimal;
3434
import java.math.BigInteger;
35-
import java.util.*;
35+
import java.util.Collection;
36+
import java.util.Enumeration;
37+
import java.util.HashMap;
38+
import java.util.Iterator;
39+
import java.util.Locale;
40+
import java.util.Map;
3641
import java.util.Map.Entry;
42+
import java.util.ResourceBundle;
43+
import java.util.Set;
3744

3845
/**
3946
* A JSONObject is an unordered collection of name/value pairs. Its external
@@ -1330,6 +1337,10 @@ public JSONObject putOpt(String key, Object value) throws JSONException {
13301337
}
13311338
return this;
13321339
}
1340+
1341+
public Object query(String jsonPointer) {
1342+
return new JSONPointer(jsonPointer).queryFrom(this);
1343+
}
13331344

13341345
/**
13351346
* Produce a string in double quotes with backslash sequences in all the

JSONPointer.java

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
package org.json;
2+
3+
import static java.lang.String.format;
4+
import static java.util.Collections.emptyList;
5+
6+
import java.io.UnsupportedEncodingException;
7+
import java.net.URLDecoder;
8+
import java.net.URLEncoder;
9+
import java.util.ArrayList;
10+
import java.util.List;
11+
12+
/*
13+
Copyright (c) 2002 JSON.org
14+
15+
Permission is hereby granted, free of charge, to any person obtaining a copy
16+
of this software and associated documentation files (the "Software"), to deal
17+
in the Software without restriction, including without limitation the rights
18+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19+
copies of the Software, and to permit persons to whom the Software is
20+
furnished to do so, subject to the following conditions:
21+
22+
The above copyright notice and this permission notice shall be included in all
23+
copies or substantial portions of the Software.
24+
25+
The Software shall be used for Good, not Evil.
26+
27+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33+
SOFTWARE.
34+
*/
35+
36+
/**
37+
* A JSON Pointer is a simple query language defined for JSON documents by
38+
* <a href="https://tools.ietf.org/html/rfc6901">RFC 6901</a>.
39+
*/
40+
public class JSONPointer {
41+
42+
private static final String ENCODING = "utf-8";
43+
44+
public static class Builder {
45+
46+
private final List<String> refTokens = new ArrayList<String>();
47+
48+
/**
49+
* Creates a {@code JSONPointer} instance using the tokens previously set using the
50+
* {@link #append(String)} method calls.
51+
*/
52+
public JSONPointer build() {
53+
return new JSONPointer(refTokens);
54+
}
55+
56+
/**
57+
* Adds an arbitary token to the list of reference tokens. It can be any non-null value.
58+
*
59+
* Unlike in the case of JSON string or URI fragment representation of JSON pointers, the
60+
* argument of this method MUST NOT be escaped. If you want to query the property called
61+
* {@code "a~b"} then you should simply pass the {@code "a~b"} string as-is, there is no
62+
* need to escape it as {@code "a~0b"}.
63+
*
64+
* @param token the new token to be appended to the list
65+
* @return {@code this}
66+
* @throws NullPointerException if {@code token} is null
67+
*/
68+
public Builder append(String token) {
69+
if (token == null) {
70+
throw new NullPointerException("token cannot be null");
71+
}
72+
refTokens.add(token);
73+
return this;
74+
}
75+
76+
/**
77+
* Adds an integer to the reference token list. Although not necessarily, mostly this token will
78+
* denote an array index.
79+
*
80+
* @param arrayIndex the array index to be added to the token list
81+
* @return {@code this}
82+
*/
83+
public Builder append(int arrayIndex) {
84+
refTokens.add(String.valueOf(arrayIndex));
85+
return this;
86+
}
87+
88+
}
89+
90+
/**
91+
* Static factory method for {@link Builder}. Example usage:
92+
*
93+
* <pre><code>
94+
* JSONPointer pointer = JSONPointer.builder()
95+
* .append("obj")
96+
* .append("other~key").append("another/key")
97+
* .append("\"")
98+
* .append(0)
99+
* .build();
100+
* </code></pre>
101+
*
102+
* @return a builder instance which can be used to construct a {@code JSONPointer} instance by chained
103+
* {@link Builder#append(String)} calls.
104+
*/
105+
public static Builder builder() {
106+
return new Builder();
107+
}
108+
109+
private final List<String> refTokens;
110+
111+
/**
112+
* Pre-parses and initializes a new {@code JSONPointer} instance. If you want to
113+
* evaluate the same JSON Pointer on different JSON documents then it is recommended
114+
* to keep the {@code JSONPointer} instances due to performance considerations.
115+
*
116+
* @param pointer the JSON String or URI Fragment representation of the JSON pointer.
117+
* @throws IllegalArgumentException if {@code pointer} is not a valid JSON pointer
118+
*/
119+
public JSONPointer(String pointer) {
120+
if (pointer == null) {
121+
throw new NullPointerException("pointer cannot be null");
122+
}
123+
if (pointer.isEmpty()) {
124+
refTokens = emptyList();
125+
return;
126+
}
127+
if (pointer.startsWith("#/")) {
128+
pointer = pointer.substring(2);
129+
try {
130+
pointer = URLDecoder.decode(pointer, ENCODING);
131+
} catch (UnsupportedEncodingException e) {
132+
throw new RuntimeException(e);
133+
}
134+
} else if (pointer.startsWith("/")) {
135+
pointer = pointer.substring(1);
136+
} else {
137+
throw new IllegalArgumentException("a JSON pointer should start with '/' or '#/'");
138+
}
139+
refTokens = new ArrayList<String>();
140+
for (String token : pointer.split("/")) {
141+
refTokens.add(unescape(token));
142+
}
143+
}
144+
145+
public JSONPointer(List<String> refTokens) {
146+
this.refTokens = new ArrayList<String>(refTokens);
147+
}
148+
149+
private String unescape(String token) {
150+
return token.replace("~1", "/").replace("~0", "~")
151+
.replace("\\\"", "\"")
152+
.replace("\\\\", "\\");
153+
}
154+
155+
/**
156+
* Evaluates this JSON Pointer on the given {@code document}. The {@code document}
157+
* is usually a {@link JSONObject} or a {@link JSONArray} instance, but the empty
158+
* JSON Pointer ({@code ""}) can be evaluated on any JSON values and in such case the
159+
* returned value will be {@code document} itself.
160+
*
161+
* @param document the JSON document which should be the subject of querying.
162+
* @return the result of the evaluation
163+
* @throws JSONPointerException if an error occurs during evaluation
164+
*/
165+
public Object queryFrom(Object document) {
166+
if (refTokens.isEmpty()) {
167+
return document;
168+
}
169+
Object current = document;
170+
for (String token : refTokens) {
171+
if (current instanceof JSONObject) {
172+
current = ((JSONObject) current).opt(unescape(token));
173+
} else if (current instanceof JSONArray) {
174+
current = readByIndexToken(current, token);
175+
} else {
176+
throw new JSONPointerException(format(
177+
"value [%s] is not an array or object therefore its key %s cannot be resolved", current,
178+
token));
179+
}
180+
}
181+
return current;
182+
}
183+
184+
private Object readByIndexToken(Object current, String indexToken) {
185+
try {
186+
int index = Integer.parseInt(indexToken);
187+
JSONArray currentArr = (JSONArray) current;
188+
if (index >= currentArr.length()) {
189+
throw new JSONPointerException(format("index %d is out of bounds - the array has %d elements", index,
190+
currentArr.length()));
191+
}
192+
return currentArr.get(index);
193+
} catch (NumberFormatException e) {
194+
throw new JSONPointerException(format("%s is not an array index", indexToken), e);
195+
}
196+
}
197+
198+
@Override
199+
public String toString() {
200+
StringBuilder rval = new StringBuilder("");
201+
for (String token: refTokens) {
202+
rval.append('/').append(escape(token));
203+
}
204+
return rval.toString();
205+
}
206+
207+
private String escape(String token) {
208+
return token.replace("~", "~0")
209+
.replace("/", "~1")
210+
.replace("\\", "\\\\")
211+
.replace("\"", "\\\"");
212+
}
213+
214+
public String toURIFragment() {
215+
try {
216+
StringBuilder rval = new StringBuilder("#");
217+
for (String token : refTokens) {
218+
rval.append('/').append(URLEncoder.encode(token, ENCODING));
219+
}
220+
return rval.toString();
221+
} catch (UnsupportedEncodingException e) {
222+
throw new RuntimeException(e);
223+
}
224+
}
225+
226+
}

JSONPointerException.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.json;
2+
3+
/*
4+
Copyright (c) 2002 JSON.org
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
The Software shall be used for Good, not Evil.
17+
18+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24+
SOFTWARE.
25+
*/
26+
27+
/**
28+
* The JSONPointerException is thrown by {@link JSONPointer} if an error occurs
29+
* during evaluating a pointer.
30+
*/
31+
public class JSONPointerException extends JSONException {
32+
private static final long serialVersionUID = 8872944667561856751L;
33+
34+
public JSONPointerException(String message) {
35+
super(message);
36+
}
37+
38+
public JSONPointerException(String message, Throwable cause) {
39+
super(message, cause);
40+
}
41+
42+
}

README

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ tokens. It can be constructed from a String, Reader, or InputStream.
3232
JSONException.java: The JSONException is the standard exception type thrown
3333
by this package.
3434

35+
JSONPointer.java: Implementation of
36+
[JSON Pointer (RFC 6901)](https://tools.ietf.org/html/rfc6901). Supports
37+
JSON Pointers both in the form of string representation and URI fragment
38+
representation.
39+
3540
JSONString.java: The JSONString interface requires a toJSONString method,
3641
allowing an object to provide its own serialization.
3742

0 commit comments

Comments
 (0)