diff --git a/pom.xml b/pom.xml
index 633aca8..bb53744 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
com.flipkart.zjsonpatch
zjsonpatch
- 0.4.10
+ 0.4.11
jar
zjsonpatch
diff --git a/src/main/java/com/flipkart/zjsonpatch/DiffFlags.java b/src/main/java/com/flipkart/zjsonpatch/DiffFlags.java
index f0d5232..7d40ac0 100644
--- a/src/main/java/com/flipkart/zjsonpatch/DiffFlags.java
+++ b/src/main/java/com/flipkart/zjsonpatch/DiffFlags.java
@@ -3,6 +3,7 @@
import java.util.EnumSet;
public enum DiffFlags {
+
/**
* This flag omits the value field on remove operations.
* This is a default flag.
@@ -32,7 +33,6 @@ 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.
*
@@ -40,11 +40,29 @@ public enum DiffFlags {
*/
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
* useful if you want to ensure data integrity prior to applying the patch.
- *
* The resulting patches are standard per RFC 6902 and should be processed correctly
* by any compliant library; due to the associated space and performance costs,
* however, this isn't default behavior.
diff --git a/src/main/java/com/flipkart/zjsonpatch/JsonDiff.java b/src/main/java/com/flipkart/zjsonpatch/JsonDiff.java
index fa036ad..effc963 100644
--- a/src/main/java/com/flipkart/zjsonpatch/JsonDiff.java
+++ b/src/main/java/com/flipkart/zjsonpatch/JsonDiff.java
@@ -22,13 +22,17 @@
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.collections4.ListUtils;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
/**
* User: gopi.vishwakarma
* Date: 30/07/14
*/
-
public final class JsonDiff {
private final List diffs = new ArrayList();
@@ -44,18 +48,29 @@ public static JsonNode asJson(final JsonNode source, final JsonNode target) {
public static JsonNode asJson(final JsonNode source, final JsonNode target, EnumSet flags) {
JsonDiff diff = new JsonDiff(flags);
+ 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 (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);
- // 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 (!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.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.getJsonNodes();
}
@@ -210,6 +225,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/test/java/com/flipkart/zjsonpatch/JsonDiffTest.java b/src/test/java/com/flipkart/zjsonpatch/JsonDiffTest.java
index f1a955d..41e7b6b 100644
--- a/src/test/java/com/flipkart/zjsonpatch/JsonDiffTest.java
+++ b/src/test/java/com/flipkart/zjsonpatch/JsonDiffTest.java
@@ -12,10 +12,11 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
-*/
+ */
package com.flipkart.zjsonpatch;
+import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
@@ -30,6 +31,8 @@
import java.util.EnumSet;
import java.util.Random;
+import static org.junit.Assert.assertEquals;
+
/**
* Unit test
*/
@@ -123,4 +126,34 @@ public void testPath() throws Exception {
JsonNode expected = objectMapper.readTree("{\"profiles\":{\"abc\":[],\"def\":[{\"hello\":\"world2\"},{\"hello\":\"world\"}]}}");
Assert.assertEquals(target, expected);
}
+
+ @Test
+ public void testJsonDiffReturnsEmptyNodeExceptionWhenBothSourceAndTargetNodeIsNull() {
+ JsonNode diff = JsonDiff.asJson(null, null);
+ assertEquals(0, diff.size());
+ }
+
+ @Test
+ public void testJsonDiffShowsDiffWhenSourceNodeIsNull() throws JsonProcessingException {
+ String target = "{ \"K1\": {\"K2\": \"V1\"} }";
+ JsonNode diff = JsonDiff.asJson(null, objectMapper.reader().readTree(target));
+ assertEquals(1, diff.size());
+
+ System.out.println(diff);
+ assertEquals(Operation.ADD.rfcName(), diff.get(0).get("op").textValue());
+ assertEquals(JsonPointer.ROOT.toString(), diff.get(0).get("path").textValue());
+ assertEquals("V1", diff.get(0).get("value").get("K1").get("K2").textValue());
+ }
+
+ @Test
+ public void testJsonDiffShowsDiffWhenTargetNodeIsNullWithFlags() throws JsonProcessingException {
+ String source = "{ \"K1\": \"V1\" }";
+ JsonNode sourceNode = objectMapper.reader().readTree(source);
+ JsonNode diff = JsonDiff.asJson(sourceNode, null, EnumSet.of(DiffFlags.ADD_ORIGINAL_VALUE_ON_REPLACE));
+
+ assertEquals(1, diff.size());
+ assertEquals(Operation.REMOVE.rfcName(), diff.get(0).get("op").textValue());
+ assertEquals(JsonPointer.ROOT.toString(), diff.get(0).get("path").textValue());
+ assertEquals("V1", diff.get(0).get("value").get("K1").textValue());
+ }
}
diff --git a/src/test/java/com/flipkart/zjsonpatch/JsonSplitReplaceOpTest.java b/src/test/java/com/flipkart/zjsonpatch/JsonSplitReplaceOpTest.java
new file mode 100644
index 0000000..677f863
--- /dev/null
+++ b/src/test/java/com/flipkart/zjsonpatch/JsonSplitReplaceOpTest.java
@@ -0,0 +1,95 @@
+package com.flipkart.zjsonpatch;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Test;
+
+import java.util.EnumSet;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author isopropylcyanide
+ */
+public class JsonSplitReplaceOpTest {
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ @Test
+ public void testJsonDiffSplitsReplaceIntoAddAndRemoveOperationWhenFlagIsAdded() throws JsonProcessingException {
+ String source = "{ \"ids\": [ \"F1\", \"F3\" ] }";
+ String target = "{ \"ids\": [ \"F1\", \"F6\", \"F4\" ] }";
+ JsonNode sourceNode = OBJECT_MAPPER.reader().readTree(source);
+ JsonNode targetNode = OBJECT_MAPPER.reader().readTree(target);
+
+ JsonNode diff = JsonDiff.asJson(sourceNode, targetNode, EnumSet.of(
+ DiffFlags.ADD_EXPLICIT_REMOVE_ADD_ON_REPLACE
+ ));
+ assertEquals(3, diff.size());
+ assertEquals(Operation.REMOVE.rfcName(), diff.get(0).get("op").textValue());
+ assertEquals("/ids/1", diff.get(0).get("path").textValue());
+ assertEquals("F3", diff.get(0).get("value").textValue());
+
+ assertEquals(Operation.ADD.rfcName(), diff.get(1).get("op").textValue());
+ assertEquals("/ids/1", diff.get(1).get("path").textValue());
+ assertEquals("F6", diff.get(1).get("value").textValue());
+
+ assertEquals(Operation.ADD.rfcName(), diff.get(2).get("op").textValue());
+ assertEquals("/ids/2", diff.get(2).get("path").textValue());
+ assertEquals("F4", diff.get(2).get("value").textValue());
+ }
+
+ @Test
+ public void testJsonDiffDoesNotSplitReplaceIntoAddAndRemoveOperationWhenFlagIsNotAdded() throws JsonProcessingException {
+ String source = "{ \"ids\": [ \"F1\", \"F3\" ] }";
+ String target = "{ \"ids\": [ \"F1\", \"F6\", \"F4\" ] }";
+ JsonNode sourceNode = OBJECT_MAPPER.reader().readTree(source);
+ JsonNode targetNode = OBJECT_MAPPER.reader().readTree(target);
+
+ JsonNode diff = JsonDiff.asJson(sourceNode, targetNode);
+ System.out.println(diff);
+ assertEquals(2, diff.size());
+ assertEquals(Operation.REPLACE.rfcName(), diff.get(0).get("op").textValue());
+ assertEquals("/ids/1", diff.get(0).get("path").textValue());
+ assertEquals("F6", diff.get(0).get("value").textValue());
+
+ assertEquals(Operation.ADD.rfcName(), diff.get(1).get("op").textValue());
+ assertEquals("/ids/2", diff.get(1).get("path").textValue());
+ assertEquals("F4", diff.get(1).get("value").textValue());
+ }
+
+ @Test
+ public void testJsonDiffDoesNotSplitsWhenThereIsNoReplaceOperationButOnlyRemove() throws JsonProcessingException {
+ String source = "{ \"ids\": [ \"F1\", \"F3\" ] }";
+ String target = "{ \"ids\": [ \"F3\"] }";
+
+ JsonNode sourceNode = OBJECT_MAPPER.reader().readTree(source);
+ JsonNode targetNode = OBJECT_MAPPER.reader().readTree(target);
+
+ JsonNode diff = JsonDiff.asJson(sourceNode, targetNode, EnumSet.of(
+ DiffFlags.ADD_EXPLICIT_REMOVE_ADD_ON_REPLACE
+ ));
+ assertEquals(1, diff.size());
+ assertEquals(Operation.REMOVE.rfcName(), diff.get(0).get("op").textValue());
+ assertEquals("/ids/0", diff.get(0).get("path").textValue());
+ assertEquals("F1", diff.get(0).get("value").textValue());
+ }
+
+ @Test
+ public void testJsonDiffDoesNotSplitsWhenThereIsNoReplaceOperationButOnlyAdd() throws JsonProcessingException {
+ String source = "{ \"ids\": [ \"F1\" ] }";
+ String target = "{ \"ids\": [ \"F1\", \"F6\"] }";
+
+ JsonNode sourceNode = OBJECT_MAPPER.reader().readTree(source);
+ JsonNode targetNode = OBJECT_MAPPER.reader().readTree(target);
+
+ JsonNode diff = JsonDiff.asJson(sourceNode, targetNode, EnumSet.of(
+ DiffFlags.ADD_EXPLICIT_REMOVE_ADD_ON_REPLACE
+ ));
+ assertEquals(1, diff.size());
+ assertEquals(Operation.ADD.rfcName(), diff.get(0).get("op").textValue());
+ assertEquals("/ids/1", diff.get(0).get("path").textValue());
+ assertEquals("F6", diff.get(0).get("value").textValue());
+ }
+}