diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 3bed1b053..1ff492a55 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -774,6 +774,8 @@ include::eventbus.adoc[] include::override/json.adoc[] +include::json-pointers.adoc[] + include::buffers.adoc[] include::net.adoc[] diff --git a/src/main/asciidoc/json-pointers.adoc b/src/main/asciidoc/json-pointers.adoc new file mode 100644 index 000000000..8e554c667 --- /dev/null +++ b/src/main/asciidoc/json-pointers.adoc @@ -0,0 +1,20 @@ +== Json Pointers + +Vert.x provides an implementation of Json Pointers from RFC6901. +You can use pointers both for querying and for writing. You can build your {@link io.vertx.core.json.pointer.JsonPointer} using +a string, a URI or manually appending paths: + +[source,java] +---- +{@link examples.JsonPointerExamples#example1Pointers} +---- + +After instantiating your pointer, use {@link io.vertx.core.json.pointer.JsonPointer#queryJson(java.lang.Object)} to query +a JSON value. You can update a Json Value using {@link io.vertx.core.json.pointer.JsonPointer#writeJson(java.lang.Object, java.lang.Object)}: + +[source,java] +---- +{@link examples.JsonPointerExamples#example2Pointers} +---- + +You can use Vert.x Json Pointer with any object model by providing a custom implementation of {@link io.vertx.core.json.pointer.JsonPointerIterator} diff --git a/src/main/java/docoverride/json/Examples.java b/src/main/java/docoverride/json/Examples.java index 0cde28b3e..621d97d9a 100644 --- a/src/main/java/docoverride/json/Examples.java +++ b/src/main/java/docoverride/json/Examples.java @@ -14,8 +14,10 @@ package docoverride.json; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; +import io.vertx.core.json.pointer.JsonPointer; import io.vertx.docgen.Source; +import java.net.URI; import java.util.HashMap; import java.util.Map; @@ -80,7 +82,4 @@ public class Examples { Boolean boolVal = array.getBoolean(2); } - - - } diff --git a/src/main/java/examples/JsonPointerExamples.java b/src/main/java/examples/JsonPointerExamples.java new file mode 100644 index 000000000..97929b95d --- /dev/null +++ b/src/main/java/examples/JsonPointerExamples.java @@ -0,0 +1,31 @@ +package examples; + +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import io.vertx.core.json.pointer.JsonPointer; + +import java.net.URI; + +public class JsonPointerExamples { + + public void example1Pointers() { + // Build a pointer from a string + JsonPointer pointer1 = JsonPointer.from("/hello/world"); + // Build a pointer manually + JsonPointer pointer2 = JsonPointer.create() + .append("hello") + .append("world"); + } + + public void example2Pointers(JsonPointer objectPointer, JsonObject jsonObject, JsonPointer arrayPointer, JsonArray jsonArray) { + // Query a JsonObject + Object result1 = objectPointer.queryJson(jsonObject); + // Query a JsonArray + Object result2 = arrayPointer.queryJson(jsonArray); + // Write starting from a JsonObject + objectPointer.writeJson(jsonObject, "new element"); + // Write starting from a JsonObject + arrayPointer.writeJson(jsonArray, "new element"); + } + +} diff --git a/src/main/java/io/vertx/core/json/pointer/JsonPointer.java b/src/main/java/io/vertx/core/json/pointer/JsonPointer.java new file mode 100644 index 000000000..05889dba1 --- /dev/null +++ b/src/main/java/io/vertx/core/json/pointer/JsonPointer.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2011-2017 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ + +package io.vertx.core.json.pointer; + +import io.vertx.codegen.annotations.Fluent; +import io.vertx.codegen.annotations.GenIgnore; +import io.vertx.codegen.annotations.Nullable; +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.json.pointer.impl.JsonPointerImpl; + +import java.net.URI; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; + +/** + * Implementation of RFC6901 Json Pointers. + * + * @author Francesco Guardiani @slinkydeveloper + */ +@VertxGen +public interface JsonPointer { + + /** + * Return {@code true} if the pointer is a root pointer + */ + boolean isRootPointer(); + + /** + * Return {@code true} if the pointer is local (URI with only fragment) + */ + boolean isLocalPointer(); + + /** + * Return {@code true} if this pointer is a parent pointer of {@code child}. + *
+ * For instance {@code "/properties"} pointer is parent pointer of {@code "/properties/parent"} + * + * @param child + */ + boolean isParent(JsonPointer child); + + /** + * Build a string representation of the JSON Pointer + */ + @Override + String toString(); + + /** + * Build a URI representation of the JSON Pointer + */ + @GenIgnore(GenIgnore.PERMITTED_TYPE) + URI toURI(); + + /** + * Return the underlying URI without the fragment + */ + @GenIgnore(GenIgnore.PERMITTED_TYPE) + URI getURIWithoutFragment(); + + /** + * Append an unescaped {@code token} to this pointer
+ * Note: If you provide escaped path the behaviour is undefined + * + * @param token the unescaped reference token + * @return a reference to this, so the API can be used fluently + */ + @Fluent + JsonPointer append(String token); + + /** + * Append the {@code index} as reference token to JsonPointer + * + * @param index + * @return a reference to this, so the API can be used fluently + */ + @Fluent + JsonPointer append(int index); + + /** + * Append an unescaped list of {@code tokens} to JsonPointer
+ * Note: If you provide escaped paths the behaviour is undefined + * + * @param tokens unescaped reference tokens + * @return a reference to this, so the API can be used fluently + */ + @Fluent + JsonPointer append(List tokens); + + /** + * Append all tokens of {@code pointer} to this pointer
+ * Note: The base URI of this pointer will remain untouched + * + * @param pointer other pointer + * @return a reference to this, so the API can be used fluently + */ + @Fluent + JsonPointer append(JsonPointer pointer); + + /** + * Remove last reference token of this pointer + * + * @return a reference to this, so the API can be used fluently + */ + @Fluent + JsonPointer parent(); + + /** + * Query {@code objectToQuery} using the provided {@link JsonPointerIterator}.
+ * If you need to query Vert.x json data structures, use {@link JsonPointer#queryJson(Object)}
+ * Note: if this pointer is a root pointer, this function returns the provided object + * + * @param objectToQuery the object to query + * @param iterator the json pointer iterator that provides the logic to access to the objectToQuery + * @return null if pointer points to not existing value, otherwise the requested value + */ + default @Nullable Object query(Object objectToQuery, JsonPointerIterator iterator) { return queryOrDefault(objectToQuery, iterator, null); } + + /** + * Query {@code objectToQuery} using the provided {@link JsonPointerIterator}. If the query result is null, returns the default.
+ * If you need to query Vert.x json data structures, use {@link JsonPointer#queryJsonOrDefault(Object, Object)}
+ * Note: if this pointer is a root pointer, this function returns the provided object + * + * @param objectToQuery the object to query + * @param iterator the json pointer iterator that provides the logic to access to the objectToQuery + * @param defaultValue default value if query result is null + * @return null if pointer points to not existing value, otherwise the requested value + */ + Object queryOrDefault(Object objectToQuery, JsonPointerIterator iterator, Object defaultValue); + + /** + * Query {@code jsonElement}.
+ * Note: if this pointer is a root pointer, this function returns the provided json element + * + * @param jsonElement the json element to query + * @return null if pointer points to not existing value, otherwise the requested value + */ + default @Nullable Object queryJson(Object jsonElement) {return query(jsonElement, JsonPointerIterator.JSON_ITERATOR); } + + /** + * Query {@code jsonElement}. If the query result is null, returns the default.
+ * Note: if this pointer is a root pointer, this function returns the provided object + * + * @param jsonElement the json element to query + * @param defaultValue default value if query result is null + * @return null if pointer points to not existing value, otherwise the requested value + */ + default @Nullable Object queryJsonOrDefault(Object jsonElement, Object defaultValue) {return queryOrDefault(jsonElement, JsonPointerIterator.JSON_ITERATOR, defaultValue); } + + /** + * Query {@code objectToQuery} tracing each element walked during the query, including the first and the result (if any).
+ * The first element of the list is objectToQuery and the last is the result, or the element before the first null was encountered + * + * @param objectToQuery the object to query + * @param iterator the json pointer iterator that provides the logic to access to the objectToQuery + * @return the stream of walked elements + */ + List tracedQuery(Object objectToQuery, JsonPointerIterator iterator); + + /** + * Write {@code newElement} in {@code objectToWrite} using this pointer. The path token "-" is handled as append to end of array
+ * If you need to write in Vert.x json data structures, use {@link JsonPointer#writeJson(Object, Object)} (Object)}
+ * + * @param objectToWrite object to write + * @param iterator the json pointer iterator that provides the logic to access to the objectToMutate + * @param newElement object to insert + * @param createOnMissing create objects when missing a object key or an array index + * @return a reference to objectToWrite if the write was completed, a reference to newElement if the pointer is a root pointer, null if the write failed + */ + Object write(Object objectToWrite, JsonPointerIterator iterator, Object newElement, boolean createOnMissing); + + /** + * Write {@code newElement} in {@code jsonElement} using this pointer. The path token "-" is handled as append to end of array. + * + * @param jsonElement json element to query and write + * @param newElement json to insert + * @return a reference to json if the write was completed, a reference to newElement if the pointer is a root pointer, null if the write failed + */ + default Object writeJson(Object jsonElement, Object newElement) { return writeJson(jsonElement, newElement, false); } + + /** + * Write {@code newElement} in {@code jsonElement} using this pointer. The path token "-" is handled as append to end of array. + * + * @param jsonElement json to query and write + * @param newElement json to insert + * @param createOnMissing create JsonObject when missing a object key or an array index + * @return a reference to json if the write was completed, a reference to newElement if the pointer is a root pointer, null if the write failed + */ + default Object writeJson(Object jsonElement, Object newElement, boolean createOnMissing) { + return write(jsonElement, JsonPointerIterator.JSON_ITERATOR, newElement, createOnMissing); + } + + /** + * Copy a JsonPointer + * + * @return a copy of this pointer + */ + JsonPointer copy(); + + /** + * Build an empty JsonPointer + * + * @return a new empty JsonPointer + */ + static JsonPointer create() { + return new JsonPointerImpl(); + } + + /** + * Build a JsonPointer from a json pointer string + * + * @param pointer the string representing a pointer + * @return new instance of JsonPointer + * @throws IllegalArgumentException if the pointer provided is not valid + */ + static JsonPointer from(String pointer) { + return new JsonPointerImpl(pointer); + } + + /** + * Build a JsonPointer from a URI. + * + * @param uri uri representing a json pointer + * @return new instance of JsonPointer + * @throws IllegalArgumentException if the pointer provided is not valid + */ + @GenIgnore(GenIgnore.PERMITTED_TYPE) + static JsonPointer fromURI(URI uri) { + return new JsonPointerImpl(uri); + } + +} diff --git a/src/main/java/io/vertx/core/json/pointer/JsonPointerIterator.java b/src/main/java/io/vertx/core/json/pointer/JsonPointerIterator.java new file mode 100644 index 000000000..154aca413 --- /dev/null +++ b/src/main/java/io/vertx/core/json/pointer/JsonPointerIterator.java @@ -0,0 +1,100 @@ +package io.vertx.core.json.pointer; + +import io.vertx.codegen.annotations.Nullable; +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.json.pointer.impl.JsonPointerIteratorImpl; + +/** + * The JsonPointerIterator is used by the read/write algorithms of the {@link JsonPointer} to read/write the querying data structure
+ * + * Every method takes the currentValue as parameter, representing the actual value held by the query algorithm.
+ * + * Implementations of this interface should be stateless, so they can be reused
+ * + * You can implement this interface to query the structure you want using json pointers + * + * @author Francesco Guardiani @slinkydeveloper + * + */ +@VertxGen +public interface JsonPointerIterator { + + /** + * @param currentValue + * @return {@code true} if the current value is a queryable object + */ + boolean isObject(@Nullable Object currentValue); + + /** + * @param currentValue + * @return {@code true} if the current value is a queryable array + */ + boolean isArray(@Nullable Object currentValue); + + /** + * @param currentValue + * @return {@code true} if the current value is null/empty + */ + boolean isNull(@Nullable Object currentValue); + + /** + * @param currentValue + * @param key object key + * @return {@code true} if current value is a queryable object that contains the specified key + */ + boolean objectContainsKey(@Nullable Object currentValue, String key); + + /** + * Returns the object parameter with specified key. + * + * @param currentValue + * @param key object key + * @param createOnMissing If the current value is an object that doesn't contain the key, put an empty object at provided key + * @return the requested object parameter, or null if the method was not able to find it + */ + Object getObjectParameter(@Nullable Object currentValue, String key, boolean createOnMissing); + + /** + * Move the iterator the the array element at specified index + * + * @param currentValue + * @param i array index + * @return the request array element, or null if the method was not able to find it + */ + Object getArrayElement(@Nullable Object currentValue, int i); + + /** + * Write object parameter at specified key + * + * @param currentValue + * @param key + * @param value + * @return true if the operation is successful + */ + boolean writeObjectParameter(@Nullable Object currentValue, String key, @Nullable Object value); + + /** + * Write array element at specified index + * + * @param currentValue + * @param i + * @param value + * @return true if the operation is successful + */ + boolean writeArrayElement(@Nullable Object currentValue, int i, @Nullable Object value); + + /** + * Append array element + * + * @param currentValue + * @param value + * @return true if the operation is successful + */ + boolean appendArrayElement(@Nullable Object currentValue, @Nullable Object value); + + /** + * Instance of a JsonPointerIterator to query Vert.x Json structures + */ + JsonPointerIterator JSON_ITERATOR = new JsonPointerIteratorImpl(); + +} diff --git a/src/main/java/io/vertx/core/json/pointer/impl/JsonPointerImpl.java b/src/main/java/io/vertx/core/json/pointer/impl/JsonPointerImpl.java new file mode 100644 index 000000000..b27e68f0b --- /dev/null +++ b/src/main/java/io/vertx/core/json/pointer/impl/JsonPointerImpl.java @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2011-2017 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ + +package io.vertx.core.json.pointer.impl; + +import io.vertx.core.json.pointer.JsonPointer; +import io.vertx.core.json.pointer.JsonPointerIterator; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * @author Francesco Guardiani @slinkydeveloper + */ +public class JsonPointerImpl implements JsonPointer { + + final public static Pattern VALID_POINTER_PATTERN = Pattern.compile("^(/(([^/~])|(~[01]))*)*$"); + + URI startingUri; + + // Empty means a pointer to root + List decodedTokens; + + public JsonPointerImpl(URI uri) { + this.startingUri = removeFragment(uri); + this.decodedTokens = parse(uri.getFragment()); + } + + public JsonPointerImpl(String pointer) { + this.startingUri = URI.create("#"); + this.decodedTokens = parse(pointer); + } + + public JsonPointerImpl() { + this.startingUri = URI.create("#"); + this.decodedTokens = parse(null); + } + + protected JsonPointerImpl(URI startingUri, List decodedTokens) { + this.startingUri = startingUri; + this.decodedTokens = new ArrayList<>(decodedTokens); + } + + private ArrayList parse(String pointer) { + if (pointer == null || "".equals(pointer)) { + return new ArrayList<>(); + } + if (VALID_POINTER_PATTERN.matcher(pointer).matches()) { + return Arrays + .stream(pointer.split("\\/", -1)) + .skip(1) //Ignore first element + .map(this::unescape) + .collect(Collectors.toCollection(ArrayList::new)); + } else + throw new IllegalArgumentException("The provided pointer is not a valid JSON Pointer"); + } + + private String escape(String path) { + return path.replace("~", "~0") + .replace("/", "~1"); + } + + private String unescape(String path) { + return path.replace("~1", "/") // https://tools.ietf.org/html/rfc6901#section-4 + .replace("~0", "~"); + } + + @Override + public boolean isRootPointer() { + return decodedTokens.size() == 0; + } + + @Override + public boolean isLocalPointer() { + return startingUri == null || startingUri.getSchemeSpecificPart() == null || startingUri.getSchemeSpecificPart().isEmpty(); + } + + @Override + public boolean isParent(JsonPointer c) { + JsonPointerImpl child = (JsonPointerImpl) c; + return child != null && + (child.getURIWithoutFragment() == null && this.getURIWithoutFragment() == null || child.getURIWithoutFragment().equals(this.getURIWithoutFragment())) && + decodedTokens.size() < child.decodedTokens.size() && + IntStream.range(0, decodedTokens.size()) + .mapToObj(i -> this.decodedTokens.get(i).equals(child.decodedTokens.get(i))) + .reduce(Boolean::logicalAnd).orElse(true); + } + + @Override + public String toString() { + if (isRootPointer()) + return ""; + else + return "/" + String.join("/", decodedTokens.stream().map(this::escape).collect(Collectors.toList())); + } + + @Override + public URI toURI() { + if (isRootPointer()) { + return replaceFragment(this.startingUri, ""); + } else + return replaceFragment( + this.startingUri, + "/" + String.join("/", decodedTokens.stream().map(this::escape).collect(Collectors.toList())) + ); + } + + @Override + public URI getURIWithoutFragment() { + return startingUri; + } + + @Override + public JsonPointer append(String path) { + decodedTokens.add(path); + return this; + } + + @Override + public JsonPointer append(int i) { + return this.append(Integer.toString(i)); + } + + @Override + public JsonPointer append(List paths) { + decodedTokens.addAll(paths); + return this; + } + + @Override + public JsonPointer append(JsonPointer pointer) { + decodedTokens.addAll(((JsonPointerImpl)pointer).decodedTokens); + return this; + } + + @Override + public JsonPointer parent() { + if (!this.isRootPointer()) decodedTokens.remove(decodedTokens.size() - 1); + return this; + } + + @Override + public JsonPointer copy() { + return new JsonPointerImpl(this.startingUri, this.decodedTokens); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + JsonPointerImpl that = (JsonPointerImpl) o; + return Objects.equals(startingUri, that.startingUri) && + Objects.equals(decodedTokens, that.decodedTokens); + } + + @Override + public int hashCode() { + return Objects.hash(startingUri, decodedTokens); + } + + @Override + public Object queryOrDefault(Object value, JsonPointerIterator iterator, Object defaultValue) { + // I should threat this as a special condition because the empty string can be a json obj key! + if (isRootPointer()) + return (iterator.isNull(value)) ? defaultValue : value; + else { + value = walkTillLastElement(value, iterator, false, null); + String lastKey = decodedTokens.get(decodedTokens.size() - 1); + if (iterator.isObject(value)) { + Object finalValue = iterator.getObjectParameter(value, lastKey, false); + return (!iterator.isNull(finalValue)) ? finalValue : defaultValue; + } else if (iterator.isArray(value) && !"-".equals(lastKey)) { + try { + Object finalValue = iterator.getArrayElement(value, Integer.parseInt(lastKey)); + return (!iterator.isNull(finalValue)) ? finalValue : defaultValue; + } catch (NumberFormatException e) { + return defaultValue; + } + } else + return defaultValue; + } + } + + @Override + public List tracedQuery(Object objectToQuery, JsonPointerIterator iterator) { + List list = new ArrayList<>(); + if (isRootPointer() && !iterator.isNull(objectToQuery)) + list.add(objectToQuery); + else { + Object lastValue = walkTillLastElement(objectToQuery, iterator, false, list::add); + if (!iterator.isNull(lastValue)) + list.add(lastValue); + String lastKey = decodedTokens.get(decodedTokens.size() - 1); + if (iterator.isObject(lastValue)) { + lastValue = iterator.getObjectParameter(lastValue, lastKey, false); + } else if (iterator.isArray(lastValue) && !"-".equals(lastKey)) { + try { + lastValue = iterator.getArrayElement(lastValue, Integer.parseInt(lastKey)); + } catch (NumberFormatException e) { } + } + if (!iterator.isNull(lastValue)) + list.add(lastValue); + } + return list; + } + + @Override + public Object write(Object valueToWrite, JsonPointerIterator iterator, Object newElement, boolean createOnMissing) { + if (isRootPointer()) { + return iterator.isNull(valueToWrite) ? null : newElement; + } else { + Object walkedValue = walkTillLastElement(valueToWrite, iterator, createOnMissing, null); + if (writeLastElement(walkedValue, iterator, newElement)) + return valueToWrite; + else + return null; + } + } + + private Object walkTillLastElement(Object value, JsonPointerIterator iterator, boolean createOnMissing, Consumer onNewValue) { + for (int i = 0; i < decodedTokens.size() - 1; i++) { + String k = decodedTokens.get(i); + if (i == 0 && "".equals(k)) { + continue; // Avoid errors with root empty string + } else if (iterator.isObject(value)) { + if (onNewValue != null) onNewValue.accept(value); + value = iterator.getObjectParameter(value, k, createOnMissing); + } else if (iterator.isArray(value)) { + if (onNewValue != null) onNewValue.accept(value); + try { + value = iterator.getArrayElement(value, Integer.parseInt(k)); + if (iterator.isNull(value) && createOnMissing) { + value = iterator.getObjectParameter(value, k, true); + } + } catch (NumberFormatException e) { + value = null; + } + } else { + return null; + } + } + return value; + } + + private boolean writeLastElement(Object valueToWrite, JsonPointerIterator iterator, Object newElement) { + String lastKey = decodedTokens.get(decodedTokens.size() - 1); + if (iterator.isObject(valueToWrite)) { + return iterator.writeObjectParameter(valueToWrite, lastKey, newElement); + } else if (iterator.isArray(valueToWrite)) { + if ("-".equals(lastKey)) { // Append to end + return iterator.appendArrayElement(valueToWrite, newElement); + } else { // We have a index + try { + return iterator.writeArrayElement(valueToWrite, Integer.parseInt(lastKey), newElement); + } catch (NumberFormatException e) { + return false; + } + } + } else + return false; + } + + private URI removeFragment(URI oldURI) { + return replaceFragment(oldURI, null); + } + + private URI replaceFragment(URI oldURI, String fragment) { + try { + if (oldURI != null) { + return new URI(oldURI.getScheme(), oldURI.getSchemeSpecificPart(), fragment); + } else return new URI(null, null, fragment); + } catch (URISyntaxException e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/src/main/java/io/vertx/core/json/pointer/impl/JsonPointerIteratorImpl.java b/src/main/java/io/vertx/core/json/pointer/impl/JsonPointerIteratorImpl.java new file mode 100644 index 000000000..8637cb3f8 --- /dev/null +++ b/src/main/java/io/vertx/core/json/pointer/impl/JsonPointerIteratorImpl.java @@ -0,0 +1,92 @@ +package io.vertx.core.json.pointer.impl; + +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import io.vertx.core.json.pointer.JsonPointerIterator; + +import java.util.List; +import java.util.Map; + +public class JsonPointerIteratorImpl implements JsonPointerIterator { + + @Override + public boolean isObject(Object value) { + return value instanceof JsonObject; + } + + @Override + public boolean isArray(Object value) { + return value instanceof JsonArray; + } + + @Override + public boolean isNull(Object value) { + return value == null; + } + + @Override + public boolean objectContainsKey(Object value, String key) { + return isObject(value) && ((JsonObject)value).containsKey(key); + } + + @Override + public Object getObjectParameter(Object value, String key, boolean createOnMissing) { + if (isObject(value)) { + if (!objectContainsKey(value, key)) { + if (createOnMissing) { + writeObjectParameter(value, key, new JsonObject()); + } else { + return null; + } + } + return jsonifyValue(((JsonObject) value).getValue(key)); + } + return null; + } + + @Override + public Object getArrayElement(Object value, int i) { + if (isArray(value)) { + try { + return jsonifyValue(((JsonArray)value).getValue(i)); + } catch (IndexOutOfBoundsException ignored) {} + } + return null; + } + + @Override + public boolean writeObjectParameter(Object value, String key, Object el) { + if (isObject(value)) { + ((JsonObject)value).put(key, el); + return true; + } else return false; + } + + @SuppressWarnings("unchecked") + @Override + public boolean writeArrayElement(Object value, int i, Object el) { + if (isArray(value)) { + try { + ((JsonArray)value).getList().add(i, el); + return true; + } catch (IndexOutOfBoundsException e) { + return false; + } + } else return false; + } + + @Override + public boolean appendArrayElement(Object value, Object el) { + if (isArray(value)) { + ((JsonArray)value).add(el); + return true; + } else return false; + } + + @SuppressWarnings("unchecked") + private Object jsonifyValue(Object v) { + if (v instanceof Map) return new JsonObject((Map)v); + else if (v instanceof List) return new JsonArray((List)v); + else return v; + } +} diff --git a/src/test/java/io/vertx/core/json/pointer/impl/JsonPointerTest.java b/src/test/java/io/vertx/core/json/pointer/impl/JsonPointerTest.java new file mode 100644 index 000000000..f50e5bbfd --- /dev/null +++ b/src/test/java/io/vertx/core/json/pointer/impl/JsonPointerTest.java @@ -0,0 +1,456 @@ +/* + * Copyright (c) 2011-2017 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package io.vertx.core.json.pointer.impl; + +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import io.vertx.core.json.pointer.JsonPointer; +import io.vertx.core.json.pointer.JsonPointerIterator; +import org.junit.Test; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * @author Francesco Guardiani @slinkydeveloper + */ +public class JsonPointerTest { + + @Test + public void testParsing() { + JsonPointer pointer = JsonPointer.from("/hello/world"); + assertEquals("/hello/world", pointer.toString()); + } + + @Test(expected = IllegalArgumentException.class) + public void testParsingErrorWrongFirstElement() { + JsonPointer.from("bla/hello/world"); + } + + @Test + public void testEncodingParsing() { + JsonPointer pointer = JsonPointer.create().append("hell/o").append("worl~d"); + assertEquals("/hell~1o/worl~0d", pointer.toString()); + } + + @Test + public void testURIParsing() { + JsonPointer pointer = JsonPointer.fromURI(URI.create("http://www.example.org#/hello/world")); + assertEquals("/hello/world", pointer.toString()); + assertEquals(URI.create("http://www.example.org#/hello/world"), pointer.toURI()); + } + + @Test + public void testURIEncodedParsing() { + JsonPointer pointer = JsonPointer.fromURI(URI.create("http://www.example.org#/hello/world/%5Ea")); + assertEquals("/hello/world/^a", pointer.toString()); + assertEquals(URI.create("http://www.example.org#/hello/world/%5Ea"), pointer.toURI()); + } + + @Test + public void testURIJsonPointerEncodedParsing() { + JsonPointer pointer = JsonPointer.fromURI(URI.create("http://www.example.org#/hell~1o/worl~0d")); + assertEquals("/hell~1o/worl~0d", pointer.toString()); + assertEquals(URI.create("http://www.example.org#/hell~1o/worl~0d"), pointer.toURI()); + } + + @Test + public void testBuilding() { + List keys = new ArrayList<>(); + keys.add("hello"); + keys.add("world"); + JsonPointer pointer = new JsonPointerImpl(URI.create("#"), keys); + assertEquals("/hello/world", pointer.toString()); + } + + @Test + public void testURIBuilding() { + JsonPointer pointer = JsonPointer.create().append("hello").append("world"); + assertEquals(URI.create("#/hello/world"), pointer.toURI()); + } + + @Test + public void testEmptyBuilding() { + JsonPointer pointer = JsonPointer.create(); + assertEquals("", pointer.toString()); + assertEquals(URI.create("#"), pointer.toURI()); + } + + @Test + public void testAppendOtherPointer() { + JsonPointer firstPointer = JsonPointer.fromURI(URI.create("http://example.com/stuff.json#/hello")).append("world"); + JsonPointer otherPointer = JsonPointer.fromURI(URI.create("http://example.com/other.json#/francesco")); + firstPointer.append(otherPointer); + assertEquals(URI.create("http://example.com/stuff.json#/hello/world/francesco"), firstPointer.toURI()); + } + + @Test + public void testNullQuerying() { + JsonPointer pointer = JsonPointer.from("/hello/world"); + assertNull(pointer.queryJson(null)); + } + + @Test + public void testNullQueryingRootPointer() { + JsonPointer pointer = JsonPointer.create(); + assertNull(pointer.queryJson(null)); + } + + @Test + public void testNullQueryingRootPointerDefault() { + JsonPointer pointer = JsonPointer.create(); + assertEquals(1, pointer.queryJsonOrDefault(null, 1)); + } + + @Test + public void testJsonObjectQuerying() { + JsonObject obj = new JsonObject() + .put("hello", + new JsonObject().put("world", 1).put("worl", "wrong") + ).put("helo", + new JsonObject().put("world", "wrong").put("worl", "wrong") + ); + JsonPointer pointer = JsonPointer.from("/hello/world"); + assertEquals(1, pointer.queryJson(obj)); + } + + @Test + public void testJsonObjectQueryingDefaultValue() { + JsonObject obj = new JsonObject() + .put("hello", + new JsonObject().put("world", 1).put("worl", "wrong") + ).put("helo", + new JsonObject().put("world", "wrong").put("worl", "wrong") + ); + JsonPointer pointer = JsonPointer.from("/hello/world/my/friend"); + assertEquals(1, pointer.queryJsonOrDefault(obj, 1)); + } + + @Test + public void testJsonArrayQuerying() { + JsonArray array = new JsonArray(); + array.add(new JsonObject() + .put("hello", + new JsonObject().put("world", 2).put("worl", "wrong") + ).put("helo", + new JsonObject().put("world", "wrong").put("worl", "wrong") + )); + array.add(new JsonObject() + .put("hello", + new JsonObject().put("world", 1).put("worl", "wrong") + ).put("helo", + new JsonObject().put("world", "wrong").put("worl", "wrong") + )); + assertEquals(1, JsonPointer.from("/1/hello/world").queryJson(array)); + assertEquals(1, JsonPointer.fromURI(URI.create("#/1/hello/world")).queryJson(array)); + } + + @Test + public void testJsonArrayQueryingOrDefault() { + JsonArray array = new JsonArray(); + array.add(new JsonObject() + .put("hello", + new JsonObject().put("world", 2).put("worl", "wrong") + ).put("helo", + new JsonObject().put("world", "wrong").put("worl", "wrong") + )); + array.add(new JsonObject() + .put("hello", + new JsonObject().put("world", 1).put("worl", "wrong") + ).put("helo", + new JsonObject().put("world", "wrong").put("worl", "wrong") + )); + assertEquals(1, JsonPointer.from("/5/hello/world").queryJsonOrDefault(array, 1)); + } + + @Test + public void testRootPointer() { + JsonPointer pointer = JsonPointer.create(); + JsonArray array = new JsonArray(); + JsonObject obj = new JsonObject() + .put("hello", + new JsonObject().put("world", 2).put("worl", "wrong") + ).put("helo", + new JsonObject().put("world", "wrong").put("worl", "wrong") + ); + array.add(obj); + array.add(new JsonObject() + .put("hello", + new JsonObject().put("world", 1).put("worl", "wrong") + ).put("helo", + new JsonObject().put("world", "wrong").put("worl", "wrong") + )); + + assertEquals(array, pointer.queryJson(array)); + assertEquals(obj, pointer.queryJson(obj)); + assertEquals("hello", pointer.queryJson("hello")); + } + + @Test + public void testRootPointerWrite() { + JsonPointer pointer = JsonPointer.create(); + JsonObject obj = new JsonObject(); + JsonArray arr = new JsonArray(); + assertSame(arr, pointer.writeJson(obj, arr, false)); + } + + @Test + public void testWrongUsageOfDashForQuerying() { + JsonArray array = new JsonArray(); + array.add(new JsonObject() + .put("hello", + new JsonObject().put("world", 2).put("worl", "wrong") + ).put("helo", + new JsonObject().put("world", "wrong").put("worl", "wrong") + )); + array.add(new JsonObject() + .put("hello", + new JsonObject().put("world", 1).put("worl", "wrong") + ).put("helo", + new JsonObject().put("world", "wrong").put("worl", "wrong") + )); + JsonPointer pointer = JsonPointer.from("/-/hello/world"); + assertNull(pointer.queryJson(array)); + } + + /* + The following JSON strings evaluate to the accompanying values: + + "" // the whole document + "/foo" ["bar", "baz"] + "/foo/0" "bar" + "/" 0 + "/a~1b" 1 + "/c%d" 2 + "/e^f" 3 + "/g|h" 4 + "/i\\j" 5 + "/k\"l" 6 + "/ " 7 + "/m~0n" 8 + + */ + @Test + public void testRFCExample() { + JsonObject obj = new JsonObject(" {\n" + + " \"foo\": [\"bar\", \"baz\"],\n" + + " \"\": 0,\n" + + " \"a/b\": 1,\n" + + " \"c%d\": 2,\n" + + " \"e^f\": 3,\n" + + " \"g|h\": 4,\n" + + " \"i\\\\j\": 5,\n" + + " \"k\\\"l\": 6,\n" + + " \" \": 7,\n" + + " \"m~n\": 8\n" + + " }"); + + assertEquals(obj, JsonPointer.from("").queryJson(obj)); + assertEquals(obj.getJsonArray("foo"), JsonPointer.from("/foo").queryJson(obj)); + assertEquals(obj.getJsonArray("foo").getString(0), JsonPointer.from("/foo/0").queryJson(obj)); + assertEquals(obj.getInteger(""), JsonPointer.from("/").queryJson(obj)); + assertEquals(obj.getInteger("a/b"), JsonPointer.from("/a~1b").queryJson(obj)); + assertEquals(obj.getInteger("c%d"), JsonPointer.from("/c%d").queryJson(obj)); + assertEquals(obj.getInteger("e^f"), JsonPointer.from("/e^f").queryJson(obj)); + assertEquals(obj.getInteger("g|h"), JsonPointer.from("/g|h").queryJson(obj)); + assertEquals(obj.getInteger("i\\\\j"), JsonPointer.from("/i\\\\j").queryJson(obj)); + assertEquals(obj.getInteger("k\\\"l"), JsonPointer.from("/k\\\"l").queryJson(obj)); + assertEquals(obj.getInteger(" "), JsonPointer.from("/ ").queryJson(obj)); + assertEquals(obj.getInteger("m~n"), JsonPointer.from("/m~0n").queryJson(obj)); + } + + @Test + public void testWriteJsonObject() { + JsonObject obj = new JsonObject() + .put("hello", + new JsonObject().put("world", 1).put("worl", "wrong") + ).put("helo", + new JsonObject().put("world", "wrong").put("worl", "wrong") + ); + Object toInsert = new JsonObject().put("github", "slinkydeveloper"); + assertEquals(obj, JsonPointer.from("/hello/francesco").writeJson(obj, toInsert)); + assertEquals(toInsert, JsonPointer.from("/hello/francesco").queryJson(obj)); + } + + @Test + public void testWriteWithCreateOnMissingJsonObject() { + JsonObject obj = new JsonObject() + .put("hello", + new JsonObject().put("world", 1).put("worl", "wrong") + ).put("helo", + new JsonObject().put("world", "wrong").put("worl", "wrong") + ); + Object toInsert = new JsonObject().put("github", "slinkydeveloper"); + assertEquals(obj, JsonPointer.from("/hello/users/francesco").write(obj, JsonPointerIterator.JSON_ITERATOR, toInsert, true)); + assertEquals(toInsert, JsonPointer.from("/hello/users/francesco").queryJson(obj)); + } + + @Test + public void testWriteJsonObjectOverride() { + JsonObject obj = new JsonObject() + .put("hello", + new JsonObject().put("world", 1).put("worl", "wrong") + ).put("helo", + new JsonObject().put("world", "wrong").put("worl", "wrong") + ); + Object toInsert = new JsonObject().put("github", "slinkydeveloper"); + assertEquals(obj, JsonPointer.from("/hello/world").writeJson(obj, toInsert)); + assertEquals(toInsert, JsonPointer.from("/hello/world").queryJson(obj)); + } + + @Test + public void testWriteJsonArray() { + JsonObject obj = new JsonObject() + .put("hello", + new JsonObject().put("world", new JsonObject()).put("worl", "wrong") + ).put("helo", + new JsonObject().put("world", "wrong").put("worl", "wrong") + ); + JsonArray array = new JsonArray(); + array.add(obj.copy()); + array.add(obj.copy()); + Object toInsert = new JsonObject().put("github", "slinkydeveloper"); + assertEquals(array, JsonPointer.from("/0/hello/world/francesco").writeJson(array, toInsert)); + assertEquals(toInsert, JsonPointer.from("/0/hello/world/francesco").queryJson(array)); + assertNotEquals(array.getValue(0), array.getValue(1)); + } + + @Test + public void testWriteJsonArrayAppend() { + JsonObject obj = new JsonObject() + .put("hello", + new JsonObject().put("world", 1).put("worl", "wrong") + ).put("helo", + new JsonObject().put("world", "wrong").put("worl", "wrong") + ); + JsonArray array = new JsonArray(); + array.add(obj.copy()); + array.add(obj.copy()); + Object toInsert = new JsonObject().put("github", "slinkydeveloper"); + assertEquals(array, JsonPointer.from("/-").writeJson(array, toInsert)); + assertEquals(toInsert, JsonPointer.from("/2").queryJson(array)); + assertEquals(array.getValue(0), array.getValue(1)); + } + + @Test + public void testWriteJsonArraySubstitute() { + JsonObject obj = new JsonObject() + .put("hello", + new JsonObject().put("world", 1).put("worl", "wrong") + ).put("helo", + new JsonObject().put("world", "wrong").put("worl", "wrong") + ); + JsonArray array = new JsonArray(); + array.add(obj.copy()); + array.add(obj.copy()); + Object toInsert = new JsonObject().put("github", "slinkydeveloper"); + assertEquals(array, JsonPointer.from("/0").writeJson(array, toInsert)); + assertEquals(toInsert, JsonPointer.from("/0").queryJson(array)); + assertNotEquals(array.getValue(0), array.getValue(1)); + } + + @Test + public void testNestedWriteJsonArraySubstitute() { + JsonObject obj = new JsonObject() + .put("hello", + new JsonObject().put("world", 1).put("worl", "wrong") + ).put("helo", + new JsonObject().put("world", "wrong").put("worl", "wrong") + ); + JsonArray array = new JsonArray(); + array.add(obj.copy()); + array.add(obj.copy()); + JsonObject root = new JsonObject().put("array", array); + + Object toInsert = new JsonObject().put("github", "slinkydeveloper"); + assertEquals(root, JsonPointer.from("/array/0").writeJson(root, toInsert)); + assertEquals(toInsert, JsonPointer.from("/array/0").queryJson(root)); + } + + @Test + public void testIsParent() { + JsonPointer parent = JsonPointer.fromURI(URI.create("yaml/valid/refs/Circular.yaml#/properties")); + JsonPointer child = JsonPointer.fromURI(URI.create("yaml/valid/refs/Circular.yaml#/properties/parent")); + assertTrue(parent.isParent(child)); + assertFalse(child.isParent(parent)); + } + + @Test + public void testIsParentDifferentURI() { + JsonPointer parent = JsonPointer.fromURI(URI.create("yaml/valid/refs/Circular.yaml#/properties")); + JsonPointer child = JsonPointer.fromURI(URI.create("json/valid/refs/Circular.yaml#/properties/parent")); + assertFalse(parent.isParent(child)); + assertFalse(child.isParent(parent)); + } + + @Test + public void testIsParentWithRootPointer() { + JsonPointer parent = JsonPointer.fromURI(URI.create("yaml/valid/refs/Circular.yaml#")); + JsonPointer child = JsonPointer.fromURI(URI.create("yaml/valid/refs/Circular.yaml#/properties/parent")); + assertTrue(parent.isParent(child)); + assertFalse(child.isParent(parent)); + } + + @Test + public void testTracedQuery() { + JsonObject child2 = new JsonObject().put("child3", 1); + JsonArray child1 = new JsonArray().add(child2); + JsonObject root = new JsonObject().put("child1", child1); + + JsonPointer pointer = JsonPointer + .create() + .append("child1") + .append("0") + .append("child3"); + + List traced = pointer.tracedQuery(root, JsonPointerIterator.JSON_ITERATOR); + assertEquals(4, traced.size()); + assertSame(root, traced.get(0)); + assertSame(child1, traced.get(1)); + assertSame(child2, traced.get(2)); + assertEquals(1, traced.get(3)); + } + + @Test + public void testEmptyTracedQuery() { + JsonPointer pointer = JsonPointer + .create() + .append("child1") + .append("0") + .append("child3"); + + List traced = pointer.tracedQuery(null, JsonPointerIterator.JSON_ITERATOR); + assertTrue(traced.isEmpty()); + } + + @Test + public void testNotFoundTracedQuery() { + JsonObject child2 = new JsonObject().put("child5", 1); + JsonArray child1 = new JsonArray().add(child2); + JsonObject root = new JsonObject().put("child1", child1); + + JsonPointer pointer = JsonPointer + .create() + .append("child1") + .append("0") + .append("child3"); + + List traced = pointer.tracedQuery(root, JsonPointerIterator.JSON_ITERATOR); + assertEquals(3, traced.size()); + assertSame(root, traced.get(0)); + assertSame(child1, traced.get(1)); + assertSame(child2, traced.get(2)); + } + +}