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));
+ }
+
+}