Более вменяемый BuilderAdapter

This commit is contained in:
DBotThePony 2022-12-31 13:11:09 +07:00
parent b3636e5a55
commit bfae6877c9
Signed by: DBot
GPG Key ID: DCC23B5715498507
4 changed files with 192 additions and 112 deletions

View File

@ -10,7 +10,9 @@ import ru.dbotthepony.kstarbound.defs.*
import ru.dbotthepony.kstarbound.io.json.ConfigurableTypeAdapter import ru.dbotthepony.kstarbound.io.json.ConfigurableTypeAdapter
import ru.dbotthepony.kstarbound.io.json.BuilderAdapter import ru.dbotthepony.kstarbound.io.json.BuilderAdapter
import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter
import ru.dbotthepony.kstarbound.util.NotNullVar
import ru.dbotthepony.kvector.vector.Color import ru.dbotthepony.kvector.vector.Color
import java.util.concurrent.ConcurrentHashMap
import kotlin.properties.Delegates import kotlin.properties.Delegates
class ConfigurableProjectile : RawPrototype<ConfigurableProjectile, ConfiguredProjectile>() { class ConfigurableProjectile : RawPrototype<ConfigurableProjectile, ConfiguredProjectile>() {
@ -182,9 +184,9 @@ class ActionConfig : IConfigurableAction {
} }
companion object { companion object {
val ADAPTER = BuilderAdapter(::ActionConfig, ActionConfig::file).ignoreProperty("action") val ADAPTER = BuilderAdapter.Builder(::ActionConfig, ActionConfig::file).ignoreKey("action").build()
private val cache = HashMap<String, CActionConfig>() private val cache = ConcurrentHashMap<String, CActionConfig>()
} }
} }
@ -192,7 +194,7 @@ class ActionConfig : IConfigurableAction {
* Создает новый прожектайл с заданными параметрами * Создает новый прожектайл с заданными параметрами
*/ */
class ActionProjectile : IConfigurableAction { class ActionProjectile : IConfigurableAction {
lateinit var type: String var type by NotNullVar<String>()
var angle = 0.0 var angle = 0.0
var inheritDamageFactor = 1.0 var inheritDamageFactor = 1.0
@ -201,11 +203,11 @@ class ActionProjectile : IConfigurableAction {
} }
companion object { companion object {
val ADAPTER = BuilderAdapter(::ActionProjectile, val ADAPTER = BuilderAdapter.Builder(::ActionProjectile,
ActionProjectile::type, ActionProjectile::type,
ActionProjectile::angle, ActionProjectile::angle,
ActionProjectile::inheritDamageFactor, ActionProjectile::inheritDamageFactor,
).ignoreProperty("action").missingPropertiesAreFatal(false) ).ignoreKey("action").build()
} }
} }
@ -220,9 +222,9 @@ class ActionSound : IConfigurableAction {
} }
companion object { companion object {
val ADAPTER = BuilderAdapter(::ActionSound, val ADAPTER = BuilderAdapter.Builder(::ActionSound,
ActionSound::options, ActionSound::options,
).ignoreProperty("action") ).ignoreKey("action").build()
} }
} }
@ -238,10 +240,10 @@ class ActionLoop : IConfigurableAction {
} }
companion object { companion object {
val ADAPTER = BuilderAdapter(::ActionLoop, val ADAPTER = BuilderAdapter.Builder(::ActionLoop,
ActionLoop::count, ActionLoop::count,
ActionLoop::body, ActionLoop::body,
).ignoreProperty("action") ).ignoreKey("action").build()
} }
} }
@ -256,8 +258,8 @@ class ActionActions : IConfigurableAction {
} }
companion object { companion object {
val ADAPTER = BuilderAdapter(::ActionActions, val ADAPTER = BuilderAdapter.Builder(::ActionActions,
ActionActions::list, ActionActions::list,
).ignoreProperty("action") ).ignoreKey("action").build()
} }
} }

View File

@ -1,140 +1,93 @@
package ru.dbotthepony.kstarbound.io.json package ru.dbotthepony.kstarbound.io.json
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import org.apache.logging.log4j.Level import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.Starbound
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KType import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.isSuperclassOf
@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()
}
/** /**
* Kotlin property aware adapter. * [TypeAdapter] для классов, которые создаются "без всего", а после наполняются данными (паттерн builder).
*
* Создаёт пустые классы, а после наполняет их данными, что подходит для builder'ов с очень
* большим количеством возможных данных внутри.
*
* Подходит для игровых структур которые могут быть "разобраны" и пересобраны.
*/ */
class BuilderAdapter<T>(val factory: () -> T, vararg fields: KMutableProperty1<T, *>) : TypeAdapter<T>() { class BuilderAdapter<T : Any> private constructor(
private val mappedFields = Object2ObjectArrayMap<String, KMutableProperty1<T, in Any?>>() /**
// потому что returnType медленный * Завод по созданию объектов типа [T]
private val mappedFieldsReturnTypes = Object2ObjectArrayMap<String, KType>() */
private val loggedMisses = ObjectArraySet<String>() val factory: () -> T,
private val ignoreProperties = ObjectArraySet<String>() /**
* Свойства объекта [T], которые можно выставлять
*/
val properties: ImmutableMap<String, WrappedProperty<T, Any?>>,
init { /**
for (field in fields) { * Ключи, которые необходимо игнорировать при чтении JSON
// потому что в котлине нет понятия KProperty который не имеет getter'а, только setter */
require(mappedFields.put(field.name, field as KMutableProperty1<T, in Any?>) == null) { "${field.name} is defined twice" } val ignoreKeys: ImmutableSet<String>,
mappedFieldsReturnTypes[field.name] = field.returnType ) : TypeAdapter<T>() {
} private val loggedMisses = ObjectOpenHashSet<String>()
}
val fields: List<KMutableProperty1<T, in Any?>> by lazy {
return@lazy ImmutableList.builder<KMutableProperty1<T, in Any?>>().let {
for (v in mappedFields.values.iterator()) {
it.add(v)
}
it.build()
}
}
fun ignoreProperty(vararg value: String): BuilderAdapter<T> {
ignoreProperties.addAll(value)
return this
}
var missingPropertiesAreFatal = true
var missingLogLevel = Level.ERROR
fun missingPropertiesAreFatal(flag: Boolean): BuilderAdapter<T> {
missingPropertiesAreFatal = flag
return this
}
fun missingLogLevel(level: Level): BuilderAdapter<T> {
missingLogLevel = level
return this
}
override fun write(writer: JsonWriter, value: T) { override fun write(writer: JsonWriter, value: T) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override fun read(reader: JsonReader): T? { override fun read(reader: JsonReader): T {
if (reader.peek() == JsonToken.NULL) { val missing = ObjectArraySet<WrappedProperty<T, Any?>>()
reader.nextNull() missing.addAll(properties.values)
return null
}
reader.beginObject() reader.beginObject()
val instance = factory.invoke()!!
val instance = factory.invoke()
while (reader.hasNext()) { while (reader.hasNext()) {
val name = reader.nextName() val name = reader.nextName()
val field = mappedFields[name]
if (field != null) { if (ignoreKeys.contains(name)) {
reader.skipValue()
continue
}
val property = properties[name]
if (property != null) {
try { try {
val peek = reader.peek() val peek = reader.peek()
val expectedType = mappedFieldsReturnTypes[name]!!
if (!expectedType.isMarkedNullable && peek == JsonToken.NULL) { if (!property.returnType.isMarkedNullable && peek == JsonToken.NULL) {
throw NullPointerException("Property ${field.name} of ${instance::class.qualifiedName} does not accept nulls") throw NullPointerException("Property ${property.property.name} of ${instance::class.qualifiedName} does not accept nulls")
} else if (peek == JsonToken.NULL) { } else if (peek == JsonToken.NULL) {
field.set(instance, null) property.property.set(instance, null)
reader.nextNull() reader.nextNull()
} else { } else {
val classifier = expectedType.classifier val readValue = property.adapter.read(reader)
property.property.set(instance, readValue)
if (classifier is KClass<*>) { check(missing.remove(property))
if (classifier.isSuperclassOf(Float::class)) {
val read = reader.nextDouble()
field.set(instance, read.toFloat())
} else if (classifier.isSuperclassOf(Double::class)) {
val read = reader.nextDouble()
field.set(instance, read)
} else if (classifier.isSuperclassOf(Int::class)) {
val read = reader.nextInt()
field.set(instance, read)
} else if (classifier.isSuperclassOf(Long::class)) {
val read = reader.nextLong()
field.set(instance, read)
} else if (classifier.isSuperclassOf(String::class)) {
val read = reader.nextString()
field.set(instance, read)
} else if (classifier.isSuperclassOf(Boolean::class)) {
val read = reader.nextBoolean()
field.set(instance, read)
} else {
field.set(instance, Starbound.gson.fromJson(reader, classifier.java))
}
} else {
throw IllegalStateException("Expected ${field.name} classifier to be KClass, got $classifier")
}
} }
} catch(err: Throwable) { } catch(err: Throwable) {
throw JsonSyntaxException( throw JsonSyntaxException("Reading property ${property.property.name} of ${instance::class.qualifiedName} near ${reader.path}", err)
"Reading property ${field.name} of ${instance::class.qualifiedName} near ${reader.path}",
err
)
} }
} else if (!ignoreProperties.contains(name) && missingPropertiesAreFatal) {
throw JsonSyntaxException("Property $name is not present in ${instance::class.qualifiedName}")
} else { } else {
if (!ignoreProperties.contains(name) && !loggedMisses.contains(name)) { if (!loggedMisses.contains(name)) {
LOGGER.log(missingLogLevel, "{} has no property for storing {}", instance::class.qualifiedName, name) LOGGER.warn("{} has no property for storing {}", instance::class.qualifiedName, name)
loggedMisses.add(name) loggedMisses.add(name)
} }
@ -143,9 +96,99 @@ class BuilderAdapter<T>(val factory: () -> T, vararg fields: KMutableProperty1<T
} }
reader.endObject() reader.endObject()
for (property in missing) {
if (property.mustBePresent == null) {
// null - проверяем, есть ли делегат
}
}
return instance return instance
} }
data class WrappedProperty<T, V : Any?>(
val property: KMutableProperty1<T, V>,
val adapter: TypeAdapter<V>,
val mustBePresent: Boolean?,
) {
// кеш
val returnType = property.returnType
}
class PropertyConfigurator<T, V : Any?>(
val property: KMutableProperty1<T, V>,
val adapter: TypeAdapter<V>,
) {
/**
* Обязана ли присутствовать эта переменная внутри JSON структуры.
*/
var mustBePresent: Boolean? = null
}
class Builder<T : Any>(val factory: () -> T, vararg fields: KMutableProperty1<T, *>) {
private val properties = ArrayList<WrappedProperty<T, *>>()
private val ignoreKeys = ObjectArraySet<String>()
init {
for (field in fields)
auto(field)
}
fun <V> add(property: KMutableProperty1<T, V>, adapter: TypeAdapter<V>, configurator: PropertyConfigurator<T, V>.() -> Unit = {}): Builder<T> {
if (properties.any { it.property == property }) {
throw IllegalArgumentException("Property $property is defined twice")
}
ignoreKeys.remove(property.name)
val config = PropertyConfigurator(property, adapter)
configurator.invoke(config)
properties.add(WrappedProperty(
property,
adapter,
mustBePresent = config.mustBePresent
))
return this
}
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() method instead")
}
if (classifier.isSubclassOf(Map::class)) {
throw IllegalArgumentException("${property.name} is a Map, please use autoMap() method instead")
}
@Suppress("unchecked_cast")
return add(property, LazyTypeProvider(classifier.java) as TypeAdapter<V>, 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
}
@Suppress("unchecked_cast")
fun build(): BuilderAdapter<T> {
val map = ImmutableMap.Builder<String, WrappedProperty<T, Any?>>()
for (property in properties)
map.put(property.property.name, property as WrappedProperty<T, Any?>)
return BuilderAdapter(factory, map.build(), ImmutableSet.copyOf(ignoreKeys))
}
}
companion object { companion object {
private val LOGGER = LogManager.getLogger(ConfigurableTypeAdapter::class.java) private val LOGGER = LogManager.getLogger(ConfigurableTypeAdapter::class.java)
} }

View File

@ -36,7 +36,7 @@ private data class PackedProperty<Clazz : Any, T>(
} }
/** /**
* TypeAdapter для классов которые создаются единожды и более не меняются ("бетонных классов"). * [TypeAdapter] для классов, которые имеют все свои свойства в главном конструкторе.
*/ */
class FactoryAdapter<T : Any> private constructor( class FactoryAdapter<T : Any> private constructor(
val bound: KClass<T>, val bound: KClass<T>,
@ -400,6 +400,15 @@ class FactoryAdapter<T : Any> private constructor(
fun <In> auto(field: KProperty1<T, In>, transformer: (In) -> In = { it }): Builder<T> { fun <In> auto(field: KProperty1<T, In>, transformer: (In) -> In = { it }): Builder<T> {
val returnType = field.returnType val returnType = field.returnType
val classifier = returnType.classifier as? KClass<*> ?: throw ClassCastException("Unable to cast ${returnType.classifier} to KClass of property ${field.name}!") val classifier = returnType.classifier as? KClass<*> ?: throw ClassCastException("Unable to cast ${returnType.classifier} to KClass of property ${field.name}!")
if (classifier.isSubclassOf(List::class)) {
throw IllegalArgumentException("${field.name} is a List, please use autoList() method instead")
}
if (classifier.isSubclassOf(Map::class)) {
throw IllegalArgumentException("${field.name} is a Map, please use autoMap() method instead")
}
types.add(PackedProperty(field, LazyTypeProvider(classifier.java) as TypeAdapter<Any?>, transformer = transformer as (Any?) -> Any?)) types.add(PackedProperty(field, LazyTypeProvider(classifier.java) as TypeAdapter<Any?>, transformer = transformer as (Any?) -> Any?))
return this return this
} }

View File

@ -0,0 +1,26 @@
package ru.dbotthepony.kstarbound.util
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import kotlin.properties.Delegates
/**
* Аналог [Delegates.notNull], но со свойством [isInitialized]
*/
class NotNullVar<V : Any> : ReadWriteProperty<Any, V> {
private var value: V? = null
/**
* Имеет ли данный делегат не-null значение
*/
val isInitialized: Boolean
get() = value != null
override fun getValue(thisRef: Any, property: KProperty<*>): V {
return value ?: throw IllegalStateException("Property ${property.name} was not initialized")
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: V) {
this.value = value
}
}