package ru.dbotthepony.kstarbound.json import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonNull import com.google.gson.JsonObject import com.google.gson.JsonSyntaxException import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kstarbound.IStarboundFile import ru.dbotthepony.kstarbound.Starbound enum class JsonPatch(val key: String) { TEST("test") { override fun apply(base: JsonElement, data: JsonObject): JsonElement { val path = JsonPath.pointer(data.get("path").asString) val value = data["value"] ?: JsonNull.INSTANCE val inverse = data.get("inverse", false) val get = path.find(base) ?: JsonNull.INSTANCE if (inverse) { if (value == JsonNull.INSTANCE && get != JsonNull.INSTANCE) { throw JsonTestException("Expected $path to not contain anything") } else if (value != JsonNull.INSTANCE && value == get) { throw JsonTestException("Expected $path to not contain $value") } } else { if (value == JsonNull.INSTANCE && get == JsonNull.INSTANCE) { throw JsonTestException("Expected $path to contain anything") } else if (value != JsonNull.INSTANCE && get == JsonNull.INSTANCE) { throw JsonTestException("Expected $path to contain '$value', but found nothing") } else if (value != JsonNull.INSTANCE && get != JsonNull.INSTANCE && value != get) { var text = get.toString() if (text.length > 40) { text = text.substring(0, 40) + "..." } throw JsonTestException("Expected $path to contain $value, but found $text") } } return base } }, REMOVE("remove") { override fun apply(base: JsonElement, data: JsonObject): JsonElement { return JsonPath.pointer(data.get("path").asString).remove(base).first } }, ADD("add") { override fun apply(base: JsonElement, data: JsonObject): JsonElement { val value = data["value"] ?: throw JsonSyntaxException("Missing 'value' to 'add' operation") return JsonPath.pointer(data.get("path").asString).add(base, value) } }, REPLACE("replace") { override fun apply(base: JsonElement, data: JsonObject): JsonElement { val value = data["value"] ?: throw JsonSyntaxException("Missing 'value' to 'add' operation") val path = JsonPath.pointer(data.get("path").asString) return path.add(path.remove(base).first, value) } }, MOVE("move") { override fun apply(base: JsonElement, data: JsonObject): JsonElement { val from = JsonPath.pointer(data.get("from").asString) val path = JsonPath.pointer(data.get("path").asString) val (newBase, removed) = from.remove(base) return path.add(newBase, removed) } }, COPY("copy") { override fun apply(base: JsonElement, data: JsonObject): JsonElement { val from = JsonPath.pointer(data.get("from").asString) val path = JsonPath.pointer(data.get("path").asString) return path.add(base, from.get(base)) } }; abstract fun apply(base: JsonElement, data: JsonObject): JsonElement class JsonTestException(message: String) : IllegalStateException(message) companion object { private val LOGGER = LogManager.getLogger() fun apply(base: JsonElement, data: JsonObject): JsonElement { val op = data["op"]?.asString ?: throw JsonSyntaxException("Missing 'op' in json patch operation") val operation = entries.firstOrNull { it.key == op } ?: throw JsonSyntaxException("Unknown patch operation $op!") return operation.apply(base, data) } @Suppress("NAME_SHADOWING") fun apply(base: JsonElement, data: JsonArray, source: IStarboundFile?): JsonElement { // original engine decides what to do based on first element encountered // in patch array... let's not. var base = base for ((i, element) in data.withIndex()) { if (element is JsonObject) { try { base = apply(base, element) } catch (err: JsonTestException) { LOGGER.debug("Test condition failed in {} at index {}: {}", source, i, err.message) } catch (err: Throwable) { LOGGER.error("Error while applying JSON patch from $source at index $i: ${err.message}") } } else if (element is JsonArray) { for ((i2, sub) in element.withIndex()) { try { if (sub !is JsonObject) throw JsonSyntaxException("Expected JSON Object as patch data, got $sub") base = apply(base, sub) } catch (err: JsonTestException) { LOGGER.debug("Test condition failed in {} at index {} -> {}: {}", source, i, i2, err.message) break } catch (err: Throwable) { LOGGER.error("Error while applying JSON patch from $source at index $i -> $i2: ${err.message}") break } } } else { LOGGER.error("Unknown data in $source at index $i") } } return base } @Suppress("NAME_SHADOWING") fun apply(base: JsonElement, source: Collection?): JsonElement { source ?: return base var base = base for (patch in source) { val read = Starbound.ELEMENTS_ADAPTER.read(patch.jsonReader()) if (read !is JsonArray) { LOGGER.error("$patch root element is not an array") } else { base = apply(base, read, patch) } } return base } } }