From ca122aa773ea762afc98aa361f0d0921b84e6293 Mon Sep 17 00:00:00 2001 From: Dan Douglas Date: Mon, 18 Apr 2022 17:40:25 -0700 Subject: [PATCH] port [zjsonpatch](https://github.com/flipkart-incubator/zjsonpatch) version [0.4.12](https://github.com/flipkart-incubator/zjsonpatch/releases/tag/0.4.12) --- .github/workflows/codeql-analysis.yml | 70 ++++++++++++++ .gitignore | 1 + NOTICE | 2 +- README.md | 13 +-- pom.xml | 16 ++-- .../java/com/ebay/bsonpatch/BsonDiff.java | 51 +++++++--- .../ebay/bsonpatch/CompatibilityFlags.java | 3 +- .../java/com/ebay/bsonpatch/DiffFlags.java | 23 ++++- .../ebay/bsonpatch/InPlaceApplyProcessor.java | 14 ++- .../com/ebay/bsonpatch/CompatibilityTest.java | 19 +++- .../java/com/ebay/bsonpatch/JsonDiffTest.java | 39 +++++++- .../bsonpatch/JsonSplitReplaceOpTest.java | 93 +++++++++++++++++++ 12 files changed, 306 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 src/test/java/com/ebay/bsonpatch/JsonSplitReplaceOpTest.java diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..001190b --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '34 20 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'java' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.gitignore b/.gitignore index 1df0aba..c645542 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ target/* .settings/* .DS_Store /target/ +.idea diff --git a/NOTICE b/NOTICE index 7a94205..82ea274 100644 --- a/NOTICE +++ b/NOTICE @@ -1,5 +1,5 @@ bsonpatch library -Copyright 2017,2018 eBay, Inc. +Copyright 2017,2018,2022 eBay, Inc. This product includes software developed at eBay, Inc. (https://www.ebay.com/). diff --git a/README.md b/README.md index d0a9e36..9d6ed95 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ -# This is an implementation of [RFC 6902 JSON Patch](http://tools.ietf.org/html/rfc6902) written in Java. +# This is an implementation of [RFC 6902 JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) written in Java. This [JSON Patch](http://jsonpatch.com) implementation works directly with [BSON documents](http://bsonspec.org/) using the [MongoDB Java driver implementation of BSON](https://www.mongodb.com/json-and-bson). The code here was ported (copied, renamed, repackaged, modified) from the [zjsonpatch project](https://github.com/flipkart-incubator/zjsonpatch). ## Description & Use-Cases -- Java Library to find / apply JSON Patches according to [RFC 6902](http://tools.ietf.org/html/rfc6902). +- Java Library to find / apply JSON Patches according to [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902). - JSON Patch defines a JSON document structure for representing changes to a JSON document. - It can be used to avoid sending a whole document when only a part has changed, thus reducing network bandwidth requirements if data (in JSON format) is required to send across multiple systems over network or in case of multi DC transfer. -- This library compares two [BsonValue](http://mongodb.github.io/mongo-java-driver/3.6/javadoc/org/bson/BsonValue.html) inputs and produces a [BsonArray](http://mongodb.github.io/mongo-java-driver/3.6/javadoc/org/bson/BsonArray.html) of the changes. +- When used in combination with the HTTP PATCH method as per [RFC 5789 HTTP PATCH](https://datatracker.ietf.org/doc/html/rfc5789), it will do partial updates for HTTP APIs in a standard way. +- This library compares two [BsonValue](https://mongodb.github.io/mongo-java-driver/3.12/javadoc/org/bson/BsonValue.html) inputs and produces a [BsonArray](https://mongodb.github.io/mongo-java-driver/3.12/javadoc/org/bson/BsonArray.html) of the changes. ### Compatible with : Java 8 and above all versions @@ -20,7 +21,7 @@ The code here was ported (copied, renamed, repackaged, modified) from the [zjson ### How to use: -### Current Version : 0.4.9 +### Current Version : 0.4.12 Add following to `` section of your pom.xml - @@ -28,7 +29,7 @@ Add following to `` section of your pom.xml - com.ebay.bsonpatch bsonpatch - 0.4.9 + 0.4.12 ``` @@ -42,7 +43,7 @@ Computes and returns a JSON `patch` (as a BsonArray) from `source` to `target`, Both `source` and `target` must be either valid BSON objects or arrays or values. Further, if resultant `patch` is applied to `source`, it will yield `target`. -The algorithm which computes this JsonPatch currently generates following operations as per [RFC 6902](https://tools.ietf.org/html/rfc6902) - +The algorithm which computes this JsonPatch currently generates following operations as per [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902#section-4) - - `add` - `remove` - `replace` diff --git a/pom.xml b/pom.xml index ca8fde6..e21982e 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.ebay.bsonpatch bsonpatch - 0.4.9 + 0.4.12 jar ${project.groupId}:${project.artifactId} @@ -47,7 +47,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.10.1 1.8 1.8 @@ -61,7 +61,7 @@ org.apache.maven.plugins maven-source-plugin - 3.0.1 + 3.2.1 attach-sources @@ -118,7 +118,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 2.10.4 + 3.3.2 attach-javadocs @@ -137,25 +137,25 @@ org.mongodb mongo-java-driver - 3.11.2 + 3.12.10 org.apache.commons commons-collections4 - 4.2 + 4.3 test commons-io commons-io - 2.6 + 2.7 test junit junit - 4.12 + 4.13.1 diff --git a/src/main/java/com/ebay/bsonpatch/BsonDiff.java b/src/main/java/com/ebay/bsonpatch/BsonDiff.java index 25f23a9..6c50735 100644 --- a/src/main/java/com/ebay/bsonpatch/BsonDiff.java +++ b/src/main/java/com/ebay/bsonpatch/BsonDiff.java @@ -48,20 +48,29 @@ public static BsonArray asBson(final BsonValue source, final BsonValue target) { public static BsonArray asBson(final BsonValue source, final BsonValue target, EnumSet flags) { BsonDiff diff = new BsonDiff(flags); - - // generating diffs in the order of their occurrence - diff.generateDiffs(JsonPointer.ROOT, source, target); - - if (!flags.contains(DiffFlags.OMIT_MOVE_OPERATION)) { - // Merging remove & add to move operation - diff.introduceMoveOperation(); + if (source == null && target != null) { + // return add node at root pointing to the target + diff.diffs.add(Diff.generateDiff(Operation.ADD, JsonPointer.ROOT, target)); } - - if (!flags.contains(DiffFlags.OMIT_COPY_OPERATION)) { - // Introduce copy operation - diff.introduceCopyOperation(source, target); + if (source != null && target == null) { + // return remove node at root pointing to the source + diff.diffs.add(Diff.generateDiff(Operation.REMOVE, JsonPointer.ROOT, source)); } + if (source != null && target != null) { + diff.generateDiffs(JsonPointer.ROOT, source, target); + + if (!flags.contains(DiffFlags.OMIT_MOVE_OPERATION)) + // Merging remove & add to move operation + diff.introduceMoveOperation(); + + if (!flags.contains(DiffFlags.OMIT_COPY_OPERATION)) + // Introduce copy operation + diff.introduceCopyOperation(source, target); + if (flags.contains(DiffFlags.ADD_EXPLICIT_REMOVE_ADD_ON_REPLACE)) + // Split replace into remove and add instructions + diff.introduceExplicitRemoveAndAddOperation(); + } return diff.getBsonNodes(); } @@ -212,6 +221,26 @@ private void introduceMoveOperation() { } } + /** + * This method splits a {@link Operation#REPLACE} operation within a diff into a {@link Operation#REMOVE} + * and {@link Operation#ADD} in order, respectively. + * Does nothing if {@link Operation#REPLACE} op does not contain a from value + */ + private void introduceExplicitRemoveAndAddOperation() { + List updatedDiffs = new ArrayList(); + for (Diff diff : diffs) { + if (!diff.getOperation().equals(Operation.REPLACE) || diff.getSrcValue() == null) { + updatedDiffs.add(diff); + continue; + } + //Split into two #REMOVE and #ADD + updatedDiffs.add(new Diff(Operation.REMOVE, diff.getPath(), diff.getSrcValue())); + updatedDiffs.add(new Diff(Operation.ADD, diff.getPath(), diff.getValue())); + } + diffs.clear(); + diffs.addAll(updatedDiffs); + } + //Note : only to be used for arrays //Finds the longest common Ancestor ending at Array private static JsonPointer computeRelativePath(JsonPointer path, int startIdx, int endIdx, List diffs) { diff --git a/src/main/java/com/ebay/bsonpatch/CompatibilityFlags.java b/src/main/java/com/ebay/bsonpatch/CompatibilityFlags.java index 7c20a2b..4293ea7 100644 --- a/src/main/java/com/ebay/bsonpatch/CompatibilityFlags.java +++ b/src/main/java/com/ebay/bsonpatch/CompatibilityFlags.java @@ -24,7 +24,8 @@ public enum CompatibilityFlags { MISSING_VALUES_AS_NULLS, REMOVE_NONE_EXISTING_ARRAY_ELEMENT, - ALLOW_MISSING_TARGET_OBJECT_ON_REPLACE; + ALLOW_MISSING_TARGET_OBJECT_ON_REPLACE, + FORBID_REMOVE_MISSING_OBJECT; public static EnumSet defaults() { return EnumSet.noneOf(CompatibilityFlags.class); diff --git a/src/main/java/com/ebay/bsonpatch/DiffFlags.java b/src/main/java/com/ebay/bsonpatch/DiffFlags.java index 4e6d61a..6a60301 100644 --- a/src/main/java/com/ebay/bsonpatch/DiffFlags.java +++ b/src/main/java/com/ebay/bsonpatch/DiffFlags.java @@ -22,6 +22,7 @@ import java.util.EnumSet; public enum DiffFlags { + /** * This flag omits the value field on remove operations. * This is a default flag. @@ -51,14 +52,32 @@ public enum DiffFlags { * fromValue represents the the value replaced by a {@link Operation#REPLACE} * operation, in other words, the original value. This can be useful for debugging * output or custom processing of the diffs by downstream systems. - * * Please note that this is a non-standard extension to RFC 6902 and will not affect * how patches produced by this library are processed by this or other libraries. * * @since 0.4.1 */ ADD_ORIGINAL_VALUE_ON_REPLACE, - + + /** + * This flag normalizes a {@link Operation#REPLACE} operation into its respective + * {@link Operation#REMOVE} and {@link Operation#ADD} operations. Although it adds + * a redundant step, this can be useful for auditing systems in which immutability + * is a requirement. + *

+ * For the flag to work, {@link DiffFlags#ADD_ORIGINAL_VALUE_ON_REPLACE} has to be + * enabled as the new instructions in the patch need to grab the old fromValue + * {@code "op": "replace", "fromValue": "F1", "value": "F2" } + * The above instruction will be split into + * {@code "op":"remove", "value":"F1" } and {@code "op":"add", "value":"F2"} respectively. + *

+ * Please note that this is a non-standard extension to RFC 6902 and will not affect + * how patches produced by this library are processed by this or other libraries. + * + * @since 0.4.11 + */ + ADD_EXPLICIT_REMOVE_ADD_ON_REPLACE, + /** * This flag instructs the diff generator to emit {@link Operation#TEST} operations * that validate the state of the source document before each mutation. This can be diff --git a/src/main/java/com/ebay/bsonpatch/InPlaceApplyProcessor.java b/src/main/java/com/ebay/bsonpatch/InPlaceApplyProcessor.java index 2a117fa..c62fc71 100644 --- a/src/main/java/com/ebay/bsonpatch/InPlaceApplyProcessor.java +++ b/src/main/java/com/ebay/bsonpatch/InPlaceApplyProcessor.java @@ -125,13 +125,17 @@ public void remove(JsonPointer path) throws JsonPointerEvaluationException { BsonValue parentNode = path.getParent().evaluate(target); JsonPointer.RefToken token = path.last(); - if (parentNode.isDocument()) + if (parentNode.isDocument()) { + if (flags.contains(CompatibilityFlags.FORBID_REMOVE_MISSING_OBJECT) && !parentNode.asDocument().containsKey(token.getField())) + throw new BsonPatchApplicationException( + "Missing field " + token.getField(), Operation.REMOVE, path.getParent()); parentNode.asDocument().remove(token.getField()); + } else if (parentNode.isArray()) { - if (!flags.contains(CompatibilityFlags.REMOVE_NONE_EXISTING_ARRAY_ELEMENT) && token.getIndex() >= parentNode.asArray().size()) { - + if (!flags.contains(CompatibilityFlags.REMOVE_NONE_EXISTING_ARRAY_ELEMENT) && + token.getIndex() >= parentNode.asArray().size()) { throw new BsonPatchApplicationException( - "Array index " + token.getIndex() + " out of bounds", Operation.REPLACE, path.getParent()); + "Array index " + token.getIndex() + " out of bounds", Operation.REMOVE, path.getParent()); } else if (token.getIndex() >= parentNode.asArray().size()) { // do nothing, don't get upset about index out of bounds if REMOVE_NONE_EXISTING_ARRAY_ELEMENT set // can't just call remove on BsonArray because it throws index out of bounds exception @@ -140,7 +144,7 @@ else if (parentNode.isArray()) { } } else { throw new BsonPatchApplicationException( - "Cannot reference past scalar value", Operation.REPLACE, path.getParent()); + "Cannot reference past scalar value", Operation.REMOVE, path.getParent()); } } diff --git a/src/test/java/com/ebay/bsonpatch/CompatibilityTest.java b/src/test/java/com/ebay/bsonpatch/CompatibilityTest.java index f371501..cc9057c 100644 --- a/src/test/java/com/ebay/bsonpatch/CompatibilityTest.java +++ b/src/test/java/com/ebay/bsonpatch/CompatibilityTest.java @@ -20,6 +20,7 @@ package com.ebay.bsonpatch; import static com.ebay.bsonpatch.CompatibilityFlags.ALLOW_MISSING_TARGET_OBJECT_ON_REPLACE; +import static com.ebay.bsonpatch.CompatibilityFlags.FORBID_REMOVE_MISSING_OBJECT; import static com.ebay.bsonpatch.CompatibilityFlags.MISSING_VALUES_AS_NULLS; import static com.ebay.bsonpatch.CompatibilityFlags.REMOVE_NONE_EXISTING_ARRAY_ELEMENT; import static org.hamcrest.core.IsEqual.equalTo; @@ -30,6 +31,7 @@ import org.bson.BsonArray; import org.bson.BsonDocument; +import org.bson.BsonValue; import org.junit.Before; import org.junit.Test; @@ -39,6 +41,7 @@ public class CompatibilityTest { BsonArray replaceNodeWithMissingValue; BsonArray removeNoneExistingArrayElement; BsonArray replaceNode; + BsonArray removeNode; @Before public void setUp() throws Exception { @@ -46,6 +49,7 @@ public void setUp() throws Exception { replaceNodeWithMissingValue = BsonArray.parse("[{\"op\":\"replace\",\"path\":\"/a\"}]"); removeNoneExistingArrayElement = BsonArray.parse("[{\"op\": \"remove\",\"path\": \"/b/0\"}]"); replaceNode = BsonArray.parse("[{\"op\":\"replace\",\"path\":\"/a\",\"value\":true}]"); + removeNode = BsonArray.parse("[{\"op\":\"remove\",\"path\":\"/b\"}]"); } @Test @@ -87,4 +91,17 @@ public void withFlagReplaceShouldAddValueWhenMissingInTarget() throws Exception BsonDocument result = BsonPatch.apply(replaceNode, BsonDocument.parse("{}"), EnumSet.of(ALLOW_MISSING_TARGET_OBJECT_ON_REPLACE)).asDocument(); assertThat(result, equalTo(expected)); } -} + + @Test(expected = BsonPatchApplicationException.class) + public void withFlagRemoveMissingValueShouldThrow() throws Exception { + BsonDocument source = BsonDocument.parse("{\"a\": true}"); + BsonPatch.apply(removeNode, source, EnumSet.of(FORBID_REMOVE_MISSING_OBJECT)); + } + + @Test + public void withFlagRemoveShouldRemove() throws Exception { + BsonDocument source = BsonDocument.parse("{\"b\": true}"); + BsonDocument expected = BsonDocument.parse("{}"); + BsonValue result = BsonPatch.apply(removeNode, source, EnumSet.of(FORBID_REMOVE_MISSING_OBJECT)); + assertThat(result, equalTo(expected)); + }} diff --git a/src/test/java/com/ebay/bsonpatch/JsonDiffTest.java b/src/test/java/com/ebay/bsonpatch/JsonDiffTest.java index fad1422..8ecf788 100644 --- a/src/test/java/com/ebay/bsonpatch/JsonDiffTest.java +++ b/src/test/java/com/ebay/bsonpatch/JsonDiffTest.java @@ -21,9 +21,12 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.EnumSet; import java.util.Random; +import static org.junit.Assert.assertEquals; + import org.apache.commons.io.IOUtils; import org.bson.BsonArray; import org.bson.BsonDocument; @@ -41,7 +44,7 @@ public class JsonDiffTest { public static void beforeClass() throws IOException { String path = "/testdata/sample.json"; InputStream resourceAsStream = JsonDiffTest.class.getResourceAsStream(path); - String testData = IOUtils.toString(resourceAsStream, "UTF-8"); + String testData = IOUtils.toString(resourceAsStream, StandardCharsets.UTF_8); jsonNode = BsonArray.parse(testData); } @@ -127,6 +130,36 @@ public void testPath() throws Exception { BsonValue target = BsonPatch.apply(patch, source); BsonValue expected = BsonDocument.parse("{\"profiles\":{\"abc\":[],\"def\":[{\"hello\":\"world2\"},{\"hello\":\"world\"}]}}"); Assert.assertEquals(target, expected); - } - + } + + + @Test + public void testJsonDiffReturnsEmptyNodeExceptionWhenBothSourceAndTargetNodeIsNull() { + BsonArray diff = BsonDiff.asBson(null, null); + assertEquals(0, diff.size()); + } + + @Test + public void testJsonDiffShowsDiffWhenSourceNodeIsNull() { + String target = "{ \"K1\": {\"K2\": \"V1\"} }"; + BsonArray diff = BsonDiff.asBson(null, BsonDocument.parse(target)); + assertEquals(1, diff.size()); + + System.out.println(diff); + assertEquals(Operation.ADD.rfcName(), diff.get(0).asDocument().getString("op").getValue()); + assertEquals(JsonPointer.ROOT.toString(), diff.get(0).asDocument().getString("path").getValue()); + assertEquals("V1", diff.get(0).asDocument().getDocument("value").getDocument("K1").getString("K2").getValue()); + } + + @Test + public void testJsonDiffShowsDiffWhenTargetNodeIsNullWithFlags() { + String source = "{ \"K1\": \"V1\" }"; + BsonDocument sourceNode = BsonDocument.parse(source); + BsonArray diff = BsonDiff.asBson(sourceNode, null, EnumSet.of(DiffFlags.ADD_ORIGINAL_VALUE_ON_REPLACE)); + + assertEquals(1, diff.size()); + assertEquals(Operation.REMOVE.rfcName(), diff.get(0).asDocument().getString("op").getValue()); + assertEquals(JsonPointer.ROOT.toString(), diff.get(0).asDocument().getString("path").getValue()); + assertEquals("V1", diff.get(0).asDocument().getDocument("value").getString("K1").getValue()); + } } diff --git a/src/test/java/com/ebay/bsonpatch/JsonSplitReplaceOpTest.java b/src/test/java/com/ebay/bsonpatch/JsonSplitReplaceOpTest.java new file mode 100644 index 0000000..74dc9dc --- /dev/null +++ b/src/test/java/com/ebay/bsonpatch/JsonSplitReplaceOpTest.java @@ -0,0 +1,93 @@ +package com.ebay.bsonpatch; + +import org.bson.BsonArray; +import org.bson.BsonDocument; +import org.junit.Test; + +import java.util.EnumSet; + +import static org.junit.Assert.assertEquals; + +/** + * @author isopropylcyanide + */ +public class JsonSplitReplaceOpTest { + + + @Test + public void testJsonDiffSplitsReplaceIntoAddAndRemoveOperationWhenFlagIsAdded() { + String source = "{ \"ids\": [ \"F1\", \"F3\" ] }"; + String target = "{ \"ids\": [ \"F1\", \"F6\", \"F4\" ] }"; + BsonDocument sourceNode = BsonDocument.parse(source); + BsonDocument targetNode = BsonDocument.parse(target); + + BsonArray diff = BsonDiff.asBson(sourceNode, targetNode, EnumSet.of( + DiffFlags.ADD_EXPLICIT_REMOVE_ADD_ON_REPLACE + )); + assertEquals(3, diff.size()); + assertEquals(Operation.REMOVE.rfcName(), diff.get(0).asDocument().getString("op").getValue()); + assertEquals("/ids/1", diff.get(0).asDocument().getString("path").getValue()); + assertEquals("F3", diff.get(0).asDocument().getString("value").getValue()); + + assertEquals(Operation.ADD.rfcName(), diff.get(1).asDocument().getString("op").getValue()); + assertEquals("/ids/1", diff.get(1).asDocument().getString("path").getValue()); + assertEquals("F6", diff.get(1).asDocument().getString("value").getValue()); + + assertEquals(Operation.ADD.rfcName(), diff.get(2).asDocument().getString("op").getValue()); + assertEquals("/ids/2", diff.get(2).asDocument().getString("path").getValue()); + assertEquals("F4", diff.get(2).asDocument().getString("value").getValue()); + } + + @Test + public void testJsonDiffDoesNotSplitReplaceIntoAddAndRemoveOperationWhenFlagIsNotAdded() { + String source = "{ \"ids\": [ \"F1\", \"F3\" ] }"; + String target = "{ \"ids\": [ \"F1\", \"F6\", \"F4\" ] }"; + BsonDocument sourceNode = BsonDocument.parse(source); + BsonDocument targetNode = BsonDocument.parse(target); + + BsonArray diff = BsonDiff.asBson(sourceNode, targetNode); + System.out.println(diff); + assertEquals(2, diff.size()); + assertEquals(Operation.REPLACE.rfcName(), diff.get(0).asDocument().getString("op").getValue()); + assertEquals("/ids/1", diff.get(0).asDocument().getString("path").getValue()); + assertEquals("F6", diff.get(0).asDocument().getString("value").getValue()); + + assertEquals(Operation.ADD.rfcName(), diff.get(1).asDocument().getString("op").getValue()); + assertEquals("/ids/2", diff.get(1).asDocument().getString("path").getValue()); + assertEquals("F4", diff.get(1).asDocument().getString("value").getValue()); + } + + @Test + public void testJsonDiffDoesNotSplitsWhenThereIsNoReplaceOperationButOnlyRemove() { + String source = "{ \"ids\": [ \"F1\", \"F3\" ] }"; + String target = "{ \"ids\": [ \"F3\"] }"; + + BsonDocument sourceNode = BsonDocument.parse(source); + BsonDocument targetNode = BsonDocument.parse(target); + + BsonArray diff = BsonDiff.asBson(sourceNode, targetNode, EnumSet.of( + DiffFlags.ADD_EXPLICIT_REMOVE_ADD_ON_REPLACE + )); + assertEquals(1, diff.size()); + assertEquals(Operation.REMOVE.rfcName(), diff.get(0).asDocument().getString("op").getValue()); + assertEquals("/ids/0", diff.get(0).asDocument().getString("path").getValue()); + assertEquals("F1", diff.get(0).asDocument().getString("value").getValue()); + } + + @Test + public void testJsonDiffDoesNotSplitsWhenThereIsNoReplaceOperationButOnlyAdd() { + String source = "{ \"ids\": [ \"F1\" ] }"; + String target = "{ \"ids\": [ \"F1\", \"F6\"] }"; + + BsonDocument sourceNode = BsonDocument.parse(source); + BsonDocument targetNode = BsonDocument.parse(target); + + BsonArray diff = BsonDiff.asBson(sourceNode, targetNode, EnumSet.of( + DiffFlags.ADD_EXPLICIT_REMOVE_ADD_ON_REPLACE + )); + assertEquals(1, diff.size()); + assertEquals(Operation.ADD.rfcName(), diff.get(0).asDocument().getString("op").getValue()); + assertEquals("/ids/1", diff.get(0).asDocument().getString("path").getValue()); + assertEquals("F6", diff.get(0).asDocument().getString("value").getValue()); + } +} \ No newline at end of file