153 lines
5.1 KiB
Kotlin
153 lines
5.1 KiB
Kotlin
package ru.dbotthepony.kstarbound.io.json
|
||
|
||
import com.google.common.collect.ImmutableList
|
||
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 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
|
||
|
||
/**
|
||
* Kotlin property aware adapter.
|
||
*
|
||
* Создаёт пустые классы, а после наполняет их данными, что подходит для 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>()
|
||
|
||
private val ignoreProperties = ObjectArraySet<String>()
|
||
|
||
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
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
reader.beginObject()
|
||
val instance = factory.invoke()!!
|
||
|
||
while (reader.hasNext()) {
|
||
val name = reader.nextName()
|
||
val field = mappedFields[name]
|
||
|
||
if (field != 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")
|
||
} else if (peek == JsonToken.NULL) {
|
||
field.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")
|
||
}
|
||
}
|
||
} catch(err: Throwable) {
|
||
throw JsonSyntaxException(
|
||
"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 {
|
||
if (!ignoreProperties.contains(name) && !loggedMisses.contains(name)) {
|
||
LOGGER.log(missingLogLevel, "{} has no property for storing {}", instance::class.qualifiedName, name)
|
||
loggedMisses.add(name)
|
||
}
|
||
|
||
reader.skipValue()
|
||
}
|
||
}
|
||
|
||
reader.endObject()
|
||
return instance
|
||
}
|
||
|
||
companion object {
|
||
private val LOGGER = LogManager.getLogger(ConfigurableTypeAdapter::class.java)
|
||
}
|
||
}
|