();
/**
* 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();
}
// 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(String pointer) {
if (pointer == null) {
throw new NullPointerException("pointer cannot be null");
}
if (pointer.isEmpty() || pointer.equals("#")) {
refTokens = Collections.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;
}
/**
* 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 JSONPointerException is thrown
*/
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);
}
}
/**
* Returns a string representing the JSONPointer path value using string representation
*/
@Override
public String toString() {
StringBuilder rval = new StringBuilder("");
for (String token : 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 : refTokens) {
rval.append('/').append(URLEncoder.encode(token, ENCODING));
}
return rval.toString();
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
}