All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.nemerosa.ontrack.json.KTJsonUtils.kt Maven / Gradle / Ivy

There is a newer version: 4.4.5
Show newest version
package net.nemerosa.ontrack.json

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.node.ArrayNode
import com.fasterxml.jackson.databind.node.ObjectNode
import kotlin.reflect.KClass

/**
 * Parses a string as JSON
 */
fun String.parseAsJson(): JsonNode = JsonUtils.parseAsNode(this)

/**
 * Map as JSON
 */
fun jsonOf(vararg pairs: Pair<*, *>) =
    mapOf(*pairs).toJson()!!

/**
 * Converts any object into JSON, or null if not defined.
 */
fun  T?.toJson(): JsonNode? =
    JsonUtils.format(this)

/**
 * Non-null JSON transformation
 */
fun  T.asJson(): JsonNode = JsonUtils.format(this)!!

/**
 * To a Map through JSON
 */
fun JsonNode.toJsonMap(): Map = JsonUtils.toMap(asJson())

/**
 * Format as a string
 */
fun JsonNode.asJsonString(): String = JsonUtils.toJSONString(this)

/**
 * Parses any node into an object.
 */
inline fun  JsonNode.parse(): T =
    JsonUtils.parse(this, T::class.java)

/**
 * Parses any node into an object.
 */
fun  JsonNode.parseInto(type: KClass): T =
    JsonUtils.parse(this, type.java)

/**
 * Formatting a JSON node as a string
 */
fun JsonNode.format(): String = JsonUtils.toJSONString(this)

/**
 * Parses any node into an object or returns `null` if parsing fails
 */
inline fun  JsonNode.parseOrNull(): T? =
    try {
        parse()
    } catch (_: JsonParseException) {
        null
    }

/**
 * Gets a field as enum
 */
inline fun > JsonNode.getEnum(field: String): E? {
    val text = path(field).asText()
    if (text.isNullOrBlank()) {
        return null
    } else {
        return enumValueOf(text)
    }
}

/**
 * Gets a field as [Int].
 */
@Deprecated(message = "Use getIntField", replaceWith = ReplaceWith("getIntField"))
fun JsonNode.getInt(field: String): Int? = getIntField(field)

/**
 * Gets a field as a JSON node, but returns `null` if this is a null node.
 */
fun JsonNode.getJsonField(field: String): JsonNode? =
    if (has(field)) {
        get(field)?.takeIf { !it.isNull }
    } else {
        null
    }

/**
 * Gets a field as [Int].
 */
fun JsonNode.getIntField(field: String): Int? =
    if (has(field)) {
        get(field).asInt()
    } else {
        null
    }

/**
 * Gets a required field as enum
 */
inline fun > JsonNode.getRequiredEnum(field: String): E =
    getEnum(field) ?: throw JsonMissingFieldException(field)

/**
 * Checks if a JSON node can be considered as null, either by being `null` itself
 * or by being an instance of the [null node][JsonNode.isNull].
 */
fun JsonNode?.isNullOrNullNode() = this == null || this.isNull

/**
 * Gets a string field
 */
fun JsonNode.getTextField(field: String): String? = if (has(field)) {
    get(field).takeIf { !it.isNull }?.asText()
} else {
    null
}

/**
 * Gets a required string field
 */
fun JsonNode.getRequiredTextField(field: String): String =
    getTextField(field)
        ?: throw JsonParseException("Missing field $field")

/**
 * Gets a boolean field
 */
fun JsonNode.getBooleanField(field: String): Boolean? = if (has(field)) {
    get(field).takeIf { !it.isNull }?.asBoolean()
} else {
    null
}

/**
 * Gets a required boolean field
 */
fun JsonNode.getRequiredBooleanField(field: String): Boolean =
    getBooleanField(field)
        ?: throw JsonParseException("Missing field $field")

/**
 * Gets a required int field
 */
fun JsonNode.getRequiredIntField(field: String): Int =
    getIntField(field)
        ?: throw JsonParseException("Missing field $field")

/**
 * Merging two JSON nodes
 *
 * @receiver Left component
 * @param node Right component
 * @param priority What to do on identical leaf fields
 * @param arrays What to do on arrays
 * @param conflictResolution What to do when fields do not have the same type
 * @return Result of the merge
 */
fun JsonNode.merge(
    node: JsonNode,
    priority: JsonMergePriority = JsonMergePriority.RIGHT,
    arrays: JsonArrayMergePriority = JsonArrayMergePriority.APPEND,
    conflictResolution: JsonConflictResolution = JsonConflictResolution.ABORT,
): JsonNode =
    when {
        node::class == this::class -> {
            when (this) {
                is ObjectNode -> mergeObject(node as ObjectNode, priority, arrays, conflictResolution)
                is ArrayNode -> mergeArray(node as ArrayNode, arrays)
                else -> when (priority) {
                    JsonMergePriority.LEFT -> this
                    JsonMergePriority.RIGHT -> node
                }
            }
        }
        node.isNullOrNullNode() -> {
            this
        }
        this.isNullOrNullNode() -> {
            node
        }
        else -> {
            when (conflictResolution) {
                JsonConflictResolution.ABORT -> error("Cannot merge JSON because of type conflict.")
                JsonConflictResolution.LEFT -> this
                JsonConflictResolution.RIGHT -> node
            }
        }
    }

fun ArrayNode.mergeArray(
    node: ArrayNode,
    arrays: JsonArrayMergePriority,
): JsonNode = when (arrays) {
    JsonArrayMergePriority.LEFT -> this
    JsonArrayMergePriority.RIGHT -> node
    JsonArrayMergePriority.APPEND -> {
        val target = arrayNode()
        target.addAll(this)
        target.addAll(node)
        target
    }
}

fun ObjectNode.mergeObject(
    node: ObjectNode,
    priority: JsonMergePriority,
    arrays: JsonArrayMergePriority,
    conflictResolution: JsonConflictResolution,
): JsonNode {
    // All field names
    val names = mutableSetOf()
    this.fieldNames().forEach { names += it }
    node.fieldNames().forEach { names += it }
    // Looping over all the fields
    val target = objectNode()
    names.forEach { name ->
        val value: JsonNode? = if (this.has(name) && node.has(name)) {
            this.get(name).merge(node.get(name), priority, arrays, conflictResolution)
        } else if (this.has(name)) {
            this.get(name)
        } else if (node.has(name)) {
            node.get(name)
        } else {
            null
        }
        if (value != null) {
            target.set(name, value)
        }
    }
    // OK
    return target
}

/**
 * What to do in case of type difference
 */
enum class JsonConflictResolution {
    ABORT,
    LEFT,
    RIGHT
}

/**
 * Priorities for JSON merge at field level
 */
enum class JsonMergePriority {
    RIGHT,
    LEFT
}

/**
 * Priorities for JSON merge at array level
 */
enum class JsonArrayMergePriority {
    APPEND,
    RIGHT,
    LEFT
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy