KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/BuilderAdapter.kt

426 lines
15 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ru.dbotthepony.kstarbound.io.json
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.internal.bind.JsonTreeReader
import com.google.gson.internal.bind.TypeAdapters
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.flattenJsonElement
import ru.dbotthepony.kstarbound.util.NotNullVar
import kotlin.properties.Delegates
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.isSubclassOf
/**
* Данный интерфейс имеет один единственный метод: [acceptJson]
*
* Используется в связке с [BuilderAdapter] для классов, которым необходимо хранить оригинальную JSON структуру
*/
interface IJsonHolder {
/**
* Выставляет [JsonObject], который является источником данных для данной структуры
*/
fun acceptJson(json: JsonObject)
}
/**
* Для классов, которые хотят принимать Java'вские [Map] напрямую, как оригинальную JSON структуру
*/
interface INativeJsonHolder : IJsonHolder {
override fun acceptJson(json: JsonObject) {
acceptJson(flattenJsonElement(json))
}
fun acceptJson(json: MutableMap<String, Any>)
}
@Suppress("FunctionName")
fun <T : Any> BuilderAdapter(factory: () -> T, vararg fields: KMutableProperty1<T, *>): BuilderAdapter<T> {
val builder = BuilderAdapter.Builder(factory)
for (field in fields) {
builder.auto(field)
}
return builder.build()
}
/**
* [TypeAdapter] для классов, которые создаются "без всего", а после наполняются данными (паттерн builder).
*/
class BuilderAdapter<T : Any> private constructor(
/**
* Завод по созданию объектов типа [T]
*/
val factory: () -> T,
/**
* Свойства объекта [T], которые можно выставлять
*/
val properties: ImmutableMap<String, WrappedProperty<T, *>>,
/**
* Свойства объекта [T], которые можно выставлять
*/
val flatProperties: ImmutableMap<String, WrappedProperty<T, *>>,
/**
* Ключи, которые необходимо игнорировать при чтении JSON
*/
val ignoreKeys: ImmutableSet<String>,
/**
* @see Builder.extraPropertiesAreFatal
*/
val extraPropertiesAreFatal: Boolean,
/**
* @see Builder.logMisses
*/
val logMisses: Boolean,
) : TypeAdapter<T>() {
private val loggedMisses = ObjectOpenHashSet<String>()
override fun write(writer: JsonWriter, value: T) {
TODO("Not yet implemented")
}
override fun read(reader: JsonReader): T {
@Suppress("name_shadowing")
var reader = reader
val missing = ObjectOpenHashSet<WrappedProperty<T, *>>()
missing.addAll(properties.values)
val instance = factory.invoke()
var json: JsonObject by Delegates.notNull()
if (instance is IJsonHolder || flatProperties.isNotEmpty()) {
json = TypeAdapters.JSON_ELEMENT.read(reader) as JsonObject
reader = JsonTreeReader(json)
if (instance is INativeJsonHolder) {
instance.acceptJson(flattenJsonElement(json, Starbound.STRING_INTERNER::intern))
} else if (instance is IJsonHolder) {
instance.acceptJson(json)
}
}
reader.beginObject()
while (reader.hasNext()) {
val name = reader.nextName()
if (ignoreKeys.contains(name)) {
reader.skipValue()
continue
}
val property = properties[name]
if (property != null) {
try {
val peek = reader.peek()
if (!property.returnType.isMarkedNullable && peek == JsonToken.NULL) {
throw NullPointerException("Property ${property.name} of ${instance::class.qualifiedName} does not accept nulls (JSON contains null)")
} else if (peek == JsonToken.NULL) {
property.set(instance, null)
reader.nextNull()
} else {
val readValue = property.adapter.read(reader)
if (!property.returnType.isMarkedNullable && readValue == null)
throw JsonSyntaxException("Property ${property.name} of ${instance::class.qualifiedName} does not accept nulls (Type provider returned null)")
property.set(instance, readValue)
check(missing.remove(property))
}
} catch(err: Throwable) {
throw JsonSyntaxException("Reading property ${property.name} of ${instance::class.qualifiedName} near ${reader.path}", err)
}
} else {
if (extraPropertiesAreFatal) {
throw JsonSyntaxException("$name is not a valid property of ${instance::class.qualifiedName}")
}
if (logMisses && loggedMisses.add(name)) {
LOGGER.warn("${instance::class.qualifiedName} has no property for storing $name")
}
reader.skipValue()
}
}
reader.endObject()
for (property in flatProperties.values) {
try {
val read = property.adapter.read(JsonTreeReader(json))
if (!property.returnType.isMarkedNullable && read == null) {
throw NullPointerException("Property ${property.name} of ${instance::class.qualifiedName} does not accept nulls (flat property adapter returned NULL)")
} else if (read == null) {
property.set(instance, null)
} else {
property.set(instance, read)
}
} catch(err: Throwable) {
throw JsonSyntaxException("Reading flat property ${property.name} of ${instance::class.qualifiedName} near ${reader.path}", err)
}
}
for (property in missing) {
if (property.mustBePresent == true) {
throw JsonSyntaxException("${instance::class.qualifiedName} demands for ${property.name} to be present, however, it is missing from JSON structure")
} else if (property.mustBePresent == null) {
if (property.returnType.isMarkedNullable) {
continue
}
val delegate = property.property.getDelegate(instance)
if (delegate is NotNullVar<*> && !delegate.isInitialized) {
throw JsonSyntaxException("${property.name} in ${instance::class.qualifiedName} can not be null, but it is missing from JSON structure")
} else {
try {
property.property.get(instance)
} catch (err: Throwable) {
throw JsonSyntaxException("${property.name} in ${instance::class.qualifiedName} does not like it being missing from JSON structure", err)
}
}
}
}
return instance
}
data class WrappedProperty<T, V : Any?>(
val property: KMutableProperty1<T, V>,
val adapter: TypeAdapter<V>,
/**
* @see PropertyConfigurator.mustBePresent
*/
val mustBePresent: Boolean?,
) {
inline val name get() = property.name
// кеш
val returnType = property.returnType
// так как дженерики тут немного слабенькие
// Так что вот так...
@Suppress("unchecked_cast")
fun set(receiver: T, value: Any?) {
property.set(receiver, value as V)
}
}
class PropertyConfigurator<T, V : Any?>(
val property: KMutableProperty1<T, V>,
val adapter: TypeAdapter<V>,
) {
/**
* Обязана ли присутствовать эта переменная внутри JSON структуры.
*
* * `true` - всегда кидать исключения
* * `false` - никогда не кидать исключения
* * `null` - кидать исключения на усмотрение реализации (по умолчанию)
*/
var mustBePresent: Boolean? = null
}
class Builder<T : Any>(val factory: () -> T, vararg fields: KMutableProperty1<T, *>) {
private val properties = ArrayList<WrappedProperty<T, *>>()
private val flatProperties = ArrayList<WrappedProperty<T, *>>()
private val ignoreKeys = ObjectArraySet<String>()
var extraPropertiesAreFatal = false
var logMisses: Boolean? = null
/**
* Являются ли "лишние" ключи в JSON структуре ошибкой.
*
* Если "лишние" ключи являются ошибкой и известны некоторые лишние ключи, которые не нужны,
* то [extraPropertiesAreFatal] можно скомбинировать с [ignoreKey].
*/
fun extraPropertiesAreFatal(flag: Boolean = true): Builder<T> {
check(flatProperties.isEmpty() || !flag) { "Can't have both flattened properties and extraPropertiesAreFatal" }
extraPropertiesAreFatal = flag
return this
}
/**
* Логировать ли несуществующие свойства у класса когда они попадаются в исходной JSON структуре
*/
fun logMisses(flag: Boolean = true): Builder<T> {
logMisses = flag
return this
}
init {
for (field in fields)
auto(field)
}
private fun <V> _add(property: KMutableProperty1<T, V>, adapter: TypeAdapter<V>, configurator: PropertyConfigurator<T, V>.() -> Unit): PropertyConfigurator<T, *> {
if (properties.any { it.property == property }) {
throw IllegalArgumentException("Property $property is defined twice")
}
if (flatProperties.any { it.property == property }) {
throw IllegalArgumentException("Property $property is defined twice")
}
ignoreKeys.remove(property.name)
val config = PropertyConfigurator(property, adapter)
configurator.invoke(config)
return config
}
/**
* Добавляет указанное свойство в будущий адаптер с указанным [adapter]
*/
fun <V> add(property: KMutableProperty1<T, V>, adapter: TypeAdapter<V>, configurator: PropertyConfigurator<T, V>.() -> Unit = {}): Builder<T> {
val config = _add(property, adapter, configurator)
properties.add(WrappedProperty(
property,
adapter,
mustBePresent = config.mustBePresent
))
return this
}
/**
* Добавляет указанное свойство в будущий адаптер, как плоский объект внутри данного на том же уровне, что и сам объект, используя адаптер [adapter]
*
* Пример:
* ```json
* {
* "prop_belong_to_a_1": ...,
* "prop_belong_to_a_2": ...,
* "prop_belong_to_b_1": ...,
* }
* ```
*
* В данном случае, можно указать `b` как плоский класс внутри `a`.
*
* Данный подход позволяет избавиться от постоянного наследования и реализации одного и того же интерфейса во множестве других классов.
*
* Флаг [extraPropertiesAreFatal] не поддерживается с данными свойствами
*
* Если [logMisses] не указан явно, то он будет выставлен на false
*/
fun <V> flat(property: KMutableProperty1<T, V>, adapter: TypeAdapter<V>, configurator: PropertyConfigurator<T, V>.() -> Unit = {}): Builder<T> {
val config = _add(property, adapter, configurator)
check(!extraPropertiesAreFatal) { "Can't have both flattened properties and extraPropertiesAreFatal" }
flatProperties.add(WrappedProperty(
property,
adapter,
mustBePresent = config.mustBePresent
))
return this
}
/**
* Автоматически определяет тип свойства и необходимый [TypeAdapter]
*/
fun <V> auto(property: KMutableProperty1<T, V>, configurator: PropertyConfigurator<T, V>.() -> Unit = {}): Builder<T> {
val returnType = property.returnType
val classifier = returnType.classifier as? KClass<*> ?: throw ClassCastException("Unable to cast ${returnType.classifier} to KClass of property ${property.name}!")
if (classifier.isSubclassOf(List::class)) {
throw IllegalArgumentException("${property.name} is a List, please use autoList() or directly specify type adapter method instead")
}
if (classifier.isSubclassOf(Map::class)) {
throw IllegalArgumentException("${property.name} is a Map, please use autoMap() or directly specify type adapter method instead")
}
@Suppress("unchecked_cast") // classifier.java не имеет обозначенного типа
return add(property, LazyTypeProvider(classifier.java) as TypeAdapter<V>, configurator)
}
/**
* Автоматически определяет тип свойства и необходимый [TypeAdapter], инкапсулированный в данный типе на одном уровне
*/
fun <V> flat(property: KMutableProperty1<T, V>, configurator: PropertyConfigurator<T, V>.() -> Unit = {}): Builder<T> {
val returnType = property.returnType
val classifier = returnType.classifier as? KClass<*> ?: throw ClassCastException("Unable to cast ${returnType.classifier} to KClass of property ${property.name}!")
if (classifier.isSubclassOf(List::class)) {
throw IllegalArgumentException("${property.name} is a List, please use autoList() or directly specify type adapter method instead")
}
if (classifier.isSubclassOf(Map::class)) {
throw IllegalArgumentException("${property.name} is a Map, please use autoMap() or directly specify type adapter method instead")
}
@Suppress("unchecked_cast") // classifier.java не имеет обозначенного типа
return flat(property, LazyTypeProvider(classifier.java) as TypeAdapter<V>, configurator)
}
/**
* Автоматически создаёт [ListAdapter] для заданного свойства
*/
inline fun <reified V : Any> autoList(property: KMutableProperty1<T, List<V>>, noinline configurator: PropertyConfigurator<T, List<V>>.() -> Unit = {}): Builder<T> {
return add(property, ListAdapter(V::class.java), configurator)
}
/**
* Автоматически создаёт [ListAdapter] для заданного свойства, но в данном случае само свойство может принимать значение null
*/
inline fun <reified V : Any> autoNullableList(property: KMutableProperty1<T, List<V>?>, noinline configurator: PropertyConfigurator<T, List<V>?>.() -> Unit = {}): Builder<T> {
return add(property, ListAdapter(V::class.java).nullSafe(), configurator)
}
fun ignoreKey(name: String): Builder<T> {
if (properties.any { it.property.name == name }) {
throw IllegalArgumentException("Can not ignore key $name because we have property with this name!")
}
ignoreKeys.add(name)
return this
}
fun build(): BuilderAdapter<T> {
val map = ImmutableMap.Builder<String, WrappedProperty<T, *>>()
for (property in properties)
map.put(property.property.name, property)
val map2 = ImmutableMap.Builder<String, WrappedProperty<T, *>>()
for (property in flatProperties)
map2.put(property.property.name, property)
return BuilderAdapter(
factory = factory,
properties = map.build(),
flatProperties = map2.build(),
ignoreKeys = ImmutableSet.copyOf(ignoreKeys),
extraPropertiesAreFatal = extraPropertiesAreFatal,
logMisses = logMisses ?: flatProperties.isEmpty(),
)
}
}
companion object {
private val LOGGER = LogManager.getLogger()
}
}