Skip to content

Commit 341eb02

Browse files
committed
Added batch_remove function
The batch remove function takes multiple array indexes(all indexes are indexes into the original array) and deletes them "atomically"(atomically to JSON patch at any rate) I also added functionality to the factorizer that condenses any array index removes that may have been emitted by it. Result is hopefully even more compact difference files and ones that are easier to create by hand.
1 parent 2196f05 commit 341eb02

File tree

10 files changed

+250
-6
lines changed

10 files changed

+250
-6
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package com.github.fge.jsonpatch;
2+
3+
import com.fasterxml.jackson.annotation.JsonCreator;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
6+
import com.fasterxml.jackson.databind.JsonNode;
7+
8+
import com.fasterxml.jackson.databind.node.ArrayNode;
9+
10+
import com.fasterxml.jackson.databind.node.MissingNode;
11+
import com.github.fge.jackson.jsonpointer.JsonPointer;
12+
13+
import java.util.ArrayList;
14+
import java.util.Collections;
15+
import java.util.List;
16+
17+
/**
18+
* Created by joel.tucci on 3/20/14.
19+
*/
20+
public class BatchRemoveOperation extends PathValueOperation {
21+
22+
public static final String OPERATION_NAME="batch_remove";
23+
24+
@JsonCreator
25+
public BatchRemoveOperation(@JsonProperty("path") final JsonPointer path,
26+
@JsonProperty("value") final JsonNode value)
27+
{
28+
super(OPERATION_NAME, path, value);
29+
}
30+
31+
private void validateDataTypes(JsonNode node) throws JsonPatchException{
32+
33+
JsonNode parentNode=path.parent().path(node);
34+
if (parentNode.isMissingNode())
35+
throw new JsonPatchException(BUNDLE.getMessage(
36+
"jsonPatch.noSuchPath"));
37+
38+
if(!value.isArray())
39+
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.valueNotArray"));
40+
if(!parentNode.isArray())
41+
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.nodeNotArray"));
42+
}
43+
44+
@Override
45+
public JsonNode apply(JsonNode node) throws JsonPatchException {
46+
if (path.isEmpty())
47+
return MissingNode.getInstance();
48+
49+
validateDataTypes(node);
50+
51+
//The simplest, if perhaps naive implementation is to do a reverse sort on the indices to be deleted and then
52+
//delete them in that order. This is O(n*m)+O(m lg m)(where n is the length of the list and m is the # of nodes to be deleted
53+
//but it can be done without copying the entire original array
54+
ArrayNode valArray=(ArrayNode) value;
55+
final JsonNode ret=node.deepCopy();
56+
final ArrayNode targetArray = (ArrayNode) path.parent().get(ret);
57+
58+
59+
List<Integer> victimIndices=new ArrayList<Integer>();
60+
for(int i=0;i<valArray.size();i++) {
61+
Integer victimIndex=valArray.get(i).asInt();
62+
if(victimIndex >= targetArray.size())
63+
throw new JsonPatchException(BUNDLE.getMessage(
64+
"jsonPatch.indexDoesNotExist"));
65+
victimIndices.add(victimIndex);
66+
}
67+
Collections.sort(victimIndices);
68+
69+
for(int i=victimIndices.size()-1;i>=0;i--) {
70+
targetArray.remove(victimIndices.get(i));
71+
}
72+
73+
74+
return ret;
75+
}
76+
77+
}

src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
@Type(name = "move", value = MoveOperation.class),
3939
@Type(name = "remove", value = RemoveOperation.class),
4040
@Type(name = "replace", value = ReplaceOperation.class),
41+
@Type(name=BatchRemoveOperation.OPERATION_NAME, value=BatchRemoveOperation.class),
4142
@Type(name = "test", value = TestOperation.class)
4243
})
4344

src/main/java/com/github/fge/jsonpatch/diff/Diff.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@
1919
package com.github.fge.jsonpatch.diff;
2020

2121
import com.fasterxml.jackson.databind.JsonNode;
22+
import com.fasterxml.jackson.databind.node.ArrayNode;
2223
import com.fasterxml.jackson.databind.node.ObjectNode;
24+
import com.github.fge.jackson.JacksonUtils;
2325
import com.github.fge.jackson.JsonNumEquals;
2426
import com.github.fge.jackson.jsonpointer.JsonPointer;
2527
import com.google.common.base.Objects;
2628

29+
import java.util.Collection;
30+
2731
/**
2832
* Difference representation. Captures diff information required to
2933
* generate JSON patch operations and factorize differences.
@@ -76,6 +80,15 @@ static Diff arrayRemove(final JsonPointer basePath,
7680
array2.getIndex(), array1.getElement().deepCopy());
7781
}
7882

83+
static Diff batchRemove(final JsonPointer basePath, final Collection<Integer> victimIndices) {
84+
ArrayNode victimNodes=new ArrayNode(JacksonUtils.nodeFactory());
85+
for(final Integer victimIndex : victimIndices) {
86+
victimNodes.add(victimIndex);
87+
}
88+
return new Diff(DiffOperation.BATCH_REMOVE, basePath,-1,-1,victimNodes);
89+
90+
}
91+
7992
static Diff arrayAdd(final JsonPointer basePath, final JsonNode node)
8093
{
8194
return new Diff(DiffOperation.ADD, basePath, -1, -1, node.deepCopy());

src/main/java/com/github/fge/jsonpatch/diff/DiffOperation.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ enum DiffOperation
3434
REPLACE("replace"),
3535
MOVE("move"),
3636
COPY("copy"),
37+
BATCH_REMOVE("batch_remove")
3738
;
3839

3940
private final String opName;

src/main/java/com/github/fge/jsonpatch/diff/JsonDiff.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ public static JsonNode asJson(final JsonNode source, final JsonNode target)
9090

9191
// factorize diffs to optimize patch operations
9292
DiffFactorizer.factorizeDiffs(diffs);
93+
RemoveCoalescer.coalesceRemoves(diffs);
9394

9495
// generate patch operations from node diffs
9596
final ArrayNode patch = FACTORY.arrayNode();
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.github.fge.jsonpatch.diff;
2+
3+
import com.github.fge.jackson.jsonpointer.JsonPointer;
4+
import com.google.common.collect.Lists;
5+
6+
import java.util.List;
7+
8+
/**
9+
* Created by joel.tucci on 3/20/14.
10+
*/
11+
final class RemoveCoalescer {
12+
13+
private RemoveCoalescer() {
14+
}
15+
16+
private static int calculateBlockRemoveLength(int originalDiffListIndex,List<Diff> diffs) {
17+
//seek ahead to find out where next instruction begins
18+
Diff curDiff=diffs.get(originalDiffListIndex);
19+
int seekIndex=originalDiffListIndex+1;
20+
while(seekIndex<diffs.size() && diffs.get(seekIndex).arrayPath.equals(curDiff.arrayPath) &&
21+
diffs.get(seekIndex).operation == DiffOperation.REMOVE) {
22+
seekIndex++;
23+
}
24+
return seekIndex-originalDiffListIndex;
25+
}
26+
27+
private static Diff condenseRemovesToBatchCommand(List<Diff> diffs,int removeBlockLength,int startIndex) {
28+
List<Integer> removedIndices=Lists.newArrayList();
29+
int curOffset=0; //Needed to map between the current remove indices and the original array
30+
Diff curDiff=diffs.get(startIndex);
31+
removedIndices.add(curDiff.firstArrayIndex);
32+
Integer previousIndex;
33+
for(int blockRemoveIndex=1;blockRemoveIndex<removeBlockLength;blockRemoveIndex++) {
34+
previousIndex=curDiff.firstArrayIndex;
35+
curDiff=diffs.get(blockRemoveIndex+startIndex);
36+
if(curDiff.firstArrayIndex == previousIndex)
37+
curOffset++;
38+
removedIndices.add(curDiff.firstArrayIndex+curOffset);
39+
}
40+
return Diff.batchRemove(diffs.get(startIndex).arrayPath, removedIndices);
41+
42+
}
43+
44+
/**
45+
* Combine multiple remove commands on an array into a single batch_remove.
46+
* This not only results in more compact JSON, it also makes diffs easier to understand and create
47+
* The value in the batch remove correspond to the values in the original array, and do not
48+
* have to either be specified in reverse order or require difficult calculations to figure out which nodes
49+
* are actually getting removed
50+
* @param diffs
51+
*/
52+
public static void coalesceRemoves(final List<Diff> diffs) {
53+
54+
//I am assuming that the previous step emits the removes in order, i.e. all removes
55+
//for a given array are consecutive. This is how the current factorizediffs implementation works
56+
//obviously if this assumption no longer holds then this part will no longer optimize the removes
57+
58+
final List<Diff> coalescedDiffs = Lists.newArrayList();
59+
for(int i=0;i<diffs.size();i++) {
60+
61+
Diff curDiff=diffs.get(i);
62+
if(curDiff.operation != DiffOperation.REMOVE) {
63+
coalescedDiffs.add(curDiff);
64+
continue; //Only coalesce remove operations
65+
}
66+
67+
if(curDiff.arrayPath == null) {
68+
coalescedDiffs.add(curDiff);
69+
continue; //not an array
70+
}
71+
72+
int removeBlockLength=calculateBlockRemoveLength(i,diffs);
73+
if(removeBlockLength == 1) {
74+
//Single remove, so just add it directly
75+
coalescedDiffs.add(diffs.get(i));
76+
continue;
77+
}
78+
79+
coalescedDiffs.add(condenseRemovesToBatchCommand(diffs,removeBlockLength,i));
80+
//Now that we have condensed all these remove commands, we need to skip ahead to the next operation
81+
i=i+removeBlockLength-1;
82+
83+
}
84+
85+
diffs.clear();
86+
diffs.addAll(coalescedDiffs);
87+
}
88+
89+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.github.fge.jsonpatch;
2+
3+
import com.fasterxml.jackson.databind.node.ArrayNode;
4+
import com.github.fge.jackson.JacksonUtils;
5+
import org.testng.annotations.Test;
6+
7+
import java.io.IOException;
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
11+
/**
12+
* Created by joel.tucci on 3/20/14.
13+
*/
14+
public class BatchRemoveOperationTest extends JsonPatchOperationTest {
15+
16+
protected BatchRemoveOperationTest(String prefix) throws IOException {
17+
super(BatchRemoveOperation.OPERATION_NAME);
18+
}
19+
20+
21+
}
22+

src/test/java/com/github/fge/jsonpatch/ReplaceOperationTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
package com.github.fge.jsonpatch;
2020

21+
import org.testng.annotations.Test;
22+
2123
import java.io.IOException;
2224

2325
public final class ReplaceOperationTest
@@ -28,4 +30,5 @@ public ReplaceOperationTest()
2830
{
2931
super("replace");
3032
}
33+
3134
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"errors": [
3+
{
4+
"op": { "op": "batch_remove", "path": "/x/y/-" },
5+
"node": { "x": {} },
6+
"message": "jsonPatch.noSuchPath"
7+
},
8+
{
9+
"op": { "op": "batch_remove", "path": "/x/y", "value":[1,3] },
10+
"node": { "x": { "y":7} },
11+
"message": "jsonPatch.nodeNotArray"
12+
},
13+
{
14+
"op": { "op": "batch_remove", "path": "/x/y/-", "value":0 },
15+
"node": { "x": { "y": [1,2,3]} },
16+
"message": "jsonPatch.valueNotArray"
17+
},
18+
{
19+
"op": { "op": "batch_remove", "path": "/x/y/-", "value": [3,5] },
20+
"node": { "x": { "y": [1,2,3]} },
21+
"message": "jsonPatch.indexDoesNotExist"
22+
}
23+
24+
25+
],
26+
"ops": [
27+
{
28+
"op": { "op": "batch_remove", "path": "/x/y/-", "value": [4,0,1] },
29+
"node": { "x": { "a": "b", "y": [1,2,3,4,5] } },
30+
"expected": { "x": { "a": "b","y":[3,4] } }
31+
}
32+
]
33+
}

src/test/resources/jsonpatch/factorizing-diff.json

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,14 @@
6363
"first": [ 1, 2, 3 ],
6464
"second": [ 1 ],
6565
"patch": [
66-
{ "op": "remove", "path": "/1" },
67-
{ "op": "remove", "path": "/1" }
66+
{ "op": "batch_remove", "path": "/-", "value":[1,2] }
67+
]
68+
},
69+
{
70+
"first": {"x":{"a":7, "y":[ 1, 2, 3,4 ]}},
71+
"second": {"x":{"a":7, "y":[ 1, 4 ]}},
72+
"patch": [
73+
{ "op": "batch_remove", "path": "/x/y/-", "value":[1,2] }
6874
]
6975
},
7076
{
@@ -133,9 +139,7 @@
133139
"first": [0, 1, 2, 3, 4, 5, 6, 7],
134140
"second": [3, 6, 4, 5, 7],
135141
"patch": [
136-
{ "op": "remove", "path": "/0" },
137-
{ "op": "remove", "path": "/0" },
138-
{ "op": "remove", "path": "/0" },
142+
{ "op": "batch_remove", "path": "/-", "value":[0,1,2] },
139143
{ "op": "move", "from": "/3", "path": "/1" }
140144
]
141145
},
@@ -289,4 +293,4 @@
289293
"second": [ 2, 1 ],
290294
"patch": [ { "op": "move", "from": "/1", "path": "/0" } ]
291295
}
292-
]
296+
]

0 commit comments

Comments
 (0)