426 lines
15 KiB
Kotlin
426 lines
15 KiB
Kotlin
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()
|
||
}
|
||
}
|