From 95b036b1c32c995f85499f8f9217270828b657f0 Mon Sep 17 00:00:00 2001 From: Piotr Dytkowski Date: Thu, 27 Jan 2022 09:32:20 +0100 Subject: [PATCH 1/8] [US642] JSON Patch Query (#1) * Add tests from spec TMF630 * Replace JsonPointer with JsonPath in Add operation * Replace JsonPointer with JsonPath in Replace operation * Replace JsonPointer with JsonPath in Remove operation * Refactor library to use String instead of JsonPointer * Format if statements * Add missing final keyword to some vals * Add comments in not active tests * Extract regex constants --- build.gradle | 2 +- project.gradle | 2 +- .../github/fge/jsonpatch/AddOperation.java | 91 ++++---- .../github/fge/jsonpatch/CopyOperation.java | 24 +- .../fge/jsonpatch/DualPathOperation.java | 29 +-- .../fge/jsonpatch/JsonPatchOperation.java | 59 +++-- .../github/fge/jsonpatch/JsonPathParser.java | 17 ++ .../github/fge/jsonpatch/MoveOperation.java | 25 +-- .../fge/jsonpatch/PathValueOperation.java | 26 +-- .../github/fge/jsonpatch/RemoveOperation.java | 54 ++--- .../fge/jsonpatch/ReplaceOperation.java | 52 ++--- .../github/fge/jsonpatch/TestOperation.java | 32 ++- .../fge/jsonpatch/diff/DiffOperation.java | 10 +- .../fge/jsonpatch/RemoveOperationTest.java | 2 +- .../query/AddQueryOperationTest.java | 12 + .../query/RemoveQueryOperationTest.java | 13 ++ .../query/ReplaceQueryOperationTest.java | 13 ++ src/test/resources/jsonpatch/query/add.json | 38 ++++ .../resources/jsonpatch/query/remove.json | 130 +++++++++++ .../resources/jsonpatch/query/replace.json | 205 ++++++++++++++++++ src/test/resources/jsonpatch/testsuite.json | 22 +- 21 files changed, 619 insertions(+), 239 deletions(-) create mode 100644 src/main/java/com/github/fge/jsonpatch/JsonPathParser.java create mode 100644 src/test/java/com/github/fge/jsonpatch/query/AddQueryOperationTest.java create mode 100644 src/test/java/com/github/fge/jsonpatch/query/RemoveQueryOperationTest.java create mode 100644 src/test/java/com/github/fge/jsonpatch/query/ReplaceQueryOperationTest.java create mode 100644 src/test/resources/jsonpatch/query/add.json create mode 100644 src/test/resources/jsonpatch/query/remove.json create mode 100644 src/test/resources/jsonpatch/query/replace.json diff --git a/build.gradle b/build.gradle index 8ffa1a34..bae122d2 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ buildscript { repositories { mavenCentral() maven { - url "http://repo.springsource.org/plugins-release"; + url "https://repo.springsource.org/plugins-release"; } } dependencies { diff --git a/project.gradle b/project.gradle index 8d571b35..9a6e2194 100644 --- a/project.gradle +++ b/project.gradle @@ -32,8 +32,8 @@ project.ext.description = "JSON Patch (RFC 6902) and JSON Merge Patch (RFC 7386) dependencies { provided(group: "com.google.code.findbugs", name: "jsr305", version: "3.0.2"); compile(group: "com.fasterxml.jackson.core", name: "jackson-databind", version: "2.11.0"); + compile(group: 'com.jayway.jsonpath', name: 'json-path', version: '2.6.0') compile(group: "com.github.java-json-tools", name: "msg-simple", version: "1.2"); - compile(group: "com.github.java-json-tools", name: "jackson-coreutils", version: "2.0"); testCompile(group: "org.testng", name: "testng", version: "7.1.0") { exclude(group: "junit", module: "junit"); diff --git a/src/main/java/com/github/fge/jsonpatch/AddOperation.java b/src/main/java/com/github/fge/jsonpatch/AddOperation.java index 5e5fb57a..66d86cf5 100644 --- a/src/main/java/com/github/fge/jsonpatch/AddOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/AddOperation.java @@ -22,13 +22,13 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.fge.jackson.jsonpointer.JsonPointer; import com.github.fge.jackson.jsonpointer.ReferenceToken; -import com.github.fge.jackson.jsonpointer.TokenResolver; - -import java.util.NoSuchElementException; +import com.jayway.jsonpath.DocumentContext; +import com.jayway.jsonpath.JsonPath; /** @@ -65,78 +65,65 @@ * [ 1, 2, 3 ] * */ -public final class AddOperation - extends PathValueOperation -{ - private static final ReferenceToken LAST_ARRAY_ELEMENT - = ReferenceToken.fromRaw("-"); +public final class AddOperation extends PathValueOperation { + public static final String LAST_ARRAY_ELEMENT_SYMBOL = "-"; @JsonCreator - public AddOperation(@JsonProperty("path") final JsonPointer path, - @JsonProperty("value") final JsonNode value) - { + public AddOperation(@JsonProperty("path") final String path, + @JsonProperty("value") final JsonNode value) { super("add", path, value); } @Override - public JsonNode apply(final JsonNode node) - throws JsonPatchException - { - if (path.isEmpty()) + public JsonNode apply(final JsonNode node) throws JsonPatchException { + if (path.isEmpty()) { return value; - + } /* * Check the parent node: it must exist and be a container (ie an array * or an object) for the add operation to work. */ - final JsonNode parentNode = path.parent().path(node); - if (parentNode.isMissingNode()) - throw new JsonPatchException(BUNDLE.getMessage( - "jsonPatch.noSuchParent")); - if (!parentNode.isContainerNode()) - throw new JsonPatchException(BUNDLE.getMessage( - "jsonPatch.parentNotContainer")); - return parentNode.isArray() - ? addToArray(path, node) - : addToObject(path, node); - } + final int lastSlashIndex = path.lastIndexOf('/'); + final String newNodeName = path.substring(lastSlashIndex + 1); + final String pathToParent = path.substring(0, lastSlashIndex); + final String jsonPath = JsonPathParser.tmfStringToJsonPath(pathToParent); + final DocumentContext nodeContext = JsonPath.parse(node.deepCopy()); - private JsonNode addToArray(final JsonPointer path, final JsonNode node) - throws JsonPatchException - { - final JsonNode ret = node.deepCopy(); - final ArrayNode target = (ArrayNode) path.parent().get(ret); + final JsonNode parentNode = nodeContext.read(jsonPath); + if (parentNode == null) { + throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.noSuchParent")); + } + if (!parentNode.isContainerNode()) { + throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.parentNotContainer")); + } - final TokenResolver token = Iterables.getLast(path); + return parentNode.isArray() + ? addToArray(nodeContext, jsonPath, newNodeName) + : addToObject(nodeContext, jsonPath, newNodeName); + } - if (token.getToken().equals(LAST_ARRAY_ELEMENT)) { - target.add(value); - return ret; + private JsonNode addToArray(final DocumentContext node, String jsonPath, String newNodeName) throws JsonPatchException { + if (newNodeName.equals(LAST_ARRAY_ELEMENT_SYMBOL)) { + return node.add(jsonPath, value).read("$", JsonNode.class); } - final int size = target.size(); + final int size = node.read(jsonPath, JsonNode.class).size(); final int index; try { - index = Integer.parseInt(token.toString()); + index = Integer.parseInt(newNodeName); } catch (NumberFormatException ignored) { - throw new JsonPatchException(BUNDLE.getMessage( - "jsonPatch.notAnIndex")); + throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.notAnIndex")); } - if (index < 0 || index > size) - throw new JsonPatchException(BUNDLE.getMessage( - "jsonPatch.noSuchIndex")); + throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.noSuchIndex")); - target.insert(index, value); - return ret; + ArrayNode updatedArray = node.read(jsonPath, ArrayNode.class).insert(index, value); + return "$".equals(jsonPath) ? updatedArray : node.set(jsonPath, updatedArray).read("$", JsonNode.class); } - private JsonNode addToObject(final JsonPointer path, final JsonNode node) - { - final TokenResolver token = Iterables.getLast(path); - final JsonNode ret = node.deepCopy(); - final ObjectNode target = (ObjectNode) path.parent().get(ret); - target.set(token.getToken().getRaw(), value); - return ret; + private JsonNode addToObject(final DocumentContext node, String jsonPath, String newNodeName) { + return node + .put(jsonPath, newNodeName, value) + .read("$", JsonNode.class); } } diff --git a/src/main/java/com/github/fge/jsonpatch/CopyOperation.java b/src/main/java/com/github/fge/jsonpatch/CopyOperation.java index 9a5a75b9..5afef3cc 100644 --- a/src/main/java/com/github/fge/jsonpatch/CopyOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/CopyOperation.java @@ -22,7 +22,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; -import com.github.fge.jackson.jsonpointer.JsonPointer; +import com.jayway.jsonpath.JsonPath; /** * JSON Patch {@code copy} operation @@ -40,24 +40,20 @@ * *

It is an error if {@code from} fails to resolve to a JSON value.

*/ -public final class CopyOperation - extends DualPathOperation -{ +public final class CopyOperation extends DualPathOperation { + @JsonCreator - public CopyOperation(@JsonProperty("from") final JsonPointer from, - @JsonProperty("path") final JsonPointer path) - { + public CopyOperation(@JsonProperty("from") final String from, @JsonProperty("path") final String path) { super("copy", from, path); } @Override - public JsonNode apply(final JsonNode node) - throws JsonPatchException - { - final JsonNode dupData = from.path(node).deepCopy(); - if (dupData.isMissingNode()) - throw new JsonPatchException(BUNDLE.getMessage( - "jsonPatch.noSuchPath")); + public JsonNode apply(final JsonNode node) throws JsonPatchException { + final String jsonPath = JsonPathParser.tmfStringToJsonPath(from); + final JsonNode dupData = JsonPath.parse(node.deepCopy()).read(jsonPath); + if (dupData == null) { + throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.noSuchPath")); + } return new AddOperation(path, dupData).apply(node); } } diff --git a/src/main/java/com/github/fge/jsonpatch/DualPathOperation.java b/src/main/java/com/github/fge/jsonpatch/DualPathOperation.java index 91455bb4..d26f7b8c 100644 --- a/src/main/java/com/github/fge/jsonpatch/DualPathOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/DualPathOperation.java @@ -25,38 +25,30 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.jsontype.TypeSerializer; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; -import com.github.fge.jackson.jsonpointer.JsonPointer; import java.io.IOException; /** * Base class for JSON Patch operations taking two JSON Pointers as arguments */ -public abstract class DualPathOperation - extends JsonPatchOperation -{ +public abstract class DualPathOperation extends JsonPatchOperation { @JsonSerialize(using = ToStringSerializer.class) - protected final JsonPointer from; + protected final String from; /** * Protected constructor * - * @param op operation name + * @param op operation name * @param from source path * @param path destination path */ - protected DualPathOperation(final String op, final JsonPointer from, - final JsonPointer path) - { + protected DualPathOperation(final String op, final String from, final String path) { super(op, path); this.from = from; } @Override - public final void serialize(final JsonGenerator jgen, - final SerializerProvider provider) - throws IOException, JsonProcessingException - { + public final void serialize(final JsonGenerator jgen, final SerializerProvider provider) throws IOException, JsonProcessingException { jgen.writeStartObject(); jgen.writeStringField("op", op); jgen.writeStringField("path", path.toString()); @@ -65,20 +57,17 @@ public final void serialize(final JsonGenerator jgen, } @Override - public final void serializeWithType(final JsonGenerator jgen, - final SerializerProvider provider, final TypeSerializer typeSer) - throws IOException, JsonProcessingException - { + public final void serializeWithType(final JsonGenerator jgen, final SerializerProvider provider, + final TypeSerializer typeSer) throws IOException, JsonProcessingException { serialize(jgen, provider); } - public final JsonPointer getFrom() { + public final String getFrom() { return from; } @Override - public final String toString() - { + public final String toString() { return "op: " + op + "; from: \"" + from + "\"; path: \"" + path + '"'; } } diff --git a/src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java b/src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java index 06a6a62d..b0109ce2 100644 --- a/src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java @@ -27,6 +27,15 @@ import com.github.fge.jackson.jsonpointer.JsonPointer; import com.github.fge.msgsimple.bundle.MessageBundle; import com.github.fge.msgsimple.load.MessageBundles; +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.Option; +import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider; +import com.jayway.jsonpath.spi.json.JsonProvider; +import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; +import com.jayway.jsonpath.spi.mapper.MappingProvider; + +import java.util.EnumSet; +import java.util.Set; import static com.fasterxml.jackson.annotation.JsonSubTypes.*; import static com.fasterxml.jackson.annotation.JsonTypeInfo.*; @@ -34,12 +43,12 @@ @JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "op") @JsonSubTypes({ - @Type(name = "add", value = AddOperation.class), - @Type(name = "copy", value = CopyOperation.class), - @Type(name = "move", value = MoveOperation.class), - @Type(name = "remove", value = RemoveOperation.class), - @Type(name = "replace", value = ReplaceOperation.class), - @Type(name = "test", value = TestOperation.class) + @Type(name = "add", value = AddOperation.class), + @Type(name = "copy", value = CopyOperation.class), + @Type(name = "move", value = MoveOperation.class), + @Type(name = "remove", value = RemoveOperation.class), + @Type(name = "replace", value = ReplaceOperation.class), + @Type(name = "test", value = TestOperation.class) }) /** @@ -56,11 +65,27 @@ * */ @JsonIgnoreProperties(ignoreUnknown = true) -public abstract class JsonPatchOperation - implements JsonSerializable -{ - protected static final MessageBundle BUNDLE - = MessageBundles.getBundle(JsonPatchMessages.class); +public abstract class JsonPatchOperation implements JsonSerializable { + protected static final MessageBundle BUNDLE = MessageBundles.getBundle(JsonPatchMessages.class); + + static { + Configuration.setDefaults(new Configuration.Defaults() { + @Override + public JsonProvider jsonProvider() { + return new JacksonJsonNodeJsonProvider(); + } + + @Override + public Set