diff --git a/CHANGELOG.md b/CHANGELOG.md index ae7b6d290ff..dc7b0edb642 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Fix #6008: removing the optional dependency on bouncy castle * Fix #6230: introduced Quantity.multiply(int) to allow for Quantity multiplication by an integer * Fix #6281: use GitHub binary repo for Kube API Tests +* Fix #5480: Move `io.fabric8:zjsonpatch` to KubernetesClient project #### Dependency Upgrade * Fix #6052: Removed dependency on no longer maintained com.github.mifmif:generex diff --git a/kubernetes-client/pom.xml b/kubernetes-client/pom.xml index 278503c4631..e0ac053b0ee 100644 --- a/kubernetes-client/pom.xml +++ b/kubernetes-client/pom.xml @@ -74,7 +74,6 @@ io.fabric8 zjsonpatch - ${zjsonpatch.version} diff --git a/platforms/karaf/features/src/main/resources/feature.xml b/platforms/karaf/features/src/main/resources/feature.xml index d340c9fec61..5b84837f65e 100644 --- a/platforms/karaf/features/src/main/resources/feature.xml +++ b/platforms/karaf/features/src/main/resources/feature.xml @@ -40,7 +40,7 @@ mvn:org.ow2.asm/asm-tree/${asm.bundle.version} mvn:org.ow2.asm/asm-util/${asm.bundle.version} mvn:io.fabric8/kubernetes-model-common/${project.version} - mvn:io.fabric8/zjsonpatch/${zjsonpatch.version} + mvn:io.fabric8/zjsonpatch/${project.version} mvn:io.fabric8/kubernetes-model-core/${project.version} mvn:io.fabric8/kubernetes-model-rbac/${project.version} diff --git a/pom.xml b/pom.xml index 244dbf3152b..e461e21b959 100644 --- a/pom.xml +++ b/pom.xml @@ -100,7 +100,6 @@ 3.9.9 3.15.0 4.5.9 - 0.3.0 3.2.2 @@ -205,6 +204,7 @@ kubernetes-model-generator + zjsonpatch kubernetes-client-api kubernetes-client junit/mockwebserver @@ -215,7 +215,6 @@ openshift-client extensions junit/openshift-server-mock - kubernetes-examples platforms kubernetes-tests uberjar @@ -226,6 +225,7 @@ httpclient-vertx kubernetes-client-deps-compatibility-tests log4j + kubernetes-examples @@ -720,6 +720,11 @@ kube-api-test-client-inject ${project.version} + + io.fabric8 + zjsonpatch + ${project.version} + @@ -901,11 +906,6 @@ - - io.fabric8 - zjsonpatch - ${zjsonpatch.version} - org.mockito mockito-core diff --git a/uberjar/pom.xml b/uberjar/pom.xml index b4c17d32449..4755c3806f1 100644 --- a/uberjar/pom.xml +++ b/uberjar/pom.xml @@ -200,6 +200,12 @@ ${project.version} + + io.fabric8 + zjsonpatch + ${project.version} + + com.squareup.okhttp3 @@ -232,12 +238,6 @@ jackson-datatype-jsr310 - - io.fabric8 - zjsonpatch - ${zjsonpatch.version} - - org.junit.jupiter junit-jupiter-engine diff --git a/zjsonpatch/pom.xml b/zjsonpatch/pom.xml new file mode 100644 index 00000000000..2ade6da0fe9 --- /dev/null +++ b/zjsonpatch/pom.xml @@ -0,0 +1,95 @@ + + + + 4.0.0 + + io.fabric8 + kubernetes-client-project + 7.0-SNAPSHOT + + + bundle + zjsonpatch + + + + com.fasterxml.jackson.*, + io.fabric8.zjsonpatch.internal.collections4 + + + io.fabric8.zjsonpatch*, + + + + + + com.fasterxml.jackson.core + jackson-databind + + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.assertj + assertj-core + test + + + + + + + org.apache.felix + maven-bundle-plugin + + + bundle + package + + bundle + + + + ${project.name} + ${project.groupId}.${project.artifactId} + ${osgi.export} + ${osgi.import} + ${osgi.dynamic.import} + ${osgi.private} + ${osgi.bundles} + ${osgi.activator} + ${osgi.export.service} + + + + + + + + diff --git a/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/CompatibilityFlags.java b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/CompatibilityFlags.java new file mode 100644 index 00000000000..e1ff3ef1b38 --- /dev/null +++ b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/CompatibilityFlags.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed 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 io.fabric8.zjsonpatch; + +import java.util.EnumSet; + +/** + * This class is ported from FlipKart + * zjsonpatch repository + */ +public enum CompatibilityFlags { + MISSING_VALUES_AS_NULLS, + REMOVE_NONE_EXISTING_ARRAY_ELEMENT, + ALLOW_MISSING_TARGET_OBJECT_ON_REPLACE, + FORBID_REMOVE_MISSING_OBJECT; + + public static EnumSet defaults() { + return EnumSet.noneOf(CompatibilityFlags.class); + } +} diff --git a/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/CopyingApplyProcessor.java b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/CopyingApplyProcessor.java new file mode 100644 index 00000000000..45136739a72 --- /dev/null +++ b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/CopyingApplyProcessor.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed 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 io.fabric8.zjsonpatch; + +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.EnumSet; + +/** + * This class is ported from FlipKart + * zjsonpatch repository + */ + +class CopyingApplyProcessor extends InPlaceApplyProcessor { + + CopyingApplyProcessor(JsonNode target, EnumSet flags) { + super(target.deepCopy(), flags); + } +} diff --git a/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/Diff.java b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/Diff.java new file mode 100644 index 00000000000..f7de52dc85f --- /dev/null +++ b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/Diff.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed 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 io.fabric8.zjsonpatch; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * This class is ported from FlipKart + * zjsonpatch repository + */ +class Diff { + private final Operation operation; + private final JsonPointer path; + private final JsonNode value; + private JsonPointer toPath; //only to be used in move operation + private final JsonNode srcValue; // only used in replace operation + + Diff(Operation operation, JsonPointer path, JsonNode value) { + this.operation = operation; + this.path = path; + this.value = value; + this.srcValue = null; + } + + Diff(Operation operation, JsonPointer fromPath, JsonPointer toPath) { + this.operation = operation; + this.path = fromPath; + this.toPath = toPath; + this.value = null; + this.srcValue = null; + } + + Diff(Operation operation, JsonPointer path, JsonNode srcValue, JsonNode value) { + this.operation = operation; + this.path = path; + this.value = value; + this.srcValue = srcValue; + } + + public Operation getOperation() { + return operation; + } + + public JsonPointer getPath() { + return path; + } + + public JsonNode getValue() { + return value; + } + + public static Diff generateDiff(Operation replace, JsonPointer path, JsonNode target) { + return new Diff(replace, path, target); + } + + public static Diff generateDiff(Operation replace, JsonPointer path, JsonNode source, JsonNode target) { + return new Diff(replace, path, source, target); + } + + JsonPointer getToPath() { + return toPath; + } + + public JsonNode getSrcValue() { + return srcValue; + } +} diff --git a/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/DiffFlags.java b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/DiffFlags.java new file mode 100644 index 00000000000..d5603ffcf26 --- /dev/null +++ b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/DiffFlags.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed 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 io.fabric8.zjsonpatch; + +import java.util.EnumSet; + +/** + * This class is ported from FlipKart + * zjsonpatch repository + */ +public enum DiffFlags { + + /** + * This flag omits the value field on remove operations. + * This is a default flag. + */ + OMIT_VALUE_ON_REMOVE, + + /** + * 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. This can be useful for debugging + * output or custom processing of the diffs by downstream systems. + * Please note that this is a non-standard extension to RFC 6902 and will not affect + * how patches produced by this library are processed by this or other libraries. + * + * @since 0.4.1 + */ + ADD_ORIGINAL_VALUE_ON_REPLACE, + + /** + * This flag normalizes a {@link Operation#REPLACE} operation into its respective + * {@link Operation#REMOVE} and {@link Operation#ADD} operations. Although it adds + * a redundant step, this can be useful for auditing systems in which immutability + * is a requirement. + *

+ * For the flag to work, {@link DiffFlags#ADD_ORIGINAL_VALUE_ON_REPLACE} has to be + * enabled as the new instructions in the patch need to grab the old fromValue + * {@code "op": "replace", "fromValue": "F1", "value": "F2" } + * The above instruction will be split into + * {@code "op":"remove", "value":"F1" } and {@code "op":"add", "value":"F2"} respectively. + *

+ * Please note that this is a non-standard extension to RFC 6902 and will not affect + * how patches produced by this library are processed by this or other libraries. + * + * @since 0.4.11 + */ + ADD_EXPLICIT_REMOVE_ADD_ON_REPLACE, + + /** + * This flag instructs the diff generator to emit {@link Operation#TEST} operations + * that validate the state of the source document before each mutation. This can be + * useful if you want to ensure data integrity prior to applying the patch. + * The resulting patches are standard per RFC 6902 and should be processed correctly + * by any compliant library; due to the associated space and performance costs, + * however, this isn't default behavior. + * + * @since 0.4.8 + */ + EMIT_TEST_OPERATIONS; + + public static EnumSet defaults() { + return EnumSet.of(OMIT_VALUE_ON_REMOVE); + } + + public static EnumSet dontNormalizeOpIntoMoveAndCopy() { + return EnumSet.of(OMIT_MOVE_OPERATION, OMIT_COPY_OPERATION); + } +} diff --git a/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/InPlaceApplyProcessor.java b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/InPlaceApplyProcessor.java new file mode 100644 index 00000000000..892581b2b3d --- /dev/null +++ b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/InPlaceApplyProcessor.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed 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 io.fabric8.zjsonpatch; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.EnumSet; + +/** + * This class is ported from FlipKart + * zjsonpatch repository + */ +class InPlaceApplyProcessor implements JsonPatchProcessor { + + private JsonNode target; + private final EnumSet flags; + + InPlaceApplyProcessor(JsonNode target, EnumSet flags) { + this.target = target; + this.flags = flags; + } + + public JsonNode result() { + return target; + } + + @Override + public void move(JsonPointer fromPath, JsonPointer toPath) throws JsonPointerEvaluationException { + JsonNode valueNode = fromPath.evaluate(target); + remove(fromPath); + set(toPath, valueNode, Operation.MOVE); + } + + @Override + public void copy(JsonPointer fromPath, JsonPointer toPath) throws JsonPointerEvaluationException { + JsonNode valueNode = fromPath.evaluate(target); + JsonNode valueToCopy = valueNode != null ? valueNode.deepCopy() : null; + set(toPath, valueToCopy, Operation.COPY); + } + + private static String show(JsonNode value) { + if (value == null || value.isNull()) + return "null"; + else if (value.isArray()) + return "array"; + else if (value.isObject()) + return "object"; + else + return "value " + value.toString(); // Caveat: numeric may differ from source (e.g. trailing zeros) + } + + @Override + public void test(JsonPointer path, JsonNode value) throws JsonPointerEvaluationException { + JsonNode valueNode = path.evaluate(target); + if (!valueNode.equals(value)) + throw new JsonPatchException("Expected " + show(value) + " but found " + show(valueNode), Operation.TEST, path); + } + + @Override + public void add(JsonPointer path, JsonNode value) throws JsonPointerEvaluationException { + set(path, value, Operation.ADD); + } + + @Override + public void replace(JsonPointer path, JsonNode value) throws JsonPointerEvaluationException { + if (path.isRoot()) { + target = value; + return; + } + + JsonNode parentNode = path.getParent().evaluate(target); + JsonPointer.RefToken token = path.last(); + if (parentNode.isObject()) { + if (!flags.contains(CompatibilityFlags.ALLOW_MISSING_TARGET_OBJECT_ON_REPLACE) && + !parentNode.has(token.getField())) + throw new JsonPatchException( + "Missing field \"" + token.getField() + "\"", Operation.REPLACE, path.getParent()); + ((ObjectNode) parentNode).replace(token.getField(), value); + } else if (parentNode.isArray()) { + if (token.getIndex() >= parentNode.size()) + throw new JsonPatchException( + "Array index " + token.getIndex() + " out of bounds", Operation.REPLACE, path.getParent()); + ((ArrayNode) parentNode).set(token.getIndex(), value); + } else { + throw new JsonPatchException( + "Can't reference past scalar value", Operation.REPLACE, path.getParent()); + } + } + + @Override + public void remove(JsonPointer path) throws JsonPointerEvaluationException { + if (path.isRoot()) + throw new JsonPatchException("Cannot remove document root", Operation.REMOVE, path); + + JsonNode parentNode = path.getParent().evaluate(target); + JsonPointer.RefToken token = path.last(); + if (parentNode.isObject()) { + if (flags.contains(CompatibilityFlags.FORBID_REMOVE_MISSING_OBJECT) && !parentNode.has(token.getField())) + throw new JsonPatchException( + "Missing field " + token.getField(), Operation.REMOVE, path.getParent()); + ((ObjectNode) parentNode).remove(token.getField()); + } else if (parentNode.isArray()) { + if (!flags.contains(CompatibilityFlags.REMOVE_NONE_EXISTING_ARRAY_ELEMENT) && + token.getIndex() >= parentNode.size()) + throw new JsonPatchException( + "Array index " + token.getIndex() + " out of bounds", Operation.REMOVE, path.getParent()); + ((ArrayNode) parentNode).remove(token.getIndex()); + } else { + throw new JsonPatchException( + "Cannot reference past scalar value", Operation.REMOVE, path.getParent()); + } + } + + private void set(JsonPointer path, JsonNode value, Operation forOp) throws JsonPointerEvaluationException { + if (path.isRoot()) + target = value; + else { + JsonNode parentNode = path.getParent().evaluate(target); + if (!parentNode.isContainerNode()) + throw new JsonPatchException("Cannot reference past scalar value", forOp, path.getParent()); + else if (parentNode.isArray()) + addToArray(path, value, parentNode); + else + addToObject(path, parentNode, value); + } + } + + private void addToObject(JsonPointer path, JsonNode node, JsonNode value) { + final ObjectNode target = (ObjectNode) node; + String key = path.last().getField(); + target.set(key, value); + } + + private void addToArray(JsonPointer path, JsonNode value, JsonNode parentNode) { + final ArrayNode target = (ArrayNode) parentNode; + int idx = path.last().getIndex(); + + if (idx == JsonPointer.LAST_INDEX) { + // see http://tools.ietf.org/html/rfc6902#section-4.1 + target.add(value); + } else { + if (idx > target.size()) + throw new JsonPatchException( + "Array index " + idx + " out of bounds", Operation.ADD, path.getParent()); + target.insert(idx, value); + } + } +} diff --git a/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonDiff.java b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonDiff.java new file mode 100644 index 00000000000..a4a5bad541c --- /dev/null +++ b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonDiff.java @@ -0,0 +1,498 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed 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 io.fabric8.zjsonpatch; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.fabric8.zjsonpatch.internal.collections4.ListUtils; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * This class is ported from FlipKart + * zjsonpatch repository + */ +public class JsonDiff { + private final List diffs = new ArrayList<>(); + private final EnumSet flags; + 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 JsonDiff(EnumSet flags) { + this.flags = flags.clone(); + } + + public static JsonNode asJson(final JsonNode source, final JsonNode target) { + return asJson(source, target, DiffFlags.defaults()); + } + + public static JsonNode asJson(final JsonNode source, final JsonNode target, EnumSet flags) { + JsonDiff diff = new JsonDiff(flags); + if (source == null && target != null) { + // return add node at root pointing to the target + diff.diffs.add(Diff.generateDiff(Operation.ADD, JsonPointer.ROOT, target)); + } + if (source != null && target == null) { + // return remove node at root pointing to the source + diff.diffs.add(Diff.generateDiff(Operation.REMOVE, JsonPointer.ROOT, source)); + } + if (source != null && target != null) { + diff.generateDiffs(JsonPointer.ROOT, source, target); + + if (!flags.contains(DiffFlags.OMIT_MOVE_OPERATION)) + // Merging remove & add to move operation + diff.introduceMoveOperation(); + + if (!flags.contains(DiffFlags.OMIT_COPY_OPERATION)) + // Introduce copy operation + diff.introduceCopyOperation(source, target); + + if (flags.contains(DiffFlags.ADD_EXPLICIT_REMOVE_ADD_ON_REPLACE)) + // Split replace into remove and add instructions + diff.introduceExplicitRemoveAndAddOperation(); + } + return diff.getJsonNodes(); + } + + private static JsonPointer getMatchingValuePath(Map unchangedValues, JsonNode value) { + return unchangedValues.get(value); + } + + private void introduceCopyOperation(JsonNode source, JsonNode target) { + Map unchangedValues = getUnchangedPart(source, target); + + for (int i = 0; i < diffs.size(); i++) { + Diff diff = diffs.get(i); + if (Operation.ADD != diff.getOperation()) + continue; + + JsonPointer matchingValuePath = getMatchingValuePath(unchangedValues, diff.getValue()); + if (matchingValuePath != null && isAllowed(matchingValuePath, diff.getPath())) { + // Matching value found; replace add with copy + if (flags.contains(DiffFlags.EMIT_TEST_OPERATIONS)) { + // Prepend test node + diffs.add(i, new Diff(Operation.TEST, matchingValuePath, diff.getValue())); + i++; + } + diffs.set(i, new Diff(Operation.COPY, matchingValuePath, diff.getPath())); + } + } + } + + private static boolean isNumber(String str) { + int size = str.length(); + + for (int i = 0; i < size; i++) { + if (!Character.isDigit(str.charAt(i))) { + return false; + } + } + + return size > 0; + } + + // TODO this is quite unclear and needs some serious documentation + private static boolean isAllowed(JsonPointer source, JsonPointer destination) { + boolean isSame = source.equals(destination); + int i = 0; + int j = 0; + // Hack to fix broken COPY operation, need better handling here + while (i < source.size() && j < destination.size()) { + JsonPointer.RefToken srcValue = source.get(i); + JsonPointer.RefToken dstValue = destination.get(j); + String srcStr = srcValue.toString(); + String dstStr = dstValue.toString(); + if (isNumber(srcStr) && isNumber(dstStr)) { + + if (srcStr.compareTo(dstStr) > 0) { + return false; + } + } + i++; + j++; + + } + return !isSame; + } + + private static Map getUnchangedPart(JsonNode source, JsonNode target) { + Map unchangedValues = new HashMap<>(); + computeUnchangedValues(unchangedValues, JsonPointer.ROOT, source, target); + return unchangedValues; + } + + private static void computeUnchangedValues(Map unchangedValues, JsonPointer path, JsonNode source, + JsonNode target) { + if (source.equals(target)) { + if (!unchangedValues.containsKey(target)) { + unchangedValues.put(target, path); + } + return; + } + + final NodeType firstType = NodeType.getNodeType(source); + final NodeType secondType = NodeType.getNodeType(target); + + if (firstType == secondType) { + switch (firstType) { + case OBJECT: + computeObject(unchangedValues, path, source, target); + break; + case ARRAY: + computeArray(unchangedValues, path, source, target); + break; + default: + /* nothing */ + } + } + } + + private static void computeArray(Map unchangedValues, JsonPointer path, JsonNode source, + JsonNode target) { + final int size = Math.min(source.size(), target.size()); + + for (int i = 0; i < size; i++) { + JsonPointer currPath = path.append(i); + computeUnchangedValues(unchangedValues, currPath, source.get(i), target.get(i)); + } + } + + private static void computeObject(Map unchangedValues, JsonPointer path, JsonNode source, + JsonNode target) { + final Iterator firstFields = source.fieldNames(); + while (firstFields.hasNext()) { + String name = firstFields.next(); + if (target.has(name)) { + JsonPointer currPath = path.append(name); + computeUnchangedValues(unchangedValues, currPath, source.get(name), target.get(name)); + } + } + } + + /** + * This method merge 2 diffs ( remove then add, or vice versa ) with same value into one Move operation, + * all the core logic resides here only + */ + private void introduceMoveOperation() { + for (int i = 0; i < diffs.size(); i++) { + Diff diff1 = diffs.get(i); + + // if not remove OR add, move to next diff + if (!(Operation.REMOVE == diff1.getOperation() || + Operation.ADD == diff1.getOperation())) { + continue; + } + + for (int j = i + 1; j < diffs.size(); j++) { + Diff diff2 = diffs.get(j); + if (!diff1.getValue().equals(diff2.getValue())) { + continue; + } + + Diff moveDiff = null; + if (Operation.REMOVE == diff1.getOperation() && + Operation.ADD == diff2.getOperation()) { + JsonPointer relativePath = computeRelativePath(diff2.getPath(), i + 1, j - 1, diffs); + moveDiff = new Diff(Operation.MOVE, diff1.getPath(), relativePath); + + } else if (Operation.ADD == diff1.getOperation() && + Operation.REMOVE == diff2.getOperation()) { + JsonPointer relativePath = computeRelativePath(diff2.getPath(), i, j - 1, diffs); // diff1's add should also be considered + moveDiff = new Diff(Operation.MOVE, relativePath, diff1.getPath()); + } + if (moveDiff != null) { + diffs.remove(j); + diffs.set(i, moveDiff); + break; + } + } + } + } + + /** + * This method splits a {@link Operation#REPLACE} operation within a diff into a {@link Operation#REMOVE} + * and {@link Operation#ADD} in order, respectively. + * Does nothing if {@link Operation#REPLACE} op does not contain a from value + */ + private void introduceExplicitRemoveAndAddOperation() { + List updatedDiffs = new ArrayList(); + for (Diff diff : diffs) { + if (!diff.getOperation().equals(Operation.REPLACE) || diff.getSrcValue() == null) { + updatedDiffs.add(diff); + continue; + } + //Split into two #REMOVE and #ADD + updatedDiffs.add(new Diff(Operation.REMOVE, diff.getPath(), diff.getSrcValue())); + updatedDiffs.add(new Diff(Operation.ADD, diff.getPath(), diff.getValue())); + } + diffs.clear(); + diffs.addAll(updatedDiffs); + } + + //Note : only to be used for arrays + //Finds the longest common Ancestor ending at Array + private static JsonPointer computeRelativePath(JsonPointer path, int startIdx, int endIdx, List diffs) { + List counters = new ArrayList<>(path.size()); + for (int i = 0; i < path.size(); i++) { + counters.add(0); + } + + for (int i = startIdx; i <= endIdx; i++) { + Diff diff = diffs.get(i); + //Adjust relative path according to #ADD and #Remove + if (Operation.ADD == diff.getOperation() || Operation.REMOVE == diff.getOperation()) { + updatePath(path, diff, counters); + } + } + return updatePathWithCounters(counters, path); + } + + private static JsonPointer updatePathWithCounters(List counters, JsonPointer path) { + List tokens = path.decompose(); + for (int i = 0; i < counters.size(); i++) { + int value = counters.get(i); + if (value != 0) { + int currValue = tokens.get(i).getIndex(); + tokens.set(i, new JsonPointer.RefToken(Integer.toString(currValue + value))); + } + } + return new JsonPointer(tokens); + } + + private static void updatePath(JsonPointer path, Diff pseudo, List counters) { + //find longest common prefix of both the paths + + if (pseudo.getPath().size() <= path.size()) { + int idx = -1; + for (int i = 0; i < pseudo.getPath().size() - 1; i++) { + if (pseudo.getPath().get(i).equals(path.get(i))) { + idx = i; + } else { + break; + } + } + if (idx == pseudo.getPath().size() - 2) { + if (pseudo.getPath().get(pseudo.getPath().size() - 1).isArrayIndex()) { + updateCounters(pseudo, pseudo.getPath().size() - 1, counters); + } + } + } + } + + private static void updateCounters(Diff pseudo, int idx, List counters) { + if (Operation.ADD == pseudo.getOperation()) { + counters.set(idx, counters.get(idx) - 1); + } else { + if (Operation.REMOVE == pseudo.getOperation()) { + counters.set(idx, counters.get(idx) + 1); + } + } + } + + private ArrayNode getJsonNodes() { + JsonNodeFactory FACTORY = JsonNodeFactory.instance; + final ArrayNode patch = FACTORY.arrayNode(); + for (Diff diff : diffs) { + ObjectNode jsonNode = getJsonNode(FACTORY, diff, flags); + patch.add(jsonNode); + } + return patch; + } + + private static ObjectNode getJsonNode(JsonNodeFactory FACTORY, Diff diff, EnumSet flags) { + ObjectNode jsonNode = FACTORY.objectNode(); + jsonNode.put(OP, diff.getOperation().rfcName()); + + switch (diff.getOperation()) { + case MOVE: + case COPY: + jsonNode.put(FROM, diff.getPath().toString()); // required {from} only in case of Move Operation + jsonNode.put(PATH, diff.getToPath().toString()); // destination Path + break; + + case REMOVE: + jsonNode.put(PATH, diff.getPath().toString()); + if (!flags.contains(DiffFlags.OMIT_VALUE_ON_REMOVE)) + jsonNode.set(VALUE, diff.getValue()); + break; + + case REPLACE: + if (flags.contains(DiffFlags.ADD_ORIGINAL_VALUE_ON_REPLACE)) { + jsonNode.set(FROM_VALUE, diff.getSrcValue()); + } + case ADD: + case TEST: + jsonNode.put(PATH, diff.getPath().toString()); + jsonNode.set(VALUE, diff.getValue()); + break; + + default: + // Safety net + throw new IllegalArgumentException("Unknown operation specified:" + diff.getOperation()); + } + + return jsonNode; + } + + private void generateDiffs(JsonPointer path, JsonNode source, JsonNode target) { + if (!source.equals(target)) { + final NodeType sourceType = NodeType.getNodeType(source); + final NodeType targetType = NodeType.getNodeType(target); + + if (sourceType == NodeType.ARRAY && targetType == NodeType.ARRAY) { + //both are arrays + compareArray(path, source, target); + } else if (sourceType == NodeType.OBJECT && targetType == NodeType.OBJECT) { + //both are json + compareObjects(path, source, target); + } else { + //can be replaced + if (flags.contains(DiffFlags.EMIT_TEST_OPERATIONS)) + diffs.add(new Diff(Operation.TEST, path, source)); + diffs.add(Diff.generateDiff(Operation.REPLACE, path, source, target)); + } + } + } + + private void compareArray(JsonPointer path, JsonNode source, JsonNode target) { + List lcs = getLCS(source, target); + int srcIdx = 0; + int targetIdx = 0; + int lcsIdx = 0; + int srcSize = source.size(); + int targetSize = target.size(); + int lcsSize = lcs.size(); + + int pos = 0; + while (lcsIdx < lcsSize) { + JsonNode lcsNode = lcs.get(lcsIdx); + JsonNode srcNode = source.get(srcIdx); + JsonNode targetNode = target.get(targetIdx); + + if (lcsNode.equals(srcNode) && lcsNode.equals(targetNode)) { // Both are same as lcs node, nothing to do here + srcIdx++; + targetIdx++; + lcsIdx++; + pos++; + } else { + if (lcsNode.equals(srcNode)) { // src node is same as lcs, but not targetNode + //addition + JsonPointer currPath = path.append(pos); + diffs.add(Diff.generateDiff(Operation.ADD, currPath, targetNode)); + pos++; + targetIdx++; + } else if (lcsNode.equals(targetNode)) { //targetNode node is same as lcs, but not src + //removal, + JsonPointer currPath = path.append(pos); + if (flags.contains(DiffFlags.EMIT_TEST_OPERATIONS)) + diffs.add(new Diff(Operation.TEST, currPath, srcNode)); + diffs.add(Diff.generateDiff(Operation.REMOVE, currPath, srcNode)); + srcIdx++; + } else { + JsonPointer currPath = path.append(pos); + //both are unequal to lcs node + generateDiffs(currPath, srcNode, targetNode); + srcIdx++; + targetIdx++; + pos++; + } + } + } + + while ((srcIdx < srcSize) && (targetIdx < targetSize)) { + JsonNode srcNode = source.get(srcIdx); + JsonNode targetNode = target.get(targetIdx); + JsonPointer currPath = path.append(pos); + generateDiffs(currPath, srcNode, targetNode); + srcIdx++; + targetIdx++; + pos++; + } + pos = addRemaining(path, target, pos, targetIdx, targetSize); + removeRemaining(path, pos, srcIdx, srcSize, source); + } + + private void removeRemaining(JsonPointer path, int pos, int srcIdx, int srcSize, JsonNode source) { + while (srcIdx < srcSize) { + JsonPointer currPath = path.append(pos); + if (flags.contains(DiffFlags.EMIT_TEST_OPERATIONS)) + diffs.add(new Diff(Operation.TEST, currPath, source.get(srcIdx))); + diffs.add(Diff.generateDiff(Operation.REMOVE, currPath, source.get(srcIdx))); + srcIdx++; + } + } + + private int addRemaining(JsonPointer path, JsonNode target, int pos, int targetIdx, int targetSize) { + while (targetIdx < targetSize) { + JsonNode jsonNode = target.get(targetIdx); + JsonPointer currPath = path.append(pos); + diffs.add(Diff.generateDiff(Operation.ADD, currPath, jsonNode.deepCopy())); + pos++; + targetIdx++; + } + return pos; + } + + private void compareObjects(JsonPointer path, JsonNode source, JsonNode target) { + Iterator keysFromSrc = source.fieldNames(); + while (keysFromSrc.hasNext()) { + String key = keysFromSrc.next(); + if (!target.has(key)) { + //remove case + JsonPointer currPath = path.append(key); + if (flags.contains(DiffFlags.EMIT_TEST_OPERATIONS)) + diffs.add(new Diff(Operation.TEST, currPath, source.get(key))); + diffs.add(Diff.generateDiff(Operation.REMOVE, currPath, source.get(key))); + continue; + } + JsonPointer currPath = path.append(key); + generateDiffs(currPath, source.get(key), target.get(key)); + } + Iterator keysFromTarget = target.fieldNames(); + while (keysFromTarget.hasNext()) { + String key = keysFromTarget.next(); + if (!source.has(key)) { + //add case + JsonPointer currPath = path.append(key); + diffs.add(Diff.generateDiff(Operation.ADD, currPath, target.get(key))); + } + } + } + + private static List getLCS(final JsonNode first, final JsonNode second) { + return ListUtils.longestCommonSubsequence(toList((ArrayNode) first), toList((ArrayNode) second)); + } + + static List toList(ArrayNode input) { + int size = input.size(); + List toReturn = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + toReturn.add(input.get(i)); + } + return toReturn; + } +} diff --git a/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonPatch.java b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonPatch.java new file mode 100644 index 00000000000..da46ee0bda5 --- /dev/null +++ b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonPatch.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed 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 io.fabric8.zjsonpatch; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.NullNode; + +import java.util.EnumSet; +import java.util.Iterator; + +/** + * This class is ported from FlipKart + * zjsonpatch repository + */ +public class JsonPatch { + + private static final String OP = "op"; + public static final String VALUE = "value"; + private static final String PATH = "path"; + public static final String FROM = "from"; + + private JsonPatch() { + } + + private static JsonNode getPatchStringAttr(JsonNode jsonNode, String attr) { + JsonNode child = getPatchAttr(jsonNode, attr); + + if (!child.isTextual()) { + throw new JsonPatchException("Invalid JSON Patch payload (non-text '" + attr + "' field)"); + } + + return child; + } + + private static JsonNode getPatchAttr(JsonNode jsonNode, String attr) { + JsonNode child = jsonNode.get(attr); + if (child == null) + throw new JsonPatchException("Invalid JSON Patch payload (missing '" + attr + "' field)"); + + return child; + } + + private static JsonNode getPatchAttrWithDefault(JsonNode jsonNode, String attr, JsonNode defaultValue) { + JsonNode child = jsonNode.get(attr); + if (child == null) + return defaultValue; + else + return child; + } + + private static void process(JsonNode patch, JsonPatchProcessor processor, EnumSet flags) { + + if (!patch.isArray()) { + throw new JsonPatchException("Invalid JSON Patch payload (not an array)"); + } + Iterator operations = patch.iterator(); + while (operations.hasNext()) { + JsonNode jsonNode = operations.next(); + if (!jsonNode.isObject()) { + throw new JsonPatchException("Invalid JSON Patch payload (not an object)"); + } + Operation operation = Operation.fromRfcName(getPatchStringAttr(jsonNode, OP).textValue()); + JsonPointer path = JsonPointer.parse(getPatchStringAttr(jsonNode, PATH).textValue()); + + try { + switch (operation) { + case REMOVE: { + processor.remove(path); + break; + } + + case ADD: { + JsonNode value; + if (!flags.contains(CompatibilityFlags.MISSING_VALUES_AS_NULLS)) + value = getPatchAttr(jsonNode, VALUE); + else + value = getPatchAttrWithDefault(jsonNode, VALUE, NullNode.getInstance()); + processor.add(path, value.deepCopy()); + break; + } + + case REPLACE: { + JsonNode value; + if (!flags.contains(CompatibilityFlags.MISSING_VALUES_AS_NULLS)) + value = getPatchAttr(jsonNode, VALUE); + else + value = getPatchAttrWithDefault(jsonNode, VALUE, NullNode.getInstance()); + processor.replace(path, value.deepCopy()); + break; + } + + case MOVE: { + JsonPointer fromPath = JsonPointer.parse(getPatchStringAttr(jsonNode, FROM).textValue()); + processor.move(fromPath, path); + break; + } + + case COPY: { + JsonPointer fromPath = JsonPointer.parse(getPatchStringAttr(jsonNode, FROM).textValue()); + processor.copy(fromPath, path); + break; + } + + case TEST: { + JsonNode value; + if (!flags.contains(CompatibilityFlags.MISSING_VALUES_AS_NULLS)) + value = getPatchAttr(jsonNode, VALUE); + else + value = getPatchAttrWithDefault(jsonNode, VALUE, NullNode.getInstance()); + processor.test(path, value.deepCopy()); + break; + } + } + } catch (JsonPointerEvaluationException e) { + throw new JsonPatchException(e.getMessage(), operation, e.getPath()); + } + } + } + + public static JsonNode apply(JsonNode patch, JsonNode source, EnumSet flags) { + CopyingApplyProcessor processor = new CopyingApplyProcessor(source, flags); + process(patch, processor, flags); + return processor.result(); + } + + public static JsonNode apply(JsonNode patch, JsonNode source) { + return apply(patch, source, CompatibilityFlags.defaults()); + } + +} diff --git a/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonPatchException.java b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonPatchException.java new file mode 100644 index 00000000000..bc81e651910 --- /dev/null +++ b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonPatchException.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed 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 io.fabric8.zjsonpatch; + +public class JsonPatchException extends RuntimeException { + + private final Operation operation; + private final JsonPointer path; + + public JsonPatchException(String message) { + this(message, null, null); + } + + public JsonPatchException(String message, Operation operation, JsonPointer path) { + super(message); + this.operation = operation; + this.path = path; + } + + public Operation getOperation() { + return operation; + } + + public JsonPointer getPath() { + return path; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (operation != null) { + sb.append('[').append(operation).append(" Operation] "); + } + sb.append(getMessage()); + if (path != null) { + sb.append(" at ").append(path.isRoot() ? "root" : path); + } + return sb.toString(); + } +} diff --git a/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonPatchProcessor.java b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonPatchProcessor.java new file mode 100644 index 00000000000..9b6a9ae68cd --- /dev/null +++ b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonPatchProcessor.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed 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 io.fabric8.zjsonpatch; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * This class is ported from FlipKart + * zjsonpatch repository + */ +public interface JsonPatchProcessor { + void remove(JsonPointer path) throws JsonPointerEvaluationException; + + void replace(JsonPointer path, JsonNode value) throws JsonPointerEvaluationException; + + void add(JsonPointer path, JsonNode value) throws JsonPointerEvaluationException; + + void move(JsonPointer fromPath, JsonPointer toPath) throws JsonPointerEvaluationException; + + void copy(JsonPointer fromPath, JsonPointer toPath) throws JsonPointerEvaluationException; + + void test(JsonPointer path, JsonNode value) throws JsonPointerEvaluationException; +} diff --git a/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonPointer.java b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonPointer.java new file mode 100644 index 00000000000..5c183fc7cc7 --- /dev/null +++ b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonPointer.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed 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 io.fabric8.zjsonpatch; + +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This class is ported from FlipKart + * zjsonpatch repository + */ +public class JsonPointer { + private final RefToken[] tokens; + + /** A JSON pointer representing the root node of a JSON document */ + public final static JsonPointer ROOT = new JsonPointer(new RefToken[] {}); + + private JsonPointer(RefToken[] tokens) { + this.tokens = tokens; + } + + /** + * Constructs a new pointer from a list of reference tokens. + * + * @param tokens The list of reference tokens from which to construct the new pointer. This list is not modified. + */ + public JsonPointer(List tokens) { + this.tokens = tokens.toArray(new RefToken[0]); + } + + /** + * Parses a valid string representation of a JSON Pointer. + * + * @param path The string representation to be parsed. + * @return An instance of {@link JsonPointer} conforming to the specified string representation. + * @throws IllegalArgumentException The specified JSON Pointer is invalid. + */ + public static JsonPointer parse(String path) throws IllegalArgumentException { + StringBuilder reftoken = null; + List result = new ArrayList<>(); + + for (int i = 0; i < path.length(); ++i) { + char c = path.charAt(i); + + // Require leading slash + if (i == 0) { + if (c != '/') + throw new IllegalArgumentException("Missing leading slash"); + reftoken = new StringBuilder(); + continue; + } + + switch (c) { + // Escape sequences + case '~': + switch (path.charAt(++i)) { + case '0': + reftoken.append('~'); + break; + case '1': + reftoken.append('/'); + break; + default: + throw new IllegalArgumentException("Invalid escape sequence ~" + path.charAt(i) + " at index " + i); + } + break; + + // New reftoken + case '/': + result.add(new RefToken(reftoken.toString())); + reftoken.setLength(0); + break; + + default: + reftoken.append(c); + break; + } + } + + if (reftoken == null) + return ROOT; + + result.add(RefToken.parse(reftoken.toString())); + return new JsonPointer(result); + } + + /** + * Indicates whether or not this instance points to the root of a JSON document. + * + * @return {@code true} if this pointer represents the root node, {@code false} otherwise. + */ + public boolean isRoot() { + return tokens.length == 0; + } + + /** + * Creates a new JSON pointer to the specified field of the object referenced by this instance. + * + * @param field The desired field name, or any valid JSON Pointer reference token + * @return The new {@link JsonPointer} instance. + */ + JsonPointer append(String field) { + RefToken[] newTokens = Arrays.copyOf(tokens, tokens.length + 1); + newTokens[tokens.length] = new RefToken(field); + return new JsonPointer(newTokens); + } + + /** + * Creates a new JSON pointer to an indexed element of the array referenced by this instance. + * + * @param index The desired index, or {@link #LAST_INDEX} to point past the end of the array. + * @return The new {@link JsonPointer} instance. + */ + JsonPointer append(int index) { + return append(Integer.toString(index)); + } + + /** Returns the number of reference tokens comprising this instance. */ + int size() { + return tokens.length; + } + + /** + * Returns a string representation of this instance + * + * @return + * An RFC 6901 compliant string + * representation of this JSON pointer. + */ + public String toString() { + StringBuilder sb = new StringBuilder(); + for (RefToken token : tokens) { + sb.append('/'); + sb.append(token); + } + return sb.toString(); + } + + /** + * Decomposes this JSON pointer into its reference tokens. + * + * @return A list of {@link RefToken}s. Modifications to this list do not affect this instance. + */ + public List decompose() { + return Arrays.asList(tokens.clone()); + } + + /** + * Retrieves the reference token at the specified index. + * + * @param index The desired reference token index. + * @return The specified instance of {@link RefToken}. + * @throws IndexOutOfBoundsException The specified index is illegal. + */ + public RefToken get(int index) throws IndexOutOfBoundsException { + if (index < 0 || index >= tokens.length) + throw new IndexOutOfBoundsException("Illegal index: " + index); + return tokens[index]; + } + + /** + * Retrieves the last reference token for this JSON pointer. + * + * @return The last {@link RefToken} comprising this instance. + * @throws IllegalStateException Last cannot be called on {@link #ROOT root} pointers. + */ + public RefToken last() { + if (isRoot()) + throw new IllegalStateException("Root pointers contain no reference tokens"); + return tokens[tokens.length - 1]; + } + + /** + * Creates a JSON pointer to the parent of the node represented by this instance. + * + * The parent of the {@link #ROOT root} pointer is the root pointer itself. + * + * @return A {@link JsonPointer} to the parent node. + */ + public JsonPointer getParent() { + return isRoot() ? this : new JsonPointer(Arrays.copyOf(tokens, tokens.length - 1)); + } + + private void error(int atToken, String message, JsonNode document) throws JsonPointerEvaluationException { + throw new JsonPointerEvaluationException( + message, + new JsonPointer(Arrays.copyOf(tokens, atToken)), + document); + } + + /** + * Takes a target document and resolves the node represented by this instance. + * + * The evaluation semantics are described in + * RFC 6901 sectino 4. + * + * @param document The target document against which to evaluate the JSON pointer. + * @return The {@link JsonNode} resolved by evaluating this JSON pointer. + * @throws JsonPointerEvaluationException The pointer could not be evaluated. + */ + public JsonNode evaluate(final JsonNode document) throws JsonPointerEvaluationException { + JsonNode current = document; + + for (int idx = 0; idx < tokens.length; ++idx) { + final RefToken token = tokens[idx]; + + if (current.isArray()) { + if (!token.isArrayIndex()) + error(idx, "Can't reference field \"" + token.getField() + "\" on array", document); + if (token.getIndex() == LAST_INDEX || token.getIndex() >= current.size()) + error(idx, "Array index " + token.toString() + " is out of bounds", document); + current = current.get(token.getIndex()); + } else if (current.isObject()) { + if (!current.has(token.getField())) + error(idx, "Missing field \"" + token.getField() + "\"", document); + current = current.get(token.getField()); + } else + error(idx, "Can't reference past scalar value", document); + } + + return current; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + JsonPointer that = (JsonPointer) o; + + // Probably incorrect - comparing Object[] arrays with Arrays.equals + return Arrays.equals(tokens, that.tokens); + } + + @Override + public int hashCode() { + return Arrays.hashCode(tokens); + } + + /** Represents a single JSON Pointer reference token. */ + static class RefToken { + private String decodedToken; + transient private Integer index = null; + + public RefToken(String decodedToken) { + if (decodedToken == null) + throw new IllegalArgumentException("Token can't be null"); + this.decodedToken = decodedToken; + } + + private static final Pattern DECODED_TILDA_PATTERN = Pattern.compile("~0"); + private static final Pattern DECODED_SLASH_PATTERN = Pattern.compile("~1"); + + private static String decodePath(Object object) { + String path = object.toString(); // see http://tools.ietf.org/html/rfc6901#section-4 + path = DECODED_SLASH_PATTERN.matcher(path).replaceAll("/"); + return DECODED_TILDA_PATTERN.matcher(path).replaceAll("~"); + } + + private static final Pattern ENCODED_TILDA_PATTERN = Pattern.compile("~"); + private static final Pattern ENCODED_SLASH_PATTERN = Pattern.compile("/"); + + 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 final Pattern VALID_ARRAY_IND = Pattern.compile("-|0|(?:[1-9][0-9]*)"); + + public static RefToken parse(String rawToken) { + if (rawToken == null) + throw new IllegalArgumentException("Token can't be null"); + return new RefToken(decodePath(rawToken)); + } + + public boolean isArrayIndex() { + if (index != null) + return true; + Matcher matcher = VALID_ARRAY_IND.matcher(decodedToken); + if (matcher.matches()) { + index = matcher.group().equals("-") ? LAST_INDEX : Integer.parseInt(matcher.group()); + return true; + } + return false; + } + + public int getIndex() { + if (!isArrayIndex()) + throw new IllegalStateException("Object operation on array target"); + return index; + } + + public String getField() { + return decodedToken; + } + + @Override + public String toString() { + return encodePath(decodedToken); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + RefToken refToken = (RefToken) o; + + return decodedToken.equals(refToken.decodedToken); + } + + @Override + public int hashCode() { + return decodedToken.hashCode(); + } + } + + /** + * Represents an array index pointing past the end of the array. + * + * Such an index is represented by the JSON pointer reference token "{@code -}"; see + * RFC 6901 section 4 for + * more details. + */ + final static int LAST_INDEX = Integer.MIN_VALUE; +} diff --git a/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonPointerEvaluationException.java b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonPointerEvaluationException.java new file mode 100644 index 00000000000..0dfcc6abd0d --- /dev/null +++ b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonPointerEvaluationException.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed 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 io.fabric8.zjsonpatch; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * This class is ported from FlipKart + * zjsonpatch repository + */ +public class JsonPointerEvaluationException extends Exception { + private final JsonPointer path; + private final JsonNode target; + + public JsonPointerEvaluationException(String message, JsonPointer path, JsonNode target) { + super(message); + this.path = path; + this.target = target; + } + + public JsonPointer getPath() { + return path; + } + + public JsonNode getTarget() { + return target; + } +} diff --git a/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/NodeType.java b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/NodeType.java new file mode 100644 index 00000000000..edae9ec42ad --- /dev/null +++ b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/NodeType.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed 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 io.fabric8.zjsonpatch; + +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.EnumMap; +import java.util.Map; + +/** + * This class is ported from FlipKart + * zjsonpatch repository + */ +enum NodeType { + /** + * Array nodes + */ + ARRAY("array"), + /** + * Boolean nodes + */ + BOOLEAN("boolean"), + /** + * Integer nodes + */ + INTEGER("integer"), + /** + * Number nodes (ie, decimal numbers) + */ + NULL("null"), + /** + * Object nodes + */ + NUMBER("number"), + /** + * Null nodes + */ + OBJECT("object"), + /** + * String nodes + */ + STRING("string"); + + /** + * The name for this type, as encountered in a JSON schema + */ + private final String name; + + private static final Map TOKEN_MAP = new EnumMap<>(JsonToken.class); + + static { + TOKEN_MAP.put(JsonToken.START_ARRAY, ARRAY); + TOKEN_MAP.put(JsonToken.VALUE_TRUE, BOOLEAN); + TOKEN_MAP.put(JsonToken.VALUE_FALSE, BOOLEAN); + TOKEN_MAP.put(JsonToken.VALUE_NUMBER_INT, INTEGER); + TOKEN_MAP.put(JsonToken.VALUE_NUMBER_FLOAT, NUMBER); + TOKEN_MAP.put(JsonToken.VALUE_NULL, NULL); + TOKEN_MAP.put(JsonToken.START_OBJECT, OBJECT); + TOKEN_MAP.put(JsonToken.VALUE_STRING, STRING); + + } + + NodeType(final String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + + public static NodeType getNodeType(final JsonNode node) { + final JsonToken token = node.asToken(); + final NodeType ret = TOKEN_MAP.get(token); + if (ret == null) + throw new NullPointerException("unhandled token type " + token); + return ret; + } +} diff --git a/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/Operation.java b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/Operation.java new file mode 100644 index 00000000000..7b6ed373a67 --- /dev/null +++ b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/Operation.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed 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 io.fabric8.zjsonpatch; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * This class is ported from FlipKart + * zjsonpatch repository + */ +public enum Operation { + ADD("add"), + REMOVE("remove"), + REPLACE("replace"), + MOVE("move"), + COPY("copy"), + TEST("test"); + + 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 final String rfcName; + + Operation(String rfcName) { + this.rfcName = rfcName; + } + + public static Operation fromRfcName(String rfcName) { + if (rfcName == null) { + throw new JsonPatchException("rfcName cannot be null"); + } + Operation op = OPS.get(rfcName.toLowerCase()); + if (op == null) { + throw new JsonPatchException("unknown / unsupported operation " + rfcName); + } + return op; + } + + public String rfcName() { + return this.rfcName; + } + +} diff --git a/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/internal/collections4/ListUtils.java b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/internal/collections4/ListUtils.java new file mode 100644 index 00000000000..a712d60d5e7 --- /dev/null +++ b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/internal/collections4/ListUtils.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed 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 io.fabric8.zjsonpatch.internal.collections4; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class ListUtils { + private ListUtils() { + } + + public static List longestCommonSubsequence(final List list1, final List list2) { + Objects.requireNonNull(list1, "listA"); + Objects.requireNonNull(list2, "listB"); + int[][] dp = new int[list1.size() + 1][list2.size() + 1]; + + for (int list1Index = 1; list1Index <= list1.size(); list1Index++) { + for (int list2Index = 1; list2Index <= list2.size(); list2Index++) { + if (list1.get(list1Index - 1).equals(list2.get(list2Index - 1))) { + dp[list1Index][list2Index] = dp[list1Index - 1][list2Index - 1] + 1; + } else { + dp[list1Index][list2Index] = Math.max(dp[list1Index - 1][list2Index], dp[list1Index][list2Index - 1]); + } + } + } + + List lcs = new ArrayList<>(); + int list1Index = list1.size(); + int list2Index = list2.size(); + while (list1Index > 0 && list2Index > 0) { + if (list1.get(list1Index - 1).equals(list2.get(list2Index - 1))) { + lcs.add(list1.get(list1Index - 1)); + list1Index--; + list2Index--; + } else if (dp[list1Index - 1][list2Index] >= dp[list1Index][list2Index - 1]) { + list1Index--; + } else { + list2Index--; + } + } + + java.util.Collections.reverse(lcs); + return lcs; + } +} diff --git a/zjsonpatch/src/test/java/io/fabric8/zjsonpatch/JsonDiffTest.java b/zjsonpatch/src/test/java/io/fabric8/zjsonpatch/JsonDiffTest.java new file mode 100644 index 00000000000..ebb197755e8 --- /dev/null +++ b/zjsonpatch/src/test/java/io/fabric8/zjsonpatch/JsonDiffTest.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed 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 io.fabric8.zjsonpatch; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class JsonDiffTest { + + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + } + + @Test + @DisplayName("asJson should use ported ListUtils.longestCommonSubsequence in absence of Apache Commons Collections") + void asJson_inAbsenceOfCommonsCollectionsDependency_shouldNotThrowError() { + // Given + Map m1 = Collections.singletonMap("key1", "value1"); + Map m2 = new HashMap<>(); + m2.put("key1", "value1"); + m2.put("key2", "value2"); + ArrayNode node1 = objectMapper.createArrayNode(); + node1.add(objectMapper.convertValue(m1, JsonNode.class)); + ArrayNode node2 = objectMapper.createArrayNode(); + node2.add(objectMapper.convertValue(m2, JsonNode.class)); + + // When + JsonNode jsonNode = JsonDiff.asJson(node1, node2); + + // Then + assertThat(jsonNode) + .satisfies(j -> assertThat(j.isArray()).isTrue()) + .satisfies(n -> assertThat(n.get(0).get("op").asText()).isEqualTo("add")) + .satisfies(n -> assertThat(n.get(0).get("path").asText()).isEqualTo("/0/key2")) + .satisfies(n -> assertThat(n.get(0).get("value").asText()).isEqualTo("value2")) + .isNotNull(); + } + + @Nested + /* + * This class is ported from FlipKart + * zjsonpatch repository + */ + class ZjsondiffTests { + + private ArrayNode jsonNode; + + @BeforeEach + public void setUp() throws IOException { + jsonNode = (ArrayNode) objectMapper.readTree(JsonDiffTest.class.getResourceAsStream("/json-diff.json")); + } + + @Test + public void testSampleJsonDiff() { + for (int i = 0; i < jsonNode.size(); i++) { + JsonNode first = jsonNode.get(i).get("first"); + JsonNode second = jsonNode.get(i).get("second"); + JsonNode actualPatch = JsonDiff.asJson(first, second); + JsonNode secondPrime = JsonPatch.apply(actualPatch, first); + assertEquals(second, secondPrime, "JSON Patch not symmetrical [index=" + i + ", first=" + first + "]"); + } + } + + @Test + public void testRenderedRemoveOperationOmitsValueByDefault() { + ObjectNode source = objectMapper.createObjectNode(); + ObjectNode target = objectMapper.createObjectNode(); + source.put("field", "value"); + + JsonNode diff = JsonDiff.asJson(source, target); + + assertEquals(Operation.REMOVE.rfcName(), diff.get(0).get("op").textValue()); + assertEquals("/field", diff.get(0).get("path").textValue()); + assertNull(diff.get(0).get("value")); + } + + @Test + public void testRenderedRemoveOperationRetainsValueIfOmitDiffFlagNotSet() { + ObjectNode source = objectMapper.createObjectNode(); + ObjectNode target = objectMapper.createObjectNode(); + source.put("field", "value"); + + EnumSet flags = DiffFlags.defaults().clone(); + assertTrue(flags.remove(DiffFlags.OMIT_VALUE_ON_REMOVE), "Expected OMIT_VALUE_ON_REMOVE by default"); + JsonNode diff = JsonDiff.asJson(source, target, flags); + + assertEquals(Operation.REMOVE.rfcName(), diff.get(0).get("op").textValue()); + assertEquals("/field", diff.get(0).get("path").textValue()); + assertEquals("value", diff.get(0).get("value").textValue()); + } + + @Test + public void testRenderedOperationsExceptMoveAndCopy() throws Exception { + JsonNode source = objectMapper.readTree("{\"age\": 10}"); + JsonNode target = objectMapper.readTree("{\"height\": 10}"); + + EnumSet flags = DiffFlags.dontNormalizeOpIntoMoveAndCopy().clone(); //only have ADD, REMOVE, REPLACE, Don't normalize operations into MOVE & COPY + + JsonNode diff = JsonDiff.asJson(source, target, flags); + + for (JsonNode d : diff) { + assertNotEquals(Operation.MOVE.rfcName(), d.get("op").textValue()); + assertNotEquals(Operation.COPY.rfcName(), d.get("op").textValue()); + } + + JsonNode targetPrime = JsonPatch.apply(diff, source); + assertEquals(target, targetPrime); + } + + @Test + public void testPath() throws Exception { + JsonNode source = objectMapper.readTree("{\"profiles\":{\"abc\":[],\"def\":[{\"hello\":\"world\"}]}}"); + JsonNode patch = objectMapper.readTree( + "[{\"op\":\"copy\",\"from\":\"/profiles/def/0\", \"path\":\"/profiles/def/0\"},{\"op\":\"replace\",\"path\":\"/profiles/def/0/hello\",\"value\":\"world2\"}]"); + + JsonNode target = JsonPatch.apply(patch, source); + JsonNode expected = objectMapper + .readTree("{\"profiles\":{\"abc\":[],\"def\":[{\"hello\":\"world2\"},{\"hello\":\"world\"}]}}"); + assertEquals(target, expected); + } + + @Test + public void testJsonDiffReturnsEmptyNodeExceptionWhenBothSourceAndTargetNodeIsNull() { + JsonNode diff = JsonDiff.asJson(null, null); + assertEquals(0, diff.size()); + } + + @Test + public void testJsonDiffShowsDiffWhenSourceNodeIsNull() throws JsonProcessingException { + String target = "{ \"K1\": {\"K2\": \"V1\"} }"; + JsonNode diff = JsonDiff.asJson(null, objectMapper.reader().readTree(target)); + assertEquals(1, diff.size()); + + System.out.println(diff); + assertEquals(Operation.ADD.rfcName(), diff.get(0).get("op").textValue()); + assertEquals(JsonPointer.ROOT.toString(), diff.get(0).get("path").textValue()); + assertEquals("V1", diff.get(0).get("value").get("K1").get("K2").textValue()); + } + + @Test + public void testJsonDiffShowsDiffWhenTargetNodeIsNullWithFlags() throws JsonProcessingException { + String source = "{ \"K1\": \"V1\" }"; + JsonNode sourceNode = objectMapper.reader().readTree(source); + JsonNode diff = JsonDiff.asJson(sourceNode, null, EnumSet.of(DiffFlags.ADD_ORIGINAL_VALUE_ON_REPLACE)); + + assertEquals(1, diff.size()); + assertEquals(Operation.REMOVE.rfcName(), diff.get(0).get("op").textValue()); + assertEquals(JsonPointer.ROOT.toString(), diff.get(0).get("path").textValue()); + assertEquals("V1", diff.get(0).get("value").get("K1").textValue()); + } + } +} diff --git a/zjsonpatch/src/test/java/io/fabric8/zjsonpatch/OperationsTest.java b/zjsonpatch/src/test/java/io/fabric8/zjsonpatch/OperationsTest.java new file mode 100644 index 00000000000..061cb682745 --- /dev/null +++ b/zjsonpatch/src/test/java/io/fabric8/zjsonpatch/OperationsTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed 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 io.fabric8.zjsonpatch; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class OperationsTest { + + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + } + + @ParameterizedTest + @ValueSource(strings = { + "/operations-add.json", + "/operations-copy.json", + "/operations-move.json", + "/operations-remove.json", + "/operations-replace.json", + "/operations-test.json" + }) + public void testPatchAppliedCleanly(String operationData) throws Exception { + final ArrayNode jsonNode = (ArrayNode) objectMapper + .readTree(OperationsTest.class.getResourceAsStream(operationData)) + .get("ops"); + for (int i = 0; i < jsonNode.size(); i++) { + JsonNode first = jsonNode.get(i).get("node"); + JsonNode second = jsonNode.get(i).get("expected"); + JsonNode patch = jsonNode.get(i).get("op"); + JsonNode secondPrime = JsonPatch.apply(patch, first); + assertThat(secondPrime).isEqualTo(second); + } + } + + @ParameterizedTest + @ValueSource(strings = { + "/operations-add.json", + "/operations-copy.json", + "/operations-move.json", + "/operations-remove.json", + "/operations-replace.json", + "/operations-test.json" + }) + public void testErrorsAreCorrectlyReported(String operationData) throws Exception { + final ArrayNode errorNode = (ArrayNode) objectMapper + .readTree(OperationsTest.class.getResourceAsStream(operationData)) + .get("errors"); + for (int i = 0; i < errorNode.size(); i++) { + JsonNode first = errorNode.get(i).get("node"); + JsonNode patch = errorNode.get(i).get("op"); + assertThatThrownBy(() -> JsonPatch.apply(patch, first)) + .isInstanceOf(JsonPatchException.class) + .extracting(Objects::toString).asString() + .contains(errorNode.get(i).get("message").asText()); + } + } +} diff --git a/zjsonpatch/src/test/java/io/fabric8/zjsonpatch/RFC6901Tests.java b/zjsonpatch/src/test/java/io/fabric8/zjsonpatch/RFC6901Tests.java new file mode 100644 index 00000000000..72ee849ac6e --- /dev/null +++ b/zjsonpatch/src/test/java/io/fabric8/zjsonpatch/RFC6901Tests.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed 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 io.fabric8.zjsonpatch; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This class is ported from FlipKart + * zjsonpatch repository + */ +class RFC6901Tests { + + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + } + + @Test + void testRFC6901Compliance() throws IOException { + JsonNode data = objectMapper.readTree(RFC6901Tests.class.getResourceAsStream("/rfc6901.json")); + JsonNode testData = data.get("testData"); + + ObjectNode emptyJson = objectMapper.createObjectNode(); + JsonNode patch = JsonDiff.asJson(emptyJson, testData); + JsonNode result = JsonPatch.apply(patch, emptyJson); + assertEquals(testData, result); + } +} diff --git a/zjsonpatch/src/test/java/io/fabric8/zjsonpatch/internal/collections4/ListUtilsTest.java b/zjsonpatch/src/test/java/io/fabric8/zjsonpatch/internal/collections4/ListUtilsTest.java new file mode 100644 index 00000000000..f77c9704278 --- /dev/null +++ b/zjsonpatch/src/test/java/io/fabric8/zjsonpatch/internal/collections4/ListUtilsTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed 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 io.fabric8.zjsonpatch.internal.collections4; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +class ListUtilsTest { + @Test + void longestCommonSubsequence() { + assertThatNullPointerException() + .isThrownBy(() -> ListUtils.longestCommonSubsequence((List) null, null)) + .withMessageContaining("listA"); + assertThatNullPointerException() + .isThrownBy(() -> ListUtils.longestCommonSubsequence(Collections.singletonList('A'), null)) + .withMessageContaining("listB"); + assertThatNullPointerException() + .isThrownBy(() -> ListUtils.longestCommonSubsequence(null, Collections.singletonList('A'))) + .withMessageContaining("listA"); + + assertThat(ListUtils.longestCommonSubsequence(Collections.emptyList(), Collections.emptyList())).isEmpty(); + + final List list1 = Arrays.asList('B', 'A', 'N', 'A', 'N', 'A'); + final List list2 = Arrays.asList('A', 'N', 'A', 'N', 'A', 'S'); + assertThat(ListUtils.longestCommonSubsequence(list1, list2)) + .containsExactly('A', 'N', 'A', 'N', 'A'); + + final List list3 = Arrays.asList('A', 'T', 'A', 'N', 'A'); + assertThat(ListUtils.longestCommonSubsequence(list1, list3)) + .containsExactly('A', 'A', 'N', 'A'); + + assertThat(ListUtils.longestCommonSubsequence(list1, Arrays.asList('Z', 'O', 'R', 'R', 'O'))).isEmpty(); + } +} diff --git a/zjsonpatch/src/test/resources/json-diff.json b/zjsonpatch/src/test/resources/json-diff.json new file mode 100644 index 00000000000..3d8e407fd10 --- /dev/null +++ b/zjsonpatch/src/test/resources/json-diff.json @@ -0,0 +1,1573 @@ +[ + { + "first": { "a": 1 }, + "second": { "b": 2 } + }, + { + "first": { "a": null }, + "second": { "b": 1 } + }, + { + "first": {}, + "second": {} + }, + { + "first": { "a": 0.1 }, + "second": { "b": 0.1 } + }, + { + "first": {}, + "second": { + "a": "b" + } + }, + { + "first": { + "a": "b" + }, + "second": {} + }, + { + "first": { + "a": "b" + }, + "second": { + "a": "c" + } + }, + { + "first": [], + "second": [ + "a" + ] + }, + { + "first": [ + "hello", + "world" + ], + "second": [ + "hello", + "world!" + ] + }, + { + "first": { + "a": "b", + "c": [ + "d" + ] + }, + "second": { + "a": "b", + "c": [ + "d", + "e" + ] + } + }, + { + "first": [ + 1, + 2, + 3, + 4 + ], + "second": [ + 0, + 2, + 3 + ] + }, + { + "first": [ + "a", + { + "b": "c" + }, + { + "d": [ + 1, + 2 + ] + } + ], + "second": [ + "x", + { + "b": 1 + }, + { + "d": [ + 1, + 3, + "" + ] + }, + null + ] + }, + { + "first": { + "b": "a" + }, + "second": { + "c": "a" + } + }, + { + "first": { + "b": [ + 1, + 2 + ] + }, + "second": { + "c": [ + 1, + 2 + ] + } + }, + { + "first": [ + 0, + 1, + 2 + ], + "second": [ + 1, + 2, + 0 + ] + }, + { + "first": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "second": [ + 1, + 3, + 4, + 0, + 5 + ] + }, + { + "first": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "second": [ + 3, + 6, + 4, + 5, + 7 + ] + }, + { + "first": { + "b": [ + 0, + 1, + 2, + 3 + ], + "c": [ + 1 + ] + }, + "second": { + "b": [ + 1, + 3 + ], + "c": [ + 0, + 1 + ] + } + }, + { + "first": { + "b": [ + 0, + 1, + 2, + 3 + ], + "c": [ + 1 + ], + "d": [] + }, + "second": { + "b": [ + 1, + 3 + ], + "c": [ + 2, + 1 + ], + "d": [ + 0 + ] + } + }, + { + "first": { + "a": 0, + "b": [ + 1, + 2 + ] + }, + "second": { + "b": [ + 1, + 2, + 0 + ] + } + }, + { + "first": { + "b": [ + 0, + 1, + 2 + ] + }, + "second": { + "b": [ + 1, + 2 + ], + "c": 0 + } + }, + { + "first": { + "b": [ + 0, + 1, + 3, + 4, + 5 + ] + }, + "second": { + "b": [ + 1, + 2, + 3, + 5 + ], + "c": 0 + } + }, + { + "first": { + "b": [ + 0, + 1, + 3, + 4, + 5 + ] + }, + "second": { + "b": [ + 1, + 2, + 3, + 5 + ], + "c": 0, + "d": 4 + } + }, + { + "first": { + "b": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ] + }, + "second": { + "b": [ + 1, + 6, + 2, + 3, + 5, + 7, + 0, + 8 + ], + "c": 4 + } + }, + { + "first": { + "b": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ] + }, + "second": { + "b": [ + 1, + 3, + 6, + 4, + 5, + 7, + 8 + ], + "c": 2 + } + }, + { + "first": { + "b": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ] + }, + "second": { + "b": [ + 1, + 3, + 6, + 4, + 5, + 7, + 0, + 8 + ], + "c": 2 + } + }, + { + "first": {}, + "second": { + "a": 1, + "b": 1 + }, + "patch": [ + { + "op": "ADD", + "path": "[b]", + "value": 1 + }, + { + "op": "ADD", + "path": "[a]", + "value": 1 + } + ] + }, + { + "first": {}, + "second": { + "a": { + "a": 1 + }, + "b": { + "a": 1 + } + } + }, + { + "first": [], + "second": [ + [ + 0 + ], + [ + 0 + ] + ] + }, + { + "first": [ + "eol" + ], + "second": [ + { + "a": 1 + }, + { + "a": 1 + }, + [], + [], + [ + 0 + ], + [ + 0 + ], + "eol" + ] + }, + { + "first": [ + 1, + 2 + ], + "second": [ + 2, + 1 + ] + }, + { + "first": [ + { + "name": "a" + }, + { + "name": "b" + }, + { + "name": "c" + } + ], + "second": [ + { + "name": "b" + } + ] + }, + { + "first": [ + 1, + 2, + 3, + 4, + 5, + { + "0": "0" + } + ], + "second": [ + 1, + 2, + 4, + 5, + { + "a": "0" + } + ] + }, + { + "first": [ + "a", + "b", + "c", + "d", + "e" + ], + "second": [ + "e", + "a", + "f", + "c", + "d", + "b" + ] + }, + { + "first": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "second": [ + 1, + 3, + 4, + 0, + 5 + ] + }, + + { + "first": {}, + "second": {} + }, + { + "first": { + "foo": 1 + }, + "second": { + "foo": 1 + } + }, + { + "first": { + "foo": 1, + "bar": 2 + }, + "second": { + "bar": 2, + "foo": 1 + } + }, + { + "first": [ + { + "foo": 1, + "bar": 2 + } + ], + "second": [ + { + "bar": 2, + "foo": 1 + } + ] + }, + { + "first": { + "foo": { + "foo": 1, + "bar": 2 + } + }, + "second": { + "foo": { + "bar": 2, + "foo": 1 + } + } + }, + { + "first": { + "foo": null + }, + "second": { + "foo": 1 + } + }, + { + "first": [], + "second": [ + "foo" + ] + }, + { + "first": [ + "foo" + ], + "second": [ + "foo" + ] + }, + { + "first": {}, + "second": { + "foo": "1" + } + }, + { + "first": {}, + "second": { + "foo": 1 + } + }, + { + "first": {}, + "second": { + "": 1 + } + }, + { + "first": { + "foo": 1 + }, + "second": { + "foo": 1, + "bar": [ + 1, + 2 + ] + } + }, + { + "first": { + "foo": 1, + "baz": [ + { + "qux": "hello" + } + ] + }, + "second": { + "foo": 1, + "baz": [ + { + "qux": "hello", + "foo": "world" + } + ] + } + }, + { + "first": { + "foo": 1 + }, + "second": { + "foo": 1, + "bar": true + } + }, + { + "first": { + "foo": 1 + }, + "second": { + "foo": 1, + "bar": false + } + }, + { + "first": { + "foo": 1 + }, + + "second": { + "foo": 1, + "bar": null + } + }, + { + "first": { + "foo": 1 + }, + + "second": { + "0": "bar", + "foo": 1 + } + }, + { + "first": [ + "foo" + ], + + "second": [ + "foo", + "bar" + ] + }, + { + "first": [ + "foo", + "sil" + ], + + "second": [ + "foo", + "bar", + "sil" + ] + }, + { + "first": [ + "foo", + "sil" + ], + + "second": [ + "bar", + "foo", + "sil" + ] + }, + { + "first": [ + "foo", + "sil" + ], + + "second": [ + "foo", + "sil", + "bar" + ] + }, + { + "first": { + "1e0": "foo" + }, + + "second": { + "1e0": "foo" + } + }, + { + "first": [ + "foo", + "sil" + ], + "second": [ + "foo", + [ + "bar", + "baz" + ], + "sil" + ] + }, + { + "first": { + "foo": 1, + "bar": [ + 1, + 2, + 3, + 4 + ] + }, + + "second": { + "foo": 1 + } + }, + { + "first": { + "foo": 1, + "baz": [ + { + "qux": "hello" + } + ] + }, + + "second": { + "foo": 1, + "baz": [ + {} + ] + } + }, + { + "first": { + "foo": 1, + "baz": [ + { + "qux": "hello" + } + ] + }, + + "second": { + "foo": [ + 1, + 2, + 3, + 4 + ], + "baz": [ + { + "qux": "hello" + } + ] + } + }, + { + "first": { + "foo": [ + 1, + 2, + 3, + 4 + ], + "baz": [ + { + "qux": "hello" + } + ] + }, + + "second": { + "foo": [ + 1, + 2, + 3, + 4 + ], + "baz": [ + { + "qux": "world" + } + ] + } + }, + { + "first": [ + "foo" + ], + + "second": [ + "bar" + ] + }, + { + "first": [ + "" + ], + + "second": [ + 0 + ] + }, + { + "first": [ + "" + ], + + "second": [ + true + ] + }, + { + "first": [ + "" + ], + + "second": [ + false + ] + }, + { + "first": [ + "" + ], + + "second": [ + null + ] + }, + { + "first": [ + "foo", + "sil" + ], + + "second": [ + "foo", + [ + "bar", + "baz" + ] + ] + }, + { + "first": { + "foo": "bar" + }, + + "second": { + "baz": "qux" + } + }, + { + "first": { + "foo": 1 + }, + + "second": { + "foo": 1 + } + }, + { + "first": { + "foo": 1 + }, + + "second": { + "foo": 1 + } + }, + { + "first": { + "foo": 1, + "baz": [ + { + "qux": "hello" + } + ] + }, + + "second": { + "baz": [ + { + "qux": "hello" + } + ], + "bar": 1 + } + }, + { + "first": { + "baz": [ + { + "qux": "hello" + } + ], + "bar": 1 + }, + + "second": { + "baz": [ + {}, + "hello" + ], + "bar": 1 + } + }, + { + "first": { + "baz": [ + { + "qux": "hello" + } + ], + "bar": 1 + }, + + "second": { + "baz": [ + { + "qux": "hello" + } + ], + "bar": 1, + "boo": { + "qux": "hello" + } + } + }, + { + "first": { + "foo": "bar" + }, + + "second": { + "baz": "qux" + } + }, + { + "first": [ + 1, + 2 + ], + + "second": [ + 1, + 2, + { + "foo": [ + "bar", + "baz" + ] + } + ] + }, + { + "first": [ + 1, + 2, + [ + 3, + [ + 4, + 5 + ] + ] + ], + + "second": [ + 1, + 2, + [ + 3, + [ + 4, + 5, + { + "foo": [ + "bar", + "baz" + ] + } + ] + ] + ] + }, + { + "first": [ + 1, + 2, + 3, + 4 + ], + + "second": [ + 2, + 3, + 4 + ] + }, + { + "first": [ + 1, + 2, + 3, + 4 + ], + + "second": [ + 1, + 3 + ] + }, + { + "first":{"a":{"b/c/d":"i m here"}}, + "second" :{"a":{"b/c/d":"i m not here"}} + }, + { + "first": {"b":[0,1,2,3]}, + "second": {"b":[1,3],"c":0} + }, + { + "first": { + "c_i": [ + { + "c_n_i": [ + { + "id": 1, + "nm": "Books_Tree" + }, + { + "id": 419, + "nm": "Children" + }, + { + "id": 420, + "nm": "General" + } + ] + }, + { + "c_n_i": [ + { + "id": 1, + "nm": "Books_Tree" + }, + { + "id": 4390, + "nm": "Teens" + }, + { + "id": 5796, + "nm": "Fantasy" + } + ] + }, + { + "c_n_i": [ + { + "id": 1, + "nm": "Books_Tree" + }, + { + "id": 4390, + "nm": "Teens" + }, + { + "id": 5800, + "nm": "Horror And Ghost Stories" + } + ] + }, + { + "c_n_i": [ + { + "id": 1, + "nm": "Books_Tree" + }, + { + "id": 419, + "nm": "Children" + }, + { + "id": 420, + "nm": "General" + } + ] + }, + { + "c_n_i": [ + { + "id": 1, + "nm": "Books_Tree" + }, + { + "id": 4390, + "nm": "Teens" + }, + { + "id": 5796, + "nm": "Fantasy" + } + ] + }, + { + "c_n_i": [ + { + "id": 1, + "nm": "Books_Tree" + }, + { + "id": 4390, + "nm": "Teens" + }, + { + "id": 5800, + "nm": "Horror And Ghost Stories" + } + ] + }, + { + "c_n_i": [ + { + "id": 1, + "nm": "Books_Tree" + }, + { + "id": 419, + "nm": "Children" + }, + { + "id": 7991, + "nm": "Children Literature" + }, + { + "id": 7993, + "nm": "Family" + } + ] + }, + { + "c_n_i": [ + { + "id": 1, + "nm": "Books_Tree" + }, + { + "id": 4390, + "nm": "Teens" + }, + { + "id": 5796, + "nm": "Fantasy" + } + ] + }, + { + "c_n_i": [ + { + "id": 1, + "nm": "Books_Tree" + }, + { + "id": 4390, + "nm": "Teens" + }, + { + "id": 5800, + "nm": "Horror And Ghost Stories" + } + ] + }, + { + "c_n_i": [ + { + "id": 1, + "nm": "Books_Tree" + }, + { + "id": 419, + "nm": "Children" + }, + { + "id": 7991, + "nm": "Children Literature" + }, + { + "id": 7993, + "nm": "Family" + } + ] + }, + { + "c_n_i": [ + { + "id": 1, + "nm": "Books_Tree" + }, + { + "id": 4390, + "nm": "Teens" + }, + { + "id": 5796, + "nm": "Fantasy" + } + ] + }, + { + "c_n_i": [ + { + "id": 1, + "nm": "Books_Tree" + }, + { + "id": 4390, + "nm": "Teens" + }, + { + "id": 5800, + "nm": "Horror And Ghost Stories" + } + ] + }, + { + "c_n_i": [ + { + "id": 1, + "nm": "Books_Tree" + }, + { + "id": 419, + "nm": "Children" + }, + { + "id": 7991, + "nm": "Children Literature" + }, + { + "id": 7993, + "nm": "Family" + } + ] + } + ] + }, + "second": { + "c_i": [ + { + "c_n_i": [ + { + "nm": "Books_Tree", + "id": 1 + }, + { + "nm": "Teens", + "id": 4390 + }, + { + "nm": "Fantasy", + "id": 5796 + } + ] + }, + { + "c_n_i": [ + { + "nm": "Books_Tree", + "id": 1 + }, + { + "nm": "Teens", + "id": 4390 + }, + { + "nm": "Horror And Ghost Stories", + "id": 5800 + } + ] + }, + { + "c_n_i": [ + { + "nm": "Books_Tree", + "id": 1 + }, + { + "nm": "Children", + "id": 419 + }, + { + "nm": "Children Literature", + "id": 7991 + }, + { + "nm": "Family", + "id": 7993 + } + ] + }, + { + "c_n_i": [ + { + "nm": "Books_Tree", + "id": 1 + }, + { + "nm": "Teens", + "id": 4390 + }, + { + "nm": "Fantasy", + "id": 5796 + } + ] + }, + { + "c_n_i": [ + { + "nm": "Books_Tree", + "id": 1 + }, + { + "nm": "Teens", + "id": 4390 + }, + { + "nm": "Horror And Ghost Stories", + "id": 5800 + } + ] + }, + { + "c_n_i": [ + { + "nm": "Books_Tree", + "id": 1 + }, + { + "nm": "Children", + "id": 419 + }, + { + "nm": "Children Literature", + "id": 7991 + }, + { + "nm": "Family", + "id": 7993 + } + ] + }, + { + "c_n_i": [ + { + "nm": "Books_Tree", + "id": 1 + }, + { + "nm": "Children", + "id": 419 + }, + { + "nm": "General", + "id": 420 + } + ] + }, + { + "c_n_i": [ + { + "nm": "Books_Tree", + "id": 1 + }, + { + "nm": "Teens", + "id": 4390 + }, + { + "nm": "Fantasy", + "id": 5796 + } + ] + }, + { + "c_n_i": [ + { + "nm": "Books_Tree", + "id": 1 + }, + { + "nm": "Teens", + "id": 4390 + }, + { + "nm": "Horror And Ghost Stories", + "id": 5800 + } + ] + }, + { + "c_n_i": [ + { + "nm": "Books_Tree", + "id": 1 + }, + { + "nm": "Children", + "id": 419 + }, + { + "nm": "General", + "id": 420 + } + ] + }, + { + "c_n_i": [ + { + "nm": "Books_Tree", + "id": 1 + }, + { + "nm": "Teens", + "id": 4390 + }, + { + "nm": "Fantasy", + "id": 5796 + } + ] + }, + { + "c_n_i": [ + { + "nm": "Books_Tree", + "id": 1 + }, + { + "nm": "Teens", + "id": 4390 + }, + { + "nm": "Horror And Ghost Stories", + "id": 5800 + } + ] + }, + { + "c_n_i": [ + { + "nm": "Books_Tree", + "id": 1 + }, + { + "nm": "Children", + "id": 419 + }, + { + "nm": "Children Literature", + "id": 7991 + }, + { + "nm": "Family", + "id": 7993 + } + ] + } + ] + } + }, + { + "first": {"b":[{"b":[2,3,4,5]},{"c":1},{"d":1},{"e":1},{"f":1}]}, + "second": {"b":[{"b":1},{"c":1},{"d":1},{"e":1},{"f":[1,2,3,4,5]}]} + }, + { + "first": {"a":[{"b":[{"c":[{"k1":"v1"},{"k2":"v2"},{"k3":"v3"},{"k4":"v4"},{"k5":"v5"}]}]}]}, + "second": {"a":[{"b":[{"c":[{"k1":"v1"},{"k3":"v3"},{"k5":"v5"},{"k2":"v2"}]}]}]} + }, + { + "first":[{"name":"winters","country":["aus","nz","sl","rsa","wi","eng"]},{"name":"winters","country":["india","aus","nz","sl"]},{"name":"autumn","country":["aus","nz","sl","rsa","wi"]},{"name":"winters","country":["aus","nz","sl","rsa","wi","eng"]},{"name":"summers","country":["nz","sl","rsa","wi","eng"]},{"name":"autumn","country":["aus","nz","sl","rsa","wi","eng"]},{"name":"rainy","country":["nz","sl"]}], + "second":[{"name":"winters","country":["india","aus","nz","sl","rsa"]},{"name":"summers","country":["nz","sl","rsa","wi","eng"]},{"name":"autumn","country":["nz","sl","rsa","wi"]},{"name":"summers","country":["india","aus","nz","sl","rsa","wi","eng"]},{"name":"autumn","country":["aus","nz","sl","rsa","wi","eng"]},{"name":"winters","country":["india","aus","nz","sl","rsa","wi","eng"]},{"name":"spring","country":["india","aus","nz","sl"]},{"name":"summers","country":["sl"]},{"name":"autumn","country":["sl","rsa","wi","eng"]}] + }, + { + "first":[], + "second":[] + }, + { + "first":[{"age":8,"country":["sl","rsa","wi","eng"]},{"age":9,"country":["aus","nz","sl"]},{"age":9,"country":["india","aus","nz","sl"]},{"age":8,"country":["sl"]},{"age":2,"country":["nz","sl","rsa","wi"]},{"age":7,"country":["nz","sl","rsa"]}], + "second":[{"age":10,"country":["sl"]},{"age":6,"country":["india","aus","nz","sl","rsa"]},{"age":1,"country":["sl","rsa","wi","eng"]},{"age":8,"country":["sl","rsa","wi"]},{"age":9,"country":["sl","rsa","wi"]},{"age":9,"country":["india","aus","nz","sl"]},{"age":5,"country":["aus","nz","sl","rsa","wi","eng"]},{"age":4,"country":["sl","rsa","wi","eng"]},{"age":9,"country":["india","aus","nz","sl"]}] + + }, + { + "first":[{"friends":"male","age":2,"name":"summers","gender":"male","country":["nz","sl","rsa","wi","eng"]},{"friends":"male","age":5,"name":"spring","gender":"male","country":["india","aus","nz","sl","rsa","wi","eng"]},{"friends":"male","age":9,"name":"spring","gender":"male","country":["india","aus","nz","sl","rsa"]},{"friends":"male","age":10,"name":"summers","gender":"female","country":["nz","sl","rsa","wi","eng"]}], + + "second":[{"friends":"male","age":8,"name":"spring","gender":"female","country":["aus","nz","sl"]},{"friends":"female","age":6,"name":"summers","gender":"male","country":["india","aus","nz","sl","rsa"]},{"friends":"male","age":10,"name":"summers","gender":"female","country":["nz","sl","rsa","wi","eng"]},{"friends":"female","age":5,"name":"spring","gender":"female","country":["nz","sl"]},{"friends":"female","age":1,"name":"summers","gender":"male","country":["aus","nz","sl"]},{"friends":"male","age":2,"name":"summers","gender":"male","country":["nz","sl","rsa","wi","eng"]},{"friends":"female","age":3,"name":"spring","gender":"female","country":["india","aus","nz","sl","rsa"]},{"friends":"female","age":1,"name":"summers","gender":"female","country":["nz","sl","rsa","wi"]},{"friends":"male","age":6,"name":"rainy","gender":"male","country":["aus","nz","sl","rsa","wi","eng"]}] + }, + { + "first":{"compare":{"":"a"},"tags":{}}, + "second":{"compare":{"":"b"},"tags":{"a":"b"}} + }, + { + "first": {"@type":"SimpleCollection","id":"17aead29-2097-436d-b9d2-d95e0de423db","notes":"sapien minim mandamus fugit postulant nominavi solet numquam","description":"qui splendide porttitor simul maiestatis fabellas viverra omnesque","version":7,"collectionValue":[{"@type":"SimpleReference","id":"a08f2ab0-cf27-440b-b9f5-71b1021aa206","notes":"intellegebat doctus signiferumque dis dicam appetere fringilla esse","description":"sapientem massa legimus nunc ultricies sed eirmod","version":0,"referenceValue":{"@type":"SimpleB","id":"33b89e6e-3cd2-4f49-b053-b654f6f8df5f","notes":"ignota adhuc convenire splendide vivendo","description":"nostra efficitur morbi sit fusce tacimates eum vitae","version":0,"intValue":899213098,"type":"SIMPLE_B"},"type":"SIMPLE_REFERENCE"},{"@type":"SimpleReference","id":"a08f2ab0-cf27-440b-b9f5-71b1021aa206","notes":"intellegebat doctus signiferumque dis dicam appetere fringilla esse","description":"sapientem massa legimus nunc ultricies sed eirmod","version":0,"referenceValue":{"@type":"SimpleB","id":"33b89e6e-3cd2-4f49-b053-b654f6f8df5f","notes":"ignota adhuc convenire splendide vivendo","description":"nostra efficitur morbi sit fusce tacimates eum vitae","version":0,"intValue":899213098,"type":"SIMPLE_B"},"type":"SIMPLE_REFERENCE"},{"@type":"SimpleD","id":"41ef6628-6ff6-4be4-b8ab-f836f30e8f58","notes":"adolescens mea phasellus facilisis unum","description":"inceptos petentium etiam efficiantur wisi venenatis","version":0,"booleanValue":false,"type":"SIMPLE_D"},{"@type":"SimpleB","id":"12a771f8-2d9d-4060-b02c-2772862154ff","notes":"hendrerit civibus sagittis congue inceptos ante facilis honestatis","description":"antiopam reprimique putent urbanitas ne volumus","version":2,"intValue":-1035459272,"type":"SIMPLE_B"}],"type":"SIMPLE_COLLECTION"} + , + "second": {"@type":"SimpleCollection","id":"17aead29-2097-436d-b9d2-d95e0de423db","notes":"sapien minim mandamus fugit postulant nominavi solet numquam","description":"qui splendide porttitor simul maiestatis fabellas viverra omnesque","version":10,"collectionValue":[{"@type":"SimpleReference","id":"f4b10497-ecb9-4e6a-aa66-bd4c874da6f0","notes":"cras habitant liber verterem neque litora eruditi vehicula","description":"te comprehensam mutat latine deterruisset quis sadipscing non verear","version":0,"referenceValue":{"@type":"SimpleD","id":"d4754e2d-edfc-4263-add2-4a17082f6b50","notes":"ceteros condimentum rhoncus mei salutatus volutpat delectus tation mollis","description":"ante ea errem mnesarchum civibus","version":0,"booleanValue":true,"type":"SIMPLE_D"},"type":"SIMPLE_REFERENCE"},{"@type":"SimpleReference","id":"a08f2ab0-cf27-440b-b9f5-71b1021aa206","notes":"intellegebat doctus signiferumque dis dicam appetere fringilla esse","description":"sapientem massa legimus nunc ultricies sed eirmod","version":0,"referenceValue":{"@type":"SimpleB","id":"33b89e6e-3cd2-4f49-b053-b654f6f8df5f","notes":"ignota adhuc convenire splendide vivendo","description":"nostra efficitur morbi sit fusce tacimates eum vitae","version":0,"intValue":899213098,"type":"SIMPLE_B"},"type":"SIMPLE_REFERENCE"},{"@type":"SimpleReference","id":"a08f2ab0-cf27-440b-b9f5-71b1021aa206","notes":"intellegebat doctus signiferumque dis dicam appetere fringilla esse","description":"sapientem massa legimus nunc ultricies sed eirmod","version":0,"referenceValue":{"@type":"SimpleB","id":"33b89e6e-3cd2-4f49-b053-b654f6f8df5f","notes":"ignota adhuc convenire splendide vivendo","description":"nostra efficitur morbi sit fusce tacimates eum vitae","version":0,"intValue":899213098,"type":"SIMPLE_B"},"type":"SIMPLE_REFERENCE"},{"@type":"SimpleB","id":"12a771f8-2d9d-4060-b02c-2772862154ff","notes":"morbi fermentum inani tritani malorum ultrices","description":"antiopam reprimique putent urbanitas ne volumus","version":3,"intValue":-1035459272,"type":"SIMPLE_B"}],"type":"SIMPLE_COLLECTION"} + + }, + { + "first":{"map":{"3000000000": {"field":3100000000,"otherField": 0}}}, + "second":{"map":{"3000000000": {"field":3100000000,"otherField": 0, "extraField" : 0}}} + } +] diff --git a/zjsonpatch/src/test/resources/operations-add.json b/zjsonpatch/src/test/resources/operations-add.json new file mode 100644 index 00000000000..39a24da7c42 --- /dev/null +++ b/zjsonpatch/src/test/resources/operations-add.json @@ -0,0 +1,75 @@ +{ + "errors": [ + { + "op": [{ "op": "add", "path": "/a" }], + "node": {}, + "type": "JsonPatchApplicationException", + "message": "Invalid JSON Patch payload (missing 'value' field)" + } + ], + "ops": [ + { + "op": [{ "op": "add", "path": "/a", "value": "b" }], + "node": {}, + "expected": { "a": "b" } + }, + { + "op": [{ "op": "add", "path": "/a", "value": 1 }], + "node": { "a": "b" }, + "expected": { "a": 1 } + }, + { + "op": [{ "op": "add", "path": "/array/-", "value": 1 }], + "node": { "array": [ 2, null, {}, 1 ] }, + "expected": { "array": [ 2, null, {}, 1, 1 ] } + }, + { + "op": [{ "op": "add", "path": "/array/2", "value": "hello" }], + "node": { "array": [ 2, null, {}, 1] }, + "expected": { "array": [ 2, null, "hello", {}, 1 ] } + }, + { + "op": [{ "op": "add", "path": "/obj/inner/b", "value": [ 1, 2 ] }], + "node": { + "obj": { + "inner": { + "a": "hello" + } + } + }, + "expected": { + "obj": { + "inner": { + "a": "hello", + "b": [ 1, 2 ] + } + } + } + }, + { + "op": [{ "op": "add", "path": "/obj/inner/b", "value": [ 1, 2 ] }], + "node": { + "obj": { + "inner": { + "a": "hello", + "b": "world" + } + } + }, + "expected": { + "obj": { + "inner": { + "a": "hello", + "b": [ 1, 2 ] + } + } + } + }, + { + "message": "support of path with /", + "op": [{ "op": "add", "path": "/b~1c~1d/3", "value": 4 }], + "node": { "b/c/d": [1, 2, 3] }, + "expected": { "b/c/d": [1, 2, 3, 4] } + } + ] +} diff --git a/zjsonpatch/src/test/resources/operations-copy.json b/zjsonpatch/src/test/resources/operations-copy.json new file mode 100644 index 00000000000..6ca294490e3 --- /dev/null +++ b/zjsonpatch/src/test/resources/operations-copy.json @@ -0,0 +1,31 @@ +{ + "errors": [ + { + "op": [{ "op": "copy", "from": "/a", "path": "/b/c" }], + "node": { "a": 1 }, + "message": "Missing field \"b\" at root" + } + ], + "ops": [ + { + "op": [{ "op": "copy", "from": "/a", "path": "/b" }], + "node": { "a": 1 }, + "expected": { "a": 1, "b": 1 } + }, + { + "op": [{ "op": "copy", "from": "/a", "path": "/b" }], + "node": { "a": 1, "b": false }, + "expected": { "a": 1, "b": 1 } + }, + { + "op": [{ "op": "copy", "from": "/0", "path": "/-" }], + "node": [ 1, 2, 3, 4 ], + "expected": [ 1, 2, 3, 4, 1 ] + }, + { + "op": [{ "op": "copy", "from": "/0", "path": "/0" }], + "node": [ true ], + "expected": [ true, true ] + } + ] +} diff --git a/zjsonpatch/src/test/resources/operations-move.json b/zjsonpatch/src/test/resources/operations-move.json new file mode 100644 index 00000000000..03f43f18259 --- /dev/null +++ b/zjsonpatch/src/test/resources/operations-move.json @@ -0,0 +1,54 @@ +{ + + "errors": [ + { + "op": [{ "op": "move", "from": "/a", "path": "/a/b" }], + "node": {}, + "message": "Missing field \"a\" at root" + }, + { + "op": [{ "op": "move", "from": "/a", "path": "/b/c" }], + "node": { "a": "b" }, + "message": "Missing field \"b\" at root" + }, + { + "op": [{ "op": "move", "path": "/b/c" }], + "node": { "a": "b" }, + "type": "InvalidJsonPatchException", + "message": "Invalid JSON Patch payload (missing 'from' field" + }, + { + "issue": 39, + "op": [{ "op": "move", "from": "/1/key", "path": "/0/key" }], + "node": [{ "key": "0" }], + "message": "Array index 1 is out of bounds at root" + } + ], + "ops": [ + { + "op": [{ "op": "move", "from": "/x/a", "path": "/x/b" }], + "node": { "x": { "a": "helo" } }, + "expected": { "x": { "b": "helo" } } + }, + { + "op": [{ "op": "move", "from": "/x/a", "path": "/x/a" }], + "node": { "x": { "a": "helo" } }, + "expected": { "x": { "a": "helo" } } + }, + { + "op": [{ "op": "move", "from": "/0", "path": "/0/x" }], + "node": [ "victim", {}, {} ], + "expected": [ { "x": "victim" }, {} ] + }, + { + "op": [{ "op": "move", "from": "/0", "path": "/-" }], + "node": [ 0, 1, 2 ], + "expected": [ 1, 2, 0 ] + }, + { + "op": [{ "op": "move", "from": "/a", "path": "/b/2" }], + "node": { "a": "helo", "b": [ 1, 2, 3, 4 ] }, + "expected": { "b": [ 1, 2, "helo", 3, 4 ] } + } + ] +} diff --git a/zjsonpatch/src/test/resources/operations-remove.json b/zjsonpatch/src/test/resources/operations-remove.json new file mode 100644 index 00000000000..baa2114a8d8 --- /dev/null +++ b/zjsonpatch/src/test/resources/operations-remove.json @@ -0,0 +1,31 @@ +{ + "errors": [ + { + "op": [{ "op": "remove", "path": "/x/y" }], + "node": { "x": "just a string" }, + "message": "Cannot reference past scalar value at /x" + }, + { + "op": [{ "op": "remove", "path": "/x/1" }], + "node": { "x": [ "single" ] }, + "message": "Array index 1 out of bounds at /x" + } + ], + "ops": [ + { + "op": [{ "op": "remove", "path": "/x/y" }], + "node": { "x": { "a": "b", "y": {} } }, + "expected": { "x": { "a": "b" } } + }, + { + "op": [{ "op": "remove", "path": "/0/2" }], + "node": [ [ "a", "b", "c"], "d", "e" ], + "expected": [ [ "a", "b" ], "d", "e" ] + }, + { + "op": [{ "op": "remove", "path": "/x/0" }], + "node": { "x": [ "y", "z" ], "foo": "bar" }, + "expected": { "x": [ "z" ], "foo": "bar" } + } + ] +} diff --git a/zjsonpatch/src/test/resources/operations-replace.json b/zjsonpatch/src/test/resources/operations-replace.json new file mode 100644 index 00000000000..f3e214efbe4 --- /dev/null +++ b/zjsonpatch/src/test/resources/operations-replace.json @@ -0,0 +1,43 @@ +{ + "errors": [ + { + "op": [{ "op": "replace", "path": "/a" }], + "node": { "a": 0 }, + "type": "InvalidJsonPatchException", + "message": "missing 'value' field" + }, + { + "op": [{ "op": "replace", "path": "/x/y", "value": false }], + "node": { "x": "a" }, + "message": "Can't reference past scalar value at /x" + }, + { + "op": [{ "op": "replace", "path": "/non-existing-path", "value": "some-value"}], + "node": { }, + "type": "JsonPatchApplicationException", + "message": "Missing field \"non-existing-path\" at root" + } + ], + "ops": [ + { + "op": [{ "op": "replace", "path": "", "value": false }], + "node": { "x": { "a": "b", "y": {} } }, + "expected": false + }, + { + "op": [{ "op": "replace", "path": "/x/y", "value": "hello" }], + "node": { "x": { "a": "b", "y": {} } }, + "expected": { "x": { "a": "b", "y": "hello" } } + }, + { + "op": [{ "op": "replace", "path": "/0/2", "value": "x" }], + "node": [ [ "a", "b", "c"], "d", "e" ], + "expected": [ [ "a", "b", "x" ], "d", "e" ] + }, + { + "op": [{ "op": "replace", "path": "/x/0", "value": null }], + "node": { "x": [ "y", "z" ], "foo": "bar" }, + "expected": { "x": [ null, "z" ], "foo": "bar" } + } + ] +} diff --git a/zjsonpatch/src/test/resources/operations-test.json b/zjsonpatch/src/test/resources/operations-test.json new file mode 100644 index 00000000000..66d825b1267 --- /dev/null +++ b/zjsonpatch/src/test/resources/operations-test.json @@ -0,0 +1,46 @@ +{ + "errors": [ + { + "op": [{ "op": "test", "path": "/x", "value": {} }], + "node": { "key": "value" }, + "message": "Missing field \"x\" at root" + }, + { + "op": [{ "op": "test", "path": "/x", "value": {} }], + "node": [ 1, 2 ], + "message": "Can't reference field \"x\" on array at root" + }, + { + "op": [{ "op": "test", "path": "", "value": true }], + "node": [ 1, 2 ], + "message": "Expected value true but found array at root" + }, + { + "op": [{ "op": "test", "path": "/x", "value": -30.000 }], + "node": { "x": -29.020 }, + "message": "Expected value -30.0 but found value -29.02 at /x" + }, + { + "op": [{ "op": "test", "path": "/x", "value": null }], + "node": { "x": 3 }, + "message": "Expected null but found value 3 at /x" + } + ], + "ops": [ + { + "op": [{ "op": "test", "path": "", "value": 1 }], + "node": 1, + "expected": 1 + }, + { + "op": [{ "op": "test", "path": "/a/1", "value": "hello" }], + "node": { "a": [ null, "hello", "world" ] }, + "expected": { "a": [ null, "hello", "world" ] } + }, + { + "op": [{ "op": "test", "path": "", "value": null }], + "node": null, + "expected": null + } + ] +} diff --git a/zjsonpatch/src/test/resources/rfc6901.json b/zjsonpatch/src/test/resources/rfc6901.json new file mode 100644 index 00000000000..03a273f69cd --- /dev/null +++ b/zjsonpatch/src/test/resources/rfc6901.json @@ -0,0 +1,15 @@ +{ + "description": "This test data is provided by the RFC6901 document (https://tools.ietf.org/html/rfc6901#section-5)", + "testData": { + "foo": ["bar", "baz"], + "": 0, + "a/b": 1, + "c%d": 2, + "e^f": 3, + "g|h": 4, + "i\\j": 5, + "k\"l": 6, + " ": 7, + "m~n": 8 + } +}