Skip to content

Commit

Permalink
Explicit Split of Replace Op | Null Json Node handling (#120)
Browse files Browse the repository at this point in the history
* Comment release plugin

* Support: Replace instruction into add remove and add

* Add tests and ensure full coverage

* Introduce null safety for null target/src nodes

* Self review: Undo basic formatting

* Add missing test: No replace split in absence of flag

* RC: Update JavaDoc

* RC: Optimize early returns and enrich existing contract for asJson
  • Loading branch information
isopropylcyanide authored Jun 23, 2020
1 parent 6eadbe6 commit 88793f4
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 15 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>com.flipkart.zjsonpatch</groupId>
<artifactId>zjsonpatch</artifactId>
<version>0.4.10</version>
<version>0.4.11</version>
<packaging>jar</packaging>

<name>zjsonpatch</name>
Expand Down
22 changes: 20 additions & 2 deletions src/main/java/com/flipkart/zjsonpatch/DiffFlags.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.EnumSet;

public enum DiffFlags {

/**
* This flag omits the <i>value</i> field on remove operations.
* This is a default flag.
Expand Down Expand Up @@ -32,19 +33,36 @@ public enum DiffFlags {
* <i>fromValue</i> 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.
* <p>
* 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 <i>fromValue</i>
* {@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.
* <p>
* 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.
Expand Down
57 changes: 46 additions & 11 deletions src/main/java/com/flipkart/zjsonpatch/JsonDiff.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Diff> diffs = new ArrayList<Diff>();
Expand All @@ -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<DiffFlags> 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();
}

Expand Down Expand Up @@ -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<Diff> updatedDiffs = new ArrayList<Diff>();
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<Diff> diffs) {
Expand Down
35 changes: 34 additions & 1 deletion src/test/java/com/flipkart/zjsonpatch/JsonDiffTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,6 +31,8 @@
import java.util.EnumSet;
import java.util.Random;

import static org.junit.Assert.assertEquals;

/**
* Unit test
*/
Expand Down Expand Up @@ -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());
}
}
95 changes: 95 additions & 0 deletions src/test/java/com/flipkart/zjsonpatch/JsonSplitReplaceOpTest.java
Original file line number Diff line number Diff line change
@@ -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());
}
}

0 comments on commit 88793f4

Please sign in to comment.