Skip to content

Commit fe7cc58

Browse files
holographvishwakarma
authored andcommitted
Add EMIT_TEST_OPERATIONS diff flag (supports #91) (#92)
* Security upgrade (see FasterXML/jackson-databind#2186) * Minor cleanup * Further cleanup * Added DiffFlags.EMIT_TEST_OPERATIONS, along with associated tests and functionality * Further (minor) cleanup * Corrected @SInCE version on EMIT_COPY_OPERATIONS
1 parent f9bb32e commit fe7cc58

File tree

5 files changed

+175
-87
lines changed

5 files changed

+175
-87
lines changed

src/main/java/com/flipkart/zjsonpatch/DiffFlags.java

+20-3
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,30 @@ public enum DiffFlags {
2828
OMIT_COPY_OPERATION,
2929

3030
/**
31-
* This flag adds a <i>fromValue</i> field to all {@link Operation#REPLACE}operations.
31+
* This flag adds a <i>fromValue</i> field to all {@link Operation#REPLACE} operations.
3232
* <i>fromValue</i> represents the the value replaced by a {@link Operation#REPLACE}
33-
* operation, in other words, the original value.
33+
* operation, in other words, the original value. This can be useful for debugging
34+
* output or custom processing of the diffs by downstream systems.
35+
*
36+
* Please note that this is a non-standard extension to RFC 6902 and will not affect
37+
* how patches produced by this library are processed by this or other libraries.
3438
*
3539
* @since 0.4.1
3640
*/
37-
ADD_ORIGINAL_VALUE_ON_REPLACE;
41+
ADD_ORIGINAL_VALUE_ON_REPLACE,
42+
43+
/**
44+
* This flag instructs the diff generator to emit {@link Operation#TEST} operations
45+
* that validate the state of the source document before each mutation. This can be
46+
* useful if you want to ensure data integrity prior to applying the patch.
47+
*
48+
* The resulting patches are standard per RFC 6902 and should be processed correctly
49+
* by any compliant library; due to the associated space and performance costs,
50+
* however, this isn't default behavior.
51+
*
52+
* @since 0.4.8
53+
*/
54+
EMIT_TEST_OPERATIONS;
3855

3956

4057
public static EnumSet<DiffFlags> defaults() {

src/main/java/com/flipkart/zjsonpatch/JsonDiff.java

+47-39
Original file line numberDiff line numberDiff line change
@@ -31,52 +31,55 @@
3131

3232
public final class JsonDiff {
3333

34-
private JsonDiff() {
34+
private final List<Diff> diffs = new ArrayList<Diff>();
35+
private final EnumSet<DiffFlags> flags;
3536

37+
private JsonDiff(EnumSet<DiffFlags> flags) {
38+
this.flags = flags.clone();
3639
}
3740

3841
public static JsonNode asJson(final JsonNode source, final JsonNode target) {
3942
return asJson(source, target, DiffFlags.defaults());
4043
}
4144

4245
public static JsonNode asJson(final JsonNode source, final JsonNode target, EnumSet<DiffFlags> flags) {
43-
final List<Diff> diffs = new ArrayList<Diff>();
44-
List<Object> path = new ArrayList<Object>(0);
46+
JsonDiff diff = new JsonDiff(flags);
4547

4648
// generating diffs in the order of their occurrence
49+
List<Object> path = new ArrayList<Object>(0);
50+
diff.generateDiffs(path, source, target);
4751

48-
generateDiffs(diffs, path, source, target);
49-
50-
if (!flags.contains(DiffFlags.OMIT_MOVE_OPERATION)) {
51-
52+
if (!flags.contains(DiffFlags.OMIT_MOVE_OPERATION))
5253
// Merging remove & add to move operation
54+
diff.introduceMoveOperation();
5355

54-
compactDiffs(diffs);
55-
}
56-
57-
if (!flags.contains(DiffFlags.OMIT_COPY_OPERATION)) {
58-
56+
if (!flags.contains(DiffFlags.OMIT_COPY_OPERATION))
5957
// Introduce copy operation
58+
diff.introduceCopyOperation(source, target);
6059

61-
introduceCopyOperation(source, target, diffs);
62-
}
63-
64-
return getJsonNodes(diffs, flags);
60+
return diff.getJsonNodes();
6561
}
6662

6763
private static List<Object> getMatchingValuePath(Map<JsonNode, List<Object>> unchangedValues, JsonNode value) {
6864
return unchangedValues.get(value);
6965
}
7066

71-
private static void introduceCopyOperation(JsonNode source, JsonNode target, List<Diff> diffs) {
67+
private void introduceCopyOperation(JsonNode source, JsonNode target) {
7268
Map<JsonNode, List<Object>> unchangedValues = getUnchangedPart(source, target);
69+
7370
for (int i = 0; i < diffs.size(); i++) {
7471
Diff diff = diffs.get(i);
75-
if (Operation.ADD == diff.getOperation()) {
76-
List<Object> matchingValuePath = getMatchingValuePath(unchangedValues, diff.getValue());
77-
if (matchingValuePath != null && isAllowed(matchingValuePath, diff.getPath())) {
78-
diffs.set(i, new Diff(Operation.COPY, matchingValuePath, diff.getPath()));
72+
if (Operation.ADD != diff.getOperation()) continue;
73+
74+
List<Object> matchingValuePath = getMatchingValuePath(unchangedValues, diff.getValue());
75+
if (matchingValuePath != null && isAllowed(matchingValuePath, diff.getPath())) {
76+
// Matching value found; replace add with copy
77+
if (flags.contains(DiffFlags.EMIT_TEST_OPERATIONS)) {
78+
// Prepend test node
79+
diffs.add(i, new Diff(Operation.TEST, matchingValuePath, diff.getValue()));
80+
i++;
7981
}
82+
diffs.set(i, new Diff(Operation.COPY, matchingValuePath, diff.getPath()));
8083
}
8184
}
8285
}
@@ -171,7 +174,7 @@ private static void computeObject(Map<JsonNode, List<Object>> unchangedValues, L
171174
* This method merge 2 diffs ( remove then add, or vice versa ) with same value into one Move operation,
172175
* all the core logic resides here only
173176
*/
174-
private static void compactDiffs(List<Diff> diffs) {
177+
private void introduceMoveOperation() {
175178
for (int i = 0; i < diffs.size(); i++) {
176179
Diff diff1 = diffs.get(i);
177180

@@ -234,7 +237,7 @@ private static void updatePathWithCounters(List<Integer> counters, List<Object>
234237
for (int i = 0; i < counters.size(); i++) {
235238
int value = counters.get(i);
236239
if (value != 0) {
237-
Integer currValue = Integer.parseInt(path.get(i).toString());
240+
int currValue = Integer.parseInt(path.get(i).toString());
238241
path.set(i, String.valueOf(currValue + value));
239242
}
240243
}
@@ -270,7 +273,7 @@ private static void updateCounters(Diff pseudo, int idx, List<Integer> counters)
270273
}
271274
}
272275

273-
private static ArrayNode getJsonNodes(List<Diff> diffs, EnumSet<DiffFlags> flags) {
276+
private ArrayNode getJsonNodes() {
274277
JsonNodeFactory FACTORY = JsonNodeFactory.instance;
275278
final ArrayNode patch = FACTORY.arrayNode();
276279
for (Diff diff : diffs) {
@@ -315,26 +318,27 @@ private static ObjectNode getJsonNode(JsonNodeFactory FACTORY, Diff diff, EnumSe
315318
return jsonNode;
316319
}
317320

318-
private static void generateDiffs(List<Diff> diffs, List<Object> path, JsonNode source, JsonNode target) {
321+
private void generateDiffs(List<Object> path, JsonNode source, JsonNode target) {
319322
if (!source.equals(target)) {
320323
final NodeType sourceType = NodeType.getNodeType(source);
321324
final NodeType targetType = NodeType.getNodeType(target);
322325

323326
if (sourceType == NodeType.ARRAY && targetType == NodeType.ARRAY) {
324327
//both are arrays
325-
compareArray(diffs, path, source, target);
328+
compareArray(path, source, target);
326329
} else if (sourceType == NodeType.OBJECT && targetType == NodeType.OBJECT) {
327330
//both are json
328-
compareObjects(diffs, path, source, target);
331+
compareObjects(path, source, target);
329332
} else {
330333
//can be replaced
331-
334+
if (flags.contains(DiffFlags.EMIT_TEST_OPERATIONS))
335+
diffs.add(new Diff(Operation.TEST, path, source));
332336
diffs.add(Diff.generateDiff(Operation.REPLACE, path, source, target));
333337
}
334338
}
335339
}
336340

337-
private static void compareArray(List<Diff> diffs, List<Object> path, JsonNode source, JsonNode target) {
341+
private void compareArray(List<Object> path, JsonNode source, JsonNode target) {
338342
List<JsonNode> lcs = getLCS(source, target);
339343
int srcIdx = 0;
340344
int targetIdx = 0;
@@ -365,12 +369,14 @@ private static void compareArray(List<Diff> diffs, List<Object> path, JsonNode s
365369
} else if (lcsNode.equals(targetNode)) { //targetNode node is same as lcs, but not src
366370
//removal,
367371
List<Object> currPath = getPath(path, pos);
372+
if (flags.contains(DiffFlags.EMIT_TEST_OPERATIONS))
373+
diffs.add(new Diff(Operation.TEST, currPath, srcNode));
368374
diffs.add(Diff.generateDiff(Operation.REMOVE, currPath, srcNode));
369375
srcIdx++;
370376
} else {
371377
List<Object> currPath = getPath(path, pos);
372378
//both are unequal to lcs node
373-
generateDiffs(diffs, currPath, srcNode, targetNode);
379+
generateDiffs(currPath, srcNode, targetNode);
374380
srcIdx++;
375381
targetIdx++;
376382
pos++;
@@ -382,26 +388,26 @@ private static void compareArray(List<Diff> diffs, List<Object> path, JsonNode s
382388
JsonNode srcNode = source.get(srcIdx);
383389
JsonNode targetNode = target.get(targetIdx);
384390
List<Object> currPath = getPath(path, pos);
385-
generateDiffs(diffs, currPath, srcNode, targetNode);
391+
generateDiffs(currPath, srcNode, targetNode);
386392
srcIdx++;
387393
targetIdx++;
388394
pos++;
389395
}
390-
pos = addRemaining(diffs, path, target, pos, targetIdx, targetSize);
391-
removeRemaining(diffs, path, pos, srcIdx, srcSize, source);
396+
pos = addRemaining(path, target, pos, targetIdx, targetSize);
397+
removeRemaining(path, pos, srcIdx, srcSize, source);
392398
}
393399

394-
private static Integer removeRemaining(List<Diff> diffs, List<Object> path, int pos, int srcIdx, int srcSize, JsonNode source) {
395-
400+
private void removeRemaining(List<Object> path, int pos, int srcIdx, int srcSize, JsonNode source) {
396401
while (srcIdx < srcSize) {
397402
List<Object> currPath = getPath(path, pos);
403+
if (flags.contains(DiffFlags.EMIT_TEST_OPERATIONS))
404+
diffs.add(new Diff(Operation.TEST, currPath, source.get(srcIdx)));
398405
diffs.add(Diff.generateDiff(Operation.REMOVE, currPath, source.get(srcIdx)));
399406
srcIdx++;
400407
}
401-
return pos;
402408
}
403409

404-
private static Integer addRemaining(List<Diff> diffs, List<Object> path, JsonNode target, int pos, int targetIdx, int targetSize) {
410+
private int addRemaining(List<Object> path, JsonNode target, int pos, int targetIdx, int targetSize) {
405411
while (targetIdx < targetSize) {
406412
JsonNode jsonNode = target.get(targetIdx);
407413
List<Object> currPath = getPath(path, pos);
@@ -412,18 +418,20 @@ private static Integer addRemaining(List<Diff> diffs, List<Object> path, JsonNod
412418
return pos;
413419
}
414420

415-
private static void compareObjects(List<Diff> diffs, List<Object> path, JsonNode source, JsonNode target) {
421+
private void compareObjects(List<Object> path, JsonNode source, JsonNode target) {
416422
Iterator<String> keysFromSrc = source.fieldNames();
417423
while (keysFromSrc.hasNext()) {
418424
String key = keysFromSrc.next();
419425
if (!target.has(key)) {
420426
//remove case
421427
List<Object> currPath = getPath(path, key);
428+
if (flags.contains(DiffFlags.EMIT_TEST_OPERATIONS))
429+
diffs.add(new Diff(Operation.TEST, currPath, source.get(key)));
422430
diffs.add(Diff.generateDiff(Operation.REMOVE, currPath, source.get(key)));
423431
continue;
424432
}
425433
List<Object> currPath = getPath(path, key);
426-
generateDiffs(diffs, currPath, source.get(key), target.get(key));
434+
generateDiffs(currPath, source.get(key), target.get(key));
427435
}
428436
Iterator<String> keysFromTarget = target.fieldNames();
429437
while (keysFromTarget.hasNext()) {

src/test/java/com/flipkart/zjsonpatch/JsonDiffTest.java

+10-36
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@
3434
* Unit test
3535
*/
3636
public class JsonDiffTest {
37-
static ObjectMapper objectMapper = new ObjectMapper();
38-
static ArrayNode jsonNode;
37+
private static ObjectMapper objectMapper = new ObjectMapper();
38+
private static ArrayNode jsonNode;
3939

4040
@BeforeClass
4141
public static void beforeClass() throws IOException {
@@ -46,48 +46,30 @@ public static void beforeClass() throws IOException {
4646
}
4747

4848
@Test
49-
public void testSampleJsonDiff() throws Exception {
49+
public void testSampleJsonDiff() {
5050
for (int i = 0; i < jsonNode.size(); i++) {
5151
JsonNode first = jsonNode.get(i).get("first");
5252
JsonNode second = jsonNode.get(i).get("second");
53-
54-
// System.out.println("Test # " + i);
55-
// System.out.println(first);
56-
// System.out.println(second);
57-
5853
JsonNode actualPatch = JsonDiff.asJson(first, second);
59-
60-
61-
// System.out.println(actualPatch);
62-
6354
JsonNode secondPrime = JsonPatch.apply(actualPatch, first);
64-
// System.out.println(secondPrime);
65-
Assert.assertTrue(second.equals(secondPrime));
55+
Assert.assertEquals(second, secondPrime);
6656
}
6757
}
6858

6959
@Test
70-
public void testGeneratedJsonDiff() throws Exception {
60+
public void testGeneratedJsonDiff() {
7161
Random random = new Random();
7262
for (int i = 0; i < 1000; i++) {
7363
JsonNode first = TestDataGenerator.generate(random.nextInt(10));
7464
JsonNode second = TestDataGenerator.generate(random.nextInt(10));
75-
7665
JsonNode actualPatch = JsonDiff.asJson(first, second);
77-
// System.out.println("Test # " + i);
78-
//
79-
// System.out.println(first);
80-
// System.out.println(second);
81-
// System.out.println(actualPatch);
82-
8366
JsonNode secondPrime = JsonPatch.apply(actualPatch, first);
84-
// System.out.println(secondPrime);
85-
Assert.assertTrue(second.equals(secondPrime));
67+
Assert.assertEquals(second, secondPrime);
8668
}
8769
}
8870

8971
@Test
90-
public void testRenderedRemoveOperationOmitsValueByDefault() throws Exception {
72+
public void testRenderedRemoveOperationOmitsValueByDefault() {
9173
ObjectNode source = objectMapper.createObjectNode();
9274
ObjectNode target = objectMapper.createObjectNode();
9375
source.put("field", "value");
@@ -100,7 +82,7 @@ public void testRenderedRemoveOperationOmitsValueByDefault() throws Exception {
10082
}
10183

10284
@Test
103-
public void testRenderedRemoveOperationRetainsValueIfOmitDiffFlagNotSet() throws Exception {
85+
public void testRenderedRemoveOperationRetainsValueIfOmitDiffFlagNotSet() {
10486
ObjectNode source = objectMapper.createObjectNode();
10587
ObjectNode target = objectMapper.createObjectNode();
10688
source.put("field", "value");
@@ -123,20 +105,13 @@ public void testRenderedOperationsExceptMoveAndCopy() throws Exception {
123105

124106
JsonNode diff = JsonDiff.asJson(source, target, flags);
125107

126-
// System.out.println(source);
127-
// System.out.println(target);
128-
// System.out.println(diff);
129-
130108
for (JsonNode d : diff) {
131109
Assert.assertNotEquals(Operation.MOVE.rfcName(), d.get("op").textValue());
132110
Assert.assertNotEquals(Operation.COPY.rfcName(), d.get("op").textValue());
133111
}
134112

135113
JsonNode targetPrime = JsonPatch.apply(diff, source);
136-
// System.out.println(targetPrime);
137-
Assert.assertTrue(target.equals(targetPrime));
138-
139-
114+
Assert.assertEquals(target, targetPrime);
140115
}
141116

142117
@Test
@@ -145,8 +120,7 @@ public void testPath() throws Exception {
145120
JsonNode patch = objectMapper.readTree("[{\"op\":\"copy\",\"from\":\"/profiles/def/0\", \"path\":\"/profiles/def/0\"},{\"op\":\"replace\",\"path\":\"/profiles/def/0/hello\",\"value\":\"world2\"}]");
146121

147122
JsonNode target = JsonPatch.apply(patch, source);
148-
// System.out.println(target);
149123
JsonNode expected = objectMapper.readTree("{\"profiles\":{\"abc\":[],\"def\":[{\"hello\":\"world2\"},{\"hello\":\"world\"}]}}");
150-
Assert.assertTrue(target.equals(expected));
124+
Assert.assertEquals(target, expected);
151125
}
152126
}

src/test/java/com/flipkart/zjsonpatch/JsonDiffTest2.java

+4-9
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
* @author ctranxuan (streamdata.io).
3434
*/
3535
public class JsonDiffTest2 {
36-
static ObjectMapper objectMapper = new ObjectMapper();
37-
static ArrayNode jsonNode;
36+
private static ObjectMapper objectMapper = new ObjectMapper();
37+
private static ArrayNode jsonNode;
3838

3939
@BeforeClass
4040
public static void beforeClass() throws IOException {
@@ -45,20 +45,15 @@ public static void beforeClass() throws IOException {
4545
}
4646

4747
@Test
48-
public void testPatchAppliedCleanly() throws Exception {
48+
public void testPatchAppliedCleanly() {
4949
for (int i = 0; i < jsonNode.size(); i++) {
5050
JsonNode first = jsonNode.get(i).get("first");
5151
JsonNode second = jsonNode.get(i).get("second");
5252
JsonNode patch = jsonNode.get(i).get("patch");
5353
String message = jsonNode.get(i).get("message").toString();
5454

55-
// System.out.println("Test # " + i);
56-
// System.out.println(first);
57-
// System.out.println(second);
58-
// System.out.println(patch);
59-
6055
JsonNode secondPrime = JsonPatch.apply(patch, first);
61-
// System.out.println(secondPrime);
56+
6257
Assert.assertThat(message, secondPrime, equalTo(second));
6358
}
6459

0 commit comments

Comments
 (0)