From 564acf4eb11e33e449a884c99f622614e869b423 Mon Sep 17 00:00:00 2001 From: JimmyLiuJia <91264074+JimmyLiuJia@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:21:40 +0800 Subject: [PATCH] Preserve plus signs in JSONPointer URI fragments --- src/main/java/org/json/JSONPointer.java | 575 +++++------ .../java/org/json/junit/JSONPointerTest.java | 923 +++++++++--------- 2 files changed, 767 insertions(+), 731 deletions(-) diff --git a/src/main/java/org/json/JSONPointer.java b/src/main/java/org/json/JSONPointer.java index 83a0690d7..e06df2da5 100644 --- a/src/main/java/org/json/JSONPointer.java +++ b/src/main/java/org/json/JSONPointer.java @@ -1,287 +1,288 @@ -package org.json; - -import static java.lang.String.format; - -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/* -Public Domain. -*/ - -/** - * A JSON Pointer is a simple query language defined for JSON documents by - * RFC 6901. - * - * In a nutshell, JSONPointer allows the user to navigate into a JSON document - * using strings, and retrieve targeted objects, like a simple form of XPATH. - * Path segments are separated by the '/' char, which signifies the root of - * the document when it appears as the first char of the string. Array - * elements are navigated using ordinals, counting from 0. JSONPointer strings - * may be extended to any arbitrary number of segments. If the navigation - * is successful, the matched item is returned. A matched item may be a - * JSONObject, a JSONArray, or a JSON value. If the JSONPointer string building - * fails, an appropriate exception is thrown. If the navigation fails to find - * a match, a JSONPointerException is thrown. - * - * @author JSON.org - * @version 2016-05-14 - */ -public class JSONPointer { - - // used for URL encoding and decoding - private static final String ENCODING = "utf-8"; - - /** - * This class allows the user to build a JSONPointer in steps, using - * exactly one segment in each step. - */ - public static class Builder { - - /** - * Constructs a new Builder object. - */ - public Builder() { - } - - // Segments for the eventual JSONPointer string - private final List refTokens = new ArrayList(); - - /** - * Creates a {@code JSONPointer} instance using the tokens previously set using the - * {@link #append(String)} method calls. - * @return a JSONPointer object - */ - 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() || "#".equals(pointer)) { - 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)); - //} - } - - /** - * Constructs a new JSONPointer instance with the provided list of reference tokens. - * - * @param refTokens A list of strings representing the reference tokens for the JSON Pointer. - * Each token identifies a step in the path to the targeted value. - */ - public JSONPointer(List refTokens) { - this.refTokens = new ArrayList(refTokens); - } - - /** - * @see rfc6901 section 3 - */ - private static String unescape(String token) { - return token.replace("~1", "/").replace("~0", "~"); - } - - /** - * 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(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 static 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 %s is out of bounds - the array has %d elements", indexToken, - Integer.valueOf(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. - * @param token the JSONPointer segment value to be escaped - * @return the escaped value for the token - * - * @see rfc6901 section 3 - */ - private static String escape(String token) { - return token.replace("~", "~0") - .replace("/", "~1"); - } - - /** - * Returns a string representing the JSONPointer path value using URI - * fragment identifier representation - * @return a uri fragment string - */ - public String toURIFragment() { - try { - StringBuilder rval = new StringBuilder("#"); - for (String token : this.refTokens) { - rval.append('/').append(URLEncoder.encode(escape(token), ENCODING)); - } - return rval.toString(); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - } - -} +package org.json; + +import static java.lang.String.format; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/* +Public Domain. +*/ + +/** + * A JSON Pointer is a simple query language defined for JSON documents by + * RFC 6901. + * + * In a nutshell, JSONPointer allows the user to navigate into a JSON document + * using strings, and retrieve targeted objects, like a simple form of XPATH. + * Path segments are separated by the '/' char, which signifies the root of + * the document when it appears as the first char of the string. Array + * elements are navigated using ordinals, counting from 0. JSONPointer strings + * may be extended to any arbitrary number of segments. If the navigation + * is successful, the matched item is returned. A matched item may be a + * JSONObject, a JSONArray, or a JSON value. If the JSONPointer string building + * fails, an appropriate exception is thrown. If the navigation fails to find + * a match, a JSONPointerException is thrown. + * + * @author JSON.org + * @version 2016-05-14 + */ +public class JSONPointer { + + // used for URL encoding and decoding + private static final String ENCODING = "utf-8"; + + /** + * This class allows the user to build a JSONPointer in steps, using + * exactly one segment in each step. + */ + public static class Builder { + + /** + * Constructs a new Builder object. + */ + public Builder() { + } + + // Segments for the eventual JSONPointer string + private final List refTokens = new ArrayList(); + + /** + * Creates a {@code JSONPointer} instance using the tokens previously set using the + * {@link #append(String)} method calls. + * @return a JSONPointer object + */ + 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() || "#".equals(pointer)) { + this.refTokens = Collections.emptyList(); + return; + } + String refs; + if (pointer.startsWith("#/")) { + refs = pointer.substring(2); + try { + // URI fragments preserve plus signs rather than treating them as form-encoded spaces. + refs = URLDecoder.decode(refs.replace("+", "%2B"), 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)); + //} + } + + /** + * Constructs a new JSONPointer instance with the provided list of reference tokens. + * + * @param refTokens A list of strings representing the reference tokens for the JSON Pointer. + * Each token identifies a step in the path to the targeted value. + */ + public JSONPointer(List refTokens) { + this.refTokens = new ArrayList(refTokens); + } + + /** + * @see rfc6901 section 3 + */ + private static String unescape(String token) { + return token.replace("~1", "/").replace("~0", "~"); + } + + /** + * 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(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 static 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 %s is out of bounds - the array has %d elements", indexToken, + Integer.valueOf(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. + * @param token the JSONPointer segment value to be escaped + * @return the escaped value for the token + * + * @see rfc6901 section 3 + */ + private static String escape(String token) { + return token.replace("~", "~0") + .replace("/", "~1"); + } + + /** + * Returns a string representing the JSONPointer path value using URI + * fragment identifier representation + * @return a uri fragment string + */ + public String toURIFragment() { + try { + StringBuilder rval = new StringBuilder("#"); + for (String token : this.refTokens) { + rval.append('/').append(URLEncoder.encode(escape(token), ENCODING).replace("+", "%20")); + } + return rval.toString(); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/test/java/org/json/junit/JSONPointerTest.java b/src/test/java/org/json/junit/JSONPointerTest.java index 7095d45da..9275efab0 100644 --- a/src/test/java/org/json/junit/JSONPointerTest.java +++ b/src/test/java/org/json/junit/JSONPointerTest.java @@ -1,444 +1,479 @@ -package org.json.junit; - -/* -Public Domain. -*/ - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import java.io.InputStream; - -import org.json.JSONArray; -import org.json.JSONObject; -import org.json.JSONPointer; -import org.json.JSONPointerException; -import org.json.JSONTokener; -import org.junit.Test; - -public class JSONPointerTest { - - private static final JSONObject document; - private static final String EXPECTED_COMPLETE_DOCUMENT = "{\"\":0,\" \":7,\"g|h\":4,\"c%d\":2,\"k\\\"l\":6,\"a/b\":1,\"i\\\\j\":5," + - "\"obj\":{\"\":{\"\":\"empty key of an object with an empty key\",\"subKey\":\"Some other value\"}," + - "\"other~key\":{\"another/key\":[\"val\"]},\"key\":\"value\"},\"foo\":[\"bar\",\"baz\"],\"e^f\":3," + - "\"m~n\":8}"; - - - static { - @SuppressWarnings("resource") - InputStream resourceAsStream = JSONPointerTest.class.getClassLoader().getResourceAsStream("jsonpointer-testdoc.json"); - if(resourceAsStream == null) { - throw new ExceptionInInitializerError("Unable to locate test file. Please check your development environment configuration"); - } - document = new JSONObject(new JSONTokener(resourceAsStream)); - } - - private Object query(String pointer) { - return new JSONPointer(pointer).queryFrom(document); - } - - @Test - public void emptyPointer() { - assertTrue(new JSONObject(EXPECTED_COMPLETE_DOCUMENT).similar(query(""))); - } - - @SuppressWarnings("unused") - @Test(expected = NullPointerException.class) - public void nullPointer() { - new JSONPointer((String) null); - } - - @Test - public void objectPropertyQuery() { - assertEquals("[\"bar\",\"baz\"]", query("/foo").toString()); - } - - @Test - public void arrayIndexQuery() { - assertEquals("bar", query("/foo/0")); - } - - @Test(expected = JSONPointerException.class) - public void stringPropOfArrayFailure() { - query("/foo/bar"); - } - - @Test - public void queryByEmptyKey() { - assertEquals(0, query("/")); - } - - @Test - public void queryByEmptyKeySubObject() { - JSONObject json = new JSONObject("{\"\":\"empty key of an object with an empty key\",\"subKey\":\"Some" + - " other value\"}"); - JSONObject obj = (JSONObject) query("/obj/"); - assertTrue(json.similar(obj)); - } - - @Test - public void queryByEmptyKeySubObjectSubOject() { - assertEquals("empty key of an object with an empty key", query("/obj//")); - } - - @Test - public void queryByEmptyKeySubObjectValue() { - assertEquals("Some other value", query("/obj//subKey")); - } - - @Test - public void slashEscaping() { - assertEquals(1, query("/a~1b")); - } - - @Test - public void tildeEscaping() { - assertEquals(8, query("/m~0n")); - } - - @Test - public void escapeSequencesInObjectKeys() { - JSONObject object = new JSONObject().put("~1", "tilde one").put("/", "slash") - .put("~0", "tilde zero").put("~", "tilde"); - assertEquals("tilde one", object.query("/~01")); - assertEquals("tilde zero", object.query("/~00")); - assertEquals("tilde one", object.query("#/~01")); - assertEquals("tilde zero", object.query("#/~00")); - } - - @Test - public void builderPreservesLiteralTokens() { - JSONObject object = new JSONObject().put("~1", "tilde one").put("/", "slash") - .put("~0", "tilde zero").put("~", "tilde"); - assertEquals("tilde one", JSONPointer.builder().append("~1").build().queryFrom(object)); - assertEquals("tilde zero", JSONPointer.builder().append("~0").build().queryFrom(object)); - } - - @Test - public void tokenListPreservesLiteralTokens() { - JSONObject object = new JSONObject().put("~1", "tilde one").put("/", "slash") - .put("~0", "tilde zero").put("~", "tilde"); - assertEquals("tilde one", new JSONPointer(java.util.Arrays.asList("~1")).queryFrom(object)); - assertEquals("tilde zero", new JSONPointer(java.util.Arrays.asList("~0")).queryFrom(object)); - } - - /** - * We pass backslashes as-is - * - * @see rfc6901 section 3 - */ - @Test - public void backslashHandling() { - assertEquals(5, query("/i\\j")); - } - - /** - * We pass quotations as-is - * - * @see rfc6901 section 3 - */ - @Test - public void quotationHandling() { - assertEquals(6, query("/k\"l")); - } - - @Test - public void whitespaceKey() { - assertEquals(7, query("/ ")); - } - - @Test - public void uriFragmentNotation() { - assertEquals("[\"bar\",\"baz\"]", query("#/foo").toString()); - } - - @Test - public void uriFragmentNotationRoot() { - assertTrue(new JSONObject(EXPECTED_COMPLETE_DOCUMENT).similar(query("#"))); - } - - @Test - public void uriFragmentPercentHandling() { - assertEquals(2, query("#/c%25d")); - assertEquals(3, query("#/e%5Ef")); - assertEquals(4, query("#/g%7Ch")); - assertEquals(8, query("#/m~0n")); - } - - @SuppressWarnings("unused") - @Test(expected = IllegalArgumentException.class) - public void syntaxError() { - new JSONPointer("key"); - } - - @Test(expected = JSONPointerException.class) - public void arrayIndexFailure() { - query("/foo/2"); - } - - @Test(expected = JSONPointerException.class) - public void primitiveFailure() { - query("/obj/key/failure"); - } - - @Test - public void builderTest() { - JSONPointer pointer = JSONPointer.builder() - .append("obj") - .append("other~key").append("another/key") - .append(0) - .build(); - assertEquals("val", pointer.queryFrom(document)); - } - - @Test(expected = NullPointerException.class) - public void nullToken() { - JSONPointer.builder().append(null); - } - - @Test - public void toStringEscaping() { - JSONPointer pointer = JSONPointer.builder() - .append("obj") - .append("other~key").append("another/key") - .append("\"") - .append(0) - .build(); - assertEquals("/obj/other~0key/another~1key/\"/0", pointer.toString()); - } - - @Test - public void emptyPointerToString() { - assertEquals("", new JSONPointer("").toString()); - } - - @Test - public void toURIFragment() { - assertEquals("#/c%25d", new JSONPointer("/c%d").toURIFragment()); - assertEquals("#/e%5Ef", new JSONPointer("/e^f").toURIFragment()); - assertEquals("#/g%7Ch", new JSONPointer("/g|h").toURIFragment()); - assertEquals("#/m%7E0n", new JSONPointer("/m~n").toURIFragment()); - } - - @Test - public void toURIFragmentEscaping() { - JSONPointer pointer = JSONPointer.builder() - .append("obj") - .append("other~key").append("another/key") - .append(0) - .build(); - assertEquals("#/obj/other%7E0key/another%7E1key/0", pointer.toURIFragment()); - assertEquals("val", new JSONPointer(pointer.toURIFragment()).queryFrom(document)); - } - - @Test - public void toURIFragmentRoundTrip() { - JSONObject object = new JSONObject().put("~1", "tilde one").put("/", "slash"); - JSONPointer tildeOne = JSONPointer.builder().append("~1").build(); - JSONPointer slash = JSONPointer.builder().append("/").build(); - assertEquals("tilde one", object.query(tildeOne.toURIFragment())); - assertEquals("slash", object.query(slash.toURIFragment())); - } - - @Test - public void tokenListIsCopiedInConstructor() { - JSONPointer.Builder b = JSONPointer.builder().append("key1"); - JSONPointer jp1 = b.build(); - b.append("key2"); - JSONPointer jp2 = b.build(); - if(jp1.toString().equals(jp2.toString())) { - fail("Oops, my pointers are sharing a backing array"); - } - } - - /** - * Coverage for JSONObject query(String) - */ - @Test - public void queryFromJSONObject() { - String str = "{"+ - "\"stringKey\":\"hello world!\","+ - "\"arrayKey\":[0,1,2],"+ - "\"objectKey\": {"+ - "\"a\":\"aVal\","+ - "\"b\":\"bVal\""+ - "}"+ - "}"; - JSONObject jsonObject = new JSONObject(str); - Object obj = jsonObject.query("/stringKey"); - assertTrue("Expected 'hello world!'", "hello world!".equals(obj)); - obj = jsonObject.query("/arrayKey/1"); - assertTrue("Expected 1", Integer.valueOf(1).equals(obj)); - obj = jsonObject.query("/objectKey/b"); - assertTrue("Expected bVal", "bVal".equals(obj)); - try { - obj = jsonObject.query("/a/b/c"); - assertTrue("Expected JSONPointerException", false); - } catch (JSONPointerException e) { - assertTrue("Expected bad key/value exception", - "value [null] is not an array or object therefore its key b cannot be resolved". - equals(e.getMessage())); - } - } - - /** - * Coverage for JSONObject query(JSONPointer) - */ - @Test - public void queryFromJSONObjectUsingPointer() { - String str = "{"+ - "\"stringKey\":\"hello world!\","+ - "\"arrayKey\":[0,1,2],"+ - "\"objectKey\": {"+ - "\"a\":\"aVal\","+ - "\"b\":\"bVal\""+ - "}"+ - "}"; - JSONObject jsonObject = new JSONObject(str); - Object obj = jsonObject.query(new JSONPointer("/stringKey")); - assertTrue("Expected 'hello world!'", "hello world!".equals(obj)); - obj = jsonObject.query(new JSONPointer("/arrayKey/1")); - assertTrue("Expected 1", Integer.valueOf(1).equals(obj)); - obj = jsonObject.query(new JSONPointer("/objectKey/b")); - assertTrue("Expected bVal", "bVal".equals(obj)); - try { - obj = jsonObject.query(new JSONPointer("/a/b/c")); - assertTrue("Expected JSONPointerException", false); - } catch (JSONPointerException e) { - assertTrue("Expected bad key/value exception", - "value [null] is not an array or object therefore its key b cannot be resolved". - equals(e.getMessage())); - } - } - - /** - * Coverage for JSONObject optQuery(JSONPointer) - */ - @Test - public void optQueryFromJSONObjectUsingPointer() { - String str = "{"+ - "\"stringKey\":\"hello world!\","+ - "\"arrayKey\":[0,1,2],"+ - "\"objectKey\": {"+ - "\"a\":\"aVal\","+ - "\"b\":\"bVal\""+ - "}"+ - "}"; - JSONObject jsonObject = new JSONObject(str); - Object obj = jsonObject.optQuery(new JSONPointer("/stringKey")); - assertTrue("Expected 'hello world!'", "hello world!".equals(obj)); - obj = jsonObject.optQuery(new JSONPointer("/arrayKey/1")); - assertTrue("Expected 1", Integer.valueOf(1).equals(obj)); - obj = jsonObject.optQuery(new JSONPointer("/objectKey/b")); - assertTrue("Expected bVal", "bVal".equals(obj)); - obj = jsonObject.optQuery(new JSONPointer("/a/b/c")); - assertTrue("Expected null", obj == null); - } - - /** - * Coverage for JSONArray query(String) - */ - @Test - public void queryFromJSONArray() { - String str = "["+ - "\"hello world!\","+ - "[0,1,2],"+ - "{"+ - "\"a\":\"aVal\","+ - "\"b\":\"bVal\""+ - "}"+ - "]"; - JSONArray jsonArray = new JSONArray(str); - Object obj = jsonArray.query("/0"); - assertTrue("Expected 'hello world!'", "hello world!".equals(obj)); - obj = jsonArray.query("/1/1"); - assertTrue("Expected 1", Integer.valueOf(1).equals(obj)); - obj = jsonArray.query("/2/b"); - assertTrue("Expected bVal", "bVal".equals(obj)); - try { - obj = jsonArray.query("/a/b/c"); - assertTrue("Expected JSONPointerException", false); - } catch (JSONPointerException e) { - assertTrue("Expected bad index exception", - "a is not an array index".equals(e.getMessage())); - } - } - - /** - * Coverage for JSONArray query(JSONPointer) - */ - @Test - public void queryFromJSONArrayUsingPointer() { - String str = "["+ - "\"hello world!\","+ - "[0,1,2],"+ - "{"+ - "\"a\":\"aVal\","+ - "\"b\":\"bVal\""+ - "}"+ - "]"; - JSONArray jsonArray = new JSONArray(str); - Object obj = jsonArray.query(new JSONPointer("/0")); - assertTrue("Expected 'hello world!'", "hello world!".equals(obj)); - obj = jsonArray.query(new JSONPointer("/1/1")); - assertTrue("Expected 1", Integer.valueOf(1).equals(obj)); - obj = jsonArray.query(new JSONPointer("/2/b")); - assertTrue("Expected bVal", "bVal".equals(obj)); - try { - obj = jsonArray.query(new JSONPointer("/a/b/c")); - assertTrue("Expected JSONPointerException", false); - } catch (JSONPointerException e) { - assertTrue("Expected bad index exception", - "a is not an array index".equals(e.getMessage())); - } - } - - /** - * Coverage for JSONArray optQuery(JSONPointer) - */ - @Test - public void optQueryFromJSONArrayUsingPointer() { - String str = "["+ - "\"hello world!\","+ - "[0,1,2],"+ - "{"+ - "\"a\":\"aVal\","+ - "\"b\":\"bVal\""+ - "}"+ - "]"; - JSONArray jsonArray = new JSONArray(str); - Object obj = jsonArray.optQuery(new JSONPointer("/0")); - assertTrue("Expected 'hello world!'", "hello world!".equals(obj)); - obj = jsonArray.optQuery(new JSONPointer("/1/1")); - assertTrue("Expected 1", Integer.valueOf(1).equals(obj)); - obj = jsonArray.optQuery(new JSONPointer("/2/b")); - assertTrue("Expected bVal", "bVal".equals(obj)); - obj = jsonArray.optQuery(new JSONPointer("/a/b/c")); - assertTrue("Expected null", obj == null); - } - - /** - * When creating a jsonObject we need to parse escaped characters "\\\\" - * --> it's the string representation of "\\", so when query'ing via the JSONPointer - * we DON'T escape them - * - */ - @Test - public void queryFromJSONObjectUsingPointer0() { - String str = "{"+ - "\"string\\\\\\\\Key\":\"hello world!\","+ - - "\"\\\\\":\"slash test\"" + - "}"; - JSONObject jsonObject = new JSONObject(str); - //Summary of issue: When a KEY in the jsonObject is "\\\\" --> it's held - // as "\\" which means when querying, we need to use "\\" - Object twoBackslahObj = jsonObject.optQuery(new JSONPointer("/\\")); - assertEquals("slash test", twoBackslahObj); - - Object fourBackslashObj = jsonObject.optQuery(new JSONPointer("/string\\\\Key")); - assertEquals("hello world!", fourBackslashObj); - } -} +package org.json.junit; + +/* +Public Domain. +*/ + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.InputStream; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.JSONPointer; +import org.json.JSONPointerException; +import org.json.JSONTokener; +import org.junit.Test; + +public class JSONPointerTest { + + private static final JSONObject document; + private static final String EXPECTED_COMPLETE_DOCUMENT = "{\"\":0,\" \":7,\"g|h\":4,\"c%d\":2,\"k\\\"l\":6,\"a/b\":1,\"i\\\\j\":5," + + "\"obj\":{\"\":{\"\":\"empty key of an object with an empty key\",\"subKey\":\"Some other value\"}," + + "\"other~key\":{\"another/key\":[\"val\"]},\"key\":\"value\"},\"foo\":[\"bar\",\"baz\"],\"e^f\":3," + + "\"m~n\":8}"; + + + static { + @SuppressWarnings("resource") + InputStream resourceAsStream = JSONPointerTest.class.getClassLoader().getResourceAsStream("jsonpointer-testdoc.json"); + if(resourceAsStream == null) { + throw new ExceptionInInitializerError("Unable to locate test file. Please check your development environment configuration"); + } + document = new JSONObject(new JSONTokener(resourceAsStream)); + } + + private Object query(String pointer) { + return new JSONPointer(pointer).queryFrom(document); + } + + @Test + public void emptyPointer() { + assertTrue(new JSONObject(EXPECTED_COMPLETE_DOCUMENT).similar(query(""))); + } + + @SuppressWarnings("unused") + @Test(expected = NullPointerException.class) + public void nullPointer() { + new JSONPointer((String) null); + } + + @Test + public void objectPropertyQuery() { + assertEquals("[\"bar\",\"baz\"]", query("/foo").toString()); + } + + @Test + public void arrayIndexQuery() { + assertEquals("bar", query("/foo/0")); + } + + @Test(expected = JSONPointerException.class) + public void stringPropOfArrayFailure() { + query("/foo/bar"); + } + + @Test + public void queryByEmptyKey() { + assertEquals(0, query("/")); + } + + @Test + public void queryByEmptyKeySubObject() { + JSONObject json = new JSONObject("{\"\":\"empty key of an object with an empty key\",\"subKey\":\"Some" + + " other value\"}"); + JSONObject obj = (JSONObject) query("/obj/"); + assertTrue(json.similar(obj)); + } + + @Test + public void queryByEmptyKeySubObjectSubOject() { + assertEquals("empty key of an object with an empty key", query("/obj//")); + } + + @Test + public void queryByEmptyKeySubObjectValue() { + assertEquals("Some other value", query("/obj//subKey")); + } + + @Test + public void slashEscaping() { + assertEquals(1, query("/a~1b")); + } + + @Test + public void tildeEscaping() { + assertEquals(8, query("/m~0n")); + } + + @Test + public void escapeSequencesInObjectKeys() { + JSONObject object = new JSONObject().put("~1", "tilde one").put("/", "slash") + .put("~0", "tilde zero").put("~", "tilde"); + assertEquals("tilde one", object.query("/~01")); + assertEquals("tilde zero", object.query("/~00")); + assertEquals("tilde one", object.query("#/~01")); + assertEquals("tilde zero", object.query("#/~00")); + } + + @Test + public void builderPreservesLiteralTokens() { + JSONObject object = new JSONObject().put("~1", "tilde one").put("/", "slash") + .put("~0", "tilde zero").put("~", "tilde"); + assertEquals("tilde one", JSONPointer.builder().append("~1").build().queryFrom(object)); + assertEquals("tilde zero", JSONPointer.builder().append("~0").build().queryFrom(object)); + } + + @Test + public void tokenListPreservesLiteralTokens() { + JSONObject object = new JSONObject().put("~1", "tilde one").put("/", "slash") + .put("~0", "tilde zero").put("~", "tilde"); + assertEquals("tilde one", new JSONPointer(java.util.Arrays.asList("~1")).queryFrom(object)); + assertEquals("tilde zero", new JSONPointer(java.util.Arrays.asList("~0")).queryFrom(object)); + } + + /** + * We pass backslashes as-is + * + * @see rfc6901 section 3 + */ + @Test + public void backslashHandling() { + assertEquals(5, query("/i\\j")); + } + + /** + * We pass quotations as-is + * + * @see rfc6901 section 3 + */ + @Test + public void quotationHandling() { + assertEquals(6, query("/k\"l")); + } + + @Test + public void whitespaceKey() { + assertEquals(7, query("/ ")); + } + + @Test + public void uriFragmentNotation() { + assertEquals("[\"bar\",\"baz\"]", query("#/foo").toString()); + } + + @Test + public void uriFragmentNotationRoot() { + assertTrue(new JSONObject(EXPECTED_COMPLETE_DOCUMENT).similar(query("#"))); + } + + @Test + public void uriFragmentPercentHandling() { + assertEquals(2, query("#/c%25d")); + assertEquals(3, query("#/e%5Ef")); + assertEquals(4, query("#/g%7Ch")); + assertEquals(8, query("#/m~0n")); + } + + @Test + public void uriFragmentPreservesLiteralPlus() { + JSONObject object = new JSONObject().put("a+b", "plus").put("a b", "space") + .put("a%2Bb", "percent"); + assertEquals("plus", object.query("#/a+b")); + assertEquals("plus", object.query("#/a%2Bb")); + assertEquals("space", object.query("#/a%20b")); + assertEquals("percent", object.query("#/a%252Bb")); + assertEquals("plus", object.query("/a+b")); + } + + @Test + public void uriFragmentEncodesSpaces() { + JSONObject object = new JSONObject().put("a+b", "plus").put("a b", "space"); + JSONPointer plus = JSONPointer.builder().append("a+b").build(); + JSONPointer space = JSONPointer.builder().append("a b").build(); + assertEquals("#/a%2Bb", plus.toURIFragment()); + assertEquals("#/a%20b", space.toURIFragment()); + assertEquals("#/%20", new JSONPointer("/ ").toURIFragment()); + assertEquals("plus", object.query(plus.toURIFragment())); + assertEquals("space", object.query(space.toURIFragment())); + } + + @Test + public void uriFragmentPreservesUtf8AndPointerEscapes() { + String key = "\u4e2d +/~1"; + JSONObject object = new JSONObject().put(key, "expected") + .put("\u4e2d /~1", "wrong space"); + assertEquals("expected", object.query("#/%E4%B8%AD%20+~1~01")); + assertEquals("expected", object.query("#/%E4%B8%AD%20%2B%7E1%7E01")); + JSONPointer pointer = JSONPointer.builder().append(key).build(); + assertEquals("#/%E4%B8%AD%20%2B%7E1%7E01", pointer.toURIFragment()); + assertEquals("expected", object.query(pointer.toURIFragment())); + } + + @SuppressWarnings("unused") + @Test(expected = IllegalArgumentException.class) + public void syntaxError() { + new JSONPointer("key"); + } + + @Test(expected = JSONPointerException.class) + public void arrayIndexFailure() { + query("/foo/2"); + } + + @Test(expected = JSONPointerException.class) + public void primitiveFailure() { + query("/obj/key/failure"); + } + + @Test + public void builderTest() { + JSONPointer pointer = JSONPointer.builder() + .append("obj") + .append("other~key").append("another/key") + .append(0) + .build(); + assertEquals("val", pointer.queryFrom(document)); + } + + @Test(expected = NullPointerException.class) + public void nullToken() { + JSONPointer.builder().append(null); + } + + @Test + public void toStringEscaping() { + JSONPointer pointer = JSONPointer.builder() + .append("obj") + .append("other~key").append("another/key") + .append("\"") + .append(0) + .build(); + assertEquals("/obj/other~0key/another~1key/\"/0", pointer.toString()); + } + + @Test + public void emptyPointerToString() { + assertEquals("", new JSONPointer("").toString()); + } + + @Test + public void toURIFragment() { + assertEquals("#/c%25d", new JSONPointer("/c%d").toURIFragment()); + assertEquals("#/e%5Ef", new JSONPointer("/e^f").toURIFragment()); + assertEquals("#/g%7Ch", new JSONPointer("/g|h").toURIFragment()); + assertEquals("#/m%7E0n", new JSONPointer("/m~n").toURIFragment()); + } + + @Test + public void toURIFragmentEscaping() { + JSONPointer pointer = JSONPointer.builder() + .append("obj") + .append("other~key").append("another/key") + .append(0) + .build(); + assertEquals("#/obj/other%7E0key/another%7E1key/0", pointer.toURIFragment()); + assertEquals("val", new JSONPointer(pointer.toURIFragment()).queryFrom(document)); + } + + @Test + public void toURIFragmentRoundTrip() { + JSONObject object = new JSONObject().put("~1", "tilde one").put("/", "slash"); + JSONPointer tildeOne = JSONPointer.builder().append("~1").build(); + JSONPointer slash = JSONPointer.builder().append("/").build(); + assertEquals("tilde one", object.query(tildeOne.toURIFragment())); + assertEquals("slash", object.query(slash.toURIFragment())); + } + + @Test + public void tokenListIsCopiedInConstructor() { + JSONPointer.Builder b = JSONPointer.builder().append("key1"); + JSONPointer jp1 = b.build(); + b.append("key2"); + JSONPointer jp2 = b.build(); + if(jp1.toString().equals(jp2.toString())) { + fail("Oops, my pointers are sharing a backing array"); + } + } + + /** + * Coverage for JSONObject query(String) + */ + @Test + public void queryFromJSONObject() { + String str = "{"+ + "\"stringKey\":\"hello world!\","+ + "\"arrayKey\":[0,1,2],"+ + "\"objectKey\": {"+ + "\"a\":\"aVal\","+ + "\"b\":\"bVal\""+ + "}"+ + "}"; + JSONObject jsonObject = new JSONObject(str); + Object obj = jsonObject.query("/stringKey"); + assertTrue("Expected 'hello world!'", "hello world!".equals(obj)); + obj = jsonObject.query("/arrayKey/1"); + assertTrue("Expected 1", Integer.valueOf(1).equals(obj)); + obj = jsonObject.query("/objectKey/b"); + assertTrue("Expected bVal", "bVal".equals(obj)); + try { + obj = jsonObject.query("/a/b/c"); + assertTrue("Expected JSONPointerException", false); + } catch (JSONPointerException e) { + assertTrue("Expected bad key/value exception", + "value [null] is not an array or object therefore its key b cannot be resolved". + equals(e.getMessage())); + } + } + + /** + * Coverage for JSONObject query(JSONPointer) + */ + @Test + public void queryFromJSONObjectUsingPointer() { + String str = "{"+ + "\"stringKey\":\"hello world!\","+ + "\"arrayKey\":[0,1,2],"+ + "\"objectKey\": {"+ + "\"a\":\"aVal\","+ + "\"b\":\"bVal\""+ + "}"+ + "}"; + JSONObject jsonObject = new JSONObject(str); + Object obj = jsonObject.query(new JSONPointer("/stringKey")); + assertTrue("Expected 'hello world!'", "hello world!".equals(obj)); + obj = jsonObject.query(new JSONPointer("/arrayKey/1")); + assertTrue("Expected 1", Integer.valueOf(1).equals(obj)); + obj = jsonObject.query(new JSONPointer("/objectKey/b")); + assertTrue("Expected bVal", "bVal".equals(obj)); + try { + obj = jsonObject.query(new JSONPointer("/a/b/c")); + assertTrue("Expected JSONPointerException", false); + } catch (JSONPointerException e) { + assertTrue("Expected bad key/value exception", + "value [null] is not an array or object therefore its key b cannot be resolved". + equals(e.getMessage())); + } + } + + /** + * Coverage for JSONObject optQuery(JSONPointer) + */ + @Test + public void optQueryFromJSONObjectUsingPointer() { + String str = "{"+ + "\"stringKey\":\"hello world!\","+ + "\"arrayKey\":[0,1,2],"+ + "\"objectKey\": {"+ + "\"a\":\"aVal\","+ + "\"b\":\"bVal\""+ + "}"+ + "}"; + JSONObject jsonObject = new JSONObject(str); + Object obj = jsonObject.optQuery(new JSONPointer("/stringKey")); + assertTrue("Expected 'hello world!'", "hello world!".equals(obj)); + obj = jsonObject.optQuery(new JSONPointer("/arrayKey/1")); + assertTrue("Expected 1", Integer.valueOf(1).equals(obj)); + obj = jsonObject.optQuery(new JSONPointer("/objectKey/b")); + assertTrue("Expected bVal", "bVal".equals(obj)); + obj = jsonObject.optQuery(new JSONPointer("/a/b/c")); + assertTrue("Expected null", obj == null); + } + + /** + * Coverage for JSONArray query(String) + */ + @Test + public void queryFromJSONArray() { + String str = "["+ + "\"hello world!\","+ + "[0,1,2],"+ + "{"+ + "\"a\":\"aVal\","+ + "\"b\":\"bVal\""+ + "}"+ + "]"; + JSONArray jsonArray = new JSONArray(str); + Object obj = jsonArray.query("/0"); + assertTrue("Expected 'hello world!'", "hello world!".equals(obj)); + obj = jsonArray.query("/1/1"); + assertTrue("Expected 1", Integer.valueOf(1).equals(obj)); + obj = jsonArray.query("/2/b"); + assertTrue("Expected bVal", "bVal".equals(obj)); + try { + obj = jsonArray.query("/a/b/c"); + assertTrue("Expected JSONPointerException", false); + } catch (JSONPointerException e) { + assertTrue("Expected bad index exception", + "a is not an array index".equals(e.getMessage())); + } + } + + /** + * Coverage for JSONArray query(JSONPointer) + */ + @Test + public void queryFromJSONArrayUsingPointer() { + String str = "["+ + "\"hello world!\","+ + "[0,1,2],"+ + "{"+ + "\"a\":\"aVal\","+ + "\"b\":\"bVal\""+ + "}"+ + "]"; + JSONArray jsonArray = new JSONArray(str); + Object obj = jsonArray.query(new JSONPointer("/0")); + assertTrue("Expected 'hello world!'", "hello world!".equals(obj)); + obj = jsonArray.query(new JSONPointer("/1/1")); + assertTrue("Expected 1", Integer.valueOf(1).equals(obj)); + obj = jsonArray.query(new JSONPointer("/2/b")); + assertTrue("Expected bVal", "bVal".equals(obj)); + try { + obj = jsonArray.query(new JSONPointer("/a/b/c")); + assertTrue("Expected JSONPointerException", false); + } catch (JSONPointerException e) { + assertTrue("Expected bad index exception", + "a is not an array index".equals(e.getMessage())); + } + } + + /** + * Coverage for JSONArray optQuery(JSONPointer) + */ + @Test + public void optQueryFromJSONArrayUsingPointer() { + String str = "["+ + "\"hello world!\","+ + "[0,1,2],"+ + "{"+ + "\"a\":\"aVal\","+ + "\"b\":\"bVal\""+ + "}"+ + "]"; + JSONArray jsonArray = new JSONArray(str); + Object obj = jsonArray.optQuery(new JSONPointer("/0")); + assertTrue("Expected 'hello world!'", "hello world!".equals(obj)); + obj = jsonArray.optQuery(new JSONPointer("/1/1")); + assertTrue("Expected 1", Integer.valueOf(1).equals(obj)); + obj = jsonArray.optQuery(new JSONPointer("/2/b")); + assertTrue("Expected bVal", "bVal".equals(obj)); + obj = jsonArray.optQuery(new JSONPointer("/a/b/c")); + assertTrue("Expected null", obj == null); + } + + /** + * When creating a jsonObject we need to parse escaped characters "\\\\" + * --> it's the string representation of "\\", so when query'ing via the JSONPointer + * we DON'T escape them + * + */ + @Test + public void queryFromJSONObjectUsingPointer0() { + String str = "{"+ + "\"string\\\\\\\\Key\":\"hello world!\","+ + + "\"\\\\\":\"slash test\"" + + "}"; + JSONObject jsonObject = new JSONObject(str); + //Summary of issue: When a KEY in the jsonObject is "\\\\" --> it's held + // as "\\" which means when querying, we need to use "\\" + Object twoBackslahObj = jsonObject.optQuery(new JSONPointer("/\\")); + assertEquals("slash test", twoBackslahObj); + + Object fourBackslashObj = jsonObject.optQuery(new JSONPointer("/string\\\\Key")); + assertEquals("hello world!", fourBackslashObj); + } +}