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)); }