diff --git a/NOTICE b/NOTICE
index 4f315d7..7a94205 100644
--- a/NOTICE
+++ b/NOTICE
@@ -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/).
diff --git a/README.md b/README.md
index fc0fb3c..cf74f84 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ 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 ` ` section of your pom.xml -
@@ -28,7 +28,7 @@ Add following to ` ` section of your pom.xml -
com.ebay.bsonpatch
bsonpatch
- 0.3.6
+ 0.4.1
```
@@ -50,18 +50,18 @@ The algorithm which computes this JsonPatch currently generates following operat
- COPY
- TEST
- ## To turn off MOVE & COPY Operations
-```xml
-EnumSet 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 flags = DiffFlags.dontNormalizeOpIntoMoveAndCopy().clone()
+BsonArray patch = BsonDiff.asJson(BsonValue source, BsonValue target, flags)
+```
+
### Example
First Json
```json
@@ -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!
diff --git a/pom.xml b/pom.xml
index 57e8eac..3627a94 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.ebay.bsonpatch
bsonpatch
- 0.3.6-SNAPSHOT
+ 0.4.1
jar
${project.groupId}:${project.artifactId}
@@ -129,19 +129,7 @@
org.mongodb
mongo-java-driver
- 3.5.0
-
-
-
- com.google.guava
- guava
- 20.0
-
-
-
- org.apache.commons
- commons-collections4
- 4.1
+ 3.6.1
diff --git a/src/main/java/com/ebay/bsonpatch/BsonDiff.java b/src/main/java/com/ebay/bsonpatch/BsonDiff.java
index bb13a95..32396d2 100644
--- a/src/main/java/com/ebay/bsonpatch/BsonDiff.java
+++ b/src/main/java/com/ebay/bsonpatch/BsonDiff.java
@@ -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 {
- @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 flags) {
final List diffs = new ArrayList();
- List path = new LinkedList();
- /*
- * generating diffs in the order of their occurrence
- */
+ List path = new ArrayList(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);
}
@@ -91,7 +69,7 @@ private static void introduceCopyOperation(BsonValue source, BsonValue target, L
Map> 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 matchingValuePath = getMatchingValuePath(unchangedValues, diff.getValue());
if (matchingValuePath != null && isAllowed(matchingValuePath, diff.getPath())) {
diffs.set(i, new Diff(Operation.COPY, matchingValuePath, diff.getPath()));
@@ -138,7 +116,7 @@ private static boolean isAllowed(List source, List destination)
private static Map> getUnchangedPart(BsonValue source, BsonValue target) {
Map> unchangedValues = new HashMap>();
- computeUnchangedValues(unchangedValues, Lists.newArrayList(), source, target);
+ computeUnchangedValues(unchangedValues, new ArrayList(), source, target);
return unchangedValues;
}
@@ -158,7 +136,7 @@ private static void computeUnchangedValues(Map> unchange
case ARRAY:
computeArray(unchangedValues, path, source, target);
default:
- /* nothing */
+ /* nothing */
}
}
}
@@ -192,8 +170,8 @@ private static void compactDiffs(List 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;
}
@@ -204,13 +182,13 @@ private static void compactDiffs(List 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());
}
@@ -226,14 +204,14 @@ private static void compactDiffs(List diffs) {
//Note : only to be used for arrays
//Finds the longest common Ancestor ending at Array
private static void computeRelativePath(List path, int startIdx, int endIdx, List diffs) {
- List counters = new ArrayList();
+ List counters = new ArrayList(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);
}
}
@@ -277,10 +255,10 @@ private static void updatePath(List path, Diff pseudo, List cou
}
private static void updateCounters(Diff pseudo, int idx, List 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);
}
}
@@ -302,20 +280,22 @@ private static BsonDocument getBsonNode(Diff diff, EnumSet 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;
@@ -327,12 +307,6 @@ private static BsonDocument getBsonNode(Diff diff, EnumSet flags) {
return bsonNode;
}
- private static String getArrayNodeRepresentation(List path) {
- return Joiner.on('/').appendTo(new StringBuilder().append('/'),
- Iterables.transform(path, ENCODE_PATH_FUNCTION)).toString();
- }
-
-
private static void generateDiffs(List diffs, List path, BsonValue source, BsonValue target) {
if (!source.equals(target)) {
if (source.isArray() && target.isArray()) {
@@ -344,7 +318,7 @@ private static void generateDiffs(List diffs, List path, BsonValue
} else {
//can be replaced
- diffs.add(Diff.generateDiff(Operation.REPLACE, path, target));
+ diffs.add(Diff.generateDiff(Operation.REPLACE, path, source, target));
}
}
}
@@ -452,17 +426,13 @@ private static void compareDocuments(List diffs, List path, BsonVa
}
private static List getPath(List path, Object key) {
- List toReturn = new ArrayList();
+ List toReturn = new ArrayList(path.size() + 1);
toReturn.addAll(path);
toReturn.add(key);
return toReturn;
}
private static List 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()));
}
}
diff --git a/src/main/java/com/ebay/bsonpatch/BsonPatch.java b/src/main/java/com/ebay/bsonpatch/BsonPatch.java
index 82f1938..093ed9f 100644
--- a/src/main/java/com/ebay/bsonpatch/BsonPatch.java
+++ b/src/main/java/com/ebay/bsonpatch/BsonPatch.java
@@ -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 {
- @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)
@@ -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 path = getPath(getPatchAttr(bsonNode, Constants.PATH));
+ List path = PathUtils.getPath(getPatchAttr(bsonNode, Constants.PATH));
switch (operation) {
case REMOVE: {
@@ -97,13 +83,13 @@ private static void process(BsonArray patch, BsonPatchProcessor processor, EnumS
}
case MOVE: {
- List fromPath = getPath(getPatchAttr(bsonNode, Constants.FROM));
+ List fromPath = PathUtils.getPath(getPatchAttr(bsonNode, Constants.FROM));
processor.move(fromPath, path);
break;
}
case COPY: {
- List fromPath = getPath(getPatchAttr(bsonNode, Constants.FROM));
+ List fromPath = PathUtils.getPath(getPatchAttr(bsonNode, Constants.FROM));
processor.copy(fromPath, path);
break;
}
@@ -130,7 +116,7 @@ public static void validate(BsonArray patch) throws InvalidBsonPatchException {
}
public static BsonValue apply(BsonArray patch, BsonValue source, EnumSet flags) throws BsonPatchApplicationException {
- CopyingApplyProcessor processor = new CopyingApplyProcessor(source);
+ CopyingApplyProcessor processor = new CopyingApplyProcessor(source, flags);
process(patch, processor, flags);
return processor.result();
}
@@ -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 flags){
- InPlaceApplyProcessor processor = new InPlaceApplyProcessor(source);
+ public static void applyInPlace(BsonArray patch, BsonValue source, EnumSet flags) {
+ InPlaceApplyProcessor processor = new InPlaceApplyProcessor(source, flags);
process(patch, processor, flags);
}
- private static List getPath(BsonValue path) {
- List paths = Splitter.on('/').splitToList(path.asString().getValue().replaceAll("\"", ""));
- return Lists.newArrayList(Iterables.transform(paths, DECODE_PATH_FUNCTION));
- }
}
diff --git a/src/main/java/com/ebay/bsonpatch/CompatibilityFlags.java b/src/main/java/com/ebay/bsonpatch/CompatibilityFlags.java
index 54eab61..6f6ebb1 100644
--- a/src/main/java/com/ebay/bsonpatch/CompatibilityFlags.java
+++ b/src/main/java/com/ebay/bsonpatch/CompatibilityFlags.java
@@ -22,7 +22,8 @@
import java.util.EnumSet;
public enum CompatibilityFlags {
- MISSING_VALUES_AS_NULLS;
+ MISSING_VALUES_AS_NULLS,
+ REMOVE_NONE_EXISTING_ARRAY_ELEMENT;
public static EnumSet defaults() {
return EnumSet.noneOf(CompatibilityFlags.class);
diff --git a/src/main/java/com/ebay/bsonpatch/Constants.java b/src/main/java/com/ebay/bsonpatch/Constants.java
index 86167b7..7ebb358 100644
--- a/src/main/java/com/ebay/bsonpatch/Constants.java
+++ b/src/main/java/com/ebay/bsonpatch/Constants.java
@@ -20,10 +20,11 @@
package com.ebay.bsonpatch;
final class Constants {
- public static String OP = "op";
- public static String VALUE = "value";
- public static String PATH = "path";
- public static String FROM = "from";
+ public static final String OP = "op";
+ public static final String VALUE = "value";
+ public static final String PATH = "path";
+ public static final String FROM = "from";
+ public static final String FROM_VALUE = "fromValue";
private Constants() {}
diff --git a/src/main/java/com/ebay/bsonpatch/CopyingApplyProcessor.java b/src/main/java/com/ebay/bsonpatch/CopyingApplyProcessor.java
index dacb361..e12bd51 100644
--- a/src/main/java/com/ebay/bsonpatch/CopyingApplyProcessor.java
+++ b/src/main/java/com/ebay/bsonpatch/CopyingApplyProcessor.java
@@ -22,13 +22,18 @@
import org.bson.BsonBinary;
import org.bson.BsonJavaScriptWithScope;
import org.bson.BsonValue;
+import java.util.EnumSet;
class CopyingApplyProcessor extends InPlaceApplyProcessor {
CopyingApplyProcessor(BsonValue target) {
- super(deepCopy(target));
+ this(target, CompatibilityFlags.defaults());
}
+ CopyingApplyProcessor(BsonValue target, EnumSet flags) {
+ super(deepCopy(target), flags);
+ }
+
static BsonValue deepCopy(BsonValue source) {
BsonValue result;
switch (source.getBsonType()) {
diff --git a/src/main/java/com/ebay/bsonpatch/Diff.java b/src/main/java/com/ebay/bsonpatch/Diff.java
index 824e7d2..55b15fb 100644
--- a/src/main/java/com/ebay/bsonpatch/Diff.java
+++ b/src/main/java/com/ebay/bsonpatch/Diff.java
@@ -28,11 +28,13 @@ class Diff {
private final List path;
private final BsonValue value;
private List toPath; //only to be used in move operation
+ private final BsonValue srcValue; // only used in replace operation
Diff(Operation operation, List path, BsonValue value) {
this.operation = operation;
this.path = path;
this.value = value;
+ this.srcValue = null;
}
Diff(Operation operation, List fromPath, List toPath) {
@@ -40,7 +42,15 @@ class Diff {
this.path = fromPath;
this.toPath = toPath;
this.value = null;
+ this.srcValue = null;
}
+
+ Diff(Operation operation, List path, BsonValue srcValue, BsonValue value) {
+ this.operation = operation;
+ this.path = path;
+ this.value = value;
+ this.srcValue = srcValue;
+ }
public Operation getOperation() {
return operation;
@@ -57,8 +67,16 @@ public BsonValue getValue() {
public static Diff generateDiff(Operation replace, List path, BsonValue target) {
return new Diff(replace, path, target);
}
+
+ public static Diff generateDiff(Operation replace, List path, BsonValue source, BsonValue target) {
+ return new Diff(replace, path, source, target);
+ }
List getToPath() {
return toPath;
}
+
+ public BsonValue getSrcValue(){
+ return srcValue;
+ }
}
diff --git a/src/main/java/com/ebay/bsonpatch/DiffFlags.java b/src/main/java/com/ebay/bsonpatch/DiffFlags.java
index 6ca741d..806a9f2 100644
--- a/src/main/java/com/ebay/bsonpatch/DiffFlags.java
+++ b/src/main/java/com/ebay/bsonpatch/DiffFlags.java
@@ -19,20 +19,48 @@
package com.ebay.bsonpatch;
-import java.util.Arrays;
import java.util.EnumSet;
public enum DiffFlags {
+ /**
+ * This flag omits the value field on remove operations.
+ * This is a default flag.
+ */
OMIT_VALUE_ON_REMOVE,
- OMIT_MOVE_OPERATION, //only have ADD, REMOVE, REPLACE, COPY Don't normalize operations into MOVE
- OMIT_COPY_OPERATION; //only have ADD, REMOVE, REPLACE, MOVE, Don't normalize operations into COPY
+
+ /**
+ * This flag omits all {@link Operation#MOVE} operations, leaving only
+ * {@link Operation#ADD}, {@link Operation#REMOVE}, {@link Operation#REPLACE}
+ * and {@link Operation#COPY} operations. In other words, without this flag,
+ * {@link Operation#ADD} and {@link Operation#REMOVE} operations are not normalized
+ * into {@link Operation#MOVE} operations.
+ */
+ OMIT_MOVE_OPERATION,
+
+ /**
+ * This flag omits all {@link Operation#COPY} operations, leaving only
+ * {@link Operation#ADD}, {@link Operation#REMOVE}, {@link Operation#REPLACE}
+ * and {@link Operation#MOVE} operations. In other words, without this flag,
+ * {@link Operation#ADD} operations are not normalized into {@link Operation#COPY}
+ * operations.
+ */
+ OMIT_COPY_OPERATION,
+
+ /**
+ * This flag adds a fromValue field to all {@link Operation#REPLACE}operations.
+ * fromValue represents the the value replaced by a {@link Operation#REPLACE}
+ * operation, in other words, the original value.
+ *
+ * @since 0.4.1
+ */
+ ADD_ORIGINAL_VALUE_ON_REPLACE;
public static EnumSet defaults() {
return EnumSet.of(OMIT_VALUE_ON_REMOVE);
}
public static EnumSet dontNormalizeOpIntoMoveAndCopy() {
- return EnumSet.copyOf(Arrays.asList(OMIT_MOVE_OPERATION, OMIT_COPY_OPERATION));
+ return EnumSet.of(OMIT_MOVE_OPERATION, OMIT_COPY_OPERATION);
}
}
diff --git a/src/main/java/com/ebay/bsonpatch/InPlaceApplyProcessor.java b/src/main/java/com/ebay/bsonpatch/InPlaceApplyProcessor.java
index 07e8827..b78aefe 100644
--- a/src/main/java/com/ebay/bsonpatch/InPlaceApplyProcessor.java
+++ b/src/main/java/com/ebay/bsonpatch/InPlaceApplyProcessor.java
@@ -19,39 +19,31 @@
package com.ebay.bsonpatch;
+import java.util.EnumSet;
import java.util.List;
import org.bson.BsonArray;
import org.bson.BsonDocument;
import org.bson.BsonValue;
-import com.google.common.base.Function;
-import com.google.common.base.Joiner;
-import com.google.common.base.Strings;
-import com.google.common.collect.Iterables;
-
class InPlaceApplyProcessor implements BsonPatchProcessor {
private BsonValue target;
+ private EnumSet flags;
InPlaceApplyProcessor(BsonValue target) {
- this.target = target;
+ this(target, CompatibilityFlags.defaults());
}
+ InPlaceApplyProcessor(BsonValue target, EnumSet flags) {
+ this.target = target;
+ this.flags = flags;
+ }
+
public BsonValue result() {
return target;
}
- private static final EncodePathFunction ENCODE_PATH_FUNCTION = new EncodePathFunction();
-
- private final static class EncodePathFunction implements Function {
- @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");
- }
- }
-
@Override
public void move(List fromPath, List toPath) {
BsonValue parentNode = getParentNode(fromPath, Operation.MOVE);
@@ -65,7 +57,7 @@ public void move(List fromPath, List toPath) {
public void copy(List fromPath, List toPath) {
BsonValue parentNode = getParentNode(fromPath, Operation.COPY);
String field = fromPath.get(fromPath.size() - 1).replaceAll("\"", "");
- BsonValue valueNode = parentNode.isArray() ? parentNode.asArray().get(Integer.parseInt(field)) : parentNode.asDocument().get(field);
+ BsonValue valueNode = parentNode.isArray() ? parentNode.asArray().get(Integer.parseInt(field)) : parentNode.asDocument().get(field);
add(toPath, valueNode);
}
@@ -77,35 +69,34 @@ public void test(List path, BsonValue value) {
BsonValue parentNode = getParentNode(path, Operation.TEST);
String fieldToReplace = path.get(path.size() - 1).replaceAll("\"", "");
if (fieldToReplace.equals("") && path.size() == 1)
- if(target.equals(value)){
+ if (target.equals(value)) {
target = value;
- }else {
+ } else {
error(Operation.TEST, "value mismatch");
}
else if (!parentNode.isDocument() && !parentNode.isArray())
- error(Operation.TEST, "parent is not a container in source, path provided : " + getArrayNodeRepresentation(path) + " | node : " + parentNode);
+ error(Operation.TEST, "parent is not a container in source, path provided : " + PathUtils.getPathRepresentation(path) + " | node : " + parentNode);
else if (parentNode.isArray()) {
final BsonArray target = parentNode.asArray();
String idxStr = path.get(path.size() - 1);
if ("-".equals(idxStr)) {
// see http://tools.ietf.org/html/rfc6902#section-4.1
- if(!target.get(target.size()-1).equals(value)){
+ if(!target.get(target.size() - 1).equals(value)) {
error(Operation.TEST, "value mismatch");
}
} else {
- int idx = arrayIndex(idxStr.replaceAll("\"", ""), target.size());
- if(!target.get(idx).equals(value)){
+ int idx = arrayIndex(idxStr.replaceAll("\"", ""), target.size(), false);
+ if (!target.get(idx).equals(value)) {
error(Operation.TEST, "value mismatch");
}
}
- }
- else {
+ } else {
final BsonDocument target = parentNode.asDocument();
String key = path.get(path.size() - 1).replaceAll("\"", "");
BsonValue actual = target.get(key);
if (actual == null)
- error(Operation.TEST, "noSuchPath in source, path provided : " + getArrayNodeRepresentation(path));
+ error(Operation.TEST, "noSuchPath in source, path provided : " + PathUtils.getPathRepresentation(path));
else if (!actual.equals(value))
error(Operation.TEST, "value mismatch");
}
@@ -122,7 +113,7 @@ public void add(List path, BsonValue value) {
if (fieldToReplace.equals("") && path.size() == 1)
target = value;
else if (!parentNode.isDocument() && !parentNode.isArray())
- error(Operation.ADD, "parent is not a container in source, path provided : " + getArrayNodeRepresentation(path) + " | node : " + parentNode);
+ error(Operation.ADD, "parent is not a container in source, path provided : " + PathUtils.getPathRepresentation(path) + " | node : " + parentNode);
else if (parentNode.isArray())
addToArray(path, value, parentNode);
else
@@ -144,7 +135,7 @@ private void addToArray(List path, BsonValue value, BsonValue parentNode
// see http://tools.ietf.org/html/rfc6902#section-4.1
target.add(value);
} else {
- int idx = arrayIndex(idxStr.replaceAll("\"", ""), target.size());
+ int idx = arrayIndex(idxStr.replaceAll("\"", ""), target.size(), false);
target.add(idx, value);
}
}
@@ -156,14 +147,14 @@ public void replace(List path, BsonValue value) {
} else {
BsonValue parentNode = getParentNode(path, Operation.REPLACE);
String fieldToReplace = path.get(path.size() - 1).replaceAll("\"", "");
- if (Strings.isNullOrEmpty(fieldToReplace) && path.size() == 1)
+ if (isNullOrEmpty(fieldToReplace) && path.size() == 1)
target = value;
else if (parentNode.isDocument())
parentNode.asDocument().put(fieldToReplace, value);
else if (parentNode.isArray())
- parentNode.asArray().set(arrayIndex(fieldToReplace, parentNode.asArray().size() - 1), value);
+ parentNode.asArray().set(arrayIndex(fieldToReplace, parentNode.asArray().size() - 1, false), value);
else
- error(Operation.REPLACE, "noSuchPath in source, path provided : " + getArrayNodeRepresentation(path));
+ error(Operation.REPLACE, "noSuchPath in source, path provided : " + PathUtils.getPathRepresentation(path));
}
}
@@ -176,10 +167,17 @@ public void remove(List path) {
String fieldToRemove = path.get(path.size() - 1).replaceAll("\"", "");
if (parentNode.isDocument())
parentNode.asDocument().remove(fieldToRemove);
- else if (parentNode.isArray())
- parentNode.asArray().remove(arrayIndex(fieldToRemove, parentNode.asArray().size() - 1));
- else
- error(Operation.REMOVE, "noSuchPath in source, path provided : " + getArrayNodeRepresentation(path));
+ else if (parentNode.isArray()) {
+ // If path specifies a non-existent array element and the REMOVE_NONE_EXISTING_ARRAY_ELEMENT flag is not set, then
+ // arrayIndex will throw an error.
+ int i = arrayIndex(fieldToRemove, parentNode.asArray().size() - 1, flags.contains(CompatibilityFlags.REMOVE_NONE_EXISTING_ARRAY_ELEMENT));
+ // However, BsonArray.remove(int) is not very forgiving, so we need to avoid making the call if the index is past the end
+ // otherwise, we'll get an IndexArrayOutOfBounds error
+ if (i < parentNode.asArray().size()) {
+ parentNode.asArray().remove(i);
+ }
+ } else
+ error(Operation.REMOVE, "noSuchPath in source, path provided : " + PathUtils.getPathRepresentation(path));
}
}
@@ -190,7 +188,8 @@ private void error(Operation forOp, String message) {
private BsonValue getParentNode(List fromPath, Operation forOp) {
List pathToParent = fromPath.subList(0, fromPath.size() - 1); // would never by out of bound, lets see
BsonValue node = getNode(target, pathToParent, 1);
- if (node == null) error(forOp, "noSuchPath in source, path provided: " + getArrayNodeRepresentation(fromPath));
+ if (node == null)
+ error(forOp, "noSuchPath in source, path provided: " + PathUtils.getPathRepresentation(fromPath));
return node;
}
@@ -220,7 +219,7 @@ private BsonValue getNode(BsonValue ret, List path, int pos) {
}
}
- private int arrayIndex(String s, int max) {
+ private int arrayIndex(String s, int max, boolean allowNoneExisting) {
int index;
try {
index = Integer.parseInt(s);
@@ -230,12 +229,13 @@ private int arrayIndex(String s, int max) {
if (index < 0) {
throw new BsonPatchApplicationException("index Out of bound, index is negative");
} else if (index > max) {
- throw new BsonPatchApplicationException("index Out of bound, index is greater than " + max);
+ if (!allowNoneExisting)
+ throw new BsonPatchApplicationException("index Out of bound, index is greater than " + max);
}
return index;
}
- private static String getArrayNodeRepresentation(List path) {
- return Joiner.on('/').appendTo(new StringBuilder().append('/'),
- Iterables.transform(path, ENCODE_PATH_FUNCTION)).toString();
+
+ private boolean isNullOrEmpty(String string) {
+ return string == null || string.length() == 0;
}
}
diff --git a/src/main/java/com/ebay/bsonpatch/InternalUtils.java b/src/main/java/com/ebay/bsonpatch/InternalUtils.java
new file mode 100644
index 0000000..ae1a448
--- /dev/null
+++ b/src/main/java/com/ebay/bsonpatch/InternalUtils.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, 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.ebay.bsonpatch;
+
+import org.bson.BsonArray;
+import org.bson.BsonValue;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+class InternalUtils {
+
+ static List toList(BsonArray input) {
+ int size = input.size();
+ List toReturn = new ArrayList(size);
+ for (int i = 0; i < size; i++) {
+ toReturn.add(input.get(i));
+ }
+ return toReturn;
+ }
+
+ static List longestCommonSubsequence(final List a, final List b) {
+ if (a == null || b == null) {
+ throw new NullPointerException("List must not be null for longestCommonSubsequence");
+ }
+
+ List toReturn = new LinkedList();
+
+ int aSize = a.size();
+ int bSize = b.size();
+ int temp[][] = new int[aSize + 1][bSize + 1];
+
+ for (int i = 1; i <= aSize; i++) {
+ for (int j = 1; j <= bSize; j++) {
+ if (i == 0 || j == 0) {
+ temp[i][j] = 0;
+ } else if (a.get(i - 1).equals(b.get(j - 1))) {
+ temp[i][j] = temp[i - 1][j - 1] + 1;
+ } else {
+ temp[i][j] = Math.max(temp[i][j - 1], temp[i - 1][j]);
+ }
+ }
+ }
+ int i = aSize, j = bSize;
+ while (i > 0 && j > 0) {
+ if (a.get(i - 1).equals(b.get(j - 1))) {
+ toReturn.add(a.get(i - 1));
+ i--;
+ j--;
+ } else if (temp[i - 1][j] > temp[i][j - 1])
+ i--;
+ else
+ j--;
+ }
+ Collections.reverse(toReturn);
+ return toReturn;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ebay/bsonpatch/NoopProcessor.java b/src/main/java/com/ebay/bsonpatch/NoopProcessor.java
index 56c73b4..2b1fa83 100644
--- a/src/main/java/com/ebay/bsonpatch/NoopProcessor.java
+++ b/src/main/java/com/ebay/bsonpatch/NoopProcessor.java
@@ -23,9 +23,11 @@
import org.bson.BsonValue;
-/** A JSON patch processor that does nothing, intended for testing and validation. */
+/**
+ * A JSON patch processor that does nothing, intended for testing and validation.
+ */
public class NoopProcessor implements BsonPatchProcessor {
- static NoopProcessor INSTANCE;
+ static final NoopProcessor INSTANCE;
static {
INSTANCE = new NoopProcessor();
}
diff --git a/src/main/java/com/ebay/bsonpatch/Operation.java b/src/main/java/com/ebay/bsonpatch/Operation.java
index c301d35..63292ed 100644
--- a/src/main/java/com/ebay/bsonpatch/Operation.java
+++ b/src/main/java/com/ebay/bsonpatch/Operation.java
@@ -19,10 +19,10 @@
package com.ebay.bsonpatch;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.Map;
-import com.google.common.collect.ImmutableMap;
-
enum Operation {
ADD("add"),
REMOVE("remove"),
@@ -31,15 +31,18 @@ enum Operation {
COPY("copy"),
TEST("test");
- private final static Map OPS = new ImmutableMap.Builder()
- .put(ADD.rfcName, ADD)
- .put(REMOVE.rfcName, REMOVE)
- .put(REPLACE.rfcName, REPLACE)
- .put(MOVE.rfcName, MOVE)
- .put(COPY.rfcName, COPY)
- .put(TEST.rfcName, TEST)
- .build();
-
+ private final static Map OPS = createImmutableMap();
+
+ private static Map createImmutableMap() {
+ Map map = new HashMap();
+ map.put(ADD.rfcName, ADD);
+ map.put(REMOVE.rfcName, REMOVE);
+ map.put(REPLACE.rfcName, REPLACE);
+ map.put(MOVE.rfcName, MOVE);
+ map.put(COPY.rfcName, COPY);
+ map.put(TEST.rfcName, TEST);
+ return Collections.unmodifiableMap(map);
+ }
private String rfcName;
diff --git a/src/main/java/com/ebay/bsonpatch/PathUtils.java b/src/main/java/com/ebay/bsonpatch/PathUtils.java
new file mode 100644
index 0000000..f14fa29
--- /dev/null
+++ b/src/main/java/com/ebay/bsonpatch/PathUtils.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, 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.ebay.bsonpatch;
+
+import org.bson.BsonValue;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+class PathUtils {
+ private static final Pattern ENCODED_TILDA_PATTERN = Pattern.compile("~");
+ private static final Pattern ENCODED_SLASH_PATTERN = Pattern.compile("/");
+
+ private static final Pattern DECODED_TILDA_PATTERN = Pattern.compile("~0");
+ private static final Pattern DECODED_SLASH_PATTERN = Pattern.compile("~1");
+
+ private static String encodePath(Object object) {
+ String path = object.toString(); // see http://tools.ietf.org/html/rfc6901#section-4
+ path = ENCODED_TILDA_PATTERN.matcher(path).replaceAll("~0");
+ return ENCODED_SLASH_PATTERN.matcher(path).replaceAll("~1");
+ }
+
+ private static String decodePath(Object object) {
+ String path = object.toString(); // see http://tools.ietf.org/html/rfc6901#section-4
+ path = DECODED_TILDA_PATTERN.matcher(path).replaceAll("~");
+ return DECODED_SLASH_PATTERN.matcher(path).replaceAll("/");
+ }
+
+ static String getPathRepresentation(List path) {
+ StringBuilder builder = new StringBuilder();
+ builder.append('/');
+ int count = 0;
+ for (Object o : path) {
+ if (++count > 1)
+ builder.append('/');
+ builder.append(encodePath(o));
+ }
+ return builder.toString();
+ }
+
+ static List getPath(BsonValue path) {
+ List result = new ArrayList();
+ StringBuilder builder = new StringBuilder();
+ String cleanPath = path.asString().getValue().replaceAll("\"", "");
+ for (int index = 0; index < cleanPath.length(); index++) {
+ char c = cleanPath.charAt(index);
+ if (c == '/') {
+ result.add(decodePath(builder.toString()));
+ builder.delete(0, builder.length());
+ } else {
+ builder.append(c);
+ }
+ }
+ result.add(decodePath(builder.toString()));
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/ebay/bsonpatch/AbstractTest.java b/src/test/java/com/ebay/bsonpatch/AbstractTest.java
index e26c2c8..5712673 100644
--- a/src/test/java/com/ebay/bsonpatch/AbstractTest.java
+++ b/src/test/java/com/ebay/bsonpatch/AbstractTest.java
@@ -19,9 +19,9 @@
package com.ebay.bsonpatch;
-import static org.hamcrest.core.IsEqual.equalTo;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.hamcrest.core.StringContains.containsString;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
@@ -62,14 +62,16 @@ public void test() throws Exception {
private void testOperation() throws Exception {
BsonDocument node = p.getNode();
- BsonValue first = node.get("node");
- BsonValue second = node.get("expected");
+ BsonValue doc = node.get("node");
+ BsonValue expected = node.get("expected");
BsonArray patch = node.getArray("op");
String message = node.containsKey("message") ? node.getString("message").getValue() : "";
- BsonValue secondPrime = BsonPatch.apply(patch, first);
-
- assertThat(message, secondPrime, equalTo(second));
+ BsonValue result = BsonPatch.apply(patch, doc);
+ String failMessage = "The following test failed: \n" +
+ "message: " + message + '\n' +
+ "at: " + p.getSourceFile();
+ assertEquals(failMessage, expected, result);
}
private Class> exceptionType(String type) throws ClassNotFoundException {
diff --git a/src/test/java/com/ebay/bsonpatch/CompatibilityTest.java b/src/test/java/com/ebay/bsonpatch/CompatibilityTest.java
index e40ffc7..7348216 100644
--- a/src/test/java/com/ebay/bsonpatch/CompatibilityTest.java
+++ b/src/test/java/com/ebay/bsonpatch/CompatibilityTest.java
@@ -20,6 +20,7 @@
package com.ebay.bsonpatch;
import static com.ebay.bsonpatch.CompatibilityFlags.MISSING_VALUES_AS_NULLS;
+import static com.ebay.bsonpatch.CompatibilityFlags.REMOVE_NONE_EXISTING_ARRAY_ELEMENT;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertThat;
@@ -35,11 +36,13 @@ public class CompatibilityTest {
BsonArray addNodeWithMissingValue;
BsonArray replaceNodeWithMissingValue;
+ BsonArray removeNoneExistingArrayElement;
@Before
public void setUp() throws Exception {
addNodeWithMissingValue = BsonArray.parse("[{\"op\":\"add\",\"path\":\"a\"}]");
replaceNodeWithMissingValue = BsonArray.parse("[{\"op\":\"replace\",\"path\":\"a\"}]");
+ removeNoneExistingArrayElement = BsonArray.parse("[{\"op\": \"remove\",\"path\": \"/b/0\"}]");
}
@Test
@@ -66,4 +69,12 @@ public void withFlagReplaceShouldTreatMissingValuesAsNull() throws IOException {
public void withFlagReplaceNodeWithMissingValueShouldValidateCorrectly() {
BsonPatch.validate(addNodeWithMissingValue, EnumSet.of(MISSING_VALUES_AS_NULLS));
}
+
+ @Test
+ public void withFlagIgnoreRemoveNoneExistingArrayElement() throws IOException {
+ BsonDocument source = BsonDocument.parse("{\"b\": []}");
+ BsonDocument expected = BsonDocument.parse("{\"b\": []}");
+ BsonDocument result = BsonPatch.apply(removeNoneExistingArrayElement, source, EnumSet.of(REMOVE_NONE_EXISTING_ARRAY_ELEMENT)).asDocument();
+ assertThat(result, equalTo(expected));
+ }
}
diff --git a/src/test/java/com/ebay/bsonpatch/JsonDiffTest.java b/src/test/java/com/ebay/bsonpatch/JsonDiffTest.java
index f2ef26c..10f26cd 100644
--- a/src/test/java/com/ebay/bsonpatch/JsonDiffTest.java
+++ b/src/test/java/com/ebay/bsonpatch/JsonDiffTest.java
@@ -51,16 +51,16 @@ public void testSampleJsonDiff() throws Exception {
BsonValue first = jsonNode.get(i).asDocument().get("first");
BsonValue second = jsonNode.get(i).asDocument().get("second");
- System.out.println("Test # " + i);
- System.out.println(first);
- System.out.println(second);
+// System.out.println("Test # " + i);
+// System.out.println(first);
+// System.out.println(second);
BsonArray actualPatch = BsonDiff.asBson(first, second);
- System.out.println(actualPatch);
+// System.out.println(actualPatch);
BsonValue secondPrime = BsonPatch.apply(actualPatch, first);
- System.out.println(secondPrime);
+// System.out.println(secondPrime);
Assert.assertTrue(second.equals(secondPrime));
}
}
@@ -73,14 +73,14 @@ public void testGeneratedJsonDiff() throws Exception {
BsonArray second = TestDataGenerator.generate(random.nextInt(10));
BsonArray actualPatch = BsonDiff.asBson(first, second);
- System.out.println("Test # " + i);
-
- System.out.println(first);
- System.out.println(second);
- System.out.println(actualPatch);
+// System.out.println("Test # " + i);
+//
+// System.out.println(first);
+// System.out.println(second);
+// System.out.println(actualPatch);
BsonArray secondPrime = BsonPatch.apply(actualPatch, first).asArray();
- System.out.println(secondPrime);
+// System.out.println(secondPrime);
Assert.assertTrue(second.equals(secondPrime));
}
}
@@ -124,9 +124,9 @@ public void testRenderedOperationsExceptMoveAndCopy() throws Exception {
BsonArray diff = BsonDiff.asBson(source, target, flags);
- System.out.println(source);
- System.out.println(target);
- System.out.println(diff);
+// System.out.println(source);
+// System.out.println(target);
+// System.out.println(diff);
for (BsonValue d : diff) {
Assert.assertNotEquals(Operation.MOVE.rfcName(), d.asDocument().getString("op").getValue());
@@ -134,7 +134,7 @@ public void testRenderedOperationsExceptMoveAndCopy() throws Exception {
}
BsonValue targetPrime = BsonPatch.apply(diff, source);
- System.out.println(targetPrime);
+// System.out.println(targetPrime);
Assert.assertTrue(target.equals(targetPrime));
diff --git a/src/test/java/com/ebay/bsonpatch/JsonDiffTest2.java b/src/test/java/com/ebay/bsonpatch/JsonDiffTest2.java
index 5483ccb..9723f76 100644
--- a/src/test/java/com/ebay/bsonpatch/JsonDiffTest2.java
+++ b/src/test/java/com/ebay/bsonpatch/JsonDiffTest2.java
@@ -53,13 +53,13 @@ public void testPatchAppliedCleanly() throws Exception {
BsonArray patch = node.getArray("patch");
String message = node.containsKey("message") ? node.getString("message").getValue() : "";
- System.out.println("Test # " + i);
- System.out.println(first);
- System.out.println(second);
- System.out.println(patch);
+// System.out.println("Test # " + i);
+// System.out.println(first);
+// System.out.println(second);
+// System.out.println(patch);
BsonValue secondPrime = BsonPatch.apply(patch, first);
- System.out.println(secondPrime);
+// System.out.println(secondPrime);
Assert.assertThat(message, secondPrime, equalTo(second));
}