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(val factory: () -> T, vararg fields: KMutableProperty1) : TypeAdapter() { private val mappedFields = Object2ObjectArrayMap>() // потому что returnType медленный private val mappedFieldsReturnTypes = Object2ObjectArrayMap() private val loggedMisses = ObjectArraySet() private val ignoreProperties = ObjectArraySet() 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 } 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) } }