From 71334795c1fd30cc32aa84de8a0244653fdb8245 Mon Sep 17 00:00:00 2001 From: Mark Fogle Date: Mon, 3 Nov 2025 07:41:01 -0800 Subject: [PATCH] chore: vendor json patch implementation for kotlin 2 The Kotlin client fails to compile under Kotlin 2.2.20 because io.github.reidsync:kotlin-json-patch 1.0.0 was built with older Kotlin metadata and does not publish the compatibility metadata variant that the 2.x compiler expects. When Gradle resolves the metadata classpath the symbols disappear, so users hit unresolved reference errors down in DefaultApplyEvents and StateManager. To unblock consumers we vendor the Apache-licensed sources under com.agui.client.jsonpatch, remove the external dependency, and retarget the call sites. The vendored copy lets us apply Kotlin 2 clean-ups (e.g. lowercase() and identity equality fixes) and guarantees the SDK keeps working while upstream is still on the 1.9 toolchain. If reidsync republishes the library with Kotlin 2.x (or at least adds the new metadata variant), we can drop the vendored folder and re-enable the dependency in libs.versions.toml. Until then this keeps Gradle, Android, and iOS builds/tests green. Tests: ./gradlew build test connectedDebugAndroidTest iosSimulatorArm64Test --- .../kotlin/library/client/build.gradle.kts | 3 - .../agui/client/jsonpatch/ApplyProcessor.kt | 23 + .../client/jsonpatch/CompatibilityFlags.kt | 31 ++ .../com/agui/client/jsonpatch/Constants.kt | 8 + .../kotlin/com/agui/client/jsonpatch/Diff.kt | 50 +++ .../jsonpatch/InvalidJsonPatchException.kt | 29 ++ .../com/agui/client/jsonpatch/JsonDiff.kt | 394 ++++++++++++++++++ .../client/jsonpatch/JsonElementExtensions.kt | 122 ++++++ .../com/agui/client/jsonpatch/JsonPatch.kt | 126 ++++++ .../JsonPatchApplicationException.kt | 29 ++ .../jsonpatch/JsonPatchApplyProcessor.kt | 49 +++ .../jsonpatch/JsonPatchEditingContext.kt | 29 ++ .../jsonpatch/JsonPatchEditingContextImpl.kt | 257 ++++++++++++ .../client/jsonpatch/JsonPatchProcessor.kt | 29 ++ .../com/agui/client/jsonpatch/NodeType.kt | 18 + .../agui/client/jsonpatch/NoopProcessor.kt | 35 ++ .../com/agui/client/jsonpatch/Operations.kt | 43 ++ .../client/jsonpatch/lcs/CommandVisitor.kt | 145 +++++++ .../client/jsonpatch/lcs/DefaultEquator.kt | 95 +++++ .../client/jsonpatch/lcs/DeleteCommand.kt | 53 +++ .../agui/client/jsonpatch/lcs/EditCommand.kt | 77 ++++ .../agui/client/jsonpatch/lcs/EditScript.kt | 119 ++++++ .../com/agui/client/jsonpatch/lcs/Equator.kt | 42 ++ .../client/jsonpatch/lcs/InsertCommand.kt | 55 +++ .../agui/client/jsonpatch/lcs/KeepCommand.kt | 56 +++ .../agui/client/jsonpatch/lcs/ListUtils.kt | 71 ++++ .../jsonpatch/lcs/SequencesComparator.kt | 341 +++++++++++++++ .../agui/client/state/DefaultApplyEvents.kt | 2 +- .../com/agui/client/state/StateManager.kt | 12 +- .../kotlin/library/gradle/libs.versions.toml | 2 - 30 files changed, 2331 insertions(+), 14 deletions(-) create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/ApplyProcessor.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/CompatibilityFlags.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/Constants.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/Diff.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/InvalidJsonPatchException.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonDiff.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonElementExtensions.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatch.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatchApplicationException.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatchApplyProcessor.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatchEditingContext.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatchEditingContextImpl.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatchProcessor.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/NodeType.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/NoopProcessor.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/Operations.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/CommandVisitor.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/DefaultEquator.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/DeleteCommand.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/EditCommand.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/EditScript.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/Equator.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/InsertCommand.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/KeepCommand.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/ListUtils.kt create mode 100644 sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/SequencesComparator.kt diff --git a/sdks/community/kotlin/library/client/build.gradle.kts b/sdks/community/kotlin/library/client/build.gradle.kts index 293059f81..67c0415d5 100644 --- a/sdks/community/kotlin/library/client/build.gradle.kts +++ b/sdks/community/kotlin/library/client/build.gradle.kts @@ -76,9 +76,6 @@ kotlin { implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.datetime) - // Json Patching - implementation(libs.kotlin.json.patch) - // HTTP client dependencies - core only (no engine) implementation(libs.ktor.client.core) implementation(libs.ktor.client.content.negotiation) diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/ApplyProcessor.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/ApplyProcessor.kt new file mode 100644 index 000000000..c8515aa63 --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/ApplyProcessor.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2016 flipkart.com zjsonpatch. + * + * 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 com.agui.client.jsonpatch +import kotlinx.serialization.json.* + +class ApplyProcessor(private val target: JsonElement) : JsonPatchApplyProcessor(target.deepCopy()) { + fun result(): JsonElement = targetSource +} + diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/CompatibilityFlags.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/CompatibilityFlags.kt new file mode 100644 index 000000000..3bcead6d7 --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/CompatibilityFlags.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2016 flipkart.com zjsonpatch. + * + * 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 com.agui.client.jsonpatch + +/** + * Created by tomerga on 04/09/2016. + */ +enum class CompatibilityFlags { + MISSING_VALUES_AS_NULLS; + + + companion object { + fun defaults(): Set { + return setOf(CompatibilityFlags.MISSING_VALUES_AS_NULLS) + } + } +} diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/Constants.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/Constants.kt new file mode 100644 index 000000000..a524802e3 --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/Constants.kt @@ -0,0 +1,8 @@ +package com.agui.client.jsonpatch + +open class Constants { + open val OP = "op" + open val VALUE = "value" + open val PATH = "path" + open val FROM = "from" +} diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/Diff.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/Diff.kt new file mode 100644 index 000000000..fff63abbd --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/Diff.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2016 flipkart.com zjsonpatch. + * + * 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 com.agui.client.jsonpatch + +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlin.jvm.JvmStatic + +internal class Diff { + val operation: Int + val path: MutableList + val value: JsonElement + val toPath: List //only to be used in move operation + + constructor(operation: Int, path: List, value: JsonElement) { + this.operation = operation + this.path = path.toMutableList() + this.toPath= listOf() + this.value = value + } + + constructor(operation: Int, fromPath: List, toPath: List) { + this.operation = operation + this.path = fromPath.toMutableList() + this.toPath = toPath + this.value = JsonNull + } + + companion object { + + @JvmStatic + fun generateDiff(replace: Int, path: List, target: JsonElement): Diff { + return Diff(replace, path, target) + } + } +} diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/InvalidJsonPatchException.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/InvalidJsonPatchException.kt new file mode 100644 index 000000000..f3133851e --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/InvalidJsonPatchException.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2016 flipkart.com zjsonpatch. + * + * 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 com.agui.client.jsonpatch + +/** + * User: holograph + * Date: 03/08/16 + */ +class InvalidJsonPatchException : JsonPatchApplicationException { + constructor(message: String) : super(message) {} + + constructor(message: String, cause: Throwable) : super(message, cause) {} + + constructor(cause: Throwable) : super(cause) {} +} diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonDiff.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonDiff.kt new file mode 100644 index 000000000..e07874b41 --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonDiff.kt @@ -0,0 +1,394 @@ +/* + * Copyright 2016 flipkart.com zjsonpatch. + * + * 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 com.agui.client.jsonpatch + +import com.agui.client.jsonpatch.lcs.ListUtils +import kotlinx.serialization.json.* +import kotlin.jvm.JvmStatic +import kotlin.math.min + +object JsonDiff { + internal var op = Operations() + internal var consts = Constants() + + + @JvmStatic + fun asJson(source: JsonElement, target: JsonElement): JsonArray { + val diffs = ArrayList() + val path = ArrayList() + /* + * generating diffs in the order of their occurrence + */ + generateDiffs(diffs, path, source, target) + /* + * Merging remove & add to move operation + */ + compactDiffs(diffs) + /* + * Introduce copy operation + */ + introduceCopyOperation(source, target, diffs) + + return getJsonNodes(diffs) + } + + private fun getMatchingValuePath(unchangedValues: Map>, value: JsonElement): List? { + return unchangedValues[value] + } + + private fun introduceCopyOperation(source: JsonElement, target: JsonElement, diffs: MutableList) { + val unchangedValues = getUnchangedPart(source, target) + for (i in diffs.indices) { + val diff = diffs[i] + if (op.ADD==diff.operation) { + val matchingValuePath = getMatchingValuePath(unchangedValues, diff.value) + if (matchingValuePath != null) { + diffs[i] = Diff(op.COPY, matchingValuePath, diff.path) + } + } + } + } + + private fun getUnchangedPart(source: JsonElement, target: JsonElement): Map> { + val unchangedValues = HashMap>() + computeUnchangedValues(unchangedValues, listOf(), source, target) + return unchangedValues + } + + private fun computeUnchangedValues(unchangedValues: MutableMap>, path: List, source: JsonElement, target: JsonElement) { + if (source == target) { + unchangedValues.put(target, path) + return + } + + val firstType = NodeType.getNodeType(source) + val secondType = NodeType.getNodeType(target) + + if (firstType == secondType) { + when (firstType) { + NodeType.OBJECT -> computeObject(unchangedValues, path, source.jsonObject, target.jsonObject) + NodeType.ARRAY -> computeArray(unchangedValues, path, source.jsonArray, target.jsonArray) + }/* nothing */ + } + } + + private fun computeArray(unchangedValues: MutableMap>, path: List, source: JsonArray, target: JsonArray) { + val size = min(source.size, target.size) + + for (i in 0..size - 1) { + val currPath = getPath(path, i) + computeUnchangedValues(unchangedValues, currPath, source.get(i), target.get(i)) + } + } + + private fun computeObject(unchangedValues: MutableMap>, path: List, source: JsonObject, target: JsonObject) { + //val firstFields = source.entrySet().iterator() + val firstFields = source.iterator() + while (firstFields.hasNext()) { + val name = firstFields.next().key + if (target.containsKey(name)) { + val currPath = getPath(path, 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 fun compactDiffs(diffs: MutableList) { + var i=-1 + while (++i <=diffs.size-1) { + val diff1 = diffs[i] + + // if not remove OR add, move to next diff + if (!(op.REMOVE==diff1.operation || op.ADD==diff1.operation)) { + continue + } + + for (j in i + 1..diffs.size - 1) { + val diff2 = diffs[j] + if (diff1.value != diff2.value) { + continue + } + + var moveDiff: Diff? = null + if (op.REMOVE==diff1.operation && op.ADD==diff2.operation) { + computeRelativePath(diff2.path, i + 1, j - 1, diffs) + moveDiff = Diff(op.MOVE, diff1.path, diff2.path) + + } else if (op.ADD==diff1.operation && op.REMOVE==diff2.operation) { + computeRelativePath(diff2.path, i, j - 1, diffs) // diff1's add should also be considered + moveDiff = Diff(op.MOVE, diff2.path, diff1.path) + } + if (moveDiff != null) { + diffs.removeAt(j) + diffs[i] = moveDiff + break + } + } + } + } + + //Note : only to be used for arrays + //Finds the longest common Ancestor ending at Array + private fun computeRelativePath(path: MutableList, startIdx: Int, endIdx: Int, diffs: List) { + val counters = ArrayList() + + resetCounters(counters, path.size) + + for (i in startIdx..endIdx) { + val diff = diffs[i] + //Adjust relative path according to #ADD and #Remove + if (op.ADD==diff.operation || op.REMOVE==diff.operation) { + updatePath(path, diff, counters) + } + } + updatePathWithCounters(counters, path) + } + + private fun resetCounters(counters: MutableList, size: Int) { + for (i in 0..size - 1) { + counters.add(0) + } + } + + private fun updatePathWithCounters(counters: List, path: MutableList) { + for (i in counters.indices) { + val value = counters[i] + if (value != 0) { + val currValue = path[i].toString().toInt() + path[i] = (currValue + value).toString() + } + } + } + + private fun updatePath(path: List, pseudo: Diff, counters: MutableList) { + //find longest common prefix of both the paths + + if (pseudo.path.size <= path.size) { + var idx = -1 + for (i in 0..pseudo.path.size - 1 - 1) { + if (pseudo.path[i] == path[i]) { + idx = i + } else { + break + } + } + if (idx == pseudo.path.size - 2) { + if (pseudo.path[pseudo.path.size - 1] is Int) { + updateCounters(pseudo, pseudo.path.size - 1, counters) + } + } + } + } + + private fun updateCounters(pseudo: Diff, idx: Int, counters: MutableList) { + if (op.ADD==pseudo.operation) { + counters[idx] = counters[idx] - 1 + } else { + if (op.REMOVE==pseudo.operation) { + counters[idx] = counters[idx] + 1 + } + } + } + + private fun getJsonNodes(diffs: List): JsonArray { + var patch = JsonArray(emptyList()) + for (diff in diffs) { + val jsonNode = getJsonNode(diff) + patch = patch.add(jsonNode) + } + return patch + } + + private fun getJsonNode(diff: Diff): JsonObject { + var jsonNode = JsonObject(emptyMap()) + jsonNode = jsonNode.addProperty(consts.OP, op.nameFromOp(diff.operation)) + if (op.MOVE==diff.operation || op.COPY==diff.operation) { + jsonNode = jsonNode.addProperty(consts.FROM, getArrayNodeRepresentation(diff.path)) //required {from} only in case of Move Operation + jsonNode = jsonNode.addProperty(consts.PATH, getArrayNodeRepresentation(diff.toPath)) // destination Path + } else { + jsonNode = jsonNode.addProperty(consts.PATH, getArrayNodeRepresentation(diff.path)) + jsonNode = jsonNode.add(consts.VALUE, diff.value) + } + return jsonNode + } + + + private fun EncodePath(`object`: Any): String { + val path = `object`.toString() // see http://tools.ietf.org/html/rfc6901#section-4 + return path.replace("~".toRegex(), "~0").replace("/".toRegex(), "~1") + } + //join path parts in argument 'path', inserting a '/' between joined elements, starting with '/' and transforming the element of the list with ENCODE_PATH_FUNCTION + private fun getArrayNodeRepresentation(path: List): String { + // return Joiner.on('/').appendTo(new StringBuilder().append('/'), + // Iterables.transform(path, ENCODE_PATH_FUNCTION)).toString(); + val sb = StringBuilder() + for (i in path.indices) { + sb.append('/') + sb.append(EncodePath(path[i])) + + } + return sb.toString() + } + + + + private fun generateDiffs(diffs: MutableList, path: List, source: JsonElement, target: JsonElement) { + if (source != target) { + val sourceType = NodeType.getNodeType(source) + val targetType = NodeType.getNodeType(target) + + if (sourceType == NodeType.ARRAY && targetType == NodeType.ARRAY) { + //both are arrays + compareArray(diffs, path, source.jsonArray, target.jsonArray) + } else if (sourceType == NodeType.OBJECT && targetType == NodeType.OBJECT) { + //both are json + compareObjects(diffs, path, source.jsonObject, target.jsonObject) + } else { + //can be replaced + + diffs.add(Diff.generateDiff(op.REPLACE, path, target)) + } + } + } + + private fun compareArray(diffs: MutableList, path: List, source: JsonArray, target: JsonArray) { + val lcs = getLCS(source, target) + var srcIdx = 0 + var targetIdx = 0 + var lcsIdx = 0 + val srcSize = source.size + val targetSize = target.size + val lcsSize = lcs.size + + var pos = 0 + while (lcsIdx < lcsSize) { + val lcsNode = lcs[lcsIdx] + val srcNode = source.get(srcIdx) + val targetNode = target.get(targetIdx) + + + if (lcsNode == srcNode && lcsNode == targetNode) { // Both are same as lcs node, nothing to do here + srcIdx++ + targetIdx++ + lcsIdx++ + pos++ + } else { + if (lcsNode == srcNode) { // src node is same as lcs, but not targetNode + //addition + val currPath = getPath(path, pos) + diffs.add(Diff.generateDiff(op.ADD, currPath, targetNode)) + pos++ + targetIdx++ + } else if (lcsNode == targetNode) { //targetNode node is same as lcs, but not src + //removal, + val currPath = getPath(path, pos) + diffs.add(Diff.generateDiff(op.REMOVE, currPath, srcNode)) + srcIdx++ + } else { + val currPath = getPath(path, pos) + //both are unequal to lcs node + generateDiffs(diffs, currPath, srcNode, targetNode) + srcIdx++ + targetIdx++ + pos++ + } + } + } + + while (srcIdx < srcSize && targetIdx < targetSize) { + val srcNode = source.get(srcIdx) + val targetNode = target.get(targetIdx) + val currPath = getPath(path, pos) + generateDiffs(diffs, currPath, srcNode, targetNode) + srcIdx++ + targetIdx++ + pos++ + } + pos = addRemaining(diffs, path, target, pos, targetIdx, targetSize) + removeRemaining(diffs, path, pos, srcIdx, srcSize, source) + } + + private fun removeRemaining(diffs: MutableList, path: List, pos: Int, srcIdx_: Int, srcSize: Int, source_: JsonElement): Int { + var srcIdx = srcIdx_ + val source = source_.jsonArray + while (srcIdx < srcSize) { + val currPath = getPath(path, pos) + diffs.add(Diff.generateDiff(op.REMOVE, currPath, source.get(srcIdx))) + srcIdx++ + } + return pos + } + + private fun addRemaining(diffs: MutableList, path: List, target_: JsonElement, pos_: Int, targetIdx_: Int, targetSize: Int): Int { + var pos = pos_ + var targetIdx = targetIdx_ + val target = target_.jsonArray + while (targetIdx < targetSize) { + val jsonNode = target.get(targetIdx) + val currPath = getPath(path, pos) + diffs.add(Diff.generateDiff(op.ADD, currPath, jsonNode.deepCopy())) + pos++ + targetIdx++ + } + return pos + } + + private fun compareObjects(diffs: MutableList, path: List, source: JsonObject, target: JsonObject) { + val keysFromSrc = source.iterator() + while (keysFromSrc.hasNext()) { + val key = keysFromSrc.next().key + if (!target.containsKey(key)) { + //remove case + val currPath = getPath(path, key) + diffs.add(Diff.generateDiff(op.REMOVE, currPath, source.get(key)!!)) + continue + } + val currPath = getPath(path, key) + generateDiffs(diffs, currPath, source.get(key)!!, target.get(key)!!) + } + val keysFromTarget = target.iterator() + while (keysFromTarget.hasNext()) { + val key = keysFromTarget.next().key + if (!source.containsKey(key)) { + //add case + val currPath = getPath(path, key) + diffs.add(Diff.generateDiff(op.ADD, currPath, target.get(key)!!)) + } + } + } + + private fun getPath(path: List, key: Any): List { + val toReturn = ArrayList() + toReturn.addAll(path) + toReturn.add(key) + return toReturn + } + + private fun getLCS(first_: JsonElement, second_: JsonElement): List { + if (first_ !is JsonArray) throw IllegalArgumentException("LCS can only work on JSON arrays") + if (second_ !is JsonArray) throw IllegalArgumentException("LCS can only work on JSON arrays") + val first = first_ as JsonArray + val second = second_ as JsonArray + return ListUtils.longestCommonSubsequence(first.toList(),second.toList()) + } +} + + diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonElementExtensions.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonElementExtensions.kt new file mode 100644 index 000000000..472e8f166 --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonElementExtensions.kt @@ -0,0 +1,122 @@ +package com.agui.client.jsonpatch + +import kotlinx.serialization.json.* + +/* + * Copyright 2023 Reid Byun. + * + * 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. +*/ + +/* +* JsonElement Extensions +* */ + +fun JsonElement.apply(patch: JsonElement): JsonElement { + return JsonPatch.apply(patch, this) +} + +fun JsonElement.generatePatch(with: JsonElement): JsonElement { + return JsonDiff.asJson(this, with) +} + +internal fun JsonElement.isContainerNode(): Boolean { + return this is JsonArray || this is JsonObject +} + +internal fun JsonElement.deepCopy(): JsonElement { + return when(this) { + is JsonArray -> this.jsonArray.copy {} + is JsonObject -> this.jsonObject.copy {} + is JsonNull -> JsonNull // An order of checking type between JsonNull and JsonPrimitive makes difference. + is JsonPrimitive -> this /* Todo check */ + } +} + +/* +* JsonArray Extensions +* */ +internal fun JsonArray.add(value_: JsonElement?): JsonArray { + val value=value_ ?: JsonNull + return copy { add(value) } +} + +internal fun JsonArray.insert(index: Int, value_: JsonElement?): JsonArray { + val value=value_ ?: JsonNull + return if(index>=size) { + this.add(value) + } + else if(index<0) { + this.copy { add(0, value)} + } + else { + this.copy { add(index, value) } + } +} + +internal fun JsonArray.set(index: Int, value_: JsonElement?): JsonArray { + val value=value_ ?: JsonNull + if(index>=size) { + throw IndexOutOfBoundsException("") + } + return copy { this[index] = value } +} + +internal fun JsonArray.remove(index:Int): JsonArray { + return copy { removeAt(index) } +} + +private inline fun JsonArray.copy(mutatorBlock: MutableList.() -> Unit): JsonArray { + return JsonArray(this.toMutableList().apply(mutatorBlock)) +} + + +/* +* JsonObject Extensions +* */ +internal fun JsonObject.add(key: String, value_: JsonElement?): JsonObject { + val value=value_ ?: JsonNull + return copy { + this[key] = value + } +} + +internal fun JsonObject.remove(key: String): JsonObject { + return copy { remove(key) } +} + +internal fun JsonObject.set(key: String, value_: JsonElement?): JsonObject { + val value=value_ ?: JsonNull + if(!this.containsKey(key)) { + throw IndexOutOfBoundsException("Key[$key] doesn't exist") + } + return copy { + this[key] = value + } +} + +internal fun JsonObject.addProperty(key: String, value: String): JsonObject { + return this.copy { + this[key] = JsonPrimitive(value) + } +} + +internal fun JsonObject.addProperty(key: String, value: Number): JsonObject { + return this.copy { + this[key] = JsonPrimitive(value) + } +} + +private inline fun JsonObject.copy(mutatorBlock: MutableMap.() -> Unit): JsonObject { + return JsonObject(this.toMutableMap().apply(mutatorBlock)) +} diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatch.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatch.kt new file mode 100644 index 000000000..03b6ae6eb --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatch.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2016 flipkart.com zjsonpatch. + * + * 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 com.agui.client.jsonpatch + +import kotlinx.serialization.json.* +import kotlin.jvm.JvmOverloads +import kotlin.jvm.JvmStatic + +object JsonPatch { + internal var op = Operations() + internal var consts = Constants() + + private fun getPatchAttr(jsonNode: JsonObject, attr: String): JsonElement { + val child = jsonNode.get(attr) ?: throw InvalidJsonPatchException("Invalid JSON Patch payload (missing '$attr' field)") + return child + } + + private fun getPatchAttrWithDefault(jsonNode: JsonObject, attr: String, defaultValue: JsonElement): JsonElement { + val child = jsonNode.get(attr) + if (child == null) + return defaultValue + else + return child + } + + @Throws(InvalidJsonPatchException::class) + private fun process(patch: JsonElement, processor: JsonPatchApplyProcessor, flags: Set) { + + if (patch !is JsonArray) + throw InvalidJsonPatchException("Invalid JSON Patch payload (not an array)") + val operations = patch.jsonArray.iterator() + while (operations.hasNext()) { + val jsonNode_ = operations.next() + if (jsonNode_ !is JsonObject) throw InvalidJsonPatchException("Invalid JSON Patch payload (not an object)") + val jsonNode = jsonNode_.jsonObject + val operation = op.opFromName(getPatchAttr(jsonNode.jsonObject, consts.OP).toString().replace("\"".toRegex(), "")) + val path = getPath(getPatchAttr(jsonNode, consts.PATH)) + + when (operation) { + op.REMOVE -> { + processor.edit { remove(path) } + } + + op.ADD -> { + val value: JsonElement + if (!flags.contains(CompatibilityFlags.MISSING_VALUES_AS_NULLS)) + value = getPatchAttr(jsonNode, consts.VALUE) + else + value = getPatchAttrWithDefault(jsonNode, consts.VALUE, JsonNull) + processor.edit { add(path, value) } + } + + op.REPLACE -> { + val value: JsonElement + if (!flags.contains(CompatibilityFlags.MISSING_VALUES_AS_NULLS)) + value = getPatchAttr(jsonNode, consts.VALUE) + else + value = getPatchAttrWithDefault(jsonNode, consts.VALUE, JsonNull) + processor.edit { replace(path, value) } + } + + op.MOVE -> { + val fromPath = getPath(getPatchAttr(jsonNode, consts.FROM)) + processor.edit { move(fromPath, path) } + } + + op.COPY -> { + val fromPath = getPath(getPatchAttr(jsonNode, consts.FROM)) + processor.edit { copy(fromPath, path) } + } + + op.TEST -> { + val value: JsonElement + if (!flags.contains(CompatibilityFlags.MISSING_VALUES_AS_NULLS)) + value = getPatchAttr(jsonNode, consts.VALUE) + else + value = getPatchAttrWithDefault(jsonNode, consts.VALUE, JsonNull) + processor.edit { test(path, value) } + } + } + } + } + + @Throws(InvalidJsonPatchException::class) + @JvmStatic + @JvmOverloads + fun validate(patch: JsonElement, flags: Set = CompatibilityFlags.defaults()) { + process(patch, NoopProcessor.INSTANCE, flags) + } + + @Throws(JsonPatchApplicationException::class) + @JvmStatic + @JvmOverloads + fun apply(patch: JsonElement, source: JsonElement, flags: Set = CompatibilityFlags.defaults()): JsonElement { + val processor = ApplyProcessor(source) + process(patch, processor, flags) + return processor.result() + } + + + private fun decodePath(path: String): String { + return path.replace("~1".toRegex(), "/").replace("~0".toRegex(), "~") // see http://tools.ietf.org/html/rfc6901#section-4 + } + + private fun getPath(path: JsonElement): List { + // List paths = Splitter.on('/').splitToList(path.toString().replaceAll("\"", "")); + // return Lists.newArrayList(Iterables.transform(paths, DECODE_PATH_FUNCTION)); + val pathstr = path.toString().replace("\"", "") + val paths = pathstr.split("/") + return paths.map { decodePath(it) } + } +} diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatchApplicationException.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatchApplicationException.kt new file mode 100644 index 000000000..73c6b07ec --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatchApplicationException.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2016 flipkart.com zjsonpatch. + * + * 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 com.agui.client.jsonpatch + +/** + * User: holograph + * Date: 03/08/16 + */ +open class JsonPatchApplicationException : RuntimeException { + constructor(message: String) : super(message) {} + + constructor(message: String, cause: Throwable) : super(message, cause) {} + + constructor(cause: Throwable) : super(cause) {} +} diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatchApplyProcessor.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatchApplyProcessor.kt new file mode 100644 index 000000000..1468ac9e0 --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatchApplyProcessor.kt @@ -0,0 +1,49 @@ +package com.agui.client.jsonpatch + +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull + +/* + * Copyright 2023 Reid Byun. + * + * 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. +*/ + +abstract class JsonPatchApplyProcessor(private val source: JsonElement = JsonNull) { + var targetSource: JsonElement = source + private set + + open fun setSource(changedSource: JsonElement) { + targetSource = changedSource + } +} +// +//fun JsonPatchApplyProcessor.edit(actions: JsonPatchEditingContext.()->Unit) { +// val context = JsonPatchEditingContextImpl(source = this.targetSource) +// context.actions() +// +// this.setSource(context.source) +//} + +fun JsonPatchApplyProcessor.edit(actions: JsonPatchEditingContext.()->Unit) { + if (this is NoopProcessor) { // for test + val context = JsonPatchEditingContextTestImpl(source = this.targetSource) + context.actions() + this.setSource(context.source) + } + else { + val context = JsonPatchEditingContextImpl(source = this.targetSource) + context.actions() + this.setSource(context.source) + } +} \ No newline at end of file diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatchEditingContext.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatchEditingContext.kt new file mode 100644 index 000000000..12827fd73 --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatchEditingContext.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2016 flipkart.com zjsonpatch. + * + * 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 com.agui.client.jsonpatch; + + +import kotlinx.serialization.json.* + +interface JsonPatchEditingContext { + fun remove(path: List) + fun replace(path: List, value: JsonElement) + fun add(path: List, value: JsonElement) + fun move(fromPath: List, toPath: List) + fun copy(fromPath: List, toPath: List) + fun test(path: List, value: JsonElement) +} \ No newline at end of file diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatchEditingContextImpl.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatchEditingContextImpl.kt new file mode 100644 index 000000000..76a7591c3 --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatchEditingContextImpl.kt @@ -0,0 +1,257 @@ +package com.agui.client.jsonpatch + +import kotlinx.serialization.json.* + +/* + * Copyright 2023 Reid Byun. + * + * 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. +*/ + +class JsonPatchEditingContextImpl(var source: JsonElement): JsonPatchEditingContext { + override fun remove(path: List) { + source = editElement(source, path, action = { root -> + if (path.isEmpty()) { + throw JsonPatchApplicationException("[Remove Operation] path is empty") + } + else { + var parentNode = root//getParentNode(root, searchPath) + if (parentNode == null) { + throw JsonPatchApplicationException("[Remove Operation] noSuchPath in source, path provided : " + path) + } + else { + val fieldToRemove = path[path.size - 1].replace("\"".toRegex(), "") + if (parentNode is JsonObject) { + parentNode = parentNode.remove(fieldToRemove) + } + else if (parentNode is JsonArray) { + parentNode = parentNode.remove(arrayIndex(fieldToRemove, parentNode.size - 1)) + //return parentNode + } + else { + throw JsonPatchApplicationException("[Remove Operation] noSuchPath in source, path provided : " + path) + } + } + parentNode + }}) ?: source + } + + override fun replace(path: List, value: JsonElement) { + source = editElement(source, path, action = { root -> + if (path.isEmpty()) { + throw JsonPatchApplicationException("[Replace Operation] path is empty") + } else { + var parentNode = getParentNode(source, path) + if (parentNode == null) { + throw JsonPatchApplicationException("[Replace Operation] noSuchPath in source, path provided : " + path) + } else { + val fieldToReplace = path[path.size - 1].replace("\"".toRegex(), "") + if (fieldToReplace.isEmpty() && path.size == 1) { + parentNode = value + } + else if (parentNode is JsonObject) { + parentNode = parentNode.add(fieldToReplace, value) + } + else if (parentNode is JsonArray) { + parentNode = parentNode.set(arrayIndex(fieldToReplace, parentNode.size - 1), value) + } + else { + throw JsonPatchApplicationException("[Replace Operation] noSuchPath in source, path provided : " + path) + } + parentNode + } + } + }) ?: source + } + + override fun add(path: List, value: JsonElement) { + source = editElement(source, path, action = { root -> + if (path.isEmpty()) { + throw JsonPatchApplicationException("[ADD Operation] path is empty , path : ") + } else { + var parentNode = root//getParentNode(root, searchPath) + if (parentNode == null) { + throw JsonPatchApplicationException("[ADD Operation] noSuchPath in source, path provided : " + path) + } else { + val fieldToReplace = path[path.size - 1].replace("\"".toRegex(), "") + if (fieldToReplace == "" && path.size == 1) + parentNode = value + else if (!parentNode.isContainerNode()) { + throw JsonPatchApplicationException("[ADD Operation] parent is not a container in source, path provided : $path | node : $parentNode") + } + else if (parentNode is JsonArray) { + parentNode = addToArray(path, value, parentNode) + } + else { + parentNode = addToObject(path, parentNode, value) + } + } + parentNode + } + }) ?: source + } + + override fun move(fromPath: List, toPath: List) { + val parentNode = getParentNode(source, fromPath) + val field = fromPath[fromPath.size - 1].replace("\"".toRegex(), "") + val valueNode = if (parentNode!! is JsonArray) { + parentNode.jsonArray[field.toInt()] + } + else { + parentNode.jsonObject[field] + } + + remove(fromPath) + add(toPath, valueNode!!) + } + + override fun copy(fromPath: List, toPath: List) { + val parentNode = getParentNode(source, fromPath) + val field = fromPath[fromPath.size - 1].replace("\"".toRegex(), "") + val valueNode = if (parentNode!! is JsonArray) { + parentNode.jsonArray[field.toInt()] + } + else { + parentNode.jsonObject[field] + } + add(toPath, valueNode!!) + } + + override fun test(path: List, value: JsonElement) { + source = editElement(source, path, action = { root -> + if (path.isEmpty()) { + throw JsonPatchApplicationException("[TEST Operation] path is empty , path : ") + } else { + var parentNode = root + if (parentNode == null) { + throw JsonPatchApplicationException("[TEST Operation] noSuchPath in source, path provided : " + path) + } + else { + val fieldToReplace = path[path.size - 1].replace("\"".toRegex(), "") + if (fieldToReplace == "" && path.size == 1) + parentNode = value + else if (!parentNode.isContainerNode()) + throw JsonPatchApplicationException("[TEST Operation] parent is not a container in source, path provided : $path | node : $parentNode") + else if (parentNode is JsonArray) { + val target = parentNode + val idxStr = path[path.size - 1] + + if ("-" == idxStr) { + // see http://tools.ietf.org/html/rfc6902#section-4.1 + if (target.get(target.size - 1) != value) { + throw JsonPatchApplicationException("[TEST Operation] value mismatch") + } + } else { + val idx = arrayIndex(idxStr.replace("\"".toRegex(), ""), target.size) + if (target.get(idx) != value) { + throw JsonPatchApplicationException("[TEST Operation] value mismatch") + } + } + } else { + val target = parentNode as JsonObject + val key = path[path.size - 1].replace("\"".toRegex(), "") + if (target.get(key) != value) { + throw JsonPatchApplicationException("[TEST Operation] value mismatch") + } + } + parentNode + } + } + }) ?: source + } + + private fun getParentNode(source: JsonElement, fromPath: List): JsonElement? { + val pathToParent = fromPath.subList(0, fromPath.size - 1) // would never by out of bound, lets see + return getNode(source, pathToParent, 1) + } + + private fun getNode(ret: JsonElement, path: List, pos_: Int): JsonElement? { + var pos = pos_ + if (pos >= path.size) { + return ret + } + val key = path[pos] + if (ret is JsonArray) { + val keyInt = (key.replace("\"".toRegex(), "")).toInt() + return getNode(ret[keyInt], path, ++pos) + } else if (ret is JsonObject) { + if (ret.containsKey(key)) { + return getNode(ret[key]!!, path, ++pos) + } + return null + } else { + return ret + } + } + + private fun editElement(source: JsonElement, fromPath: List, action: (JsonElement)-> JsonElement?): JsonElement? { + val pathToParent = fromPath.subList(0, fromPath.size - 1) // would never by out of bound, lets see + return findAndAction(source, pathToParent, 1, action) + } + + private fun findAndAction(ret: JsonElement, path: List, pos_: Int, action: (JsonElement)-> JsonElement?): JsonElement? { + var pos = pos_ + if (pos >= path.size) { + // Result + return action(ret) + } + val key = path[pos] + if (ret is JsonArray) { + val keyInt = (key.replace("\"".toRegex(), "")).toInt() + return ret.set(keyInt, findAndAction(ret[keyInt], path, ++pos, action)) + } + else if (ret is JsonObject) { + if (ret.containsKey(key)) { + return ret.set(key, findAndAction(ret[key]!!, path, ++pos, action)) + } + return null + } else { + // Result + return action(ret) + } + } + + private fun arrayIndex(s: String, max: Int): Int { + val index = s.toInt() + if (index < 0) { + throw JsonPatchApplicationException("index Out of bound, index is negative") + } else if (index > max) { + throw JsonPatchApplicationException("index Out of bound, index is greater than " + max) + } + return index + } + + private fun addToObject(path: List, node: JsonElement, value: JsonElement): JsonObject { + val target = node as JsonObject + val key = path[path.size - 1].replace("\"".toRegex(), "") + + return target.add(key, value) + } + + private fun addToArray(path: List, value: JsonElement, parentNode: JsonElement): JsonElement { + var target = parentNode as JsonArray + val idxStr = path[path.size - 1] + + if ("-" == idxStr) { + // see http://tools.ietf.org/html/rfc6902#section-4.1 + //target.add(value) + target = target.add(value) + } else { + //val idx = arrayIndex(idxStr.replace("\"".toRegex(), ""), target.size()) + val idx = arrayIndex(idxStr.replace("\"".toRegex(), ""), target.size) + target = target.insert(idx, value) + } + + return target + } +} diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatchProcessor.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatchProcessor.kt new file mode 100644 index 000000000..afa4139c6 --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/JsonPatchProcessor.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2016 flipkart.com zjsonpatch. + * + * 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 com.agui.client.jsonpatch; + + +import kotlinx.serialization.json.* + +interface JsonPatchProcessor { + fun remove(path: List) + fun replace(path: List, value: JsonElement) + fun add(path: List, value: JsonElement) + fun move(fromPath: List, toPath: List) + fun copy(fromPath: List, toPath: List) + fun test(path: List, value: JsonElement) +} \ No newline at end of file diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/NodeType.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/NodeType.kt new file mode 100644 index 000000000..465cd0557 --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/NodeType.kt @@ -0,0 +1,18 @@ +package com.agui.client.jsonpatch + +import kotlinx.serialization.json.* + + +internal object NodeType { + val ARRAY = 1 + val OBJECT = 2 + // static final int NULL=3; + val PRIMITIVE_OR_NULL = 3 + + fun getNodeType(node: JsonElement): Int { + if (node is JsonArray) return ARRAY + if (node is JsonObject) return OBJECT + // if(node.isJsonNull()) return NULL; + return PRIMITIVE_OR_NULL + } +} diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/NoopProcessor.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/NoopProcessor.kt new file mode 100644 index 000000000..8a0bd2262 --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/NoopProcessor.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2016 flipkart.com zjsonpatch. + * + * 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 com.agui.client.jsonpatch + +import kotlinx.serialization.json.JsonElement + +/** A JSON patch processor that does nothing, intended for testing and validation. */ +class NoopProcessor : JsonPatchApplyProcessor() { + companion object { + val INSTANCE: NoopProcessor = NoopProcessor() + } +} + +class JsonPatchEditingContextTestImpl(var source: JsonElement): JsonPatchEditingContext { + override fun remove(path: List) {} + override fun replace(path: List, value: JsonElement) {} + override fun add(path: List, value: JsonElement) {} + override fun move(fromPath: List, toPath: List) {} + override fun copy(fromPath: List, toPath: List) {} + override fun test(path: List, value: JsonElement) {} +} \ No newline at end of file diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/Operations.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/Operations.kt new file mode 100644 index 000000000..f42cffa69 --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/Operations.kt @@ -0,0 +1,43 @@ +package com.agui.client.jsonpatch + +internal open class Operations { + val ADD: Int = 0 + val REMOVE: Int = 1 + val REPLACE: Int = 2 + val MOVE: Int = 3 + val COPY: Int = 4 + val TEST: Int = 5 + + open val ADD_name = "add" + open val REMOVE_name = "remove" + open val REPLACE_name = "replace" + open val MOVE_name = "move" + open val COPY_name = "copy" + open val TEST_name = "test" + private val OPS = mapOf( + ADD_name to ADD, + REMOVE_name to REMOVE, + REPLACE_name to REPLACE, + MOVE_name to MOVE, + COPY_name to COPY, + TEST_name to TEST) + private val NAMES = mapOf( + ADD to ADD_name, + REMOVE to REMOVE_name, + REPLACE to REPLACE_name, + MOVE to MOVE_name, + COPY to COPY_name, + TEST to TEST_name) + + fun opFromName(rfcName: String): Int { + val res=OPS.get(rfcName.lowercase()) + if(res==null) throw InvalidJsonPatchException("unknown / unsupported operation $rfcName") + return res + } + + fun nameFromOp(operation: Int): String { + val res= NAMES.get(operation) + if(res==null) throw InvalidJsonPatchException("unknown / unsupported operation $operation") + return res + } +} diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/CommandVisitor.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/CommandVisitor.kt new file mode 100644 index 000000000..99e55f2ee --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/CommandVisitor.kt @@ -0,0 +1,145 @@ +package com.agui.client.jsonpatch.lcs +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This interface should be implemented by user object to walk + * through [EditScript] objects. + * + * + * Users should implement this interface in order to walk through + * the [EditScript] object created by the comparison + * of two sequences. This is a direct application of the visitor + * design pattern. The [EditScript.visit] + * method takes an object implementing this interface as an argument, + * it will perform the loop over all commands in the script and the + * proper methods of the user class will be called as the commands are + * encountered. + * + * + * The implementation of the user visitor class will depend on the + * need. Here are two examples. + * + * + * The first example is a visitor that build the longest common + * subsequence: + *
+ * import org.apache.commons.collections4.comparators.sequence.CommandVisitor;
+ *
+ * import java.util.ArrayList;
+ *
+ * public class LongestCommonSubSequence implements CommandVisitor {
+ *
+ * public LongestCommonSubSequence() {
+ * a = new ArrayList();
+ * }
+ *
+ * public void visitInsertCommand(Object object) {
+ * }
+ *
+ * public void visitKeepCommand(Object object) {
+ * a.add(object);
+ * }
+ *
+ * public void visitDeleteCommand(Object object) {
+ * }
+ *
+ * public Object[] getSubSequence() {
+ * return a.toArray();
+ * }
+ *
+ * private ArrayList a;
+ *
+ * }
+
* + * + * + * The second example is a visitor that shows the commands and the way + * they transform the first sequence into the second one: + *
+ * import org.apache.commons.collections4.comparators.sequence.CommandVisitor;
+ *
+ * import java.util.Arrays;
+ * import java.util.ArrayList;
+ * import java.util.Iterator;
+ *
+ * public class ShowVisitor implements CommandVisitor {
+ *
+ * public ShowVisitor(Object[] sequence1) {
+ * v = new ArrayList();
+ * v.addAll(Arrays.asList(sequence1));
+ * index = 0;
+ * }
+ *
+ * public void visitInsertCommand(Object object) {
+ * v.insertElementAt(object, index++);
+ * display("insert", object);
+ * }
+ *
+ * public void visitKeepCommand(Object object) {
+ * ++index;
+ * display("keep  ", object);
+ * }
+ *
+ * public void visitDeleteCommand(Object object) {
+ * v.remove(index);
+ * display("delete", object);
+ * }
+ *
+ * private void display(String commandName, Object object) {
+ * System.out.println(commandName + " " + object + " ->" + this);
+ * }
+ *
+ * public String toString() {
+ * StringBuffer buffer = new StringBuffer();
+ * for (Iterator iter = v.iterator(); iter.hasNext();) {
+ * buffer.append(' ').append(iter.next());
+ * }
+ * return buffer.toString();
+ * }
+ *
+ * private ArrayList v;
+ * private int index;
+ *
+ * }
+
* + * + * @since 4.0 + * @version $Id: CommandVisitor.java 1477760 2013-04-30 18:34:03Z tn $ + */ +interface CommandVisitor { + /** + * Method called when an insert command is encountered. + * + * @param object object to insert (this object comes from the second sequence) + */ + fun visitInsertCommand(`object`: T) + + /** + * Method called when a keep command is encountered. + * + * @param object object to keep (this object comes from the first sequence) + */ + fun visitKeepCommand(`object`: T) + + /** + * Method called when a delete command is encountered. + * + * @param object object to delete (this object comes from the first sequence) + */ + fun visitDeleteCommand(`object`: T) +} \ No newline at end of file diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/DefaultEquator.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/DefaultEquator.kt new file mode 100644 index 000000000..f86b3dda4 --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/DefaultEquator.kt @@ -0,0 +1,95 @@ +package com.agui.client.jsonpatch.lcs + + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Default [Equator] implementation. + * + * @param the types of object this [Equator] can evaluate. + * @since 4.0 + * @version $Id: DefaultEquator.java 1543950 2013-11-20 21:13:35Z tn $ + */ +class DefaultEquator +/** + * Restricted constructor. + */ +private constructor() : Equator { + /** + * {@inheritDoc} Delegates to [Object.equals]. + */ + override fun equate(o1: T, o2: T): Boolean { + return o1 === o2 || o1 != null && o1 == o2 + } + + /** + * {@inheritDoc} + * + * @return `o.hashCode()` if `o` is non- + * `null`, else [.HASHCODE_NULL]. + */ + override fun hash(o: T): Int { + return o?.hashCode() ?: HASHCODE_NULL + } + + private fun readResolve(): Any { + return INSTANCE + } + + companion object { + /** Serial version UID */ + private const val serialVersionUID = 825802648423525485L + + /** Static instance */ + // the static instance works for all types + val INSTANCE: DefaultEquator<*> = DefaultEquator() + + /** + * Hashcode used for `null` objects. + */ + const val HASHCODE_NULL = -1 + + /** + * Factory returning the typed singleton instance. + * + * @param the object type + * @return the singleton instance + */ + // the static instance works for all types + fun defaultEquator(): DefaultEquator { + return INSTANCE as DefaultEquator + } + } +} diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/DeleteCommand.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/DeleteCommand.kt new file mode 100644 index 000000000..3187c9bd7 --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/DeleteCommand.kt @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.agui.client.jsonpatch.lcs + +/** + * Command representing the deletion of one object of the first sequence. + * + * + * When one object of the first sequence has no corresponding object in the + * second sequence at the right place, the [edit script][EditScript] + * transforming the first sequence into the second sequence uses an instance of + * this class to represent the deletion of this object. The objects embedded in + * these type of commands always come from the first sequence. + * + * @see SequencesComparator + * + * @see EditScript + * + * + * @since 4.0 + * @version $Id: DeleteCommand.java 1477760 2013-04-30 18:34:03Z tn $ + */ +class DeleteCommand +/** + * Simple constructor. Creates a new instance of [DeleteCommand]. + * + * @param object the object of the first sequence that should be deleted + */ + (`object`: T) : EditCommand(`object`) { + /** + * Accept a visitor. When a `DeleteCommand` accepts a visitor, it calls + * its [visitDeleteCommand][CommandVisitor.visitDeleteCommand] method. + * + * @param visitor the visitor to be accepted + */ + override fun accept(visitor: CommandVisitor?) { + visitor?.visitDeleteCommand(`object`) + } +} \ No newline at end of file diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/EditCommand.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/EditCommand.kt new file mode 100644 index 000000000..84e2a9bb0 --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/EditCommand.kt @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.agui.client.jsonpatch.lcs + +/** + * Abstract base class for all commands used to transform an objects sequence + * into another one. + * + * + * When two objects sequences are compared through the + * [SequencesComparator.getScript] method, + * the result is provided has a [script][EditScript] containing the commands + * that progressively transform the first sequence into the second one. + * + * + * There are only three types of commands, all of which are subclasses of this + * abstract class. Each command is associated with one object belonging to at + * least one of the sequences. These commands are [ InsertCommand][InsertCommand] which correspond to an object of the second sequence being + * inserted into the first sequence, [DeleteCommand] which + * correspond to an object of the first sequence being removed and + * [KeepCommand] which correspond to an object of the first + * sequence which `equals` an object in the second sequence. It is + * guaranteed that comparison is always performed this way (i.e. the + * `equals` method of the object from the first sequence is used and + * the object passed as an argument comes from the second sequence) ; this can + * be important if subclassing is used for some elements in the first sequence + * and the `equals` method is specialized. + * + * @see SequencesComparator + * + * @see EditScript + * + * + * @since 4.0 + * @version $Id: EditCommand.java 1477760 2013-04-30 18:34:03Z tn $ + */ +abstract class EditCommand +/** + * Simple constructor. Creates a new instance of EditCommand + * + * @param object reference to the object associated with this command, this + * refers to an element of one of the sequences being compared + */ protected constructor( + /** Object on which the command should be applied. */ + protected val `object`: T +) { + /** + * Returns the object associated with this command. + * + * @return the object on which the command is applied + */ + + /** + * Accept a visitor. + * + * + * This method is invoked for each commands belonging to + * an [EditScript], in order to implement the visitor design pattern + * + * @param visitor the visitor to be accepted + */ + abstract fun accept(visitor: CommandVisitor?) +} \ No newline at end of file diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/EditScript.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/EditScript.kt new file mode 100644 index 000000000..4c2f1cf4d --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/EditScript.kt @@ -0,0 +1,119 @@ +package com.agui.client.jsonpatch.lcs +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ /** + * This class gathers all the [commands][EditCommand] needed to transform + * one objects sequence into another objects sequence. + * + * + * An edit script is the most general view of the differences between two + * sequences. It is built as the result of the comparison between two sequences + * by the [SequencesComparator] class. The user can + * walk through it using the *visitor* design pattern. + * + * + * It is guaranteed that the objects embedded in the [insert][InsertCommand] come from the second sequence and that the objects embedded in + * either the [delete commands][DeleteCommand] or [keep][KeepCommand] come from the first sequence. This can be important if subclassing + * is used for some elements in the first sequence and the `equals` + * method is specialized. + * + * @see SequencesComparator + * + * @see EditCommand + * + * @see CommandVisitor + * + * + * @since 4.0 + * @version $Id: EditScript.java 1477760 2013-04-30 18:34:03Z tn $ + */ +class EditScript { + /** Container for the commands. */ + private val commands: MutableList> + /** + * Get the length of the Longest Common Subsequence (LCS). The length of the + * longest common subsequence is the number of [keep][KeepCommand] in the script. + * + * @return length of the Longest Common Subsequence + */ + /** Length of the longest common subsequence. */ + var lCSLength: Int + private set + /** + * Get the number of effective modifications. The number of effective + * modification is the number of [delete][DeleteCommand] and + * [insert][InsertCommand] commands in the script. + * + * @return number of effective modifications + */ + /** Number of modifications. */ + var modifications: Int + private set + + /** + * Simple constructor. Creates a new empty script. + */ + init { + commands = ArrayList>() + lCSLength = 0 + modifications = 0 + } + + /** + * Add a keep command to the script. + * + * @param command command to add + */ + fun append(command: KeepCommand) { + commands.add(command) + ++lCSLength + } + + /** + * Add an insert command to the script. + * + * @param command command to add + */ + fun append(command: InsertCommand) { + commands.add(command) + ++modifications + } + + /** + * Add a delete command to the script. + * + * @param command command to add + */ + fun append(command: DeleteCommand) { + commands.add(command) + ++modifications + } + + /** + * Visit the script. The script implements the *visitor* design + * pattern, this method is the entry point to which the user supplies its + * own visitor, the script will be responsible to drive it through the + * commands in order and call the appropriate method as each command is + * encountered. + * + * @param visitor the visitor that will visit all commands in turn + */ + fun visit(visitor: CommandVisitor) { + for (command in commands) { + command.accept(visitor) + } + } +} \ No newline at end of file diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/Equator.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/Equator.kt new file mode 100644 index 000000000..927f2d02d --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/Equator.kt @@ -0,0 +1,42 @@ +package com.agui.client.jsonpatch.lcs +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable + * law or agreed to in writing, software distributed under the License is distributed on an "AS IS" + * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + */ /** + * An equation function, which determines equality between objects of type T. + * + * + * It is the functional sibling of [java.util.Comparator]; [Equator] is to + * [Object] as [java.util.Comparator] is to [java.lang.Comparable]. + * + * @param the types of object this [Equator] can evaluate. + * @since 4.0 + * @version $Id: Equator.java 1540567 2013-11-10 22:19:29Z tn $ + */ +interface Equator { + /** + * Evaluates the two arguments for their equality. + * + * @param o1 the first object to be equated. + * @param o2 the second object to be equated. + * @return whether the two objects are equal. + */ + fun equate(o1: T, o2: T): Boolean + + /** + * Calculates the hash for the object, based on the method of equality used in the equate + * method. This is used for classes that delegate their [equals(Object)][Object.equals] method to an + * Equator (and so must also delegate their [hashCode()][Object.hashCode] method), or for implementations + * of org.apache.commons.collections4.map.HashedMap that use an Equator for the key objects. + * + * @param o the object to calculate the hash for. + * @return the hash of the object. + */ + fun hash(o: T): Int +} \ No newline at end of file diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/InsertCommand.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/InsertCommand.kt new file mode 100644 index 000000000..64eaea378 --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/InsertCommand.kt @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.agui.client.jsonpatch.lcs + +/** + * Command representing the insertion of one object of the second sequence. + * + * + * When one object of the second sequence has no corresponding object in the + * first sequence at the right place, the [edit script][EditScript] + * transforming the first sequence into the second sequence uses an instance of + * this class to represent the insertion of this object. The objects embedded in + * these type of commands always come from the second sequence. + * + * @see SequencesComparator + * + * @see EditScript + * + * + * @since 4.0 + * @version $Id: InsertCommand.java 1477760 2013-04-30 18:34:03Z tn $ + */ +class InsertCommand +/** + * Simple constructor. Creates a new instance of InsertCommand + * + * @param object the object of the second sequence that should be inserted + */ + (`object`: T) : EditCommand(`object`) { + /** + * Accept a visitor. When an `InsertCommand` accepts a visitor, + * it calls its [visitInsertCommand][CommandVisitor.visitInsertCommand] + * method. + * + * @param visitor the visitor to be accepted + */ + + override fun accept(visitor: CommandVisitor?) { + visitor?.visitInsertCommand(`object`) + } +} \ No newline at end of file diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/KeepCommand.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/KeepCommand.kt new file mode 100644 index 000000000..5b255dc1f --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/KeepCommand.kt @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.agui.client.jsonpatch.lcs + +/** + * Command representing the keeping of one object present in both sequences. + * + * + * When one object of the first sequence `equals` another objects in + * the second sequence at the right place, the [edit script][EditScript] + * transforming the first sequence into the second sequence uses an instance of + * this class to represent the keeping of this object. The objects embedded in + * these type of commands always come from the first sequence. + * + * @see SequencesComparator + * + * @see EditScript + * + * + * @since 4.0 + * @version $Id: KeepCommand.java 1477760 2013-04-30 18:34:03Z tn $ + */ +class KeepCommand +/** + * Simple constructor. Creates a new instance of KeepCommand + * + * @param object the object belonging to both sequences (the object is a + * reference to the instance in the first sequence which is known + * to be equal to an instance in the second sequence) + */ + (`object`: T) : EditCommand(`object`) { + /** + * Accept a visitor. When a `KeepCommand` accepts a visitor, it + * calls its [visitKeepCommand][CommandVisitor.visitKeepCommand] method. + * + * @param visitor the visitor to be accepted + */ + + override fun accept(visitor: CommandVisitor?) { + visitor?.visitKeepCommand(`object`) + } +} \ No newline at end of file diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/ListUtils.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/ListUtils.kt new file mode 100644 index 000000000..b226435f7 --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/ListUtils.kt @@ -0,0 +1,71 @@ +package com.agui.client.jsonpatch.lcs + +/** + * code extracted from Apache Commons Collections 4.1 + * Created by daely on 7/22/2016. + */ +object ListUtils { + //----------------------------------------------------------------------- + /** + * Returns the longest common subsequence (LCS) of two sequences (lists). + * + * @param the element type + * @param a the first list + * @param b the second list + * @return the longest common subsequence + * @throws NullPointerException if either list is `null` + * @since 4.0 + */ + fun longestCommonSubsequence(a: List?, b: List?): List { + return longestCommonSubsequence(a, b, DefaultEquator.defaultEquator()) + } + + /** + * Returns the longest common subsequence (LCS) of two sequences (lists). + * + * @param the element type + * @param a the first list + * @param b the second list + * @param equator the equator used to test object equality + * @return the longest common subsequence + * @throws NullPointerException if either list or the equator is `null` + * @since 4.0 + */ + fun longestCommonSubsequence( + a: List?, b: List?, + equator: Equator? + ): List { + if (a == null || b == null) { + throw NullPointerException("List must not be null") + } + if (equator == null) { + throw NullPointerException("Equator must not be null") + } + val comparator: SequencesComparator = + SequencesComparator(a, b, equator) + val script: EditScript = comparator.getScript() + val visitor = LcsVisitor() + script.visit(visitor) + return visitor.subSequence + } + + /** + * A helper class used to construct the longest common subsequence. + */ + private class LcsVisitor : CommandVisitor { + private val sequence: ArrayList + + init { + sequence = ArrayList() + } + + override fun visitInsertCommand(`object`: E) {} + override fun visitDeleteCommand(`object`: E) {} + override fun visitKeepCommand(`object`: E) { + sequence.add(`object`) + } + + val subSequence: List + get() = sequence + } +} \ No newline at end of file diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/SequencesComparator.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/SequencesComparator.kt new file mode 100644 index 000000000..c874c190d --- /dev/null +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/jsonpatch/lcs/SequencesComparator.kt @@ -0,0 +1,341 @@ +package com.agui.client.jsonpatch.lcs + +import kotlin.jvm.JvmOverloads + +/* +* Licensed to the Apache Software Foundation (ASF) under one or more +* contributor license agreements. See the NOTICE file distributed with +* this work for additional information regarding copyright ownership. +* The ASF licenses this file to You under the Apache License, Version 2.0 +* (the "License"); you may not use this file except in compliance with +* the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +/** + * This class allows to compare two objects sequences. + * + * + * The two sequences can hold any object type, as only the `equals` + * method is used to compare the elements of the sequences. It is guaranteed + * that the comparisons will always be done as `o1.equals(o2)` where + * `o1` belongs to the first sequence and `o2` belongs to + * the second sequence. This can be important if subclassing is used for some + * elements in the first sequence and the `equals` method is + * specialized. + * + * + * Comparison can be seen from two points of view: either as giving the smallest + * modification allowing to transform the first sequence into the second one, or + * as giving the longest sequence which is a subsequence of both initial + * sequences. The `equals` method is used to compare objects, so any + * object can be put into sequences. Modifications include deleting, inserting + * or keeping one object, starting from the beginning of the first sequence. + * + * + * This class implements the comparison algorithm, which is the very efficient + * algorithm from Eugene W. Myers + * [ + * An O(ND) Difference Algorithm and Its Variations](http://www.cis.upenn.edu/~bcpierce/courses/dd/papers/diff.ps). This algorithm produces + * the shortest possible + * [edit script][EditScript] + * containing all the + * [commands][EditCommand] + * needed to transform the first sequence into the second one. + * + * @see EditScript + * + * @see EditCommand + * + * @see CommandVisitor + * + * + * @since 4.0 + * @version $Id: SequencesComparator.java 1540567 2013-11-10 22:19:29Z tn $ + */ + +class SequencesComparator @JvmOverloads constructor( +//class SequencesComparator constructor( +sequence1: List, +sequence2: List, +equator: Equator = DefaultEquator.defaultEquator() +) { + /** First sequence. */ + private val sequence1: List + + /** Second sequence. */ + private val sequence2: List + + /** The equator used for testing object equality. */ + private val equator: Equator + + /** Temporary variables. */ + private val vDown: IntArray + private val vUp: IntArray + /** + * Simple constructor. + * + * + * Creates a new instance of SequencesComparator with a custom [Equator]. + * + * + * It is *guaranteed* that the comparisons will always be done as + * `Equator.equate(o1, o2)` where `o1` belongs to the first + * sequence and `o2` belongs to the second sequence. + * + * @param sequence1 first sequence to be compared + * @param sequence2 second sequence to be compared + * @param equator the equator to use for testing object equality + */ + /** + * Simple constructor. + * + * + * Creates a new instance of SequencesComparator using a [DefaultEquator]. + * + * + * It is *guaranteed* that the comparisons will always be done as + * `o1.equals(o2)` where `o1` belongs to the first + * sequence and `o2` belongs to the second sequence. This can be + * important if subclassing is used for some elements in the first sequence + * and the `equals` method is specialized. + * + * @param sequence1 first sequence to be compared + * @param sequence2 second sequence to be compared + */ + init { + this.sequence1 = sequence1 + this.sequence2 = sequence2 + this.equator = equator + val size = sequence1.size + sequence2.size + 2 + vDown = IntArray(size) + vUp = IntArray(size) + } + + /** + * Get the [EditScript] object. + * + * + * It is guaranteed that the objects embedded in the [ insert commands][InsertCommand] come from the second sequence and that the objects + * embedded in either the [delete commands][DeleteCommand] or + * [keep commands][KeepCommand] come from the first sequence. This can + * be important if subclassing is used for some elements in the first + * sequence and the `equals` method is specialized. + * + * @return the edit script resulting from the comparison of the two + * sequences + */ + fun getScript(): EditScript { + val script = EditScript() + buildScript(0, sequence1.size, 0, sequence2.size, script) + return script + } + + /** + * Build a snake. + * + * @param start the value of the start of the snake + * @param diag the value of the diagonal of the snake + * @param end1 the value of the end of the first sequence to be compared + * @param end2 the value of the end of the second sequence to be compared + * @return the snake built + */ + private fun buildSnake(start: Int, diag: Int, end1: Int, end2: Int): Snake { + var end = start + while (end - diag < end2 && end < end1 && equator.equate( + sequence1[end], + sequence2[end - diag] + ) + ) { + ++end + } + return Snake(start, end, diag) + } + + /** + * Get the middle snake corresponding to two subsequences of the + * main sequences. + * + * + * The snake is found using the MYERS Algorithm (this algorithms has + * also been implemented in the GNU diff program). This algorithm is + * explained in Eugene Myers article: + * [ + * An O(ND) Difference Algorithm and Its Variations](http://www.cs.arizona.edu/people/gene/PAPERS/diff.ps). + * + * @param start1 the begin of the first sequence to be compared + * @param end1 the end of the first sequence to be compared + * @param start2 the begin of the second sequence to be compared + * @param end2 the end of the second sequence to be compared + * @return the middle snake + */ + private fun getMiddleSnake(start1: Int, end1: Int, start2: Int, end2: Int): Snake? { + // Myers Algorithm + // Initialisations + val m = end1 - start1 + val n = end2 - start2 + if (m == 0 || n == 0) { + return null + } + val delta = m - n + val sum = n + m + val offset = (if (sum % 2 == 0) sum else sum + 1) / 2 + vDown[1 + offset] = start1 + vUp[1 + offset] = end1 + 1 + for (d in 0..offset) { + // Down + run { + var k = -d + while (k <= d) { + + // First step + val i = k + offset + if (k == -d || k != d && vDown[i - 1] < vDown[i + 1]) { + vDown[i] = vDown[i + 1] + } else { + vDown[i] = vDown[i - 1] + 1 + } + var x = vDown[i] + var y = x - start1 + start2 - k + while (x < end1 && y < end2 && equator.equate( + sequence1[x], + sequence2[y] + ) + ) { + vDown[i] = ++x + ++y + } + // Second step + if (delta % 2 != 0 && delta - d <= k && k <= delta + d) { + if (vUp[i - delta] <= vDown[i]) { + return buildSnake(vUp[i - delta], k + start1 - start2, end1, end2) + } + } + k += 2 + } + } + + // Up + var k = delta - d + while (k <= delta + d) { + + // First step + val i = k + offset - delta + if (k == delta - d + || k != delta + d && vUp[i + 1] <= vUp[i - 1] + ) { + vUp[i] = vUp[i + 1] - 1 + } else { + vUp[i] = vUp[i - 1] + } + var x = vUp[i] - 1 + var y = x - start1 + start2 - k + while (x >= start1 && y >= start2 && equator.equate(sequence1[x], sequence2[y])) { + vUp[i] = x-- + y-- + } + // Second step + if (delta % 2 == 0 && -d <= k && k <= d) { + if (vUp[i] <= vDown[i + delta]) { + return buildSnake(vUp[i], k + start1 - start2, end1, end2) + } + } + k += 2 + } + } + throw RuntimeException("Internal Error") + } + + /** + * Build an edit script. + * + * @param start1 the begin of the first sequence to be compared + * @param end1 the end of the first sequence to be compared + * @param start2 the begin of the second sequence to be compared + * @param end2 the end of the second sequence to be compared + * @param script the edited script + */ + private fun buildScript( + start1: Int, end1: Int, start2: Int, end2: Int, + script: EditScript + ) { + val middle = getMiddleSnake(start1, end1, start2, end2) + if (middle == null || (middle.start == end1 && middle.diag == end1 - end2) || (middle.end == start1 && middle.diag == start1 - start2)) { + var i = start1 + var j = start2 + while (i < end1 || j < end2) { + if (i < end1 && j < end2 && equator.equate(sequence1[i], sequence2[j])) { + script.append(KeepCommand(sequence1[i])) + ++i + ++j + } else { + if (end1 - start1 > end2 - start2) { + script.append(DeleteCommand(sequence1[i])) + ++i + } else { + script.append(InsertCommand(sequence2[j])) + ++j + } + } + } + } else { + buildScript( + start1, middle.start, + start2, middle.start - middle.diag, + script + ) + for (i in middle.start until middle.end) { + script.append(KeepCommand(sequence1[i])) + } + buildScript( + middle.end, end1, + middle.end - middle.diag, end2, + script + ) + } + } + /** + * This class is a simple placeholder to hold the end part of a path + * under construction in a [SequencesComparator]. + */ + + + private class Snake + /** + * Simple constructor. Creates a new instance of Snake with specified indices. + * + * @param start start index of the snake + * @param end end index of the snake + * @param diag diagonal number + */( + /** Start index. */ + val start: Int, + /** End index. */ + val end: Int, + /** Diagonal number. */ + val diag: Int + ) { + /** + * Get the start index of the snake. + * + * @return start index of the snake + */ + /** + * Get the end index of the snake. + * + * @return end index of the snake + */ + /** + * Get the diagonal number of the snake. + * + * @return diagonal number of the snake + */ + + } +} diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/state/DefaultApplyEvents.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/state/DefaultApplyEvents.kt index 55057426e..7d9eb1215 100644 --- a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/state/DefaultApplyEvents.kt +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/state/DefaultApplyEvents.kt @@ -8,7 +8,7 @@ import com.agui.client.agent.AgentSubscriber import com.agui.client.agent.ThinkingTelemetryState import com.agui.client.agent.runSubscribersWithMutation import com.agui.core.types.* -import com.reidsync.kxjsonpatch.JsonPatch +import com.agui.client.jsonpatch.JsonPatch import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.transform import co.touchlab.kermit.Logger diff --git a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/state/StateManager.kt b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/state/StateManager.kt index 8c145d003..c1170daa1 100644 --- a/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/state/StateManager.kt +++ b/sdks/community/kotlin/library/client/src/commonMain/kotlin/com/agui/client/state/StateManager.kt @@ -1,16 +1,16 @@ package com.agui.client.state +import co.touchlab.kermit.Logger +import com.agui.client.jsonpatch.JsonPatch import com.agui.core.types.* -import com.reidsync.kxjsonpatch.JsonPatch import kotlinx.coroutines.flow.* import kotlinx.serialization.json.* -import co.touchlab.kermit.Logger private val logger = Logger.withTag("StateManager") /** * Manages client-side state with JSON Patch support. - * Uses kotlin-json-patch (io.github.reidsync:kotlin-json-patch). + * Uses a vendored JsonPatch implementation derived from io.github.reidsync:kotlin-json-patch. * Provides reactive state management with StateFlow and handles both * full state snapshots and incremental JSON Patch deltas. * @@ -49,9 +49,7 @@ class StateManager( logger.d { "Applying ${delta.size} state operations" } try { - // Use JsonPatch library val newState = JsonPatch.apply(delta, currentState.value) - _currentState.value = newState handler?.onStateDelta(delta) } catch (e: Exception) { @@ -62,8 +60,6 @@ class StateManager( /** * Gets a value by JSON Pointer path. - * Note: The 'kotlin-json-patch' library does not provide a public - * implementation of JSON Pointer, so we've implemented one. * * @param path JSON Pointer path (e.g., "/user/name" or "/items/0") * @return JsonElement? the value at the specified path, or null if not found or on error @@ -89,4 +85,4 @@ class StateManager( null } } -} \ No newline at end of file +} diff --git a/sdks/community/kotlin/library/gradle/libs.versions.toml b/sdks/community/kotlin/library/gradle/libs.versions.toml index 163192409..bd3f40ef4 100644 --- a/sdks/community/kotlin/library/gradle/libs.versions.toml +++ b/sdks/community/kotlin/library/gradle/libs.versions.toml @@ -1,7 +1,6 @@ [versions] core-ktx = "1.16.0" kotlin = "2.2.20" -kotlin-json-patch = "1.0.0" #Downgrading to avoid an R8 error ktor = "3.1.3" kotlinx-serialization = "1.8.1" @@ -13,7 +12,6 @@ kermit = "2.0.6" [libraries] # Ktor core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" } -kotlin-json-patch = { module = "io.github.reidsync:kotlin-json-patch", version.ref = "kotlin-json-patch" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }