From bfae6877c97708bf67e636b01ada1f3f421207eb Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Sat, 31 Dec 2022 13:11:09 +0700 Subject: [PATCH] =?UTF-8?q?=D0=91=D0=BE=D0=BB=D0=B5=D0=B5=20=D0=B2=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=8F=D0=B5=D0=BC=D1=8B=D0=B9=20BuilderAdapter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../defs/projectile/Configurable.kt | 24 +- .../kstarbound/io/json/BuilderAdapter.kt | 243 +++++++++++------- .../kstarbound/io/json/FactoryAdapter.kt | 11 +- .../dbotthepony/kstarbound/util/NotNullVar.kt | 26 ++ 4 files changed, 192 insertions(+), 112 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/util/NotNullVar.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configurable.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configurable.kt index 9f6b79ec..c7c9f208 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configurable.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configurable.kt @@ -10,7 +10,9 @@ import ru.dbotthepony.kstarbound.defs.* import ru.dbotthepony.kstarbound.io.json.ConfigurableTypeAdapter import ru.dbotthepony.kstarbound.io.json.BuilderAdapter import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter +import ru.dbotthepony.kstarbound.util.NotNullVar import ru.dbotthepony.kvector.vector.Color +import java.util.concurrent.ConcurrentHashMap import kotlin.properties.Delegates class ConfigurableProjectile : RawPrototype() { @@ -182,9 +184,9 @@ class ActionConfig : IConfigurableAction { } companion object { - val ADAPTER = BuilderAdapter(::ActionConfig, ActionConfig::file).ignoreProperty("action") + val ADAPTER = BuilderAdapter.Builder(::ActionConfig, ActionConfig::file).ignoreKey("action").build() - private val cache = HashMap() + private val cache = ConcurrentHashMap() } } @@ -192,7 +194,7 @@ class ActionConfig : IConfigurableAction { * Создает новый прожектайл с заданными параметрами */ class ActionProjectile : IConfigurableAction { - lateinit var type: String + var type by NotNullVar() var angle = 0.0 var inheritDamageFactor = 1.0 @@ -201,11 +203,11 @@ class ActionProjectile : IConfigurableAction { } companion object { - val ADAPTER = BuilderAdapter(::ActionProjectile, + val ADAPTER = BuilderAdapter.Builder(::ActionProjectile, ActionProjectile::type, ActionProjectile::angle, ActionProjectile::inheritDamageFactor, - ).ignoreProperty("action").missingPropertiesAreFatal(false) + ).ignoreKey("action").build() } } @@ -220,9 +222,9 @@ class ActionSound : IConfigurableAction { } companion object { - val ADAPTER = BuilderAdapter(::ActionSound, + val ADAPTER = BuilderAdapter.Builder(::ActionSound, ActionSound::options, - ).ignoreProperty("action") + ).ignoreKey("action").build() } } @@ -238,10 +240,10 @@ class ActionLoop : IConfigurableAction { } companion object { - val ADAPTER = BuilderAdapter(::ActionLoop, + val ADAPTER = BuilderAdapter.Builder(::ActionLoop, ActionLoop::count, ActionLoop::body, - ).ignoreProperty("action") + ).ignoreKey("action").build() } } @@ -256,8 +258,8 @@ class ActionActions : IConfigurableAction { } companion object { - val ADAPTER = BuilderAdapter(::ActionActions, + val ADAPTER = BuilderAdapter.Builder(::ActionActions, ActionActions::list, - ).ignoreProperty("action") + ).ignoreKey("action").build() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/BuilderAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/BuilderAdapter.kt index af0d36fe..105dc174 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/BuilderAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/BuilderAdapter.kt @@ -1,140 +1,93 @@ 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.TypeAdapter import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter -import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap 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 ru.dbotthepony.kstarbound.Starbound import kotlin.reflect.KClass import kotlin.reflect.KMutableProperty1 -import kotlin.reflect.KType -import kotlin.reflect.full.isSuperclassOf +import kotlin.reflect.full.isSubclassOf + +@Suppress("FunctionName") +fun BuilderAdapter(factory: () -> T, vararg fields: KMutableProperty1): BuilderAdapter { + val builder = BuilderAdapter.Builder(factory) + + for (field in fields) { + builder.auto(field) + } + + return builder.build() +} /** - * Kotlin property aware adapter. - * - * Создаёт пустые классы, а после наполняет их данными, что подходит для builder'ов с очень - * большим количеством возможных данных внутри. - * - * Подходит для игровых структур которые могут быть "разобраны" и пересобраны. + * [TypeAdapter] для классов, которые создаются "без всего", а после наполняются данными (паттерн builder). */ -class BuilderAdapter(val factory: () -> T, vararg fields: KMutableProperty1) : TypeAdapter() { - private val mappedFields = Object2ObjectArrayMap>() - // потому что returnType медленный - private val mappedFieldsReturnTypes = Object2ObjectArrayMap() - private val loggedMisses = ObjectArraySet() +class BuilderAdapter private constructor( + /** + * Завод по созданию объектов типа [T] + */ + val factory: () -> T, - private val ignoreProperties = ObjectArraySet() + /** + * Свойства объекта [T], которые можно выставлять + */ + val properties: ImmutableMap>, - init { - for (field in fields) { - // потому что в котлине нет понятия KProperty который не имеет getter'а, только setter - require(mappedFields.put(field.name, field as KMutableProperty1) == null) { "${field.name} is defined twice" } - mappedFieldsReturnTypes[field.name] = field.returnType - } - } - - val fields: List> by lazy { - return@lazy ImmutableList.builder>().let { - for (v in mappedFields.values.iterator()) { - it.add(v) - } - - it.build() - } - } - - fun ignoreProperty(vararg value: String): BuilderAdapter { - ignoreProperties.addAll(value) - return this - } - - var missingPropertiesAreFatal = true - var missingLogLevel = Level.ERROR - - fun missingPropertiesAreFatal(flag: Boolean): BuilderAdapter { - missingPropertiesAreFatal = flag - return this - } - - fun missingLogLevel(level: Level): BuilderAdapter { - missingLogLevel = level - return this - } + /** + * Ключи, которые необходимо игнорировать при чтении JSON + */ + val ignoreKeys: ImmutableSet, +) : TypeAdapter() { + private val loggedMisses = ObjectOpenHashSet() override fun write(writer: JsonWriter, value: T) { TODO("Not yet implemented") } - override fun read(reader: JsonReader): T? { - if (reader.peek() == JsonToken.NULL) { - reader.nextNull() - return null - } + override fun read(reader: JsonReader): T { + val missing = ObjectArraySet>() + missing.addAll(properties.values) reader.beginObject() - val instance = factory.invoke()!! + + val instance = factory.invoke() while (reader.hasNext()) { 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 { val peek = reader.peek() - val expectedType = mappedFieldsReturnTypes[name]!! - if (!expectedType.isMarkedNullable && peek == JsonToken.NULL) { - throw NullPointerException("Property ${field.name} of ${instance::class.qualifiedName} does not accept nulls") + if (!property.returnType.isMarkedNullable && peek == JsonToken.NULL) { + throw NullPointerException("Property ${property.property.name} of ${instance::class.qualifiedName} does not accept nulls") } else if (peek == JsonToken.NULL) { - field.set(instance, null) + property.property.set(instance, null) reader.nextNull() } else { - val classifier = expectedType.classifier - - if (classifier is KClass<*>) { - 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") - } + val readValue = property.adapter.read(reader) + property.property.set(instance, readValue) + check(missing.remove(property)) } } catch(err: Throwable) { - throw JsonSyntaxException( - "Reading property ${field.name} of ${instance::class.qualifiedName} near ${reader.path}", - err - ) + throw JsonSyntaxException("Reading property ${property.property.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 { - if (!ignoreProperties.contains(name) && !loggedMisses.contains(name)) { - LOGGER.log(missingLogLevel, "{} has no property for storing {}", instance::class.qualifiedName, name) + if (!loggedMisses.contains(name)) { + LOGGER.warn("{} has no property for storing {}", instance::class.qualifiedName, name) loggedMisses.add(name) } @@ -143,9 +96,99 @@ class BuilderAdapter(val factory: () -> T, vararg fields: KMutableProperty1( + val property: KMutableProperty1, + val adapter: TypeAdapter, + val mustBePresent: Boolean?, + ) { + // кеш + val returnType = property.returnType + } + + class PropertyConfigurator( + val property: KMutableProperty1, + val adapter: TypeAdapter, + ) { + /** + * Обязана ли присутствовать эта переменная внутри JSON структуры. + */ + var mustBePresent: Boolean? = null + } + + class Builder(val factory: () -> T, vararg fields: KMutableProperty1) { + private val properties = ArrayList>() + private val ignoreKeys = ObjectArraySet() + + init { + for (field in fields) + auto(field) + } + + fun add(property: KMutableProperty1, adapter: TypeAdapter, configurator: PropertyConfigurator.() -> Unit = {}): Builder { + 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 auto(property: KMutableProperty1, configurator: PropertyConfigurator.() -> Unit = {}): Builder { + 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, configurator) + } + + fun ignoreKey(name: String): Builder { + 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 { + val map = ImmutableMap.Builder>() + + for (property in properties) + map.put(property.property.name, property as WrappedProperty) + + return BuilderAdapter(factory, map.build(), ImmutableSet.copyOf(ignoreKeys)) + } + } + companion object { private val LOGGER = LogManager.getLogger(ConfigurableTypeAdapter::class.java) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/FactoryAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/FactoryAdapter.kt index ce87d367..e7e4ba34 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/FactoryAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/FactoryAdapter.kt @@ -36,7 +36,7 @@ private data class PackedProperty( } /** - * TypeAdapter для классов которые создаются единожды и более не меняются ("бетонных классов"). + * [TypeAdapter] для классов, которые имеют все свои свойства в главном конструкторе. */ class FactoryAdapter private constructor( val bound: KClass, @@ -400,6 +400,15 @@ class FactoryAdapter private constructor( fun auto(field: KProperty1, transformer: (In) -> In = { it }): Builder { val returnType = field.returnType 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, transformer = transformer as (Any?) -> Any?)) return this } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/NotNullVar.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/NotNullVar.kt new file mode 100644 index 00000000..4fa18d42 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/NotNullVar.kt @@ -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 : ReadWriteProperty { + 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 + } +}