Implemented JsonPointer
Project: http://git-wip-us.apache.org/repos/asf/johnzon/repo Commit: http://git-wip-us.apache.org/repos/asf/johnzon/commit/45faea0e Tree: http://git-wip-us.apache.org/repos/asf/johnzon/tree/45faea0e Diff: http://git-wip-us.apache.org/repos/asf/johnzon/diff/45faea0e Branch: refs/heads/master Commit: 45faea0eb3e04f7adbe0724a448eb0cb62a4f258 Parents: d8a56d0 Author: Armin Hasler <[email protected]> Authored: Wed Jul 6 17:06:26 2016 +0200 Committer: Mark Struberg <[email protected]> Committed: Tue Nov 22 21:28:49 2016 +0100 ---------------------------------------------------------------------- .../org/apache/johnzon/core/JsonPointer.java | 308 +++++++++++++++++++ .../apache/johnzon/core/JsonPointerTest.java | 197 ++++++++++++ .../test/resources/json/jsonPointerTest.json | 15 + 3 files changed, 520 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/johnzon/blob/45faea0e/johnzon-core/src/main/java/org/apache/johnzon/core/JsonPointer.java ---------------------------------------------------------------------- diff --git a/johnzon-core/src/main/java/org/apache/johnzon/core/JsonPointer.java b/johnzon-core/src/main/java/org/apache/johnzon/core/JsonPointer.java new file mode 100644 index 0000000..af228be --- /dev/null +++ b/johnzon-core/src/main/java/org/apache/johnzon/core/JsonPointer.java @@ -0,0 +1,308 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.johnzon.core; + +import javax.json.JsonArray; +import javax.json.JsonException; +import javax.json.JsonObject; +import javax.json.JsonStructure; +import javax.json.JsonValue; + +/** + * <p>This class is an immutable representation of a JSON Pointer as specified in + * <a href="http://tools.ietf.org/html/rfc6901">RFC 6901</a>. + * </p> + * <p> A JSON Pointer, when applied to a target {@link JsonValue}, + * defines a reference location in the target.</p> + * <p> An empty JSON Pointer string defines a reference to the target itself.</p> + * <p> If the JSON Pointer string is non-empty, it must be a sequence + * of '/' prefixed tokens, and the target must either be a {@link JsonArray} + * or {@link JsonObject}. If the target is a {@code JsonArray}, the pointer + * defines a reference to an array element, and the last token specifies the index. + * If the target is a {@link JsonObject}, the pointer defines a reference to a + * name/value pair, and the last token specifies the name. + * </p> + * <p> The method {@link JsonPointer#getValue getValue()} returns the referenced value. + * The methods {@link JsonPointer#add add()}, {@link JsonPointer#replace replace()}, + * and {@link JsonPointer#remove remove()} executes the operations specified in + * <a href="http://tools.ietf.org/html/rfc6902">RFC 6902</a>. </p> + * + * @since 1.1 + */ + +public class JsonPointer { + + private final String jsonPointer; + private final String[] referenceTokens; + + /** + * Constructs and initializes a JsonPointer. + * + * @param jsonPointer the JSON Pointer string + * @throws NullPointerException if {@code jsonPointer} is {@code null} + * @throws JsonException if {@code jsonPointer} is not a valid JSON Pointer + */ + public JsonPointer(String jsonPointer) { + if (jsonPointer == null) { + throw new NullPointerException("jsonPointer must not be null"); + } + if (!jsonPointer.equals("") && !jsonPointer.startsWith("/")) { + throw new JsonException("A non-empty JSON pointer must begin with a '/'"); + } + + this.jsonPointer = jsonPointer; + referenceTokens = jsonPointer.split("/", -1); + } + + /** + * Compares this {@code JsonPointer} with another object. + * + * @param obj the object to compare this {@code JsonPointer} against + * @return true if the given object is a {@code JsonPointer} with the same + * reference tokens as this one, false otherwise. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + JsonPointer that = (JsonPointer) obj; + return jsonPointer.equals(that.jsonPointer); + } + + /** + * Returns the hash code value for this {@code JsonPointer} object. + * The hash code of this object is defined by the hash codes of it's reference tokens. + * + * @return the hash code value for this {@code JsonPointer} object + */ + @Override + public int hashCode() { + return jsonPointer.hashCode(); + } + + /** + * Returns the value at the referenced location in the specified {@code target} + * + * @param target the target referenced by this {@code JsonPointer} + * @return the referenced value in the target. + * @throws NullPointerException if {@code target} is null + * @throws JsonException if the referenced value does not exist + */ + public JsonValue getValue(JsonStructure target) { + if (target == null) { + throw new NullPointerException("target must not be null"); + } + + if (jsonPointer.equals("")) { + return target; + } + + JsonValue jsonValue = target; + + for (int i = 1; i < referenceTokens.length; i++) { + String decodedReferenceToken = JsonPointerUtil.decode(referenceTokens[i]); + + if (jsonValue instanceof JsonObject) { + JsonObject jsonObject = (JsonObject) jsonValue; + jsonValue = jsonObject.get(decodedReferenceToken); + + if (jsonValue == null) { + throw new JsonException("The JsonObject " + jsonObject + " contains no value for token " + decodedReferenceToken); + } + } else if (jsonValue instanceof JsonArray) { + JsonArray jsonArray = (JsonArray) jsonValue; + + try { + int index = Integer.parseInt(decodedReferenceToken); + if (index >= jsonArray.size()) { + throw new JsonException("The JsonArray " + jsonArray + " contains no element for index " + index); + } + if (decodedReferenceToken.startsWith("0") && decodedReferenceToken.length() > 1) { + throw new JsonException("The token " + decodedReferenceToken + " with leading zeros is not allowed to reference an element of a JsonArray"); + } + + jsonValue = jsonArray.get(index); + } catch (NumberFormatException e) { + throw new JsonException("The token " + decodedReferenceToken + " for the JsonArray " + jsonArray + " is not a number", e); + } + } + + } + + return jsonValue; + } + + /** + * Adds or replaces a value at the referenced location in the specified + * {@code target} with the specified {@code value}. + * <ol> + * <li>If the reference is the target (empty JSON Pointer string), + * the specified {@code value}, which must be the same type as + * specified {@code target}, is returned.</li> + * <li>If the reference is an array element, the specified {@code value} is inserted + * into the array, at the referenced index. The value currently at that location, and + * any subsequent values, are shifted to the right (adds one to the indices). + * Index starts with 0. If the reference is specified with a "-", or if the + * index is equal to the size of the array, the value is appended to the array.</li> + * <li>If the reference is a name/value pair of a {@code JsonObject}, and the + * referenced value exists, the value is replaced by the specified {@code value}. + * If the value does not exist, a new name/value pair is added to the object.</li> + * </ol> + * + * @param target the target referenced by this {@code JsonPointer} + * @param value the value to be added + * @return the transformed {@code target} after the value is added. + * @throws NullPointerException if {@code target} is {@code null} + * @throws JsonException if the reference is an array element and + * the index is out of range ({@code index < 0 || index > array size}), + * or if the pointer contains references to non-existing objects or arrays. + */ + public JsonStructure add(JsonStructure target, JsonValue value) { + return null; + } + + /** + * Replaces the value at the referenced location in the specified + * {@code target} with the specified {@code value}. + * + * @param target the target referenced by this {@code JsonPointer} + * @param value the value to be stored at the referenced location + * @return the transformed {@code target} after the value is replaced. + * @throws NullPointerException if {@code target} is {@code null} + * @throws JsonException if the referenced value does not exist, + * or if the reference is the target. + */ + public JsonStructure replace(JsonStructure target, JsonValue value) { + return null; + } + + /** + * Removes the value at the reference location in the specified {@code target} + * + * @param target the target referenced by this {@code JsonPointer} + * @return the transformed {@code target} after the value is removed. + * @throws NullPointerException if {@code target} is {@code null} + * @throws JsonException if the referenced value does not exist, + * or if the reference is the target. + */ + public JsonStructure remove(JsonStructure target) { + return null; + } + + /** + * Adds or replaces a value at the referenced location in the specified + * {@code target} with the specified {@code value}. + * + * @param target the target referenced by this {@code JsonPointer} + * @param value the value to be added + * @return the transformed {@code target} after the value is added. + * @throws NullPointerException if {@code target} is {@code null} + * @throws JsonException if the reference is an array element and + * the index is out of range ({@code index < 0 || index > array size}), + * or if the pointer contains references to non-existing objects or arrays. + * @see #add(JsonStructure, JsonValue) + */ + public JsonObject add(JsonObject target, JsonValue value) { + return (JsonObject) this.add((JsonStructure) target, value); + } + + /** + * Adds or replaces a value at the referenced location in the specified + * {@code target} with the specified {@code value}. + * + * @param target the target referenced by this {@code JsonPointer} + * @param value the value to be added + * @return the transformed {@code target} after the value is added. + * @throws NullPointerException if {@code target} is {@code null} + * @throws JsonException if the reference is an array element and + * the index is out of range ({@code index < 0 || index > array size}), + * or if the pointer contains references to non-existing objects or arrays. + * @see #add(JsonStructure, JsonValue) + */ + public JsonArray add(JsonArray target, JsonValue value) { + return (JsonArray) this.add((JsonStructure) target, value); + } + + /** + * Replaces the value at the referenced location in the specified + * + * @param target the target referenced by this {@code JsonPointer} + * @param value the value to be stored at the referenced location + * @return the transformed {@code target} after the value is replaced. + * @throws NullPointerException if {@code target} is {@code null} + * @throws JsonException if the referenced value does not exist, + * or if the reference is the target. + * {@code target} with the specified {@code value}. + * @see #replace(JsonStructure, JsonValue) + */ + public JsonObject replace(JsonObject target, JsonValue value) { + return (JsonObject) this.replace((JsonStructure) target, value); + } + + /** + * Replaces the value at the referenced location in the specified + * + * @param target the target referenced by this {@code JsonPointer} + * @param value the value to be stored at the referenced location + * @return the transformed {@code target} after the value is replaced. + * @throws NullPointerException if {@code target} is {@code null} + * @throws JsonException if the referenced value does not exist, + * or if the reference is the target. + * {@code target} with the specified {@code value}. + * @see #replace(JsonStructure, JsonValue) + */ + public JsonArray replace(JsonArray target, JsonValue value) { + return (JsonArray) this.replace((JsonStructure) target, value); + } + + /** + * Removes the value at the reference location in the specified {@code target} + * + * @param target the target referenced by this {@code JsonPointer} + * @return the transformed {@code target} after the value is removed. + * @throws NullPointerException if {@code target} is {@code null} + * @throws JsonException if the referenced value does not exist, + * or if the reference is the target. + * @see #remove(JsonStructure) + */ + public JsonObject remove(JsonObject target) { + return (JsonObject) this.remove((JsonStructure) target); + } + + /** + * Removes the value at the reference location in the specified {@code target} + * + * @param target the target referenced by this {@code JsonPointer} + * @return the transformed {@code target} after the value is removed. + * @throws NullPointerException if {@code target} is {@code null} + * @throws JsonException if the referenced value does not exist, + * or if the reference is the target. + * @see #remove(JsonStructure) + */ + public JsonArray remove(JsonArray target) { + return (JsonArray) this.remove((JsonStructure) target); + } + + +} http://git-wip-us.apache.org/repos/asf/johnzon/blob/45faea0e/johnzon-core/src/test/java/org/apache/johnzon/core/JsonPointerTest.java ---------------------------------------------------------------------- diff --git a/johnzon-core/src/test/java/org/apache/johnzon/core/JsonPointerTest.java b/johnzon-core/src/test/java/org/apache/johnzon/core/JsonPointerTest.java new file mode 100644 index 0000000..7750ba6 --- /dev/null +++ b/johnzon-core/src/test/java/org/apache/johnzon/core/JsonPointerTest.java @@ -0,0 +1,197 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.johnzon.core; + +import org.junit.Test; + +import javax.json.Json; +import javax.json.JsonException; +import javax.json.JsonReader; +import javax.json.JsonStructure; +import javax.json.JsonValue; +import java.util.Collections; + +import static org.junit.Assert.assertEquals; + +public class JsonPointerTest { + + @Test(expected = NullPointerException.class) + public void testConstructorWithNullShouldThrowNullPointerException() { + new JsonPointer(null); + } + + @Test(expected = JsonException.class) + public void testConstructorWithInvalidJsonPointerShouldThrowJsonException() { + new JsonPointer("a"); + } + + @Test(expected = NullPointerException.class) + public void testGetValueWithNullShouldThrowNullPointerException() { + JsonPointer jsonPointer = new JsonPointer(""); + jsonPointer.getValue(null); + } + + @Test + public void testGetValueWholeDocument() { + JsonStructure jsonDocument = getJsonDocument(); + + JsonPointer jsonPointer = new JsonPointer(""); + JsonValue result = jsonPointer.getValue(jsonDocument); + assertEquals(jsonDocument.toString(), result.toString()); + } + + @Test + public void testGetValue0() { + JsonStructure jsonDocument = getJsonDocument(); + + JsonPointer jsonPointer = new JsonPointer("/"); + JsonValue result = jsonPointer.getValue(jsonDocument); + assertEquals("0", result.toString()); + } + + @Test + public void testGetValue1() { + JsonStructure jsonDocument = getJsonDocument(); + + JsonPointer jsonPointer = new JsonPointer("/a~1b"); + JsonValue result = jsonPointer.getValue(jsonDocument); + assertEquals("1", result.toString()); + } + + @Test + public void testGetValue2() { + JsonStructure jsonDocument = getJsonDocument(); + + JsonPointer jsonPointer = new JsonPointer("/c%d"); + JsonValue result = jsonPointer.getValue(jsonDocument); + assertEquals("2", result.toString()); + } + + @Test + public void testGetValue3() { + JsonStructure jsonDocument = getJsonDocument(); + + JsonPointer jsonPointer = new JsonPointer("/e^f"); + JsonValue result = jsonPointer.getValue(jsonDocument); + assertEquals("3", result.toString()); + } + + @Test + public void testGetValue4() { + JsonStructure jsonDocument = getJsonDocument(); + + JsonPointer jsonPointer = new JsonPointer("/g|h"); + JsonValue result = jsonPointer.getValue(jsonDocument); + assertEquals("4", result.toString()); + } + + @Test + public void testGetValue5() { + JsonStructure jsonDocument = getJsonDocument(); + + JsonPointer jsonPointer = new JsonPointer("/i\\j"); + JsonValue result = jsonPointer.getValue(jsonDocument); + assertEquals("5", result.toString()); + } + + @Test + public void testGetValue6() { + JsonStructure jsonDocument = getJsonDocument(); + + JsonPointer jsonPointer = new JsonPointer("/k\"l"); + JsonValue result = jsonPointer.getValue(jsonDocument); + assertEquals("6", result.toString()); + } + + @Test + public void testGetValue7() { + JsonStructure jsonDocument = getJsonDocument(); + + JsonPointer jsonPointer = new JsonPointer("/ "); + JsonValue result = jsonPointer.getValue(jsonDocument); + assertEquals("7", result.toString()); + } + + @Test + public void testGetValue8() { + JsonStructure jsonDocument = getJsonDocument(); + + JsonPointer jsonPointer = new JsonPointer("/m~0n"); + JsonValue result = jsonPointer.getValue(jsonDocument); + assertEquals("8", result.toString()); + } + + @Test(expected = JsonException.class) + public void testGetValueElementNotExistentShouldThrowJsonException() { + JsonStructure jsonDocument = getJsonDocument(); + + JsonPointer jsonPointer = new JsonPointer("/fool"); + jsonPointer.getValue(jsonDocument); + } + + @Test + public void testGetValueWholeJsonArray() { + JsonStructure jsonDocument = getJsonDocument(); + + JsonPointer jsonPointer = new JsonPointer("/foo"); + JsonValue result = jsonPointer.getValue(jsonDocument); + assertEquals("[\"bar\",\"baz\"]", result.toString()); + } + + @Test + public void testGetValueJsonArrayElement() { + JsonStructure jsonDocument = getJsonDocument(); + + JsonPointer jsonPointer = new JsonPointer("/foo/0"); + JsonValue result = jsonPointer.getValue(jsonDocument); + assertEquals("\"bar\"", result.toString()); + } + + @Test(expected = JsonException.class) + public void testGetValueJsonArrayElementNotExistentShouldThrowJsonException() { + JsonStructure jsonDocument = getJsonDocument(); + + JsonPointer jsonPointer = new JsonPointer("/foo/2"); + jsonPointer.getValue(jsonDocument); + } + + @Test(expected = JsonException.class) + public void testGetValueJsonArrayElementNoNumberShouldThrowJsonException() { + JsonStructure jsonDocument = getJsonDocument(); + + JsonPointer jsonPointer = new JsonPointer("/foo/a"); + jsonPointer.getValue(jsonDocument); + } + + @Test(expected = JsonException.class) + public void testGetValueJsonArrayElementLeadingZeroShouldThrowJsonException() { + JsonStructure jsonDocument = getJsonDocument(); + + JsonPointer jsonPointer = new JsonPointer("/foo/001"); + JsonValue result = jsonPointer.getValue(jsonDocument); + assertEquals("\"bar\"", result.toString()); + } + + private JsonStructure getJsonDocument() { + JsonReader reader = Json.createReaderFactory(Collections.<String, Object>emptyMap()).createReader( + Thread.currentThread().getContextClassLoader().getResourceAsStream("json/jsonPointerTest.json")); + return reader.read(); + } + +} http://git-wip-us.apache.org/repos/asf/johnzon/blob/45faea0e/johnzon-core/src/test/resources/json/jsonPointerTest.json ---------------------------------------------------------------------- diff --git a/johnzon-core/src/test/resources/json/jsonPointerTest.json b/johnzon-core/src/test/resources/json/jsonPointerTest.json new file mode 100644 index 0000000..8cdc1c9 --- /dev/null +++ b/johnzon-core/src/test/resources/json/jsonPointerTest.json @@ -0,0 +1,15 @@ +{ + "foo": [ + "bar", + "baz" + ], + "": 0, + "a/b": 1, + "c%d": 2, + "e^f": 3, + "g|h": 4, + "i\\j": 5, + "k\"l": 6, + " ": 7, + "m~n": 8 +} \ No newline at end of file
