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

tornadofx.Json.kt Maven / Gradle / Ivy

package tornadofx

import javafx.beans.property.*
import javafx.beans.value.ObservableValue
import tornadofx.FX.Companion.log
import java.io.InputStream
import java.io.OutputStream
import java.io.StringWriter
import java.lang.reflect.ParameterizedType
import java.math.BigDecimal
import java.math.BigInteger
import java.net.URL
import java.nio.file.Files
import java.nio.file.OpenOption
import java.nio.file.Path
import java.nio.file.StandardOpenOption.*
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.*
import javax.json.*
import javax.json.JsonValue.ValueType.NULL
import javax.json.stream.JsonGenerator
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaField
import kotlin.reflect.jvm.javaType

interface JsonModel {
    /**
     * Fetch JSON values and update the model properties
     * @param json The json to extract values from
     */
    fun updateModel(json: JsonObject) {

    }

    /**
     * Build a JSON representation of the model properties
     * @param json A builder that should be filled with the model properties
     */
    fun toJSON(json: JsonBuilder) {

    }

    /**
     * Build a JSON representation of the model directly to JsonObject
     */
    fun toJSON(): JsonObject {
        val builder = JsonBuilder()
        toJSON(builder)
        return builder.build()
    }

    /**
     * Copy all properties from this object to the given target object by converting to JSON and then updating the target.
     * @param target The target object to update with the properties of this model
     */
    fun copy(target: JsonModel) {
        val builder = JsonBuilder()
        toJSON(builder)
        target.updateModel(builder.build())
    }

    /**
     * Copy all properties from the given source object to this object by converting to JSON and then updating this object.
     * @param source The source object to extract properties from
     */
    fun update(source: JsonModel) {
        val builder = JsonBuilder()
        source.toJSON(builder)
        updateModel(builder.build())
    }

    /**
     * Duplicate this model object by creating a new object of the same type and copy over all the model properties.

     * @param  The type of object
     * *
     * @return A new object of type T with the model properties of this object
     */
    @Suppress("UNCHECKED_CAST")
    fun  copy(): T {
        try {
            val clone = javaClass.newInstance() as T
            val builder = JsonBuilder()
            toJSON(builder)
            clone.updateModel(builder.build())
            return clone
        } catch (e: InstantiationException) {
            throw RuntimeException(e)
        } catch (e: IllegalAccessException) {
            throw RuntimeException(e)
        }

    }

}

object JsonConfig {
    var DefaultDateTimeMillis = false
}

/**
 * Nullable getters are lowercase, not null getters are prependend with get + camelcase
 * JsonObject already provide some not null getters like getString, getBoolean etc, the rest
 * are added here
 */

fun JsonObject.isNotNullOrNULL(key: String): Boolean = containsKey(key) && get(key)?.valueType != NULL

fun  JsonObject.firstNonNull(vararg keys: String, extractor: (key: String) -> T): T? = keys
        .firstOrNull { isNotNullOrNULL(it) }
        ?.let { extractor(it) }

fun JsonObject.string(vararg key: String): String? = firstNonNull(*key) { getString(it) }
fun JsonObject.getString(vararg key: String): String = string(*key)!!

fun JsonObject.double(vararg key: String): Double? = jsonNumber(*key)?.doubleValue()
fun JsonObject.getDouble(vararg key: String): Double = double(*key)!!

fun JsonObject.jsonNumber(vararg key: String): JsonNumber? = firstNonNull(*key) { getJsonNumber(it) }
fun JsonObject.getJsonNumber(vararg key: String): JsonNumber = jsonNumber(*key)!!

fun JsonObject.float(vararg key: String): Float? = firstNonNull(*key) { getFloat(it) }
fun JsonObject.getFloat(vararg key: String): Float = float(*key)!!

fun JsonObject.bigdecimal(vararg key: String): BigDecimal? = jsonNumber(*key)?.bigDecimalValue()
fun JsonObject.getBigDecimal(vararg key: String): BigDecimal = bigdecimal(*key)!!

fun JsonObject.long(vararg key: String): Long? = jsonNumber(*key)?.longValue()
fun JsonObject.getLong(vararg key: String) = long(*key)!!

fun JsonObject.bool(vararg key: String): Boolean? = firstNonNull(*key) { getBoolean(it) }
fun JsonObject.boolean(vararg key: String) = bool(*key) // Alias

fun JsonObject.date(vararg key: String): LocalDate? = string(*key)?.let { LocalDate.parse(it) }
fun JsonObject.getDate(vararg key: String): LocalDate = date(*key)!!

fun JsonNumber.datetime(millis: Boolean = JsonConfig.DefaultDateTimeMillis): LocalDateTime = LocalDateTime.ofEpochSecond(longValue() / (if (millis) 1000 else 1), 0, ZoneOffset.UTC)
fun JsonObject.datetime(vararg key: String, millis: Boolean = JsonConfig.DefaultDateTimeMillis): LocalDateTime? = jsonNumber(*key)?.datetime(millis)
fun JsonObject.getDateTime(vararg key: String, millis: Boolean = JsonConfig.DefaultDateTimeMillis): LocalDateTime = getJsonNumber(*key).datetime(millis)

fun JsonObject.uuid(vararg key: String): UUID? = string(*key)?.let { UUID.fromString(it) }
fun JsonObject.getUUID(vararg key: String) = uuid(*key)!!

fun JsonObject.int(vararg key: String): Int? = firstNonNull(*key) { getInt(it) }
fun JsonObject.getInt(vararg key: String): Int = int(*key)!!

fun JsonObject.jsonObject(vararg key: String): JsonObject? = firstNonNull(*key) { getJsonObject(it) }
inline fun  JsonObject.jsonModel(vararg key: String) = firstNonNull(*key) { T::class.java.newInstance().apply { updateModel(getJsonObject(it)) } }

fun JsonObject.jsonArray(vararg key: String): JsonArray? = firstNonNull(*key) { getJsonArray(it) }

class JsonBuilder {
    private val delegate: JsonObjectBuilder = Json.createObjectBuilder()

    fun > add(key: String, observable: T) {
        observable.value?.apply {
            when (this) {
                is Number -> add(key, this)
                is Boolean -> add(key, this)
                is UUID -> add(key, this)
                is LocalDate -> add(key, this)
                is LocalDateTime -> add(key, this)
                is String -> add(key, this)
                is JsonModel -> add(key, this)
            }
        }
    }

    fun add(key: String, value: Number?) = apply {
        when (value) {
            is Int -> delegate.add(key, value)
            is BigDecimal -> delegate.add(key, value)
            is BigInteger -> delegate.add(key, value)
            is Float -> delegate.add(key, value.toDouble())
            is Double -> delegate.add(key, value)
            is Long -> delegate.add(key, value)
        }
    }

    fun add(key: String, value: Boolean?) = apply {
        if (value != null)
            delegate.add(key, value)
    }

    fun add(key: String, value: UUID?) = apply {
        if (value != null)
            delegate.add(key, value.toString())
    }

    fun add(key: String, value: LocalDate?) = apply {
        if (value != null)
            delegate.add(key, value.toString())
    }

    fun add(key: String, value: LocalDateTime?, millis: Boolean = JsonConfig.DefaultDateTimeMillis) = apply {
        if (value != null)
            delegate.add(key, value.toEpochSecond(ZoneOffset.UTC) * (if (millis) 1000 else 1))
    }

    fun add(key: String, value: String?) = apply {
        if (value != null && value.isNotBlank())
            delegate.add(key, value)
    }

    fun add(key: String, value: JsonBuilder?) = apply {
        if (value != null)
            delegate.add(key, value.build())
    }

    fun add(key: String, value: JsonObjectBuilder?) = apply {
        if (value != null)
            delegate.add(key, value.build())
    }

    fun add(key: String, value: JsonObject?) = apply {
        if (value != null)
            delegate.add(key, value)
    }

    fun add(key: String, value: JsonModel?) = apply {
        if (value != null)
            add(key, value.toJSON())

    }

    fun add(key: String, value: JsonArrayBuilder?) = apply {
        if (value != null) {
            val built = value.build()
            if (built.isNotEmpty())
                delegate.add(key, built)
        }
    }

    fun add(key: String, value: JsonArray?) = apply {
        if (!value.isNullOrEmpty())
            delegate.add(key, value)
    }

    fun add(key: String, value: Iterable?) = apply {
        if (value != null) {
            val builder = Json.createArrayBuilder()
            value.forEach {
                when (it) {
                    is Int -> builder.add(it)
                    is String -> builder.add(it)
                    is Float -> builder.add(it.toDouble())
                    is Long -> builder.add(it)
                    is BigDecimal -> builder.add(it)
                    is Boolean -> builder.add(it)
                    is JsonModel -> builder.add(it.toJSON())
                    is JsonArray -> builder.add(it)
                    is JsonArrayBuilder -> builder.add(it)
                    is JsonObject -> builder.add(it)
                    is JsonObjectBuilder -> builder.add(it)
                    is JsonValue -> builder.add(it)
                }
            }
            delegate.add(key, builder.build())
        }

        return this
    }

    fun build(): JsonObject {
        return delegate.build()
    }

}


/**
 * Requires kotlin-reflect on classpath
 */
private fun  KProperty.generic(): Class<*> =
        (this.javaField?.genericType as ParameterizedType).actualTypeArguments[0] as Class<*>

/**
 * Requires kotlin-reflect on classpath
 */
@Suppress("UNCHECKED_CAST")
interface JsonModelAuto : JsonModel {
    val jsonProperties: Collection> get() {
        val props = javaClass.kotlin.memberProperties
        val propNames = props.map { it.name }
        return props.filterNot { it.name.endsWith("Property") && it.name.substringBefore("Property") in propNames }.filterNot { it.name == "jsonProperties" }
    }

    override fun updateModel(json: JsonObject) {
        jsonProperties.forEach {
            when (val pr = it.get(this)) {
                is BooleanProperty -> pr.value = json.bool(it.name)
                is LongProperty -> pr.value = json.long(it.name)
                is IntegerProperty -> pr.value = json.int(it.name)
                is DoubleProperty -> pr.value = json.double(it.name)
                is FloatProperty -> pr.value = json.double(it.name)?.toFloat()
                is StringProperty -> pr.value = json.string(it.name)
                is ObjectProperty<*> -> {
                    when (it.generic()) {
                        Boolean::class.java -> (pr as ObjectProperty).value = json.bool(it.name)
                        Long::class.java -> (pr as ObjectProperty).value = json.long(it.name)
                        Integer::class.java -> (pr as ObjectProperty).value = json.int(it.name)
                        Double::class.java -> (pr as ObjectProperty).value = json.double(it.name)
                        Float::class.java -> (pr as ObjectProperty).value = json.float(it.name)
                        String::class.java -> (pr as ObjectProperty).value = json.string(it.name)
                        LocalDate::class.java -> (pr as ObjectProperty).value = json.date(it.name)
                        LocalDateTime::class.java -> (pr as ObjectProperty).value = json.datetime(it.name)
                    }
                }
                is MutableList<*> -> {
                    val list = pr as MutableList
                    list.clear()
                    json.getJsonArray(it.name)?.forEach { jsonObj ->
                        val entry = it.generic().newInstance()
                        if (entry is JsonModel) {
                            list.add(entry.apply { updateModel(jsonObj as JsonObject) })
                        }
                    }
                }
                else -> {
                    if (it is KMutableProperty1<*, *>) {
                        when (it.returnType.javaType) {
                            Boolean::class.javaPrimitiveType -> (it as KMutableProperty1).set(this, json.bool(it.name))
                            Boolean::class.javaObjectType -> (it as KMutableProperty1).set(this, json.bool(it.name))
                            Long::class.javaObjectType -> (it as KMutableProperty1).set(this, json.long(it.name))
                            Long::class.javaPrimitiveType -> (it as KMutableProperty1).set(this, json.long(it.name))
                            Integer::class.javaObjectType -> (it as KMutableProperty1).set(this, json.int(it.name))
                            Integer::class.javaPrimitiveType -> (it as KMutableProperty1).set(this, json.int(it.name))
                            Double::class.javaObjectType -> (it as KMutableProperty1).set(this, json.double(it.name))
                            Double::class.javaPrimitiveType -> (it as KMutableProperty1).set(this, json.double(it.name))
                            Float::class.javaObjectType -> (it as KMutableProperty1).set(this, json.float(it.name))
                            Float::class.javaPrimitiveType -> (it as KMutableProperty1).set(this, json.float(it.name))
                            String::class.java -> (it as KMutableProperty1).set(this, json.string(it.name))
                            LocalDate::class.java -> (it as KMutableProperty1).set(this, json.date(it.name))
                            LocalDateTime::class.java -> (it as KMutableProperty1).set(this, json.datetime(it.name))
                            else -> {
                                log.warning("AutoModel doesn't know how to handle ${it.returnType}/${it.returnType.javaType}")
                            }
                        }
                    }
                }
            }
        }
    }

    override fun toJSON(json: JsonBuilder) {
        with(json) {
            jsonProperties.forEach {
                when (val pr = it.get(this@JsonModelAuto)) {
                    is BooleanProperty -> add(it.name, pr.value)
                    is LongProperty -> add(it.name, pr.value)
                    is IntegerProperty -> add(it.name, pr.value)
                    is DoubleProperty -> add(it.name, pr.value)
                    is FloatProperty -> add(it.name, pr.value.toDouble())
                    is StringProperty -> add(it.name, pr.value)
                    is ObjectProperty<*> -> {
                        when (it.generic()) {
                            Boolean::class.java -> add(it.name, (pr as ObjectProperty).value)
                            Long::class.java -> add(it.name, (pr as ObjectProperty).value)
                            Integer::class.java -> add(it.name, (pr as ObjectProperty).value)
                            Double::class.java -> add(it.name, (pr as ObjectProperty).value)
                            Float::class.java -> add(it.name, (pr as ObjectProperty).value)
                            String::class.java -> add(it.name, (pr as ObjectProperty).value)
                            LocalDate::class.java -> add(it.name, (pr as ObjectProperty).value)
                            LocalDateTime::class.java -> add(it.name, (pr as ObjectProperty).value)
                        }
                    }
                    is Boolean -> add(it.name, pr)
                    is Long -> add(it.name, pr)
                    is Int -> add(it.name, pr)
                    is Double -> add(it.name, pr)
                    is Float -> add(it.name, pr.toDouble())
                    is String -> add(it.name, pr)
                    is LocalDate -> add(it.name, pr)
                    is LocalDateTime -> add(it.name, pr)
                    is MutableList<*> -> {
                        val list = pr as? List
                        val jsonArray = Json.createArrayBuilder()
                        list?.forEach { jsonArray.add(it.toJSON()) }
                        add(it.name, jsonArray.build())
                    }
                }
            }
        }
    }
}

fun JsonStructure.toPrettyString(): String {
    return toString(JsonGenerator.PRETTY_PRINTING)
}

fun JsonStructure.toString(vararg options: String): String {
    val stringWriter = StringWriter()
    val config = options.associate { it to  true }
    val writerFactory = Json.createWriterFactory(config)
    val jsonWriter = writerFactory.createWriter(stringWriter)
    jsonWriter.write(this)
    jsonWriter.close()
    return stringWriter.toString()
}

fun  Iterable.toJSON() = Json.createArrayBuilder().apply { forEach { add(it.toJSON()) } }.build()

fun InputStream.toJSONArray(): JsonArray = Json.createReader(this).use { it.readArray() }
fun InputStream.toJSON(): JsonObject = Json.createReader(this).use { it.readObject() }

fun JsonObject?.contains(text: String?, ignoreCase: Boolean = true) =
        if (this == null || text == null) false else toString().toLowerCase().contains(text, ignoreCase)

fun JsonModel?.contains(text: String?, ignoreCase: Boolean = true) = this?.toJSON()?.contains(text, ignoreCase) ?: false

/**
 * Save this Json structure (JsonObject or JsonArray) to the given output stream and close it.
 */
fun JsonStructure.save(output: OutputStream) = Json.createWriter(output).use { it.write(this) }

/**
 * Save this Json structure (JsonObject or JsonArray) to the given output path.
 */
fun JsonStructure.save(output: Path, vararg options: OpenOption = arrayOf(CREATE, TRUNCATE_EXISTING)) = this.save(Files.newOutputStream(output, *options))

/**
 * Save this JsonModel to the given output stream and close it.
 */
fun JsonModel.save(output: OutputStream) = toJSON().save(output)

/**
 * Save this JsonModel to the given output path.
 */
fun JsonModel.save(output: Path, vararg options: OpenOption = arrayOf(CREATE, TRUNCATE_EXISTING)) = toJSON().save(output, *options)

/**
 * Load a JsonObject from the given URL
 */
fun loadJsonObject(url: URL) = loadJsonObject(url.openStream())

/**
 * Load a JsonObject from the given InputStream
 */
fun loadJsonObject(input: InputStream) = Json.createReader(input).use { it.readObject() }

/**
 * Load a JsonObject from the given path with the optional OpenOptions
 */
fun loadJsonObject(path: Path, vararg options: OpenOption = arrayOf(READ)) = Files.newInputStream(path, *options).use { loadJsonObject(it) }

/**
 * Load a JsonObject from the string source.
 */
fun loadJsonObject(source: String) = loadJsonObject(source.byteInputStream())

/**
 * Load a JsonArray from the given URL
 */
fun loadJsonArray(url: URL) = loadJsonArray(url.openStream())

/**
 * Load a JsonArray from the given string source
 */
fun loadJsonArray(source: String) = loadJsonArray(source.byteInputStream())

/**
 * Load a JsonArray from the given InputStream
 */
fun loadJsonArray(input: InputStream) = Json.createReader(input).use { it.readArray() }

/**
 * Load a JsonArray from the given path with the optional OpenOptions
 */
fun loadJsonArray(path: Path, vararg options: OpenOption = arrayOf(READ)) = Files.newInputStream(path, *options).use { loadJsonArray(it) }

/**
 * Load a JsonModel of the given type from the given URL
 */
inline fun  loadJsonModel(url: URL) = loadJsonObject(url).toModel()

/**
 * Load a JsonModel of the given type from the given InputStream
 */
inline fun  loadJsonModel(input: InputStream) = loadJsonObject(input).toModel()

/**
 * Load a JsonModel of the given type from the given path with the optional OpenOptions
 */
inline fun  loadJsonModel(path: Path, vararg options: OpenOption = arrayOf(READ)) = loadJsonObject(path, *options).toModel()

/**
 * Load a JsonModel from the given String source
 */
inline fun  loadJsonModel(source: String) = loadJsonObject(source).toModel()




© 2015 - 2025 Weber Informatics LLC | Privacy Policy