Более вменяемый BuilderAdapter
This commit is contained in:
parent
b3636e5a55
commit
bfae6877c9
@ -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<ConfigurableProjectile, ConfiguredProjectile>() {
|
||||
@ -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<String, CActionConfig>()
|
||||
private val cache = ConcurrentHashMap<String, CActionConfig>()
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,7 +194,7 @@ class ActionConfig : IConfigurableAction {
|
||||
* Создает новый прожектайл с заданными параметрами
|
||||
*/
|
||||
class ActionProjectile : IConfigurableAction {
|
||||
lateinit var type: String
|
||||
var type by NotNullVar<String>()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -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 <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.
|
||||
*
|
||||
* Создаёт пустые классы, а после наполняет их данными, что подходит для builder'ов с очень
|
||||
* большим количеством возможных данных внутри.
|
||||
*
|
||||
* Подходит для игровых структур которые могут быть "разобраны" и пересобраны.
|
||||
* [TypeAdapter] для классов, которые создаются "без всего", а после наполняются данными (паттерн builder).
|
||||
*/
|
||||
class BuilderAdapter<T>(val factory: () -> T, vararg fields: KMutableProperty1<T, *>) : TypeAdapter<T>() {
|
||||
private val mappedFields = Object2ObjectArrayMap<String, KMutableProperty1<T, in Any?>>()
|
||||
// потому что returnType медленный
|
||||
private val mappedFieldsReturnTypes = Object2ObjectArrayMap<String, KType>()
|
||||
private val loggedMisses = ObjectArraySet<String>()
|
||||
class BuilderAdapter<T : Any> private constructor(
|
||||
/**
|
||||
* Завод по созданию объектов типа [T]
|
||||
*/
|
||||
val factory: () -> T,
|
||||
|
||||
private val ignoreProperties = ObjectArraySet<String>()
|
||||
/**
|
||||
* Свойства объекта [T], которые можно выставлять
|
||||
*/
|
||||
val properties: ImmutableMap<String, WrappedProperty<T, Any?>>,
|
||||
|
||||
init {
|
||||
for (field in fields) {
|
||||
// потому что в котлине нет понятия KProperty который не имеет getter'а, только setter
|
||||
require(mappedFields.put(field.name, field as KMutableProperty1<T, in Any?>) == null) { "${field.name} is defined twice" }
|
||||
mappedFieldsReturnTypes[field.name] = field.returnType
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
/**
|
||||
* Ключи, которые необходимо игнорировать при чтении JSON
|
||||
*/
|
||||
val ignoreKeys: ImmutableSet<String>,
|
||||
) : TypeAdapter<T>() {
|
||||
private val loggedMisses = ObjectOpenHashSet<String>()
|
||||
|
||||
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<WrappedProperty<T, Any?>>()
|
||||
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<T>(val factory: () -> T, vararg fields: KMutableProperty1<T
|
||||
}
|
||||
|
||||
reader.endObject()
|
||||
|
||||
for (property in missing) {
|
||||
if (property.mustBePresent == null) {
|
||||
// null - проверяем, есть ли делегат
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
private val LOGGER = LogManager.getLogger(ConfigurableTypeAdapter::class.java)
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ private data class PackedProperty<Clazz : Any, T>(
|
||||
}
|
||||
|
||||
/**
|
||||
* TypeAdapter для классов которые создаются единожды и более не меняются ("бетонных классов").
|
||||
* [TypeAdapter] для классов, которые имеют все свои свойства в главном конструкторе.
|
||||
*/
|
||||
class FactoryAdapter<T : Any> private constructor(
|
||||
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> {
|
||||
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<Any?>, transformer = transformer as (Any?) -> Any?))
|
||||
return this
|
||||
}
|
||||
|
26
src/main/kotlin/ru/dbotthepony/kstarbound/util/NotNullVar.kt
Normal file
26
src/main/kotlin/ru/dbotthepony/kstarbound/util/NotNullVar.kt
Normal 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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user