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
+ }
+}