Более вменяемый 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.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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
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