diff --git a/README.md b/README.md index 64cae4cb..337b1cdb 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Its features are: * {de,}serialization of JSON Patch and JSON Merge Patch instances with Jackson; * full support for RFC 6902 operations, including `test`; * JSON "diff" (RFC 6902 only) with operation factorization. +* support for `JsonPointer` and `JsonPath` ## Versions @@ -266,6 +267,129 @@ final JsonNode patched = patch.apply(orig); } ```
+ +### Add if not exists +It's possible to add element to JsonNode if it does not exist using JsonPath expressions [see more examples of JsonPath](#jsonpath-examples) +* Add `color` field to `bicycle` object if it doesn't exist +`{ "op": "add", "path": "$.store.bicycle[?(!@.color)].color", "value": "red" }` + + Before: + ```json + { + "store": { + "bicycle": { + "price": 19.95 + } + } + } + ``` + + After: + ```json + { + "store": { + "bicycle": { + "price": 19.95, + "color": "red" + } + } + } + ``` +* Add value for `color` field to `bicycle` object if it is equal to `null` + `{ "op": "add", "path": "$.store.bicycle[?(@.color == null)].color", "value": "red" }` + + Before: + ```json + { + "store": { + "bicycle": { + "price": 19.95, + "color": null + } + } + } + ``` + + After: + ```json + { + "store": { + "bicycle": { + "price": 19.95, + "color": "red" + } + } + } + ``` + +* Add field `pages` to `book` array if `book` does not contain this field, or it is equal to `null` + `{ "op": "add", "path": "$..book[?(!@.pages || @.pages == null)].pages", "value": 250 }` + + Before: + ```json + { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99, + "pages": null + }, + { + "category": "fiction", + "author": "J.R.R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99, + "pages": 100 + } + ] + } + } + ``` + + After: + ```json + { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95, + "pages": 250 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99, + "pages": 250 + }, + { + "category": "fiction", + "author": "J.R.R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99, + "pages": 100 + } + ] + } + } + ``` + ### Remove operation * Remove element with name `a` diff --git a/src/main/java/com/gravity9/jsonpatch/CopyOperation.java b/src/main/java/com/gravity9/jsonpatch/CopyOperation.java index 41a8d14e..7d932563 100644 --- a/src/main/java/com/gravity9/jsonpatch/CopyOperation.java +++ b/src/main/java/com/gravity9/jsonpatch/CopyOperation.java @@ -49,7 +49,7 @@ public CopyOperation(@JsonProperty("from") final String from, @JsonProperty("pat @Override public JsonNode applyInternal(final JsonNode node) throws JsonPatchException { - final String jsonPath = JsonPathParser.tmfStringToJsonPath(from); + final String jsonPath = JsonPathParser.parsePathToJsonPath(from); final JsonNode dupData = JsonPath.parse(node.deepCopy()).read(jsonPath); return new AddOperation(path, dupData).apply(node); } diff --git a/src/main/java/com/gravity9/jsonpatch/JsonPathParser.java b/src/main/java/com/gravity9/jsonpatch/JsonPathParser.java index fe480293..c7718286 100644 --- a/src/main/java/com/gravity9/jsonpatch/JsonPathParser.java +++ b/src/main/java/com/gravity9/jsonpatch/JsonPathParser.java @@ -1,10 +1,16 @@ package com.gravity9.jsonpatch; -public class JsonPathParser { +class JsonPathParser { private static final String ARRAY_ELEMENT_REGEX = "(?<=\\.)(\\d+)"; - public static String tmfStringToJsonPath(String path) throws JsonPatchException { + /** + * Method parses JsonPointer or JsonPath path to JsonPath syntax + * @param path String containing JsonPath or JsonPointer expression + * @return String containing JsonPath expression + * @throws JsonPatchException throws when invalid JsonPointer expression provided + */ + static String parsePathToJsonPath(String path) throws JsonPatchException { if (path.startsWith("$")) { return path; } else if (path.contains("?")) { diff --git a/src/main/java/com/gravity9/jsonpatch/MoveOperation.java b/src/main/java/com/gravity9/jsonpatch/MoveOperation.java index 2c0621a9..f9a5587a 100644 --- a/src/main/java/com/gravity9/jsonpatch/MoveOperation.java +++ b/src/main/java/com/gravity9/jsonpatch/MoveOperation.java @@ -73,7 +73,7 @@ public JsonNode applyInternal(final JsonNode node) throws JsonPatchException { if (from.equals(path)) { return node.deepCopy(); } - String jsonPath = JsonPathParser.tmfStringToJsonPath(from); + String jsonPath = JsonPathParser.parsePathToJsonPath(from); final JsonNode movedNode = JsonPath.parse(node.deepCopy()).read(jsonPath, JsonNode.class); final JsonPatchOperation remove = new RemoveOperation(from); final JsonPatchOperation add = new AddOperation(path, movedNode); diff --git a/src/main/java/com/gravity9/jsonpatch/PathParser.java b/src/main/java/com/gravity9/jsonpatch/PathParser.java index eb376ac3..249c776f 100644 --- a/src/main/java/com/gravity9/jsonpatch/PathParser.java +++ b/src/main/java/com/gravity9/jsonpatch/PathParser.java @@ -19,7 +19,7 @@ public class PathParser { * @throws JsonPatchException when invalid path provided * */ public static PathDetails getParentPathAndNewNodeName(String path) throws JsonPatchException { - final String fullJsonPath = JsonPathParser.tmfStringToJsonPath(path); + final String fullJsonPath = JsonPathParser.parsePathToJsonPath(path); final Path compiledPath = compilePath(fullJsonPath); String[] splitJsonPath = splitJsonPath(compiledPath); diff --git a/src/main/java/com/gravity9/jsonpatch/RemoveOperation.java b/src/main/java/com/gravity9/jsonpatch/RemoveOperation.java index 35f498b5..bb4df8e1 100644 --- a/src/main/java/com/gravity9/jsonpatch/RemoveOperation.java +++ b/src/main/java/com/gravity9/jsonpatch/RemoveOperation.java @@ -22,13 +22,13 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.jsontype.TypeSerializer; import com.fasterxml.jackson.databind.node.MissingNode; import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; + import java.io.IOException; /** @@ -51,23 +51,23 @@ public JsonNode applyInternal(final JsonNode node) throws JsonPatchException { } final DocumentContext nodeContext = JsonPath.parse(node.deepCopy()); - final String jsonPath = JsonPathParser.tmfStringToJsonPath(path); + final String jsonPath = JsonPathParser.parsePathToJsonPath(path); return nodeContext .delete(jsonPath) .read("$", JsonNode.class); } @Override - public void serialize(final JsonGenerator jgen, final SerializerProvider provider) throws IOException, JsonProcessingException { + public void serialize(final JsonGenerator jgen, final SerializerProvider provider) throws IOException { jgen.writeStartObject(); jgen.writeStringField("op", "remove"); - jgen.writeStringField("path", path.toString()); + jgen.writeStringField("path", path); jgen.writeEndObject(); } @Override public void serializeWithType(final JsonGenerator jgen, final SerializerProvider provider, final TypeSerializer typeSer) - throws IOException, JsonProcessingException { + throws IOException { serialize(jgen, provider); } diff --git a/src/main/java/com/gravity9/jsonpatch/ReplaceOperation.java b/src/main/java/com/gravity9/jsonpatch/ReplaceOperation.java index 4767eb1c..27687e8e 100644 --- a/src/main/java/com/gravity9/jsonpatch/ReplaceOperation.java +++ b/src/main/java/com/gravity9/jsonpatch/ReplaceOperation.java @@ -43,7 +43,7 @@ public ReplaceOperation(@JsonProperty("path") final String path, @JsonProperty(" @Override public JsonNode applyInternal(final JsonNode node) throws JsonPatchException { - final String jsonPath = JsonPathParser.tmfStringToJsonPath(path); + final String jsonPath = JsonPathParser.parsePathToJsonPath(path); final DocumentContext nodeContext = JsonPath.parse(node.deepCopy()); final JsonNode replacement = value.deepCopy(); if (path.isEmpty()) { diff --git a/src/main/java/com/gravity9/jsonpatch/TestOperation.java b/src/main/java/com/gravity9/jsonpatch/TestOperation.java index e408349c..985786d0 100644 --- a/src/main/java/com/gravity9/jsonpatch/TestOperation.java +++ b/src/main/java/com/gravity9/jsonpatch/TestOperation.java @@ -49,7 +49,7 @@ public TestOperation(@JsonProperty("path") final String path, @JsonProperty("val @Override public JsonNode applyInternal(final JsonNode node) throws JsonPatchException { - final String jsonPath = JsonPathParser.tmfStringToJsonPath(path); + final String jsonPath = JsonPathParser.parsePathToJsonPath(path); final JsonNode tested = JsonPath.parse(node.deepCopy()).read(jsonPath); if (!EQUIVALENCE.equivalent(tested, value)) { throw JsonPatchException.valueTestFailure(value, tested); diff --git a/src/test/java/com/gravity9/jsonpatch/JsonPathParserTest.java b/src/test/java/com/gravity9/jsonpatch/JsonPathParserTest.java index 81afb84f..097efc37 100644 --- a/src/test/java/com/gravity9/jsonpatch/JsonPathParserTest.java +++ b/src/test/java/com/gravity9/jsonpatch/JsonPathParserTest.java @@ -10,7 +10,7 @@ public class JsonPathParserTest { public void shouldConvertPointerToJsonPath() throws JsonPatchException { String jsonPointerWithQuery = "/productPrice/prodPriceAlteration"; String expected = "$.productPrice.prodPriceAlteration"; - String result = JsonPathParser.tmfStringToJsonPath(jsonPointerWithQuery); + String result = JsonPathParser.parsePathToJsonPath(jsonPointerWithQuery); assertEquals(result, expected); } @@ -18,7 +18,7 @@ public void shouldConvertPointerToJsonPath() throws JsonPatchException { public void shouldConvertPointerWithArrayToJsonPath() throws JsonPatchException { String jsonPointerWithQuery = "/productPrice/1/prodPriceAlteration"; String expected = "$.productPrice.[1].prodPriceAlteration"; - String result = JsonPathParser.tmfStringToJsonPath(jsonPointerWithQuery); + String result = JsonPathParser.parsePathToJsonPath(jsonPointerWithQuery); assertEquals(result, expected); } @@ -26,7 +26,7 @@ public void shouldConvertPointerWithArrayToJsonPath() throws JsonPatchException public void shouldConvertPointerWithArrayAtTheEndToJsonPath() throws JsonPatchException { String jsonPointerWithQuery = "/productPrice/prodPriceAlteration/1"; String expected = "$.productPrice.prodPriceAlteration.[1]"; - String result = JsonPathParser.tmfStringToJsonPath(jsonPointerWithQuery); + String result = JsonPathParser.parsePathToJsonPath(jsonPointerWithQuery); assertEquals(result, expected); } @@ -34,7 +34,7 @@ public void shouldConvertPointerWithArrayAtTheEndToJsonPath() throws JsonPatchEx public void shouldConvertArrayPathToJsonPath() throws JsonPatchException { String jsonPointer = "/2/1/-"; String expected = "$.[2].[1].-"; - String result = JsonPathParser.tmfStringToJsonPath(jsonPointer); + String result = JsonPathParser.parsePathToJsonPath(jsonPointer); assertEquals(result, expected); } @@ -42,19 +42,19 @@ public void shouldConvertArrayPathToJsonPath() throws JsonPatchException { public void shouldLeaveJsonPathStatementsUntouched() throws JsonPatchException { String filterQuery = "$.arrayPath[?(@.innerArray[?(@.nestedVal=='as')] empty false)].innerArray[?(@.nestedVal=='df')].name"; String expected = "$.arrayPath[?(@.innerArray[?(@.nestedVal=='as')] empty false)].innerArray[?(@.nestedVal=='df')].name"; - String result = JsonPathParser.tmfStringToJsonPath(filterQuery); + String result = JsonPathParser.parsePathToJsonPath(filterQuery); assertEquals(result, expected); } @Test(expectedExceptions = JsonPatchException.class, expectedExceptionsMessageRegExp = "Invalid path, `//` is not allowed in JsonPointer expressions.") public void shouldThrowExceptionWhenDoubleSlashesInJsonPointerPath() throws JsonPatchException { String filterQuery = "/characteristic/0//age"; - JsonPathParser.tmfStringToJsonPath(filterQuery); + JsonPathParser.parsePathToJsonPath(filterQuery); } @Test(expectedExceptions = JsonPatchException.class) public void shouldThrowExceptionWhenQuestionMarkInJsonPointerPath() throws JsonPatchException { String filterQuery = "/characteristic/0/age?"; - JsonPathParser.tmfStringToJsonPath(filterQuery); + JsonPathParser.parsePathToJsonPath(filterQuery); } } \ No newline at end of file diff --git a/src/test/resources/jsonpatch/add.json b/src/test/resources/jsonpatch/add.json index d475c78d..588aca5b 100644 --- a/src/test/resources/jsonpatch/add.json +++ b/src/test/resources/jsonpatch/add.json @@ -1507,6 +1507,221 @@ }, "expensive": 10 } + }, + { + "op": { "op": "add", "path": "$.store.bicycle[?(!@.color)].color", "value": "red" }, + "node": + { + "store": { + "bicycle": { + "price": 19.95 + } + }, + "expensive": 10 + }, + "expected": + { + "store": { + "bicycle": { + "color": "red", + "price": 19.95 + } + }, + "expensive": 10 + } + }, + + { + "op": { "op": "add", "path": "$.store.bicycle[?(@.color == null)].color", "value": "red" }, + "node": + { + "store": { + "bicycle": { + "price": 19.95, + "color": null + } + }, + "expensive": 10 + }, + "expected": + { + "store": { + "bicycle": { + "color": "red", + "price": 19.95 + } + }, + "expensive": 10 + } + }, + + { + "op": { "op": "add", "path": "$.store.bicycle[?(!@.color)].color", "value": "red" }, + "node": + { + "store": { + "bicycle": { + "color": "green", + "price": 19.95 + } + } + }, + "expected": + { + "store": { + "bicycle": { + "color": "green", + "price": 19.95 + } + } + } + }, + + { + "op": { "op": "add", "path": "$..book[?(!@.pages || @.pages == null)].pages", "value": 250 }, + "node": + { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99, + "pages": null + }, + { + "category": "fiction", + "author": "J.R.R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99, + "pages": 100 + } + ], + "bicycle": { + "color": "red", + "price": 19.95 + } + }, + "expensive": 10 + }, + "expected": + { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95, + "pages": 250 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99, + "pages": 250 + }, + { + "category": "fiction", + "author": "J.R.R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99, + "pages": 100 + } + ], + "bicycle": { + "color": "red", + "price": 19.95 + } + }, + "expensive": 10 + } + }, + + { + "op": { "op": "add", "path": "$..book[?(!@.pages)].pages", "value": 250 }, + "node": + { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99, + "pages": null + }, + { + "category": "fiction", + "author": "J.R.R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99, + "pages": 100 + } + ], + "bicycle": { + "color": "red", + "price": 19.95 + } + }, + "expensive": 10 + }, + "expected": + { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95, + "pages": 250 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99, + "pages": null + }, + { + "category": "fiction", + "author": "J.R.R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99, + "pages": 100 + } + ], + "bicycle": { + "color": "red", + "price": 19.95 + } + }, + "expensive": 10 + } } + ] } \ No newline at end of file diff --git a/src/test/resources/jsonpatch/test.json b/src/test/resources/jsonpatch/test.json index a1cd4202..d5602480 100644 --- a/src/test/resources/jsonpatch/test.json +++ b/src/test/resources/jsonpatch/test.json @@ -1255,6 +1255,80 @@ }, "expensive": 10 } + }, + + { + "op": { "op": "test", "path": "$..book[?(@.category in ['fiction', 'reference'])].pages", "value": [null, null, null] }, + "node": + { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95, + "pages": null + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99, + "pages": null + }, + { + "category": "fiction", + "author": "J.R.R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99, + "pages": null + } + ], + "bicycle": { + "color": "red", + "price": 19.95 + } + }, + "expensive": 10 + }, + "expected": + { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95, + "pages": null + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99, + "pages": null + }, + { + "category": "fiction", + "author": "J.R.R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99, + "pages": null + } + ], + "bicycle": { + "color": "red", + "price": 19.95 + } + }, + "expensive": 10 + } } ] } \ No newline at end of file