();
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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;
+ }
+ }
+
+ /**
+ * 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();
+ }
+
+ // 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.
+ *
+ * @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));
+ //}
+ }
+
+ 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;
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+}