diff --git a/CHANGELOG.md b/CHANGELOG.md
index ae7b6d290ff..3715ee9a682 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@
* `Config.errorMessages` has been removed. Please use Kubernetes status messages directly.
* Fix #6138: Removed unused `io:fabric8:kubernetes-model` artifact
* Fix #6156: Removed deprecated extension `io:fabric8:service-catalog`
+* Fix #5480: Migrate from `io.fabric8:zjsonpatch` to `com.flipkart.zjsonpatch:zjsonpatch` . In case you're relying on `io.fabric8.zjsonpatch.*` classes, you need to migrate to `com.flipkart.zjsonpatch.*` classes.
### 6.13.3 (2024-08-13)
diff --git a/kubernetes-client/pom.xml b/kubernetes-client/pom.xml
index 278503c4631..e0ac053b0ee 100644
--- a/kubernetes-client/pom.xml
+++ b/kubernetes-client/pom.xml
@@ -74,7 +74,6 @@
io.fabric8
zjsonpatch
- ${zjsonpatch.version}
diff --git a/platforms/karaf/features/src/main/resources/feature.xml b/platforms/karaf/features/src/main/resources/feature.xml
index d340c9fec61..5b84837f65e 100644
--- a/platforms/karaf/features/src/main/resources/feature.xml
+++ b/platforms/karaf/features/src/main/resources/feature.xml
@@ -40,7 +40,7 @@
mvn:org.ow2.asm/asm-tree/${asm.bundle.version}
mvn:org.ow2.asm/asm-util/${asm.bundle.version}
mvn:io.fabric8/kubernetes-model-common/${project.version}
- mvn:io.fabric8/zjsonpatch/${zjsonpatch.version}
+ mvn:io.fabric8/zjsonpatch/${project.version}
mvn:io.fabric8/kubernetes-model-core/${project.version}
mvn:io.fabric8/kubernetes-model-rbac/${project.version}
diff --git a/pom.xml b/pom.xml
index 66809d15e7f..dddfe4d6c7a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -100,7 +100,7 @@
3.9.9
3.15.0
4.5.9
- 0.3.0
+ 0.4.16
3.0.2
@@ -226,6 +226,7 @@
httpclient-vertx
kubernetes-client-deps-compatibility-tests
log4j
+ zjsonpatch
@@ -715,6 +716,11 @@
kube-api-test-client-inject
${project.version}
+
+ io.fabric8
+ zjsonpatch
+ ${project.version}
+
@@ -890,12 +896,19 @@
true
-
- io.fabric8
+ com.flipkart.zjsonpatch
zjsonpatch
${zjsonpatch.version}
+
+
+ org.apache.commons
+ commons-collections4
+
+
+
+
org.mockito
mockito-core
diff --git a/uberjar/pom.xml b/uberjar/pom.xml
index b4c17d32449..9c7944bc9e0 100644
--- a/uberjar/pom.xml
+++ b/uberjar/pom.xml
@@ -199,6 +199,12 @@
openshift-server-mock
${project.version}
+
+
+ io.fabric8
+ zjsonpatch
+ ${project.version}
+
@@ -233,7 +239,7 @@
- io.fabric8
+ com.flipkart.zjsonpatch
zjsonpatch
${zjsonpatch.version}
diff --git a/zjsonpatch/pom.xml b/zjsonpatch/pom.xml
new file mode 100644
index 00000000000..b1bf94c6787
--- /dev/null
+++ b/zjsonpatch/pom.xml
@@ -0,0 +1,97 @@
+
+
+
+ 4.0.0
+
+ io.fabric8
+ kubernetes-client-project
+ 7.0-SNAPSHOT
+
+
+ bundle
+ zjsonpatch
+
+
+
+ *
+
+
+ io.fabric8.zjsonpatch*,
+ com.flipkart.zjsonpatch,
+
+
+
+
+
+ com.flipkart.zjsonpatch
+ zjsonpatch
+ ${zjsonpatch.version}
+
+
+ org.apache.commons
+ commons-collections4
+
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+
+
+ bundle
+ package
+
+ bundle
+
+
+
+ ${project.name}
+ ${project.groupId}.${project.artifactId}
+ ${osgi.export}
+ ${osgi.import}
+ ${osgi.dynamic.import}
+ ${osgi.private}
+ ${osgi.bundles}
+ ${osgi.activator}
+ ${osgi.export.service}
+
+
+
+
+
+
+
+
diff --git a/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/Diff.java b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/Diff.java
new file mode 100644
index 00000000000..1ad32fc099b
--- /dev/null
+++ b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/Diff.java
@@ -0,0 +1,82 @@
+/*
+ * 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.flipkart.zjsonpatch.Operation;
+
+/**
+ * This class is ported from FlipKart
+ * JSONPatch 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/JsonDiff.java b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonDiff.java
new file mode 100644
index 00000000000..e3562ff9549
--- /dev/null
+++ b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonDiff.java
@@ -0,0 +1,500 @@
+/*
+ * 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 com.flipkart.zjsonpatch.DiffFlags;
+import com.flipkart.zjsonpatch.Operation;
+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
+ * JSONPatch 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..110522f3de3
--- /dev/null
+++ b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/JsonPatch.java
@@ -0,0 +1,29 @@
+/*
+ * 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.flipkart.zjsonpatch.CompatibilityFlags;
+import com.flipkart.zjsonpatch.JsonPatchApplicationException;
+
+public class JsonPatch {
+ private JsonPatch() {
+ }
+
+ public static JsonNode apply(JsonNode patch, JsonNode source) throws JsonPatchApplicationException {
+ return com.flipkart.zjsonpatch.JsonPatch.apply(patch, source, CompatibilityFlags.defaults());
+ }
+}
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..5e03593986c
--- /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
+ * JSONPatch 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..0c895ab3ca5
--- /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
+ * JSONPatch 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..ce66eccf6c7
--- /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
+ * JSONPatch 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/internal/collections4/ListUtils.java b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/internal/collections4/ListUtils.java
new file mode 100644
index 00000000000..e141ca58abe
--- /dev/null
+++ b/zjsonpatch/src/main/java/io/fabric8/zjsonpatch/internal/collections4/ListUtils.java
@@ -0,0 +1,64 @@
+/*
+ * 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;
+
+/**
+ * Ported from Apache
+ * Commons Collections
+ */
+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..595eba41dd4
--- /dev/null
+++ b/zjsonpatch/src/test/java/io/fabric8/zjsonpatch/JsonDiffTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+
+class JsonDiffTest {
+ private static final ObjectMapper 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();
+ }
+}
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();
+ }
+}