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 ] + } + } + } + } + ] +}