3 import static java.lang.String.format;
5 import java.io.UnsupportedEncodingException;
6 import java.net.URLDecoder;
7 import java.net.URLEncoder;
8 import java.util.ArrayList;
9 import java.util.Collections;
10 import java.util.List;
13 Copyright (c) 2002 JSON.org
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:
22 The above copyright notice and this permission notice shall be included in all
23 copies or substantial portions of the Software.
25 The Software shall be used for Good, not Evil.
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
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>.
40 * In a nutshell, JSONPointer allows the user to navigate into a JSON document
41 * using strings, and retrieve targeted objects, like a simple form of XPATH.
42 * Path segments are separated by the '/' char, which signifies the root of the
43 * document when it appears as the first char of the string. Array elements are
44 * navigated using ordinals, counting from 0. JSONPointer strings may be
45 * extended to any arbitrary number of segments. If the navigation is
46 * successful, the matched item is returned. A matched item may be a JSONObject,
47 * a JSONArray, or a JSON value. If the JSONPointer string building fails, an
48 * appropriate exception is thrown. If the navigation fails to find a match, a
49 * JSONPointerException is thrown.
54 public class JSONPointer
57 // used for URL encoding and decoding
58 private static final String ENCODING = "utf-8";
61 * This class allows the user to build a JSONPointer in steps, using exactly
62 * one segment in each step.
64 public static class Builder
67 // Segments for the eventual JSONPointer string
68 private final List<String> refTokens = new ArrayList<String>();
71 * Creates a {@code JSONPointer} instance using the tokens previously set
72 * using the {@link #append(String)} method calls.
74 public JSONPointer build()
76 return new JSONPointer(this.refTokens);
80 * Adds an arbitrary token to the list of reference tokens. It can be any
83 * Unlike in the case of JSON string or URI fragment representation of JSON
84 * pointers, the argument of this method MUST NOT be escaped. If you want to
85 * query the property called {@code "a~b"} then you should simply pass the
86 * {@code "a~b"} string as-is, there is no need to escape it as
90 * the new token to be appended to the list
91 * @return {@code this}
92 * @throws NullPointerException
93 * if {@code token} is null
95 public Builder append(String token)
99 throw new NullPointerException("token cannot be null");
101 this.refTokens.add(token);
106 * Adds an integer to the reference token list. Although not necessarily,
107 * mostly this token will denote an array index.
110 * the array index to be added to the token list
111 * @return {@code this}
113 public Builder append(int arrayIndex)
115 this.refTokens.add(String.valueOf(arrayIndex));
121 * Static factory method for {@link Builder}. Example usage:
125 * JSONPointer pointer = JSONPointer.builder()
127 * .append("other~key").append("another/key")
134 * @return a builder instance which can be used to construct a
135 * {@code JSONPointer} instance by chained
136 * {@link Builder#append(String)} calls.
138 public static Builder builder()
140 return new Builder();
143 // Segments for the JSONPointer string
144 private final List<String> refTokens;
147 * Pre-parses and initializes a new {@code JSONPointer} instance. If you want
148 * to evaluate the same JSON Pointer on different JSON documents then it is
149 * recommended to keep the {@code JSONPointer} instances due to performance
153 * the JSON String or URI Fragment representation of the JSON
155 * @throws IllegalArgumentException
156 * if {@code pointer} is not a valid JSON pointer
158 public JSONPointer(final String pointer)
162 throw new NullPointerException("pointer cannot be null");
164 if (pointer.isEmpty() || pointer.equals("#"))
166 this.refTokens = Collections.emptyList();
170 if (pointer.startsWith("#/"))
172 refs = pointer.substring(2);
175 refs = URLDecoder.decode(refs, ENCODING);
176 } catch (UnsupportedEncodingException e)
178 throw new RuntimeException(e);
181 else if (pointer.startsWith("/"))
183 refs = pointer.substring(1);
187 throw new IllegalArgumentException(
188 "a JSON pointer should start with '/' or '#/'");
190 this.refTokens = new ArrayList<String>();
192 int prevSlashIdx = 0;
195 prevSlashIdx = slashIdx + 1;
196 slashIdx = refs.indexOf('/', prevSlashIdx);
197 if (prevSlashIdx == slashIdx || prevSlashIdx == refs.length())
199 // found 2 slashes in a row ( obj//next )
200 // or single slash at the end of a string ( obj/test/ )
201 this.refTokens.add("");
203 else if (slashIdx >= 0)
205 final String token = refs.substring(prevSlashIdx, slashIdx);
206 this.refTokens.add(unescape(token));
210 // last item after separator, or no separator at all.
211 final String token = refs.substring(prevSlashIdx);
212 this.refTokens.add(unescape(token));
214 } while (slashIdx >= 0);
215 // using split does not take into account consecutive separators or "ending
217 // for (String token : refs.split("/")) {
218 // this.refTokens.add(unescape(token));
222 public JSONPointer(List<String> refTokens)
224 this.refTokens = new ArrayList<String>(refTokens);
227 private String unescape(String token)
229 return token.replace("~1", "/").replace("~0", "~").replace("\\\"", "\"")
230 .replace("\\\\", "\\");
234 * Evaluates this JSON Pointer on the given {@code document}. The
235 * {@code document} is usually a {@link JSONObject} or a {@link JSONArray}
236 * instance, but the empty JSON Pointer ({@code ""}) can be evaluated on any
237 * JSON values and in such case the returned value will be {@code document}
241 * the JSON document which should be the subject of querying.
242 * @return the result of the evaluation
243 * @throws JSONPointerException
244 * if an error occurs during evaluation
246 public Object queryFrom(Object document) throws JSONPointerException
248 if (this.refTokens.isEmpty())
252 Object current = document;
253 for (String token : this.refTokens)
255 if (current instanceof JSONObject)
257 current = ((JSONObject) current).opt(unescape(token));
259 else if (current instanceof JSONArray)
261 current = readByIndexToken(current, token);
265 throw new JSONPointerException(format(
266 "value [%s] is not an array or object therefore its key %s cannot be resolved",
274 * Matches a JSONArray element by ordinal position
277 * the JSONArray to be evaluated
279 * the array index in string form
280 * @return the matched object. If no matching item is found a
281 * @throws JSONPointerException
282 * is thrown if the index is out of bounds
284 private Object readByIndexToken(Object current, String indexToken)
285 throws JSONPointerException
289 int index = Integer.parseInt(indexToken);
290 JSONArray currentArr = (JSONArray) current;
291 if (index >= currentArr.length())
293 throw new JSONPointerException(format(
294 "index %d is out of bounds - the array has %d elements",
295 index, currentArr.length()));
299 return currentArr.get(index);
300 } catch (JSONException e)
302 throw new JSONPointerException(
303 "Error reading value at index position " + index, e);
305 } catch (NumberFormatException e)
307 throw new JSONPointerException(
308 format("%s is not an array index", indexToken), e);
313 * Returns a string representing the JSONPointer path value using string
317 public String toString()
319 StringBuilder rval = new StringBuilder("");
320 for (String token : this.refTokens)
322 rval.append('/').append(escape(token));
324 return rval.toString();
328 * Escapes path segment values to an unambiguous form. The escape char to be
329 * inserted is '~'. The chars to be escaped are ~, which maps to ~0, and /,
330 * which maps to ~1. Backslashes and double quote chars are also escaped.
333 * the JSONPointer segment value to be escaped
334 * @return the escaped value for the token
336 private String escape(String token)
338 return token.replace("~", "~0").replace("/", "~1").replace("\\", "\\\\")
339 .replace("\"", "\\\"");
343 * Returns a string representing the JSONPointer path value using URI fragment
344 * identifier representation
346 public String toURIFragment()
350 StringBuilder rval = new StringBuilder("#");
351 for (String token : this.refTokens)
353 rval.append('/').append(URLEncoder.encode(token, ENCODING));
355 return rval.toString();
356 } catch (UnsupportedEncodingException e)
358 throw new RuntimeException(e);