Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
dandoug committed Jan 9, 2018
1 parent dba15de commit 0049416
Show file tree
Hide file tree
Showing 19 changed files with 362 additions and 197 deletions.
2 changes: 1 addition & 1 deletion NOTICE
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
bsonpatch library
Copyright 2017 eBay, Inc.
Copyright 2017,2018 eBay, Inc.

This product includes software developed at
eBay, Inc. (https://www.ebay.com/).
Expand Down
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ The code here was ported (copied, renamed, repackaged, modified) from the [zjson

### How to use:

### Current Version : 0.3.6
### Current Version : 0.4.1

Add following to `<dependencies/>` section of your pom.xml -

```xml
<dependency>
<groupId>com.ebay.bsonpatch</groupId>
<artifactId>bsonpatch</artifactId>
<version>0.3.6</version>
<version>0.4.1</version>
</dependency>
```

Expand All @@ -50,18 +50,18 @@ The algorithm which computes this JsonPatch currently generates following operat
- COPY
- TEST

## To turn off MOVE & COPY Operations
```xml
EnumSet<DiffFlags> flags = DiffFlags.dontNormalizeOpIntoMoveAndCopy().clone()
BsonArray patch = BsonDiff.asJson(BsonValue source, BsonValue target, flags)
```

### Apply Json Patch
```xml
BsonValue target = BsonPatch.apply(BsonArray patch, BsonValue source);
```
Given a Patch, it apply it to source Bson and return a target Bson which can be ( Bson object or array or value ). This operation performed on a clone of source Bson ( thus, source Bson is untouched and can be used further).

## To turn off MOVE & COPY Operations
```xml
EnumSet<DiffFlags> flags = DiffFlags.dontNormalizeOpIntoMoveAndCopy().clone()
BsonArray patch = BsonDiff.asJson(BsonValue source, BsonValue target, flags)
```

### Example
First Json
```json
Expand Down Expand Up @@ -89,6 +89,8 @@ a new instance with the patch applied, leaving the `source` unchanged.
1. 100+ selective hardcoded different input jsons , with their driver test classes present under /test directory.
2. Apart from selective input, a deterministic random json generator is present under ( TestDataGenerator.java ), and its driver test class method is JsonDiffTest.testGeneratedJsonDiff().

#### *** Tests can only show presence of bugs and not their absence ***

## Get Involved

* **Contributing**: Pull requests are welcome!
Expand Down
16 changes: 2 additions & 14 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.ebay.bsonpatch</groupId>
<artifactId>bsonpatch</artifactId>
<version>0.3.6-SNAPSHOT</version>
<version>0.4.1</version>
<packaging>jar</packaging>

<name>${project.groupId}:${project.artifactId}</name>
Expand Down Expand Up @@ -129,19 +129,7 @@
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>3.5.0</version>
</dependency>
<!-- For functional operators used in computing diffs -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
</dependency>
<!-- included to compute longest common subsequence of arrays in diff calculation -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.1</version>
<version>3.6.1</version>
</dependency>
<!-- For IOUtils.toString(inputStream, charset) and StringBuilderWriter -->
<dependency>
Expand Down
88 changes: 29 additions & 59 deletions src/main/java/com/ebay/bsonpatch/BsonDiff.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,60 +23,38 @@
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.collections4.ListUtils;
import org.bson.BsonArray;
import org.bson.BsonDocument;
import org.bson.BsonString;
import org.bson.BsonValue;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

public final class BsonDiff {

private static final EncodePathFunction ENCODE_PATH_FUNCTION = new EncodePathFunction();

private BsonDiff() {
}

private final static class EncodePathFunction implements Function<Object, String> {
@Override
public String apply(Object object) {
String path = object.toString(); // see http://tools.ietf.org/html/rfc6901#section-4
return path.replaceAll("~", "~0").replaceAll("/", "~1");
}
}

public static BsonArray asBson(final BsonValue source, final BsonValue target) {
return asBson(source, target, DiffFlags.defaults());
}

public static BsonArray asBson(final BsonValue source, final BsonValue target, EnumSet<DiffFlags> flags) {
final List<Diff> diffs = new ArrayList<Diff>();
List<Object> path = new LinkedList<Object>();
/*
* generating diffs in the order of their occurrence
*/
List<Object> path = new ArrayList<Object>(0);

// generating diffs in the order of their occurrence
generateDiffs(diffs, path, source, target);

if (!flags.contains(DiffFlags.OMIT_MOVE_OPERATION)) {
/*
* Merging remove & add to move operation
*/
// Merging remove & add to move operation
compactDiffs(diffs);
}

if (!flags.contains(DiffFlags.OMIT_COPY_OPERATION)) {
/*
* Introduce copy operation
*/
// Introduce copy operation
introduceCopyOperation(source, target, diffs);
}

Expand All @@ -91,7 +69,7 @@ private static void introduceCopyOperation(BsonValue source, BsonValue target, L
Map<BsonValue, List<Object>> unchangedValues = getUnchangedPart(source, target);
for (int i = 0; i < diffs.size(); i++) {
Diff diff = diffs.get(i);
if (Operation.ADD.equals(diff.getOperation())) {
if (Operation.ADD == diff.getOperation()) {
List<Object> matchingValuePath = getMatchingValuePath(unchangedValues, diff.getValue());
if (matchingValuePath != null && isAllowed(matchingValuePath, diff.getPath())) {
diffs.set(i, new Diff(Operation.COPY, matchingValuePath, diff.getPath()));
Expand Down Expand Up @@ -138,7 +116,7 @@ private static boolean isAllowed(List<Object> source, List<Object> destination)

private static Map<BsonValue, List<Object>> getUnchangedPart(BsonValue source, BsonValue target) {
Map<BsonValue, List<Object>> unchangedValues = new HashMap<BsonValue, List<Object>>();
computeUnchangedValues(unchangedValues, Lists.newArrayList(), source, target);
computeUnchangedValues(unchangedValues, new ArrayList<Object>(), source, target);
return unchangedValues;
}

Expand All @@ -158,7 +136,7 @@ private static void computeUnchangedValues(Map<BsonValue, List<Object>> unchange
case ARRAY:
computeArray(unchangedValues, path, source, target);
default:
/* nothing */
/* nothing */
}
}
}
Expand Down Expand Up @@ -192,8 +170,8 @@ private static void compactDiffs(List<Diff> diffs) {
Diff diff1 = diffs.get(i);

// if not remove OR add, move to next diff
if (!(Operation.REMOVE.equals(diff1.getOperation()) ||
Operation.ADD.equals(diff1.getOperation()))) {
if (!(Operation.REMOVE == diff1.getOperation() ||
Operation.ADD == diff1.getOperation())) {
continue;
}

Expand All @@ -204,13 +182,13 @@ private static void compactDiffs(List<Diff> diffs) {
}

Diff moveDiff = null;
if (Operation.REMOVE.equals(diff1.getOperation()) &&
Operation.ADD.equals(diff2.getOperation())) {
if (Operation.REMOVE == diff1.getOperation() &&
Operation.ADD == diff2.getOperation()) {
computeRelativePath(diff2.getPath(), i + 1, j - 1, diffs);
moveDiff = new Diff(Operation.MOVE, diff1.getPath(), diff2.getPath());

} else if (Operation.ADD.equals(diff1.getOperation()) &&
Operation.REMOVE.equals(diff2.getOperation())) {
} else if (Operation.ADD == diff1.getOperation() &&
Operation.REMOVE == diff2.getOperation()) {
computeRelativePath(diff2.getPath(), i, j - 1, diffs); // diff1's add should also be considered
moveDiff = new Diff(Operation.MOVE, diff2.getPath(), diff1.getPath());
}
Expand All @@ -226,14 +204,14 @@ private static void compactDiffs(List<Diff> diffs) {
//Note : only to be used for arrays
//Finds the longest common Ancestor ending at Array
private static void computeRelativePath(List<Object> path, int startIdx, int endIdx, List<Diff> diffs) {
List<Integer> counters = new ArrayList<Integer>();
List<Integer> counters = new ArrayList<Integer>(path.size());

resetCounters(counters, path.size());

for (int i = startIdx; i <= endIdx; i++) {
Diff diff = diffs.get(i);
//Adjust relative path according to #ADD and #Remove
if (Operation.ADD.equals(diff.getOperation()) || Operation.REMOVE.equals(diff.getOperation())) {
if (Operation.ADD == diff.getOperation() || Operation.REMOVE == diff.getOperation()) {
updatePath(path, diff, counters);
}
}
Expand Down Expand Up @@ -277,10 +255,10 @@ private static void updatePath(List<Object> path, Diff pseudo, List<Integer> cou
}

private static void updateCounters(Diff pseudo, int idx, List<Integer> counters) {
if (Operation.ADD.equals(pseudo.getOperation())) {
if (Operation.ADD == pseudo.getOperation()) {
counters.set(idx, counters.get(idx) - 1);
} else {
if (Operation.REMOVE.equals(pseudo.getOperation())) {
if (Operation.REMOVE == pseudo.getOperation()) {
counters.set(idx, counters.get(idx) + 1);
}
}
Expand All @@ -302,20 +280,22 @@ private static BsonDocument getBsonNode(Diff diff, EnumSet<DiffFlags> flags) {
switch (diff.getOperation()) {
case MOVE:
case COPY:
bsonNode.put(Constants.FROM, new BsonString(getArrayNodeRepresentation(diff.getPath()))); // required {from} only in case of Move Operation
bsonNode.put(Constants.PATH, new BsonString(getArrayNodeRepresentation(diff.getToPath()))); // destination Path
bsonNode.put(Constants.FROM, new BsonString(PathUtils.getPathRepresentation(diff.getPath()))); // required {from} only in case of Move Operation
bsonNode.put(Constants.PATH, new BsonString(PathUtils.getPathRepresentation(diff.getToPath()))); // destination Path
break;

case REMOVE:
bsonNode.put(Constants.PATH, new BsonString(getArrayNodeRepresentation(diff.getPath())));
bsonNode.put(Constants.PATH, new BsonString(PathUtils.getPathRepresentation(diff.getPath())));
if (!flags.contains(DiffFlags.OMIT_VALUE_ON_REMOVE))
bsonNode.put(Constants.VALUE, diff.getValue());
break;

case ADD:
case REPLACE:
if (flags.contains(DiffFlags.ADD_ORIGINAL_VALUE_ON_REPLACE)) {
bsonNode.put(Constants.FROM_VALUE, diff.getSrcValue());
}
case ADD:
case TEST:
bsonNode.put(Constants.PATH, new BsonString(getArrayNodeRepresentation(diff.getPath())));
bsonNode.put(Constants.PATH, new BsonString(PathUtils.getPathRepresentation(diff.getPath())));
bsonNode.put(Constants.VALUE, diff.getValue());
break;

Expand All @@ -327,12 +307,6 @@ private static BsonDocument getBsonNode(Diff diff, EnumSet<DiffFlags> flags) {
return bsonNode;
}

private static String getArrayNodeRepresentation(List<Object> path) {
return Joiner.on('/').appendTo(new StringBuilder().append('/'),
Iterables.transform(path, ENCODE_PATH_FUNCTION)).toString();
}


private static void generateDiffs(List<Diff> diffs, List<Object> path, BsonValue source, BsonValue target) {
if (!source.equals(target)) {
if (source.isArray() && target.isArray()) {
Expand All @@ -344,7 +318,7 @@ private static void generateDiffs(List<Diff> diffs, List<Object> path, BsonValue
} else {
//can be replaced

diffs.add(Diff.generateDiff(Operation.REPLACE, path, target));
diffs.add(Diff.generateDiff(Operation.REPLACE, path, source, target));
}
}
}
Expand Down Expand Up @@ -452,17 +426,13 @@ private static void compareDocuments(List<Diff> diffs, List<Object> path, BsonVa
}

private static List<Object> getPath(List<Object> path, Object key) {
List<Object> toReturn = new ArrayList<Object>();
List<Object> toReturn = new ArrayList<Object>(path.size() + 1);
toReturn.addAll(path);
toReturn.add(key);
return toReturn;
}

private static List<BsonValue> getLCS(final BsonValue first, final BsonValue second) {

Preconditions.checkArgument(first.isArray(), "LCS can only work on BSON arrays");
Preconditions.checkArgument(second.isArray(), "LCS can only work on BSON arrays");

return ListUtils.longestCommonSubsequence(Lists.newArrayList(first.asArray()), Lists.newArrayList(second.asArray()));
return InternalUtils.longestCommonSubsequence(InternalUtils.toList(first.asArray()), InternalUtils.toList(second.asArray()));
}
}
32 changes: 7 additions & 25 deletions src/main/java/com/ebay/bsonpatch/BsonPatch.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,10 @@
import org.bson.BsonNull;
import org.bson.BsonValue;

import com.google.common.base.Function;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

public final class BsonPatch {

private static final DecodePathFunction DECODE_PATH_FUNCTION = new DecodePathFunction();

private BsonPatch() {}

private final static class DecodePathFunction implements Function<String, String> {
@Override
public String apply(String path) {
return path.replaceAll("~1", "/").replaceAll("~0", "~"); // see http://tools.ietf.org/html/rfc6901#section-4
}
}

private static BsonValue getPatchAttr(BsonValue bsonNode, String attr) {
BsonValue child = bsonNode.asDocument().get(attr);
if (child == null)
Expand All @@ -68,7 +54,7 @@ private static void process(BsonArray patch, BsonPatchProcessor processor, EnumS
BsonValue bsonNode = operations.next();
if (!bsonNode.isDocument()) throw new InvalidBsonPatchException("Invalid BSON Patch payload (not an object)");
Operation operation = Operation.fromRfcName(getPatchAttr(bsonNode, Constants.OP).asString().getValue().replaceAll("\"", ""));
List<String> path = getPath(getPatchAttr(bsonNode, Constants.PATH));
List<String> path = PathUtils.getPath(getPatchAttr(bsonNode, Constants.PATH));

switch (operation) {
case REMOVE: {
Expand Down Expand Up @@ -97,13 +83,13 @@ private static void process(BsonArray patch, BsonPatchProcessor processor, EnumS
}

case MOVE: {
List<String> fromPath = getPath(getPatchAttr(bsonNode, Constants.FROM));
List<String> fromPath = PathUtils.getPath(getPatchAttr(bsonNode, Constants.FROM));
processor.move(fromPath, path);
break;
}

case COPY: {
List<String> fromPath = getPath(getPatchAttr(bsonNode, Constants.FROM));
List<String> fromPath = PathUtils.getPath(getPatchAttr(bsonNode, Constants.FROM));
processor.copy(fromPath, path);
break;
}
Expand All @@ -130,7 +116,7 @@ public static void validate(BsonArray patch) throws InvalidBsonPatchException {
}

public static BsonValue apply(BsonArray patch, BsonValue source, EnumSet<CompatibilityFlags> flags) throws BsonPatchApplicationException {
CopyingApplyProcessor processor = new CopyingApplyProcessor(source);
CopyingApplyProcessor processor = new CopyingApplyProcessor(source, flags);
process(patch, processor, flags);
return processor.result();
}
Expand All @@ -139,17 +125,13 @@ public static BsonValue apply(BsonArray patch, BsonValue source) throws BsonPatc
return apply(patch, source, CompatibilityFlags.defaults());
}

public static void applyInPlace(BsonArray patch, BsonValue source){
public static void applyInPlace(BsonArray patch, BsonValue source) {
applyInPlace(patch, source, CompatibilityFlags.defaults());
}

public static void applyInPlace(BsonArray patch, BsonValue source, EnumSet<CompatibilityFlags> flags){
InPlaceApplyProcessor processor = new InPlaceApplyProcessor(source);
public static void applyInPlace(BsonArray patch, BsonValue source, EnumSet<CompatibilityFlags> flags) {
InPlaceApplyProcessor processor = new InPlaceApplyProcessor(source, flags);
process(patch, processor, flags);
}

private static List<String> getPath(BsonValue path) {
List<String> paths = Splitter.on('/').splitToList(path.asString().getValue().replaceAll("\"", ""));
return Lists.newArrayList(Iterables.transform(paths, DECODE_PATH_FUNCTION));
}
}
Loading

0 comments on commit 0049416

Please sign in to comment.