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