From 2b6ac741b04ff53274ba306d3585aec67ee25859 Mon Sep 17 00:00:00 2001 From: Hieu Nguyen Date: Wed, 19 Mar 2014 15:52:16 -0700 Subject: [PATCH 1/4] add "create" operation --- .../github/fge/jsonpatch/AddOperation.java | 68 +------------- .../fge/jsonpatch/AddableValueOperation.java | 87 ++++++++++++++++++ .../github/fge/jsonpatch/CreateOperation.java | 44 +++++++++ .../fge/jsonpatch/JsonPatchOperation.java | 1 + .../fge/jsonpatch/CreateOperationTest.java | 13 +++ src/test/resources/jsonpatch/create.json | 91 +++++++++++++++++++ 6 files changed, 239 insertions(+), 65 deletions(-) create mode 100644 src/main/java/com/github/fge/jsonpatch/AddableValueOperation.java create mode 100644 src/main/java/com/github/fge/jsonpatch/CreateOperation.java create mode 100644 src/test/java/com/github/fge/jsonpatch/CreateOperationTest.java create mode 100644 src/test/resources/jsonpatch/create.json diff --git a/src/main/java/com/github/fge/jsonpatch/AddOperation.java b/src/main/java/com/github/fge/jsonpatch/AddOperation.java index e2d20785..d4d6f8c9 100644 --- a/src/main/java/com/github/fge/jsonpatch/AddOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/AddOperation.java @@ -21,12 +21,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; -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 com.google.common.collect.Iterables; /** @@ -64,11 +59,8 @@ * */ public final class AddOperation - extends PathValueOperation + extends AddableValueOperation { - private static final ReferenceToken LAST_ARRAY_ELEMENT - = ReferenceToken.fromRaw("-"); - @JsonCreator public AddOperation(@JsonProperty("path") final JsonPointer path, @JsonProperty("value") final JsonNode value) @@ -77,62 +69,8 @@ public AddOperation(@JsonProperty("path") final JsonPointer path, } @Override - 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); - } - - 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 TokenResolver token = Iterables.getLast(path); - - if (token.getToken().equals(LAST_ARRAY_ELEMENT)) { - target.add(value); - return ret; - } - - final int size = target.size(); - final int index; - try { - index = Integer.parseInt(token.toString()); - } catch (NumberFormatException ignored) { - throw new JsonPatchException(BUNDLE.getMessage( - "jsonPatch.notAnIndex")); - } - - if (index < 0 || index > size) - throw new JsonPatchException(BUNDLE.getMessage( - "jsonPatch.noSuchIndex")); - - target.insert(index, value); - return ret; - } - - private JsonNode addToObject(final JsonPointer path, final JsonNode node) + protected JsonNode addToObject(final JsonPointer path, final JsonNode node) { - final JsonNode ret = node.deepCopy(); - final ObjectNode target = (ObjectNode) path.parent().get(ret); - target.put(Iterables.getLast(path).getToken().getRaw(), value); - return ret; + return addToObjectWithOverwrite(path, node); } } diff --git a/src/main/java/com/github/fge/jsonpatch/AddableValueOperation.java b/src/main/java/com/github/fge/jsonpatch/AddableValueOperation.java new file mode 100644 index 00000000..6eb07de5 --- /dev/null +++ b/src/main/java/com/github/fge/jsonpatch/AddableValueOperation.java @@ -0,0 +1,87 @@ +package com.github.fge.jsonpatch; + +import com.fasterxml.jackson.databind.JsonNode; +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 com.google.common.collect.Iterables; + +/** + * Represents an operation that can add a {@code value} given a {@code path}. + */ +public abstract class AddableValueOperation + extends PathValueOperation +{ + private static final ReferenceToken LAST_ARRAY_ELEMENT + = ReferenceToken.fromRaw("-"); + + public AddableValueOperation(final String op, final JsonPointer path, final JsonNode value) + { + super(op, path, value); + } + + @Override + 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); + } + + protected JsonNode addToArray(final JsonPointer path, final JsonNode node) + throws JsonPatchException + { + final JsonNode ret = node.deepCopy(); + final ArrayNode target = (ArrayNode) path.parent().get(ret); + final TokenResolver token = Iterables.getLast(path); + + if (token.getToken().equals(LAST_ARRAY_ELEMENT)) { + target.add(value); + return ret; + } + + final int size = target.size(); + final int index; + try { + index = Integer.parseInt(token.toString()); + } catch (NumberFormatException ignored) { + throw new JsonPatchException(BUNDLE.getMessage( + "jsonPatch.notAnIndex")); + } + + if (index < 0 || index > size) + throw new JsonPatchException(BUNDLE.getMessage( + "jsonPatch.noSuchIndex")); + + target.insert(index, value); + return ret; + } + + abstract protected JsonNode addToObject(final JsonPointer path, final JsonNode node) + throws JsonPatchException; + + protected JsonNode addToObjectWithOverwrite(final JsonPointer path, final JsonNode node) + { + final JsonNode ret = node.deepCopy(); + final ObjectNode target = (ObjectNode) path.parent().get(ret); + target.put(Iterables.getLast(path).getToken().getRaw(), value); + return ret; + } +} diff --git a/src/main/java/com/github/fge/jsonpatch/CreateOperation.java b/src/main/java/com/github/fge/jsonpatch/CreateOperation.java new file mode 100644 index 00000000..e4f83c04 --- /dev/null +++ b/src/main/java/com/github/fge/jsonpatch/CreateOperation.java @@ -0,0 +1,44 @@ +package com.github.fge.jsonpatch; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.MissingNode; +import com.github.fge.jackson.jsonpointer.JsonPointer; + +/** + * JSON Patch {@code create} operation. + * + *

For this operation, {@code path} is the JSON Pointer where the value + * should be added, and {@code value} is the value to add.

+ * + *

This is the same as the {@code add} operation with one exception. If the {@code path} + * points to an existing non-array-element, the operation will fail. This is + * designed to prevent clients from accidentally overwriting values they don't + * think exist.

+ */ +public final class CreateOperation + extends AddableValueOperation +{ + @JsonCreator + public CreateOperation(@JsonProperty("path") final JsonPointer path, + @JsonProperty("value") final JsonNode value) + { + super("create", path, value); + } + + /** + * Checks if any value exists at the path before adding to the object. + */ + @Override + protected JsonNode addToObject(final JsonPointer path, final JsonNode node) + throws JsonPatchException + { + final JsonNode existingNode = path.path(node); + if (existingNode != MissingNode.getInstance()) + throw new JsonPatchException(BUNDLE.getMessage( + "jsonPatch.valueAtPathAlreadyExists")); + + return addToObjectWithOverwrite(path, node); + } +} diff --git a/src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java b/src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java index d6e30447..06c23be7 100644 --- a/src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java @@ -35,6 +35,7 @@ @JsonSubTypes({ @Type(name = "add", value = AddOperation.class), @Type(name = "copy", value = CopyOperation.class), + @Type(name = "create", value = CreateOperation.class), @Type(name = "move", value = MoveOperation.class), @Type(name = "remove", value = RemoveOperation.class), @Type(name = "replace", value = ReplaceOperation.class), diff --git a/src/test/java/com/github/fge/jsonpatch/CreateOperationTest.java b/src/test/java/com/github/fge/jsonpatch/CreateOperationTest.java new file mode 100644 index 00000000..b8ccce42 --- /dev/null +++ b/src/test/java/com/github/fge/jsonpatch/CreateOperationTest.java @@ -0,0 +1,13 @@ +package com.github.fge.jsonpatch; + +import java.io.IOException; + +public final class CreateOperationTest + extends JsonPatchOperationTest +{ + public CreateOperationTest() + throws IOException + { + super("create"); + } +} diff --git a/src/test/resources/jsonpatch/create.json b/src/test/resources/jsonpatch/create.json new file mode 100644 index 00000000..58502651 --- /dev/null +++ b/src/test/resources/jsonpatch/create.json @@ -0,0 +1,91 @@ +{ + "errors": [ + { + "op": { "op": "create", "path": "/a/b/c", "value": 1 }, + "node": { "a": "b" }, + "message": "jsonPatch.noSuchParent" + }, + { + "op": { "op": "create", "path": "/a", "value": 1 }, + "node": { "a": "b" }, + "message": "jsonPatch.valueAtPathAlreadyExists" + }, + { + "op": { "op": "create", "path": "/a", "value": 1 }, + "node": { "a": null }, + "message": "jsonPatch.valueAtPathAlreadyExists" + }, + { + "op": { "op": "create", "path": "/obj/inner/b", "value": [ 1, 2 ] }, + "node": { + "obj": { + "inner": { + "a": "hello", + "b": "world" + } + } + }, + "message": "jsonPatch.valueAtPathAlreadyExists" + }, + { + "op": { "op": "create", "path": "/~1", "value": 1 }, + "node": [], + "message": "jsonPatch.notAnIndex" + }, + { + "op": { "op": "create", "path": "/3", "value": 1 }, + "node": [ 1, 2 ], + "message": "jsonPatch.noSuchIndex" + }, + { + "op": { "op": "create", "path": "/-2", "value": 1 }, + "node": [ 1, 2 ], + "message": "jsonPatch.noSuchIndex" + }, + { + "op": { "op": "create", "path": "/foo/f", "value": "bar" }, + "node": { "foo": "bar" }, + "message": "jsonPatch.parentNotContainer" + } + ], + "ops": [ + { + "op": { "op": "create", "path": "", "value": null }, + "node": {}, + "expected": null + }, + { + "op": { "op": "create", "path": "/a", "value": "b" }, + "node": {}, + "expected": { "a": "b" } + }, + { + "op": { "op": "create", "path": "/array/-", "value": 1 }, + "node": { "array": [ 2, null, {}, 1 ] }, + "expected": { "array": [ 2, null, {}, 1, 1 ] } + }, + { + "op": { "op": "create", "path": "/array/2", "value": "hello" }, + "node": { "array": [ 2, null, {}, 1] }, + "expected": { "array": [ 2, null, "hello", {}, 1 ] } + }, + { + "op": { "op": "create", "path": "/obj/inner/b", "value": [ 1, 2 ] }, + "node": { + "obj": { + "inner": { + "a": "hello" + } + } + }, + "expected": { + "obj": { + "inner": { + "a": "hello", + "b": [ 1, 2 ] + } + } + } + } + ] +} From 72f62c916fef43d0c6a8d32d203d9f3609d4dba8 Mon Sep 17 00:00:00 2001 From: Hieu Nguyen Date: Mon, 24 Mar 2014 23:30:24 -0700 Subject: [PATCH 2/4] Update "value already exists" error messaging Refactor AdditionOperation --- .../github/fge/jsonpatch/AddOperation.java | 10 ++---- ...eOperation.java => AdditionOperation.java} | 31 ++++++++++++++----- .../github/fge/jsonpatch/CreateOperation.java | 28 ++++------------- .../github/fge/jsonpatch/messages.properties | 1 + 4 files changed, 32 insertions(+), 38 deletions(-) rename src/main/java/com/github/fge/jsonpatch/{AddableValueOperation.java => AdditionOperation.java} (70%) diff --git a/src/main/java/com/github/fge/jsonpatch/AddOperation.java b/src/main/java/com/github/fge/jsonpatch/AddOperation.java index d4d6f8c9..1201fd3a 100644 --- a/src/main/java/com/github/fge/jsonpatch/AddOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/AddOperation.java @@ -59,18 +59,12 @@ * */ public final class AddOperation - extends AddableValueOperation + extends AdditionOperation { @JsonCreator public AddOperation(@JsonProperty("path") final JsonPointer path, @JsonProperty("value") final JsonNode value) { - super("add", path, value); - } - - @Override - protected JsonNode addToObject(final JsonPointer path, final JsonNode node) - { - return addToObjectWithOverwrite(path, node); + super("add", path, value, true); } } diff --git a/src/main/java/com/github/fge/jsonpatch/AddableValueOperation.java b/src/main/java/com/github/fge/jsonpatch/AdditionOperation.java similarity index 70% rename from src/main/java/com/github/fge/jsonpatch/AddableValueOperation.java rename to src/main/java/com/github/fge/jsonpatch/AdditionOperation.java index 6eb07de5..e973508b 100644 --- a/src/main/java/com/github/fge/jsonpatch/AddableValueOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/AdditionOperation.java @@ -11,15 +11,24 @@ /** * Represents an operation that can add a {@code value} given a {@code path}. */ -public abstract class AddableValueOperation +public abstract class AdditionOperation extends PathValueOperation { private static final ReferenceToken LAST_ARRAY_ELEMENT = ReferenceToken.fromRaw("-"); - public AddableValueOperation(final String op, final JsonPointer path, final JsonNode value) + private boolean overwriteExisting; + + /** + * @param op operation name + * @param path affected path + * @param value value to add + * @param overwriteExisting whether the operation is allowed to overwrite an existing value at the specified path + */ + protected AdditionOperation(final String op, final JsonPointer path, final JsonNode value, boolean overwriteExisting) { super(op, path, value); + this.overwriteExisting = overwriteExisting; } @Override @@ -31,7 +40,7 @@ public JsonNode apply(final JsonNode node) /* * Check the parent node: it must exist and be a container (ie an array - * or an object) for the add operation to work. + * or an object) for the addition operation to work. */ final JsonNode parentNode = path.parent().path(node); if (parentNode.isMissingNode()) @@ -45,7 +54,7 @@ public JsonNode apply(final JsonNode node) : addToObject(path, node); } - protected JsonNode addToArray(final JsonPointer path, final JsonNode node) + private JsonNode addToArray(final JsonPointer path, final JsonNode node) throws JsonPatchException { final JsonNode ret = node.deepCopy(); @@ -74,11 +83,17 @@ protected JsonNode addToArray(final JsonPointer path, final JsonNode node) return ret; } - abstract protected JsonNode addToObject(final JsonPointer path, final JsonNode node) - throws JsonPatchException; - - protected JsonNode addToObjectWithOverwrite(final JsonPointer path, final JsonNode node) + private JsonNode addToObject(final JsonPointer path, final JsonNode node) + throws JsonPatchException { + if (!overwriteExisting) + { + final JsonNode existingNode = path.path(node); + if (!existingNode.isMissingNode()) + throw new JsonPatchException(BUNDLE.getMessage( + "jsonPatch.valueAtPathAlreadyExists")); + } + final JsonNode ret = node.deepCopy(); final ObjectNode target = (ObjectNode) path.parent().get(ret); target.put(Iterables.getLast(path).getToken().getRaw(), value); diff --git a/src/main/java/com/github/fge/jsonpatch/CreateOperation.java b/src/main/java/com/github/fge/jsonpatch/CreateOperation.java index e4f83c04..8033d6bd 100644 --- a/src/main/java/com/github/fge/jsonpatch/CreateOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/CreateOperation.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.MissingNode; import com.github.fge.jackson.jsonpointer.JsonPointer; /** @@ -12,33 +11,18 @@ *

For this operation, {@code path} is the JSON Pointer where the value * should be added, and {@code value} is the value to add.

* - *

This is the same as the {@code add} operation with one exception. If the {@code path} - * points to an existing non-array-element, the operation will fail. This is - * designed to prevent clients from accidentally overwriting values they don't - * think exist.

+ *

This operation behaves like {@code add}, except for JSON Objects, + * where it will raise an error if the target {@code path} points to an + * existing value. This is designed to prevent clients from actually + * overwriting values they don't think exist.

*/ public final class CreateOperation - extends AddableValueOperation + extends AdditionOperation { @JsonCreator public CreateOperation(@JsonProperty("path") final JsonPointer path, @JsonProperty("value") final JsonNode value) { - super("create", path, value); - } - - /** - * Checks if any value exists at the path before adding to the object. - */ - @Override - protected JsonNode addToObject(final JsonPointer path, final JsonNode node) - throws JsonPatchException - { - final JsonNode existingNode = path.path(node); - if (existingNode != MissingNode.getInstance()) - throw new JsonPatchException(BUNDLE.getMessage( - "jsonPatch.valueAtPathAlreadyExists")); - - return addToObjectWithOverwrite(path, node); + super("create", path, value, false); } } diff --git a/src/main/resources/com/github/fge/jsonpatch/messages.properties b/src/main/resources/com/github/fge/jsonpatch/messages.properties index db82a908..100c58eb 100644 --- a/src/main/resources/com/github/fge/jsonpatch/messages.properties +++ b/src/main/resources/com/github/fge/jsonpatch/messages.properties @@ -22,5 +22,6 @@ jsonPatch.notAnIndex=reference token is not an array index jsonPatch.noSuchIndex=no such index in target array jsonPatch.noSuchPath=no such path in target JSON document jsonPatch.parentNotContainer=parent of path to add to is not a container +jsonPatch.valueAtPathAlreadyExists=value at path already exists jsonPatch.valueTestFailure=value differs from expectations mergePatch.notContainer=value is neither an object or an array (found %s) From 35102dbc9796afd9a11ea8da32c7325688509309 Mon Sep 17 00:00:00 2001 From: Hieu Nguyen Date: Wed, 19 Mar 2014 15:52:16 -0700 Subject: [PATCH 3/4] add "create" operation --- .../github/fge/jsonpatch/AddOperation.java | 72 +------------ .../fge/jsonpatch/AdditionOperation.java | 102 ++++++++++++++++++ .../github/fge/jsonpatch/CreateOperation.java | 28 +++++ .../fge/jsonpatch/JsonPatchOperation.java | 1 + .../github/fge/jsonpatch/messages.properties | 1 + .../fge/jsonpatch/CreateOperationTest.java | 13 +++ src/test/resources/jsonpatch/create.json | 91 ++++++++++++++++ 7 files changed, 238 insertions(+), 70 deletions(-) create mode 100644 src/main/java/com/github/fge/jsonpatch/AdditionOperation.java create mode 100644 src/main/java/com/github/fge/jsonpatch/CreateOperation.java create mode 100644 src/test/java/com/github/fge/jsonpatch/CreateOperationTest.java create mode 100644 src/test/resources/jsonpatch/create.json diff --git a/src/main/java/com/github/fge/jsonpatch/AddOperation.java b/src/main/java/com/github/fge/jsonpatch/AddOperation.java index e2d20785..1201fd3a 100644 --- a/src/main/java/com/github/fge/jsonpatch/AddOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/AddOperation.java @@ -21,12 +21,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; -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 com.google.common.collect.Iterables; /** @@ -64,75 +59,12 @@ * */ public final class AddOperation - extends PathValueOperation + extends AdditionOperation { - private static final ReferenceToken LAST_ARRAY_ELEMENT - = ReferenceToken.fromRaw("-"); - @JsonCreator public AddOperation(@JsonProperty("path") final JsonPointer path, @JsonProperty("value") final JsonNode value) { - super("add", path, value); - } - - @Override - 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); - } - - 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 TokenResolver token = Iterables.getLast(path); - - if (token.getToken().equals(LAST_ARRAY_ELEMENT)) { - target.add(value); - return ret; - } - - final int size = target.size(); - final int index; - try { - index = Integer.parseInt(token.toString()); - } catch (NumberFormatException ignored) { - throw new JsonPatchException(BUNDLE.getMessage( - "jsonPatch.notAnIndex")); - } - - if (index < 0 || index > size) - throw new JsonPatchException(BUNDLE.getMessage( - "jsonPatch.noSuchIndex")); - - target.insert(index, value); - return ret; - } - - private JsonNode addToObject(final JsonPointer path, final JsonNode node) - { - final JsonNode ret = node.deepCopy(); - final ObjectNode target = (ObjectNode) path.parent().get(ret); - target.put(Iterables.getLast(path).getToken().getRaw(), value); - return ret; + super("add", path, value, true); } } diff --git a/src/main/java/com/github/fge/jsonpatch/AdditionOperation.java b/src/main/java/com/github/fge/jsonpatch/AdditionOperation.java new file mode 100644 index 00000000..e973508b --- /dev/null +++ b/src/main/java/com/github/fge/jsonpatch/AdditionOperation.java @@ -0,0 +1,102 @@ +package com.github.fge.jsonpatch; + +import com.fasterxml.jackson.databind.JsonNode; +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 com.google.common.collect.Iterables; + +/** + * Represents an operation that can add a {@code value} given a {@code path}. + */ +public abstract class AdditionOperation + extends PathValueOperation +{ + private static final ReferenceToken LAST_ARRAY_ELEMENT + = ReferenceToken.fromRaw("-"); + + private boolean overwriteExisting; + + /** + * @param op operation name + * @param path affected path + * @param value value to add + * @param overwriteExisting whether the operation is allowed to overwrite an existing value at the specified path + */ + protected AdditionOperation(final String op, final JsonPointer path, final JsonNode value, boolean overwriteExisting) + { + super(op, path, value); + this.overwriteExisting = overwriteExisting; + } + + @Override + 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 addition 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); + } + + 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 TokenResolver token = Iterables.getLast(path); + + if (token.getToken().equals(LAST_ARRAY_ELEMENT)) { + target.add(value); + return ret; + } + + final int size = target.size(); + final int index; + try { + index = Integer.parseInt(token.toString()); + } catch (NumberFormatException ignored) { + throw new JsonPatchException(BUNDLE.getMessage( + "jsonPatch.notAnIndex")); + } + + if (index < 0 || index > size) + throw new JsonPatchException(BUNDLE.getMessage( + "jsonPatch.noSuchIndex")); + + target.insert(index, value); + return ret; + } + + private JsonNode addToObject(final JsonPointer path, final JsonNode node) + throws JsonPatchException + { + if (!overwriteExisting) + { + final JsonNode existingNode = path.path(node); + if (!existingNode.isMissingNode()) + throw new JsonPatchException(BUNDLE.getMessage( + "jsonPatch.valueAtPathAlreadyExists")); + } + + final JsonNode ret = node.deepCopy(); + final ObjectNode target = (ObjectNode) path.parent().get(ret); + target.put(Iterables.getLast(path).getToken().getRaw(), value); + return ret; + } +} diff --git a/src/main/java/com/github/fge/jsonpatch/CreateOperation.java b/src/main/java/com/github/fge/jsonpatch/CreateOperation.java new file mode 100644 index 00000000..8033d6bd --- /dev/null +++ b/src/main/java/com/github/fge/jsonpatch/CreateOperation.java @@ -0,0 +1,28 @@ +package com.github.fge.jsonpatch; + +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; + +/** + * JSON Patch {@code create} operation. + * + *

For this operation, {@code path} is the JSON Pointer where the value + * should be added, and {@code value} is the value to add.

+ * + *

This operation behaves like {@code add}, except for JSON Objects, + * where it will raise an error if the target {@code path} points to an + * existing value. This is designed to prevent clients from actually + * overwriting values they don't think exist.

+ */ +public final class CreateOperation + extends AdditionOperation +{ + @JsonCreator + public CreateOperation(@JsonProperty("path") final JsonPointer path, + @JsonProperty("value") final JsonNode value) + { + super("create", path, value, false); + } +} diff --git a/src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java b/src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java index d6e30447..06c23be7 100644 --- a/src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java @@ -35,6 +35,7 @@ @JsonSubTypes({ @Type(name = "add", value = AddOperation.class), @Type(name = "copy", value = CopyOperation.class), + @Type(name = "create", value = CreateOperation.class), @Type(name = "move", value = MoveOperation.class), @Type(name = "remove", value = RemoveOperation.class), @Type(name = "replace", value = ReplaceOperation.class), diff --git a/src/main/resources/com/github/fge/jsonpatch/messages.properties b/src/main/resources/com/github/fge/jsonpatch/messages.properties index db82a908..100c58eb 100644 --- a/src/main/resources/com/github/fge/jsonpatch/messages.properties +++ b/src/main/resources/com/github/fge/jsonpatch/messages.properties @@ -22,5 +22,6 @@ jsonPatch.notAnIndex=reference token is not an array index jsonPatch.noSuchIndex=no such index in target array jsonPatch.noSuchPath=no such path in target JSON document jsonPatch.parentNotContainer=parent of path to add to is not a container +jsonPatch.valueAtPathAlreadyExists=value at path already exists jsonPatch.valueTestFailure=value differs from expectations mergePatch.notContainer=value is neither an object or an array (found %s) diff --git a/src/test/java/com/github/fge/jsonpatch/CreateOperationTest.java b/src/test/java/com/github/fge/jsonpatch/CreateOperationTest.java new file mode 100644 index 00000000..b8ccce42 --- /dev/null +++ b/src/test/java/com/github/fge/jsonpatch/CreateOperationTest.java @@ -0,0 +1,13 @@ +package com.github.fge.jsonpatch; + +import java.io.IOException; + +public final class CreateOperationTest + extends JsonPatchOperationTest +{ + public CreateOperationTest() + throws IOException + { + super("create"); + } +} diff --git a/src/test/resources/jsonpatch/create.json b/src/test/resources/jsonpatch/create.json new file mode 100644 index 00000000..58502651 --- /dev/null +++ b/src/test/resources/jsonpatch/create.json @@ -0,0 +1,91 @@ +{ + "errors": [ + { + "op": { "op": "create", "path": "/a/b/c", "value": 1 }, + "node": { "a": "b" }, + "message": "jsonPatch.noSuchParent" + }, + { + "op": { "op": "create", "path": "/a", "value": 1 }, + "node": { "a": "b" }, + "message": "jsonPatch.valueAtPathAlreadyExists" + }, + { + "op": { "op": "create", "path": "/a", "value": 1 }, + "node": { "a": null }, + "message": "jsonPatch.valueAtPathAlreadyExists" + }, + { + "op": { "op": "create", "path": "/obj/inner/b", "value": [ 1, 2 ] }, + "node": { + "obj": { + "inner": { + "a": "hello", + "b": "world" + } + } + }, + "message": "jsonPatch.valueAtPathAlreadyExists" + }, + { + "op": { "op": "create", "path": "/~1", "value": 1 }, + "node": [], + "message": "jsonPatch.notAnIndex" + }, + { + "op": { "op": "create", "path": "/3", "value": 1 }, + "node": [ 1, 2 ], + "message": "jsonPatch.noSuchIndex" + }, + { + "op": { "op": "create", "path": "/-2", "value": 1 }, + "node": [ 1, 2 ], + "message": "jsonPatch.noSuchIndex" + }, + { + "op": { "op": "create", "path": "/foo/f", "value": "bar" }, + "node": { "foo": "bar" }, + "message": "jsonPatch.parentNotContainer" + } + ], + "ops": [ + { + "op": { "op": "create", "path": "", "value": null }, + "node": {}, + "expected": null + }, + { + "op": { "op": "create", "path": "/a", "value": "b" }, + "node": {}, + "expected": { "a": "b" } + }, + { + "op": { "op": "create", "path": "/array/-", "value": 1 }, + "node": { "array": [ 2, null, {}, 1 ] }, + "expected": { "array": [ 2, null, {}, 1, 1 ] } + }, + { + "op": { "op": "create", "path": "/array/2", "value": "hello" }, + "node": { "array": [ 2, null, {}, 1] }, + "expected": { "array": [ 2, null, "hello", {}, 1 ] } + }, + { + "op": { "op": "create", "path": "/obj/inner/b", "value": [ 1, 2 ] }, + "node": { + "obj": { + "inner": { + "a": "hello" + } + } + }, + "expected": { + "obj": { + "inner": { + "a": "hello", + "b": [ 1, 2 ] + } + } + } + } + ] +} From cef8df0e655f0331877e27b691c84db13bbc3c4a Mon Sep 17 00:00:00 2001 From: Hieu Nguyen Date: Wed, 19 Mar 2014 15:52:16 -0700 Subject: [PATCH 4/4] add "create" operation --- .../github/fge/jsonpatch/AddOperation.java | 72 +----------- .../fge/jsonpatch/AdditionOperation.java | 104 ++++++++++++++++++ .../github/fge/jsonpatch/CreateOperation.java | 28 +++++ .../fge/jsonpatch/JsonPatchOperation.java | 1 + .../github/fge/jsonpatch/messages.properties | 1 + .../fge/jsonpatch/CreateOperationTest.java | 13 +++ src/test/resources/jsonpatch/create.json | 91 +++++++++++++++ 7 files changed, 240 insertions(+), 70 deletions(-) create mode 100644 src/main/java/com/github/fge/jsonpatch/AdditionOperation.java create mode 100644 src/main/java/com/github/fge/jsonpatch/CreateOperation.java create mode 100644 src/test/java/com/github/fge/jsonpatch/CreateOperationTest.java create mode 100644 src/test/resources/jsonpatch/create.json diff --git a/src/main/java/com/github/fge/jsonpatch/AddOperation.java b/src/main/java/com/github/fge/jsonpatch/AddOperation.java index e2d20785..1201fd3a 100644 --- a/src/main/java/com/github/fge/jsonpatch/AddOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/AddOperation.java @@ -21,12 +21,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; -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 com.google.common.collect.Iterables; /** @@ -64,75 +59,12 @@ * */ public final class AddOperation - extends PathValueOperation + extends AdditionOperation { - private static final ReferenceToken LAST_ARRAY_ELEMENT - = ReferenceToken.fromRaw("-"); - @JsonCreator public AddOperation(@JsonProperty("path") final JsonPointer path, @JsonProperty("value") final JsonNode value) { - super("add", path, value); - } - - @Override - 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); - } - - 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 TokenResolver token = Iterables.getLast(path); - - if (token.getToken().equals(LAST_ARRAY_ELEMENT)) { - target.add(value); - return ret; - } - - final int size = target.size(); - final int index; - try { - index = Integer.parseInt(token.toString()); - } catch (NumberFormatException ignored) { - throw new JsonPatchException(BUNDLE.getMessage( - "jsonPatch.notAnIndex")); - } - - if (index < 0 || index > size) - throw new JsonPatchException(BUNDLE.getMessage( - "jsonPatch.noSuchIndex")); - - target.insert(index, value); - return ret; - } - - private JsonNode addToObject(final JsonPointer path, final JsonNode node) - { - final JsonNode ret = node.deepCopy(); - final ObjectNode target = (ObjectNode) path.parent().get(ret); - target.put(Iterables.getLast(path).getToken().getRaw(), value); - return ret; + super("add", path, value, true); } } diff --git a/src/main/java/com/github/fge/jsonpatch/AdditionOperation.java b/src/main/java/com/github/fge/jsonpatch/AdditionOperation.java new file mode 100644 index 00000000..67ff8005 --- /dev/null +++ b/src/main/java/com/github/fge/jsonpatch/AdditionOperation.java @@ -0,0 +1,104 @@ +package com.github.fge.jsonpatch; + +import com.fasterxml.jackson.databind.JsonNode; +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 com.google.common.collect.Iterables; + +/** + * Represents an operation that can add a {@code value} given a {@code path}. + */ +public abstract class AdditionOperation + extends PathValueOperation +{ + private static final ReferenceToken LAST_ARRAY_ELEMENT + = ReferenceToken.fromRaw("-"); + + private final boolean overwriteExisting; + + /** + * @param op operation name + * @param path affected path + * @param value value to add + * @param overwriteExisting whether the operation is allowed to overwrite + * an existing value at the specified path + */ + protected AdditionOperation(final String op, final JsonPointer path, + final JsonNode value, final boolean overwriteExisting) + { + super(op, path, value); + this.overwriteExisting = overwriteExisting; + } + + @Override + 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 addition 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); + } + + 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 TokenResolver token = Iterables.getLast(path); + + if (token.getToken().equals(LAST_ARRAY_ELEMENT)) { + target.add(value); + return ret; + } + + final int size = target.size(); + final int index; + try { + index = Integer.parseInt(token.toString()); + } catch (NumberFormatException ignored) { + throw new JsonPatchException(BUNDLE.getMessage( + "jsonPatch.notAnIndex")); + } + + if (index < 0 || index > size) + throw new JsonPatchException(BUNDLE.getMessage( + "jsonPatch.noSuchIndex")); + + target.insert(index, value); + return ret; + } + + private JsonNode addToObject(final JsonPointer path, final JsonNode node) + throws JsonPatchException + { + if (!overwriteExisting) + { + final JsonNode existingNode = path.path(node); + if (!existingNode.isMissingNode()) + throw new JsonPatchException(BUNDLE.getMessage( + "jsonPatch.valueAtPathAlreadyExists")); + } + + final JsonNode ret = node.deepCopy(); + final ObjectNode target = (ObjectNode) path.parent().get(ret); + target.put(Iterables.getLast(path).getToken().getRaw(), value); + return ret; + } +} diff --git a/src/main/java/com/github/fge/jsonpatch/CreateOperation.java b/src/main/java/com/github/fge/jsonpatch/CreateOperation.java new file mode 100644 index 00000000..8033d6bd --- /dev/null +++ b/src/main/java/com/github/fge/jsonpatch/CreateOperation.java @@ -0,0 +1,28 @@ +package com.github.fge.jsonpatch; + +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; + +/** + * JSON Patch {@code create} operation. + * + *

For this operation, {@code path} is the JSON Pointer where the value + * should be added, and {@code value} is the value to add.

+ * + *

This operation behaves like {@code add}, except for JSON Objects, + * where it will raise an error if the target {@code path} points to an + * existing value. This is designed to prevent clients from actually + * overwriting values they don't think exist.

+ */ +public final class CreateOperation + extends AdditionOperation +{ + @JsonCreator + public CreateOperation(@JsonProperty("path") final JsonPointer path, + @JsonProperty("value") final JsonNode value) + { + super("create", path, value, false); + } +} diff --git a/src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java b/src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java index d6e30447..06c23be7 100644 --- a/src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java @@ -35,6 +35,7 @@ @JsonSubTypes({ @Type(name = "add", value = AddOperation.class), @Type(name = "copy", value = CopyOperation.class), + @Type(name = "create", value = CreateOperation.class), @Type(name = "move", value = MoveOperation.class), @Type(name = "remove", value = RemoveOperation.class), @Type(name = "replace", value = ReplaceOperation.class), diff --git a/src/main/resources/com/github/fge/jsonpatch/messages.properties b/src/main/resources/com/github/fge/jsonpatch/messages.properties index db82a908..100c58eb 100644 --- a/src/main/resources/com/github/fge/jsonpatch/messages.properties +++ b/src/main/resources/com/github/fge/jsonpatch/messages.properties @@ -22,5 +22,6 @@ jsonPatch.notAnIndex=reference token is not an array index jsonPatch.noSuchIndex=no such index in target array jsonPatch.noSuchPath=no such path in target JSON document jsonPatch.parentNotContainer=parent of path to add to is not a container +jsonPatch.valueAtPathAlreadyExists=value at path already exists jsonPatch.valueTestFailure=value differs from expectations mergePatch.notContainer=value is neither an object or an array (found %s) diff --git a/src/test/java/com/github/fge/jsonpatch/CreateOperationTest.java b/src/test/java/com/github/fge/jsonpatch/CreateOperationTest.java new file mode 100644 index 00000000..b8ccce42 --- /dev/null +++ b/src/test/java/com/github/fge/jsonpatch/CreateOperationTest.java @@ -0,0 +1,13 @@ +package com.github.fge.jsonpatch; + +import java.io.IOException; + +public final class CreateOperationTest + extends JsonPatchOperationTest +{ + public CreateOperationTest() + throws IOException + { + super("create"); + } +} diff --git a/src/test/resources/jsonpatch/create.json b/src/test/resources/jsonpatch/create.json new file mode 100644 index 00000000..58502651 --- /dev/null +++ b/src/test/resources/jsonpatch/create.json @@ -0,0 +1,91 @@ +{ + "errors": [ + { + "op": { "op": "create", "path": "/a/b/c", "value": 1 }, + "node": { "a": "b" }, + "message": "jsonPatch.noSuchParent" + }, + { + "op": { "op": "create", "path": "/a", "value": 1 }, + "node": { "a": "b" }, + "message": "jsonPatch.valueAtPathAlreadyExists" + }, + { + "op": { "op": "create", "path": "/a", "value": 1 }, + "node": { "a": null }, + "message": "jsonPatch.valueAtPathAlreadyExists" + }, + { + "op": { "op": "create", "path": "/obj/inner/b", "value": [ 1, 2 ] }, + "node": { + "obj": { + "inner": { + "a": "hello", + "b": "world" + } + } + }, + "message": "jsonPatch.valueAtPathAlreadyExists" + }, + { + "op": { "op": "create", "path": "/~1", "value": 1 }, + "node": [], + "message": "jsonPatch.notAnIndex" + }, + { + "op": { "op": "create", "path": "/3", "value": 1 }, + "node": [ 1, 2 ], + "message": "jsonPatch.noSuchIndex" + }, + { + "op": { "op": "create", "path": "/-2", "value": 1 }, + "node": [ 1, 2 ], + "message": "jsonPatch.noSuchIndex" + }, + { + "op": { "op": "create", "path": "/foo/f", "value": "bar" }, + "node": { "foo": "bar" }, + "message": "jsonPatch.parentNotContainer" + } + ], + "ops": [ + { + "op": { "op": "create", "path": "", "value": null }, + "node": {}, + "expected": null + }, + { + "op": { "op": "create", "path": "/a", "value": "b" }, + "node": {}, + "expected": { "a": "b" } + }, + { + "op": { "op": "create", "path": "/array/-", "value": 1 }, + "node": { "array": [ 2, null, {}, 1 ] }, + "expected": { "array": [ 2, null, {}, 1, 1 ] } + }, + { + "op": { "op": "create", "path": "/array/2", "value": "hello" }, + "node": { "array": [ 2, null, {}, 1] }, + "expected": { "array": [ 2, null, "hello", {}, 1 ] } + }, + { + "op": { "op": "create", "path": "/obj/inner/b", "value": [ 1, 2 ] }, + "node": { + "obj": { + "inner": { + "a": "hello" + } + } + }, + "expected": { + "obj": { + "inner": { + "a": "hello", + "b": [ 1, 2 ] + } + } + } + } + ] +}