package ru.dbotthepony.kstarbound.io.json import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableSet import com.google.gson.JsonObject import com.google.gson.JsonSyntaxException import com.google.gson.TypeAdapter import com.google.gson.internal.bind.JsonTreeReader import com.google.gson.internal.bind.TypeAdapters import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.flattenJsonElement import ru.dbotthepony.kstarbound.util.NotNullVar import kotlin.properties.Delegates import kotlin.reflect.KClass import kotlin.reflect.KMutableProperty1 import kotlin.reflect.full.isSubclassOf /** * Данный интерфейс имеет один единственный метод: [acceptJson] * * Используется в связке с [BuilderAdapter] для классов, которым необходимо хранить оригинальную JSON структуру */ interface IJsonHolder { /** * Выставляет [JsonObject], который является источником данных для данной структуры */ fun acceptJson(json: JsonObject) } /** * Для классов, которые хотят принимать Java'вские [Map] напрямую, как оригинальную JSON структуру */ interface INativeJsonHolder : IJsonHolder { override fun acceptJson(json: JsonObject) { acceptJson(flattenJsonElement(json)) } fun acceptJson(json: MutableMap) } @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() } /** * [TypeAdapter] для классов, которые создаются "без всего", а после наполняются данными (паттерн builder). */ class BuilderAdapter private constructor( /** * Завод по созданию объектов типа [T] */ val factory: () -> T, /** * Свойства объекта [T], которые можно выставлять */ val properties: ImmutableMap>, /** * Свойства объекта [T], которые можно выставлять */ val flatProperties: ImmutableMap>, /** * Ключи, которые необходимо игнорировать при чтении JSON */ val ignoreKeys: ImmutableSet, /** * @see Builder.extraPropertiesAreFatal */ val extraPropertiesAreFatal: Boolean, /** * @see Builder.logMisses */ val logMisses: Boolean, ) : TypeAdapter() { private val loggedMisses = ObjectOpenHashSet() override fun write(writer: JsonWriter, value: T) { TODO("Not yet implemented") } override fun read(reader: JsonReader): T { @Suppress("name_shadowing") var reader = reader val missing = ObjectOpenHashSet>() missing.addAll(properties.values) val instance = factory.invoke() var json: JsonObject by Delegates.notNull() if (instance is IJsonHolder || flatProperties.isNotEmpty()) { json = TypeAdapters.JSON_ELEMENT.read(reader) as JsonObject reader = JsonTreeReader(json) if (instance is INativeJsonHolder) { instance.acceptJson(flattenJsonElement(json, Starbound.STRING_INTERNER::intern)) } else if (instance is IJsonHolder) { instance.acceptJson(json) } } reader.beginObject() while (reader.hasNext()) { val name = reader.nextName() if (ignoreKeys.contains(name)) { reader.skipValue() continue } val property = properties[name] if (property != null) { try { val peek = reader.peek() if (!property.returnType.isMarkedNullable && peek == JsonToken.NULL) { throw NullPointerException("Property ${property.name} of ${instance::class.qualifiedName} does not accept nulls (JSON contains null)") } else if (peek == JsonToken.NULL) { property.set(instance, null) reader.nextNull() } else { val readValue = property.adapter.read(reader) if (!property.returnType.isMarkedNullable && readValue == null) throw JsonSyntaxException("Property ${property.name} of ${instance::class.qualifiedName} does not accept nulls (Type provider returned null)") property.set(instance, readValue) check(missing.remove(property)) } } catch(err: Throwable) { throw JsonSyntaxException("Reading property ${property.name} of ${instance::class.qualifiedName} near ${reader.path}", err) } } else { if (extraPropertiesAreFatal) { throw JsonSyntaxException("$name is not a valid property of ${instance::class.qualifiedName}") } if (logMisses && loggedMisses.add(name)) { LOGGER.warn("${instance::class.qualifiedName} has no property for storing $name") } reader.skipValue() } } reader.endObject() for (property in flatProperties.values) { try { val read = property.adapter.read(JsonTreeReader(json)) if (!property.returnType.isMarkedNullable && read == null) { throw NullPointerException("Property ${property.name} of ${instance::class.qualifiedName} does not accept nulls (flat property adapter returned NULL)") } else if (read == null) { property.set(instance, null) } else { property.set(instance, read) } } catch(err: Throwable) { throw JsonSyntaxException("Reading flat property ${property.name} of ${instance::class.qualifiedName} near ${reader.path}", err) } } for (property in missing) { if (property.mustBePresent == true) { throw JsonSyntaxException("${instance::class.qualifiedName} demands for ${property.name} to be present, however, it is missing from JSON structure") } else if (property.mustBePresent == null) { if (property.returnType.isMarkedNullable) { continue } val delegate = property.property.getDelegate(instance) if (delegate is NotNullVar<*> && !delegate.isInitialized) { throw JsonSyntaxException("${property.name} in ${instance::class.qualifiedName} can not be null, but it is missing from JSON structure") } else { try { property.property.get(instance) } catch (err: Throwable) { throw JsonSyntaxException("${property.name} in ${instance::class.qualifiedName} does not like it being missing from JSON structure", err) } } } } return instance } data class WrappedProperty( val property: KMutableProperty1, val adapter: TypeAdapter, /** * @see PropertyConfigurator.mustBePresent */ val mustBePresent: Boolean?, ) { inline val name get() = property.name // кеш val returnType = property.returnType // так как дженерики тут немного слабенькие // Так что вот так... @Suppress("unchecked_cast") fun set(receiver: T, value: Any?) { property.set(receiver, value as V) } } class PropertyConfigurator( val property: KMutableProperty1, val adapter: TypeAdapter, ) { /** * Обязана ли присутствовать эта переменная внутри JSON структуры. * * * `true` - всегда кидать исключения * * `false` - никогда не кидать исключения * * `null` - кидать исключения на усмотрение реализации (по умолчанию) */ var mustBePresent: Boolean? = null } class Builder(val factory: () -> T, vararg fields: KMutableProperty1) { private val properties = ArrayList>() private val flatProperties = ArrayList>() private val ignoreKeys = ObjectArraySet() var extraPropertiesAreFatal = false var logMisses: Boolean? = null /** * Являются ли "лишние" ключи в JSON структуре ошибкой. * * Если "лишние" ключи являются ошибкой и известны некоторые лишние ключи, которые не нужны, * то [extraPropertiesAreFatal] можно скомбинировать с [ignoreKey]. */ fun extraPropertiesAreFatal(flag: Boolean = true): Builder { check(flatProperties.isEmpty() || !flag) { "Can't have both flattened properties and extraPropertiesAreFatal" } extraPropertiesAreFatal = flag return this } /** * Логировать ли несуществующие свойства у класса когда они попадаются в исходной JSON структуре */ fun logMisses(flag: Boolean = true): Builder { logMisses = flag return this } init { for (field in fields) auto(field) } private fun _add(property: KMutableProperty1, adapter: TypeAdapter, configurator: PropertyConfigurator.() -> Unit): PropertyConfigurator { if (properties.any { it.property == property }) { throw IllegalArgumentException("Property $property is defined twice") } if (flatProperties.any { it.property == property }) { throw IllegalArgumentException("Property $property is defined twice") } ignoreKeys.remove(property.name) val config = PropertyConfigurator(property, adapter) configurator.invoke(config) return config } /** * Добавляет указанное свойство в будущий адаптер с указанным [adapter] */ fun add(property: KMutableProperty1, adapter: TypeAdapter, configurator: PropertyConfigurator.() -> Unit = {}): Builder { val config = _add(property, adapter, configurator) properties.add(WrappedProperty( property, adapter, mustBePresent = config.mustBePresent )) return this } /** * Добавляет указанное свойство в будущий адаптер, как плоский объект внутри данного на том же уровне, что и сам объект, используя адаптер [adapter] * * Пример: * ```json * { * "prop_belong_to_a_1": ..., * "prop_belong_to_a_2": ..., * "prop_belong_to_b_1": ..., * } * ``` * * В данном случае, можно указать `b` как плоский класс внутри `a`. * * Данный подход позволяет избавиться от постоянного наследования и реализации одного и того же интерфейса во множестве других классов. * * Флаг [extraPropertiesAreFatal] не поддерживается с данными свойствами * * Если [logMisses] не указан явно, то он будет выставлен на false */ fun flat(property: KMutableProperty1, adapter: TypeAdapter, configurator: PropertyConfigurator.() -> Unit = {}): Builder { val config = _add(property, adapter, configurator) check(!extraPropertiesAreFatal) { "Can't have both flattened properties and extraPropertiesAreFatal" } flatProperties.add(WrappedProperty( property, adapter, mustBePresent = config.mustBePresent )) return this } /** * Автоматически определяет тип свойства и необходимый [TypeAdapter] */ 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() or directly specify type adapter method instead") } if (classifier.isSubclassOf(Map::class)) { throw IllegalArgumentException("${property.name} is a Map, please use autoMap() or directly specify type adapter method instead") } @Suppress("unchecked_cast") // classifier.java не имеет обозначенного типа return add(property, LazyTypeProvider(classifier.java) as TypeAdapter, configurator) } /** * Автоматически определяет тип свойства и необходимый [TypeAdapter], инкапсулированный в данный типе на одном уровне */ fun flat(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() or directly specify type adapter method instead") } if (classifier.isSubclassOf(Map::class)) { throw IllegalArgumentException("${property.name} is a Map, please use autoMap() or directly specify type adapter method instead") } @Suppress("unchecked_cast") // classifier.java не имеет обозначенного типа return flat(property, LazyTypeProvider(classifier.java) as TypeAdapter, configurator) } /** * Автоматически создаёт [ListAdapter] для заданного свойства */ inline fun autoList(property: KMutableProperty1>, noinline configurator: PropertyConfigurator>.() -> Unit = {}): Builder { return add(property, ListAdapter(V::class.java), configurator) } /** * Автоматически создаёт [ListAdapter] для заданного свойства, но в данном случае само свойство может принимать значение null */ inline fun autoNullableList(property: KMutableProperty1?>, noinline configurator: PropertyConfigurator?>.() -> Unit = {}): Builder { return add(property, ListAdapter(V::class.java).nullSafe(), 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 } fun build(): BuilderAdapter { val map = ImmutableMap.Builder>() for (property in properties) map.put(property.property.name, property) val map2 = ImmutableMap.Builder>() for (property in flatProperties) map2.put(property.property.name, property) return BuilderAdapter( factory = factory, properties = map.build(), flatProperties = map2.build(), ignoreKeys = ImmutableSet.copyOf(ignoreKeys), extraPropertiesAreFatal = extraPropertiesAreFatal, logMisses = logMisses ?: flatProperties.isEmpty(), ) } } companion object { private val LOGGER = LogManager.getLogger() } }