Делаем композицию вместо билдера + наследования

This commit is contained in:
DBotThePony 2023-08-13 19:51:01 +07:00
parent a97e51a51d
commit 3617b38196
Signed by: DBot
GPG Key ID: DCC23B5715498507
31 changed files with 487 additions and 777 deletions

View File

@ -19,20 +19,20 @@ import ru.dbotthepony.kstarbound.api.PhysicalFile
import ru.dbotthepony.kstarbound.defs.* import ru.dbotthepony.kstarbound.defs.*
import ru.dbotthepony.kstarbound.defs.image.AtlasConfiguration import ru.dbotthepony.kstarbound.defs.image.AtlasConfiguration
import ru.dbotthepony.kstarbound.defs.image.ImageReference import ru.dbotthepony.kstarbound.defs.image.ImageReference
import ru.dbotthepony.kstarbound.defs.item.impl.BackArmorItemPrototype import ru.dbotthepony.kstarbound.defs.item.impl.BackArmorItemDefinition
import ru.dbotthepony.kstarbound.defs.item.impl.ChestArmorItemPrototype import ru.dbotthepony.kstarbound.defs.item.impl.ChestArmorItemDefinition
import ru.dbotthepony.kstarbound.defs.item.impl.CurrencyItemPrototype import ru.dbotthepony.kstarbound.defs.item.impl.CurrencyItemDefinition
import ru.dbotthepony.kstarbound.defs.item.impl.FlashlightPrototype import ru.dbotthepony.kstarbound.defs.item.impl.FlashlightDefinition
import ru.dbotthepony.kstarbound.defs.item.impl.HarvestingToolPrototype import ru.dbotthepony.kstarbound.defs.item.impl.HarvestingToolPrototype
import ru.dbotthepony.kstarbound.defs.item.impl.HeadArmorItemPrototype import ru.dbotthepony.kstarbound.defs.item.impl.HeadArmorItemDefinition
import ru.dbotthepony.kstarbound.defs.item.api.IArmorItemDefinition import ru.dbotthepony.kstarbound.defs.item.api.IArmorItemDefinition
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.defs.item.InventoryIcon import ru.dbotthepony.kstarbound.defs.item.InventoryIcon
import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition
import ru.dbotthepony.kstarbound.defs.item.impl.ItemPrototype import ru.dbotthepony.kstarbound.defs.item.impl.ItemDefinition
import ru.dbotthepony.kstarbound.defs.item.impl.LegsArmorItemPrototype import ru.dbotthepony.kstarbound.defs.item.impl.LegsArmorItemDefinition
import ru.dbotthepony.kstarbound.defs.item.impl.LiquidItemPrototype import ru.dbotthepony.kstarbound.defs.item.impl.LiquidItemDefinition
import ru.dbotthepony.kstarbound.defs.item.impl.MaterialItemPrototype import ru.dbotthepony.kstarbound.defs.item.impl.MaterialItemDefinition
import ru.dbotthepony.kstarbound.defs.monster.MonsterSkillDefinition import ru.dbotthepony.kstarbound.defs.monster.MonsterSkillDefinition
import ru.dbotthepony.kstarbound.defs.monster.MonsterTypeDefinition import ru.dbotthepony.kstarbound.defs.monster.MonsterTypeDefinition
import ru.dbotthepony.kstarbound.defs.npc.NpcTypeDefinition import ru.dbotthepony.kstarbound.defs.npc.NpcTypeDefinition
@ -85,7 +85,6 @@ import java.lang.ref.ReferenceQueue
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.text.DateFormat import java.text.DateFormat
import java.time.Duration import java.time.Duration
import java.util.*
import java.util.function.BiConsumer import java.util.function.BiConsumer
import java.util.function.BinaryOperator import java.util.function.BinaryOperator
import java.util.function.Function import java.util.function.Function
@ -1056,16 +1055,16 @@ class Starbound : ISBFileLocator {
private fun loadItemDefinitions(callback: (String) -> Unit, files: Map<String, Collection<IStarboundFile>>) { private fun loadItemDefinitions(callback: (String) -> Unit, files: Map<String, Collection<IStarboundFile>>) {
val fileMap = mapOf( val fileMap = mapOf(
"item" to ItemPrototype::class.java, "item" to ItemDefinition::class.java,
"currency" to CurrencyItemPrototype::class.java, "currency" to CurrencyItemDefinition::class.java,
"liqitem" to LiquidItemPrototype::class.java, "liqitem" to LiquidItemDefinition::class.java,
"matitem" to MaterialItemPrototype::class.java, "matitem" to MaterialItemDefinition::class.java,
"flashlight" to FlashlightPrototype::class.java, "flashlight" to FlashlightDefinition::class.java,
"harvestingtool" to HarvestingToolPrototype::class.java, "harvestingtool" to HarvestingToolPrototype::class.java,
"head" to HeadArmorItemPrototype::class.java, "head" to HeadArmorItemDefinition::class.java,
"chest" to ChestArmorItemPrototype::class.java, "chest" to ChestArmorItemDefinition::class.java,
"legs" to LegsArmorItemPrototype::class.java, "legs" to LegsArmorItemDefinition::class.java,
"back" to BackArmorItemPrototype::class.java, "back" to BackArmorItemDefinition::class.java,
) )
for ((ext, clazz) in fileMap) { for ((ext, clazz) in fileMap) {
@ -1075,7 +1074,7 @@ class Starbound : ISBFileLocator {
try { try {
callback("Loading $listedFile") callback("Loading $listedFile")
val json = gson.fromJson(listedFile.reader(), JsonObject::class.java) val json = gson.fromJson(listedFile.reader(), JsonObject::class.java)
val def: ItemPrototype = pathStack(listedFile.computeDirectory()) { gson.fromJson(JsonTreeReader(json), clazz) } val def: IItemDefinition = pathStack(listedFile.computeDirectory()) { gson.fromJson(JsonTreeReader(json), clazz) }
_items.add(def, json, listedFile, gson, pathStack) _items.add(def, json, listedFile, gson, pathStack)
} catch (err: Throwable) { } catch (err: Throwable) {
logger.error("Loading item definition file $listedFile", err) logger.error("Loading item definition file $listedFile", err)

View File

@ -1,108 +0,0 @@
package ru.dbotthepony.kstarbound.defs
import ru.dbotthepony.kstarbound.io.json.builder.JsonIgnoreProperty
import ru.dbotthepony.kstarbound.util.INotNullDelegate
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
abstract class FreezableDefintionBuilder {
interface IProperty {
fun checkAndThrow()
}
inner class Nullable<V>(private var value: V? = null) : ReadWriteProperty<Any?, V?>, IProperty {
override fun getValue(thisRef: Any?, property: KProperty<*>): V? {
return value
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: V?) {
if (isFrozen) throw IllegalStateException("$thisRef is frozen!")
this.value = value
}
override fun checkAndThrow() {
// no op
}
override fun equals(other: Any?): Boolean {
return other is Nullable<*> && other.value == value
}
override fun hashCode(): Int {
return value?.hashCode() ?: 0
}
override fun toString(): String {
return "Nullable[$value]"
}
}
inner class NotNull<V : Any>(private var value: V? = null) : ReadWriteProperty<Any?, V>, IProperty, INotNullDelegate {
override val isPresent: Boolean
get() = value != null
fun getNullable(): V? {
return value
}
override fun getValue(thisRef: Any?, property: KProperty<*>): V {
return checkNotNull(value) {"${property.name} was not initialized"}
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: V) {
if (isFrozen) throw IllegalStateException("$thisRef is frozen!")
this.value = value
}
override fun checkAndThrow() {
if (value == null) {
throw NullPointerException("Value was not initialized")
}
}
override fun equals(other: Any?): Boolean {
return other is NotNull<*> && other.value == value
}
override fun hashCode(): Int {
return value?.hashCode() ?: 0
}
override fun toString(): String {
return "NotNull[$value]"
}
}
@JsonIgnoreProperty
var isFrozen = false
private set
@JsonIgnoreProperty
private val properties = ArrayList<IProperty>()
override fun equals(other: Any?): Boolean {
if (other is FreezableDefintionBuilder && other.javaClass === this.javaClass)
return other.properties == properties
return false
}
override fun hashCode(): Int {
return properties.hashCode()
}
override fun toString(): String {
return "${this::class.simpleName}[frozen=$isFrozen,values={${properties.joinToString(",")}}]"
}
protected open fun onFreeze() {
}
fun freeze() {
if (isFrozen) return
onFreeze()
properties.forEach { it.checkAndThrow() }
isFrozen = true
}
}

View File

@ -1,5 +1,10 @@
package ru.dbotthepony.kstarbound.defs package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableList
import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.io.json.builder.JsonImplementation
@JsonImplementation(IScriptable.Impl::class)
interface IScriptable { interface IScriptable {
/** /**
* Lua скрипты для выполнения * Lua скрипты для выполнения
@ -10,4 +15,10 @@ interface IScriptable {
* Через какое количество тиков вызывать обновления скриптов * Через какое количество тиков вызывать обновления скриптов
*/ */
val scriptDelta: Int val scriptDelta: Int
@JsonFactory
data class Impl(
override val scripts: ImmutableList<DirectAssetReference> = ImmutableList.of(),
override val scriptDelta: Int = 1
) : IScriptable
} }

View File

@ -125,11 +125,15 @@ data class ThingDescription(
"shortdescription" -> shortdescription = `in`.nextString() "shortdescription" -> shortdescription = `in`.nextString()
"description" -> description = `in`.nextString() "description" -> description = `in`.nextString()
else -> { else -> {
if (name.endsWith("shortdescription") || name.endsWith("shortDescription") || name.endsWith("Shortdescription") || name.endsWith("ShortDescription")) { try {
racialShort.put(interner.intern(name.substring(0, name.length - "shortdescription".length)), interner.intern(`in`.nextString())) if (name.endsWith("shortdescription") || name.endsWith("shortDescription") || name.endsWith("Shortdescription") || name.endsWith("ShortDescription")) {
} else if (name.endsWith("description") || name.endsWith("Description")) { racialShort.put(interner.intern(name.substring(0, name.length - "shortdescription".length)), interner.intern(`in`.nextString()))
racial.put(interner.intern(name.substring(0, name.length - "description".length)), interner.intern(`in`.nextString())) } else if (name.endsWith("description") || name.endsWith("Description")) {
} else { racial.put(interner.intern(name.substring(0, name.length - "description".length)), interner.intern(`in`.nextString()))
} else {
`in`.skipValue()
}
} catch (_: IllegalStateException) {
`in`.skipValue() `in`.skipValue()
} }
} }

View File

@ -31,8 +31,8 @@ data class ItemReference(
override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? { override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (type.rawType == ItemReference::class.java) { if (type.rawType == ItemReference::class.java) {
return object : TypeAdapter<ItemReference>() { return object : TypeAdapter<ItemReference>() {
private val regularObject = FactoryAdapter.createFor(ItemReference::class, JsonFactory(storesJson = false, logMisses = true, asList = false), gson, stringInterner) private val regularObject = FactoryAdapter.createFor(ItemReference::class, JsonFactory(storesJson = false, asList = false), gson, stringInterner)
private val regularList = FactoryAdapter.createFor(ItemReference::class, JsonFactory(storesJson = false, logMisses = true, asList = true), gson, stringInterner) private val regularList = FactoryAdapter.createFor(ItemReference::class, JsonFactory(storesJson = false, asList = true), gson, stringInterner)
private val references = gson.getAdapter(TypeToken.getParameterized(RegistryReference::class.java, IItemDefinition::class.java)) as TypeAdapter<RegistryReference<IItemDefinition>> private val references = gson.getAdapter(TypeToken.getParameterized(RegistryReference::class.java, IItemDefinition::class.java)) as TypeAdapter<RegistryReference<IItemDefinition>>
override fun write(out: JsonWriter, value: ItemReference?) { override fun write(out: JsonWriter, value: ItemReference?) {

View File

@ -4,7 +4,7 @@ import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
@JsonFactory @JsonFactory
data class JumpProfile( data class JumpProfile(
val jumpSpeed: Double, val jumpSpeed: Double = 0.0,
val jumpInitialPercentage: Double, val jumpInitialPercentage: Double = 0.0,
val jumpHoldTime: Double, val jumpHoldTime: Double = 0.0,
) )

View File

@ -4,7 +4,10 @@ import ru.dbotthepony.kstarbound.defs.IThingWithDescription
import ru.dbotthepony.kstarbound.defs.RegistryReference import ru.dbotthepony.kstarbound.defs.RegistryReference
import ru.dbotthepony.kstarbound.defs.item.IInventoryIcon import ru.dbotthepony.kstarbound.defs.item.IInventoryIcon
import ru.dbotthepony.kstarbound.defs.item.ItemRarity import ru.dbotthepony.kstarbound.defs.item.ItemRarity
import ru.dbotthepony.kstarbound.defs.item.impl.ItemDefinition
import ru.dbotthepony.kstarbound.io.json.builder.JsonImplementation
@JsonImplementation(ItemDefinition::class)
interface IItemDefinition : IThingWithDescription { interface IItemDefinition : IThingWithDescription {
/** /**
* Внутреннее имя предмета (ID). * Внутреннее имя предмета (ID).

View File

@ -1,6 +1,5 @@
package ru.dbotthepony.kstarbound.defs.item.api package ru.dbotthepony.kstarbound.defs.item.api
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.util.Either import ru.dbotthepony.kstarbound.util.Either
interface ILiquidItem : IItemDefinition { interface ILiquidItem : IItemDefinition {

View File

@ -0,0 +1,51 @@
package ru.dbotthepony.kstarbound.defs.item.impl
import com.google.common.collect.ImmutableList
import ru.dbotthepony.kstarbound.defs.DirectAssetReference
import ru.dbotthepony.kstarbound.defs.IScriptable
import ru.dbotthepony.kstarbound.defs.item.api.IArmorItemDefinition
import ru.dbotthepony.kstarbound.defs.item.LeveledStatusEffect
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.io.json.builder.JsonBuilder
import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.io.json.builder.JsonFlat
@JsonFactory
data class ArmorItemDefinition(
@JsonFlat
val parent: IItemDefinition,
@JsonFlat
val script: IScriptable.Impl,
override val maxStack: Long = 1L,
override val colorOptions: ImmutableList<Map<String, String>> = ImmutableList.of(),
override val maleFrames: IArmorItemDefinition.Frames,
override val femaleFrames: IArmorItemDefinition.Frames,
override val level: Double = 1.0,
override val leveledStatusEffects: ImmutableList<LeveledStatusEffect> = ImmutableList.of(),
) : IArmorItemDefinition, IItemDefinition by parent, IScriptable by script
@JsonFactory
class HeadArmorItemDefinition(@JsonFlat val parent: ArmorItemDefinition) : IArmorItemDefinition by parent {
override val itemType: String
get() = "headarmor"
}
@JsonFactory
class ChestArmorItemDefinition(@JsonFlat val parent: ArmorItemDefinition) : IArmorItemDefinition by parent {
override val itemType: String
get() = "chestarmor"
}
@JsonFactory
class LegsArmorItemDefinition(@JsonFlat val parent: ArmorItemDefinition) : IArmorItemDefinition by parent {
override val itemType: String
get() = "legsarmor"
}
@JsonFactory
class BackArmorItemDefinition(@JsonFlat val parent: ArmorItemDefinition) : IArmorItemDefinition by parent {
override val itemType: String
get() = "backarmor"
}

View File

@ -1,47 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.impl
import com.google.common.collect.ImmutableList
import ru.dbotthepony.kstarbound.defs.DirectAssetReference
import ru.dbotthepony.kstarbound.defs.item.api.IArmorItemDefinition
import ru.dbotthepony.kstarbound.defs.item.LeveledStatusEffect
import ru.dbotthepony.kstarbound.io.json.builder.JsonBuilder
@JsonBuilder
abstract class ArmorItemPrototype : ItemPrototype(), IArmorItemDefinition {
final override var colorOptions: ImmutableList<Map<String, String>> by NotNull(ImmutableList.of())
final override var maleFrames: IArmorItemDefinition.Frames by NotNull()
final override var femaleFrames: IArmorItemDefinition.Frames by NotNull()
final override var level: Double by NotNull(1.0)
final override var leveledStatusEffects: ImmutableList<LeveledStatusEffect> by NotNull(ImmutableList.of())
final override var scripts: ImmutableList<DirectAssetReference> by NotNull(ImmutableList.of())
final override var scriptDelta: Int by NotNull(1)
init {
maxStack = 1L
}
}
@JsonBuilder
class HeadArmorItemPrototype : ArmorItemPrototype() {
override val itemType: String
get() = "headarmor"
}
@JsonBuilder
class ChestArmorItemPrototype : ArmorItemPrototype() {
override val itemType: String
get() = "chestarmor"
}
@JsonBuilder
class LegsArmorItemPrototype : ArmorItemPrototype() {
override val itemType: String
get() = "legsarmor"
}
@JsonBuilder
class BackArmorItemPrototype : ArmorItemPrototype() {
override val itemType: String
get() = "backarmor"
}

View File

@ -0,0 +1,19 @@
package ru.dbotthepony.kstarbound.defs.item.impl
import ru.dbotthepony.kstarbound.defs.item.api.ICurrencyItemDefinition
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.io.json.builder.JsonBuilder
import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.io.json.builder.JsonFlat
@JsonFactory
data class CurrencyItemDefinition(
@JsonFlat
val parent: IItemDefinition,
override val maxStack: Long = 16777216L,
override var currency: String,
override var value: Long,
) : ICurrencyItemDefinition, IItemDefinition by parent {
override val itemType: String
get() = super<ICurrencyItemDefinition>.itemType
}

View File

@ -1,14 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.impl
import ru.dbotthepony.kstarbound.defs.item.api.ICurrencyItemDefinition
import ru.dbotthepony.kstarbound.io.json.builder.JsonBuilder
@JsonBuilder
class CurrencyItemPrototype : ItemPrototype(), ICurrencyItemDefinition {
override var currency: String by NotNull()
override var value: Long by NotNull()
init {
maxStack = 16777216L
}
}

View File

@ -0,0 +1,21 @@
package ru.dbotthepony.kstarbound.defs.item.impl
import ru.dbotthepony.kstarbound.defs.item.api.IFlashlightDefinition
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.io.json.builder.JsonFlat
import ru.dbotthepony.kvector.vector.Color
import ru.dbotthepony.kvector.vector.ndouble.Vector2d
@JsonFactory
class FlashlightDefinition(
@JsonFlat
val parent: IItemDefinition,
override val lightPosition: Vector2d,
override val lightColor: Color,
override val beamLevel: Int,
override val beamAmbience: Double,
override val handPosition: Vector2d,
override val maxStack: Long = 1L,
) : IItemDefinition by parent, IFlashlightDefinition

View File

@ -1,19 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.impl
import ru.dbotthepony.kstarbound.defs.item.api.IFlashlightDefinition
import ru.dbotthepony.kstarbound.io.json.builder.JsonBuilder
import ru.dbotthepony.kvector.vector.Color
import ru.dbotthepony.kvector.vector.ndouble.Vector2d
@JsonBuilder
class FlashlightPrototype : ItemPrototype(), IFlashlightDefinition {
override var lightPosition: Vector2d by NotNull()
override var lightColor: Color by NotNull()
override var beamLevel: Int by NotNull()
override var beamAmbience: Double by NotNull()
override var handPosition: Vector2d by NotNull()
init {
maxStack = 1L
}
}

View File

@ -2,21 +2,23 @@ package ru.dbotthepony.kstarbound.defs.item.impl
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import ru.dbotthepony.kstarbound.defs.item.api.IHarvestingToolDefinition import ru.dbotthepony.kstarbound.defs.item.api.IHarvestingToolDefinition
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.io.json.builder.JsonBuilder import ru.dbotthepony.kstarbound.io.json.builder.JsonBuilder
import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.io.json.builder.JsonFlat
import ru.dbotthepony.kvector.vector.ndouble.Vector2d import ru.dbotthepony.kvector.vector.ndouble.Vector2d
@JsonBuilder @JsonFactory
class HarvestingToolPrototype : ItemPrototype(), IHarvestingToolDefinition { class HarvestingToolPrototype(
override var frames: Int by NotNull() @JsonFlat
override var animationCycle: Double by NotNull() val parent: IItemDefinition,
override var blockRadius: Int by NotNull() override val frames: Int,
override var altBlockRadius: Int by NotNull(0) override val animationCycle: Double,
override var idleSound: ImmutableList<String> by NotNull(ImmutableList.of()) override val blockRadius: Int,
override var strikeSounds: ImmutableList<String> by NotNull(ImmutableList.of()) override val altBlockRadius: Int = 0,
override var handPosition: Vector2d by NotNull() override val idleSound: ImmutableList<String> = ImmutableList.of(),
override var fireTime: Double by NotNull() override val strikeSounds: ImmutableList<String> = ImmutableList.of(),
override val handPosition: Vector2d,
init { override val fireTime: Double,
maxStack = 1L override val maxStack: Long = 1L,
} ) : IItemDefinition by parent, IHarvestingToolDefinition
}

View File

@ -0,0 +1,39 @@
package ru.dbotthepony.kstarbound.defs.item.impl
import com.google.common.collect.ImmutableList
import ru.dbotthepony.kstarbound.defs.IThingWithDescription
import ru.dbotthepony.kstarbound.defs.RegistryReference
import ru.dbotthepony.kstarbound.defs.ThingDescription
import ru.dbotthepony.kstarbound.defs.item.IInventoryIcon
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.defs.item.InventoryIcon
import ru.dbotthepony.kstarbound.defs.item.ItemRarity
import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.io.json.builder.JsonFlat
@JsonFactory
data class ItemDefinition(
@JsonFlat
val descriptionData: ThingDescription,
override var itemName: String,
override var price: Long = 0,
override var rarity: ItemRarity = ItemRarity.COMMON,
override var category: String,
override var inventoryIcon: ImmutableList<IInventoryIcon>? = null,
override var itemTags: ImmutableList<String> = ImmutableList.of(),
override var learnBlueprintsOnPickup: ImmutableList<RegistryReference<IItemDefinition>> = ImmutableList.of(),
override var maxStack: Long = 9999L,
override var eventCategory: String? = null,
override var consumeOnPickup: Boolean = false,
override var pickupQuestTemplates: ImmutableList<String> = ImmutableList.of(),
override var tooltipKind: String = "normal",
override var twoHanded: Boolean = false,
override var radioMessagesOnPickup: ImmutableList<String> = ImmutableList.of(),
override var fuelAmount: Long = 0,
override var pickupSoundsSmall: ImmutableList<String> = ImmutableList.of(),
override var pickupSoundsMedium: ImmutableList<String> = ImmutableList.of(),
override var pickupSoundsLarge: ImmutableList<String> = ImmutableList.of(),
override var smallStackLimit: Long? = null,
override var mediumStackLimit: Long? = null,
) : IItemDefinition, IThingWithDescription by descriptionData

View File

@ -1,56 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.impl
import com.google.common.collect.ImmutableList
import ru.dbotthepony.kstarbound.defs.FreezableDefintionBuilder
import ru.dbotthepony.kstarbound.defs.RegistryReference
import ru.dbotthepony.kstarbound.defs.ThingDescription
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.defs.item.InventoryIcon
import ru.dbotthepony.kstarbound.defs.item.ItemRarity
import ru.dbotthepony.kstarbound.io.json.builder.JsonBuilder
import ru.dbotthepony.kstarbound.io.json.builder.JsonPropertyConfig
import ru.dbotthepony.kstarbound.io.json.builder.JsonIgnoreProperty
@JsonBuilder
open class ItemPrototype : FreezableDefintionBuilder(), IItemDefinition {
@JsonIgnoreProperty
final override val shortdescription: String
get() = descriptionData.shortdescription
@JsonIgnoreProperty
final override val description: String
get() = descriptionData.description
@JsonIgnoreProperty
final override val racialDescription: Map<String, String>
get() = descriptionData.racialDescription
@JsonIgnoreProperty
final override val racialShortDescription: Map<String, String>
get() = descriptionData.racialShortDescription
@JsonPropertyConfig(isFlat = true)
var descriptionData: ThingDescription by NotNull()
final override var itemName: String by NotNull()
final override var price: Long by NotNull(0L)
final override var rarity: ItemRarity by NotNull(ItemRarity.COMMON)
final override var category: String by NotNull()
final override var inventoryIcon: ImmutableList<InventoryIcon>? by Nullable()
final override var itemTags: ImmutableList<String> by NotNull(ImmutableList.of())
final override var learnBlueprintsOnPickup: ImmutableList<RegistryReference<IItemDefinition>> by NotNull(ImmutableList.of())
final override var maxStack: Long by NotNull(9999L)
final override var eventCategory: String? by Nullable()
final override var consumeOnPickup: Boolean by NotNull(false)
final override var pickupQuestTemplates: ImmutableList<String> by NotNull(ImmutableList.of())
final override var tooltipKind: String by NotNull("normal")
final override var twoHanded: Boolean by NotNull(false)
final override var radioMessagesOnPickup: ImmutableList<String> by NotNull(ImmutableList.of())
final override var fuelAmount: Long by NotNull(0L)
final override var pickupSoundsSmall: ImmutableList<String> by NotNull(ImmutableList.of())
final override var pickupSoundsMedium: ImmutableList<String> by NotNull(ImmutableList.of())
final override var pickupSoundsLarge: ImmutableList<String> by NotNull(ImmutableList.of())
final override var smallStackLimit: Long? by Nullable()
final override var mediumStackLimit: Long? by Nullable()
}

View File

@ -0,0 +1,20 @@
package ru.dbotthepony.kstarbound.defs.item.impl
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.defs.item.api.ILiquidItem
import ru.dbotthepony.kstarbound.io.json.builder.JsonAlias
import ru.dbotthepony.kstarbound.io.json.builder.JsonBuilder
import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.io.json.builder.JsonFlat
import ru.dbotthepony.kstarbound.util.Either
@JsonFactory
data class LiquidItemDefinition(
@JsonFlat
val parent: IItemDefinition,
@JsonAlias("liquidId", "liquidName")
override val liquid: Either<Int, String>,
) : IItemDefinition by parent, ILiquidItem {
override val itemType: String
get() = super<ILiquidItem>.itemType
}

View File

@ -1,25 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.impl
import ru.dbotthepony.kstarbound.defs.item.api.ILiquidItem
import ru.dbotthepony.kstarbound.io.json.builder.JsonBuilder
import ru.dbotthepony.kstarbound.util.Either
@JsonBuilder
class LiquidItemPrototype : ItemPrototype(), ILiquidItem {
private val liquidDelegate = NotNull<Either<Int, String>>()
override var liquid: Either<Int, String> by liquidDelegate
var liquidId: Int?
get() = liquidDelegate.getNullable()?.left
set(value) { if (liquidDelegate.getNullable() == null) liquid = Either.left(value!!) }
var liquidName: String?
get() = liquidDelegate.getNullable()?.right
set(value) { liquid = Either.right(value!!) }
override fun onFreeze() {
if (liquidDelegate.getNullable() == null) {
throw NullPointerException("no liquidId nor liquidName was defined")
}
}
}

View File

@ -0,0 +1,21 @@
package ru.dbotthepony.kstarbound.defs.item.impl
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.defs.item.api.ILiquidItem
import ru.dbotthepony.kstarbound.defs.item.api.IMaterialItem
import ru.dbotthepony.kstarbound.io.json.builder.JsonAlias
import ru.dbotthepony.kstarbound.io.json.builder.JsonBuilder
import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.io.json.builder.JsonFlat
import ru.dbotthepony.kstarbound.util.Either
@JsonFactory
data class MaterialItemDefinition(
@JsonFlat
val parent: IItemDefinition,
@JsonAlias("materialId", "materialName")
override val material: Either<Int, String>
) : IMaterialItem, IItemDefinition by parent {
override val itemType: String
get() = super<IMaterialItem>.itemType
}

View File

@ -1,25 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.impl
import ru.dbotthepony.kstarbound.defs.item.api.IMaterialItem
import ru.dbotthepony.kstarbound.io.json.builder.JsonBuilder
import ru.dbotthepony.kstarbound.util.Either
@JsonBuilder
class MaterialItemPrototype : ItemPrototype(), IMaterialItem {
private val materialDelegate = NotNull<Either<Int, String>>()
override var material: Either<Int, String> by materialDelegate
var materialId: Int?
get() = materialDelegate.getNullable()?.left
set(value) { if (materialDelegate.getNullable() == null) material = Either.left(value!!) }
var materialName: String?
get() = materialDelegate.getNullable()?.right
set(value) { material = Either.right(value!!) }
override fun onFreeze() {
if (material == null) {
throw NullPointerException("no materialId nor materialName was defined")
}
}
}

View File

@ -12,23 +12,26 @@ import ru.dbotthepony.kstarbound.defs.RegistryReference
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition
import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.io.json.builder.JsonFlat
import ru.dbotthepony.kstarbound.util.Either
@JsonFactory @JsonFactory
data class MonsterTypeDefinition( data class MonsterTypeDefinition(
val type: String, val type: String,
override val shortdescription: String, @JsonFlat
override val description: String, val desc: IThingWithDescription,
val categories: ImmutableSet<String> = ImmutableSet.of(), val categories: ImmutableSet<String> = ImmutableSet.of(),
val parts: ImmutableSet<String> = ImmutableSet.of(), val parts: ImmutableSet<String> = ImmutableSet.of(),
val animation: AssetReference<AnimationDefinition>, val animation: AssetReference<AnimationDefinition>,
// [ { "default" : "poptopTreasure", "bow" : "poptopHunting" } ], // [ { "default" : "poptopTreasure", "bow" : "poptopHunting" } ],
val dropPools: ImmutableList<ImmutableMap<String, RegistryReference<TreasurePoolDefinition>>>, // "dropPools" : [ "smallRobotTreasure" ],
val dropPools: Either<ImmutableList<ImmutableMap<String, RegistryReference<TreasurePoolDefinition>>>, ImmutableList<RegistryReference<TreasurePoolDefinition>>>,
val baseParameters: BaseParameters val baseParameters: BaseParameters
) : IThingWithDescription { ) : IThingWithDescription by desc {
@JsonFactory @JsonFactory
data class BaseParameters( data class BaseParameters(
val movementSettings: MovementParameters? = null, val movementSettings: MovementParameters? = null,
override val scriptDelta: Int = 1, @JsonFlat
override val scripts: ImmutableList<DirectAssetReference> val script: IScriptable,
) : IScriptable ) : IScriptable by script
} }

View File

@ -1,13 +1,11 @@
package ru.dbotthepony.kstarbound.defs.tile package ru.dbotthepony.kstarbound.defs.tile
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.gson.GsonBuilder
import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.IThingWithDescription import ru.dbotthepony.kstarbound.defs.IThingWithDescription
import ru.dbotthepony.kstarbound.defs.ThingDescription import ru.dbotthepony.kstarbound.defs.ThingDescription
import ru.dbotthepony.kstarbound.io.json.builder.FactoryAdapter
import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.io.json.builder.JsonPropertyConfig import ru.dbotthepony.kstarbound.io.json.builder.JsonFlat
@JsonFactory @JsonFactory
data class MaterialModifier( data class MaterialModifier(
@ -23,7 +21,7 @@ data class MaterialModifier(
val footstepSound: String? = null, val footstepSound: String? = null,
val miningSounds: ImmutableList<String> = ImmutableList.of(), val miningSounds: ImmutableList<String> = ImmutableList.of(),
@JsonPropertyConfig(isFlat = true) @JsonFlat
val descriptionData: ThingDescription, val descriptionData: ThingDescription,
override val renderTemplate: AssetReference<RenderTemplate>, override val renderTemplate: AssetReference<RenderTemplate>,

View File

@ -1,14 +1,11 @@
package ru.dbotthepony.kstarbound.defs.tile package ru.dbotthepony.kstarbound.defs.tile
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.gson.GsonBuilder
import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.IThingWithDescription import ru.dbotthepony.kstarbound.defs.IThingWithDescription
import ru.dbotthepony.kstarbound.defs.ThingDescription import ru.dbotthepony.kstarbound.defs.ThingDescription
import ru.dbotthepony.kstarbound.io.json.builder.FactoryAdapter
import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.io.json.builder.JsonPropertyConfig import ru.dbotthepony.kstarbound.io.json.builder.JsonFlat
import ru.dbotthepony.kstarbound.registerTypeAdapter
import ru.dbotthepony.kvector.vector.Color import ru.dbotthepony.kvector.vector.Color
@JsonFactory @JsonFactory
@ -26,7 +23,7 @@ data class TileDefinition(
val health: Double = 0.0, val health: Double = 0.0,
val category: String, val category: String,
@JsonPropertyConfig(isFlat = true) @JsonFlat
val descriptionData: ThingDescription, val descriptionData: ThingDescription,
override val renderTemplate: AssetReference<RenderTemplate>, override val renderTemplate: AssetReference<RenderTemplate>,

View File

@ -1,19 +1,20 @@
package ru.dbotthepony.kstarbound.defs.util package ru.dbotthepony.kstarbound.defs.util
import com.github.benmanes.caffeine.cache.Interner
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
/** /**
* Возвращает глубокую, неизменяемую копию [input] примитивов/List'ов/Map'ов * Возвращает глубокую, неизменяемую копию [input] примитивов/List'ов/Map'ов
*/ */
fun enrollList(input: List<Any>, interner: (String) -> String = String::intern): ImmutableList<Any> { fun enrollList(input: List<Any>, interner: Interner<String> = Interner { it }): ImmutableList<Any> {
val builder = ImmutableList.builder<Any>() val builder = ImmutableList.builder<Any>()
for (v in input) { for (v in input) {
when (v) { when (v) {
is Map<*, *> -> builder.add(enrollMap(v as Map<String, Any>, interner)) is Map<*, *> -> builder.add(enrollMap(v as Map<String, Any>, interner))
is List<*> -> builder.add(enrollList(v as List<Any>, interner)) is List<*> -> builder.add(enrollList(v as List<Any>, interner))
else -> builder.add((v as? String)?.let(interner) ?: v) else -> builder.add((v as? String)?.let(interner::intern) ?: v)
} }
} }
@ -23,14 +24,14 @@ fun enrollList(input: List<Any>, interner: (String) -> String = String::intern):
/** /**
* Возвращает глубокую, неизменяемую копию [input] примитивов/List'ов/Map'ов * Возвращает глубокую, неизменяемую копию [input] примитивов/List'ов/Map'ов
*/ */
fun enrollMap(input: Map<String, Any>, interner: (String) -> String = String::intern): ImmutableMap<String, Any> { fun enrollMap(input: Map<String, Any>, interner: Interner<String> = Interner { it }): ImmutableMap<String, Any> {
val builder = ImmutableMap.builder<String, Any>() val builder = ImmutableMap.builder<String, Any>()
for ((k, v) in input) { for ((k, v) in input) {
when (v) { when (v) {
is Map<*, *> -> builder.put(interner(k), enrollMap(v as Map<String, Any>, interner)) is Map<*, *> -> builder.put(interner.intern(k), enrollMap(v as Map<String, Any>, interner))
is List<*> -> builder.put(interner(k), enrollList(v as List<Any>, interner)) is List<*> -> builder.put(interner.intern(k), enrollList(v as List<Any>, interner))
else -> builder.put(interner(k), (v as? String)?.let(interner) ?: v) else -> builder.put(interner.intern(k), (v as? String)?.let(interner::intern) ?: v)
} }
} }

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.defs.util package ru.dbotthepony.kstarbound.defs.util
import com.github.benmanes.caffeine.cache.Interner
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonNull import com.google.gson.JsonNull
@ -7,17 +8,17 @@ import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
private fun flattenJsonPrimitive(input: JsonPrimitive, interner: (String) -> String = String::intern): Any { private fun flattenJsonPrimitive(input: JsonPrimitive, interner: Interner<String> = Interner { it }): Any {
if (input.isNumber) { if (input.isNumber) {
return input.asNumber return input.asNumber
} else if (input.isString) { } else if (input.isString) {
return interner(input.asString) return interner.intern(input.asString)
} else { } else {
return input.asBoolean return input.asBoolean
} }
} }
private fun flattenJsonArray(input: JsonArray, interner: (String) -> String = String::intern): ArrayList<Any> { private fun flattenJsonArray(input: JsonArray, interner: Interner<String> = Interner { it }): ArrayList<Any> {
val flattened = ArrayList<Any>(input.size()) val flattened = ArrayList<Any>(input.size())
for (v in input) { for (v in input) {
@ -32,7 +33,7 @@ private fun flattenJsonArray(input: JsonArray, interner: (String) -> String = St
return flattened return flattened
} }
private fun flattenJsonObject(input: JsonObject, interner: (String) -> String = String::intern): MutableMap<String, Any> { private fun flattenJsonObject(input: JsonObject, interner: Interner<String> = Interner { it }): MutableMap<String, Any> {
val flattened = Object2ObjectOpenHashMap<String, Any>() val flattened = Object2ObjectOpenHashMap<String, Any>()
for ((k, v) in input.entrySet()) { for ((k, v) in input.entrySet()) {
@ -46,7 +47,7 @@ private fun flattenJsonObject(input: JsonObject, interner: (String) -> String =
return flattened return flattened
} }
fun flattenJsonElement(input: JsonElement, interner: (String) -> String = String::intern): Any? { fun flattenJsonElement(input: JsonElement, interner: Interner<String> = Interner { it }): Any? {
return when (input) { return when (input) {
is JsonObject -> flattenJsonObject(input, interner) is JsonObject -> flattenJsonObject(input, interner)
is JsonArray -> flattenJsonArray(input, interner) is JsonArray -> flattenJsonArray(input, interner)
@ -56,6 +57,6 @@ fun flattenJsonElement(input: JsonElement, interner: (String) -> String = String
} }
} }
fun flattenJsonElement(input: JsonObject, interner: (String) -> String = String::intern) = flattenJsonObject(input, interner) fun flattenJsonElement(input: JsonObject, interner: Interner<String> = Interner { it }) = flattenJsonObject(input, interner)
fun flattenJsonElement(input: JsonArray, interner: (String) -> String = String::intern) = flattenJsonArray(input, interner) fun flattenJsonElement(input: JsonArray, interner: Interner<String> = Interner { it }) = flattenJsonArray(input, interner)
fun flattenJsonElement(input: JsonPrimitive, interner: (String) -> String = String::intern) = flattenJsonPrimitive(input, interner) fun flattenJsonElement(input: JsonPrimitive, interner: Interner<String> = Interner { it }) = flattenJsonPrimitive(input, interner)

View File

@ -1,9 +1,11 @@
package ru.dbotthepony.kstarbound.io.json package ru.dbotthepony.kstarbound.io.json
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonElement
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory import com.google.gson.TypeAdapterFactory
import com.google.gson.internal.bind.JsonTreeReader
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
@ -24,6 +26,7 @@ object EitherTypeAdapter : TypeAdapterFactory {
return object : TypeAdapter<Either<Any, Any>>() { return object : TypeAdapter<Either<Any, Any>>() {
private val leftAdapter = gson.getAdapter(TypeToken.get(left)) as TypeAdapter<Any?> private val leftAdapter = gson.getAdapter(TypeToken.get(left)) as TypeAdapter<Any?>
private val rightAdapter = gson.getAdapter(TypeToken.get(right)) as TypeAdapter<Any?> private val rightAdapter = gson.getAdapter(TypeToken.get(right)) as TypeAdapter<Any?>
private val elemAdapter = gson.getAdapter(JsonElement::class.java)
override fun write(out: JsonWriter, value: Either<Any, Any>?) { override fun write(out: JsonWriter, value: Either<Any, Any>?) {
if (value == null) if (value == null)
@ -36,11 +39,13 @@ object EitherTypeAdapter : TypeAdapterFactory {
if (`in`.peek() == JsonToken.NULL) if (`in`.peek() == JsonToken.NULL)
return null return null
val elem = elemAdapter.read(`in`)
return try { return try {
Either.left(leftAdapter.read(`in`) ?: throw NullPointerException("left was empty")) Either.left(leftAdapter.read(JsonTreeReader(elem)) ?: throw NullPointerException("left was empty"))
} catch(leftError: Throwable) { } catch(leftError: Throwable) {
try { try {
Either.right(rightAdapter.read(`in`) ?: throw NullPointerException("right was empty")) Either.right(rightAdapter.read(JsonTreeReader(elem)) ?: throw NullPointerException("right was empty"))
} catch(rightError: Throwable) { } catch(rightError: Throwable) {
val error = JsonSyntaxException("Can't read Either of values (left is $left, right is $right)") val error = JsonSyntaxException("Can't read Either of values (left is $left, right is $right)")
error.addSuppressed(leftError) error.addSuppressed(leftError)

View File

@ -1,6 +1,11 @@
package ru.dbotthepony.kstarbound.io.json package ru.dbotthepony.kstarbound.io.json
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory import com.google.gson.TypeAdapterFactory
@ -93,3 +98,44 @@ fun JsonReader.consumeNull(): Boolean {
return false return false
} }
fun JsonWriter.value(element: JsonPrimitive) {
if (element.isBoolean) {
value(element.asBoolean)
} else if (element.isNumber) {
value(element.asNumber)
} else if (element.isString) {
value(element.asString)
} else {
throw IllegalArgumentException(element.toString())
}
}
fun JsonWriter.value(element: JsonNull) {
nullValue()
}
fun JsonWriter.value(element: JsonArray) {
beginArray()
for (v in element) value(v)
endArray()
}
fun JsonWriter.value(element: JsonObject) {
beginObject()
for ((k, v) in element.entrySet()) {
name(k)
value(v)
}
endObject()
}
fun JsonWriter.value(element: JsonElement) {
when (element) {
is JsonPrimitive -> value(element)
is JsonNull -> value(element)
is JsonArray -> value(element)
is JsonObject -> value(element)
else -> throw IllegalArgumentException(element.toString())
}
}

View File

@ -6,79 +6,54 @@ import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import kotlin.reflect.KClass import kotlin.reflect.KClass
private fun Int.toBool() = if (this == 0) null else this > 0
/**
* Указывает, что для данного класса можно автоматически создать [BuilderAdapter] для всех его свойств,
* которые не указаны как [JsonIgnoreProperty]
*
* @see JsonIgnoreProperty
* @see JsonPropertyConfig
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class JsonBuilder(
/**
* @see BuilderAdapter.Builder.extraPropertiesAreFatal
*/
val extraPropertiesAreFatal: Boolean = false,
/**
* @see BuilderAdapter.Builder.ignoreKey
*/
val ignoreKeys: Array<String> = [],
/**
* 0 = null
* -1 = false
* 1 = true
*
* @see BuilderAdapter.Builder.logMisses
*/
val logMisses: Int = 0,
/**
* Включать ли свойства родительского класса в данный [BuilderAdapter]
*/
val includeSuperclassProperties: Boolean = true,
)
val JsonBuilder.realLogMisses get() = logMisses.toBool()
/** /**
* Заставляет указанное свойство быть проигнорированным при автоматическом создании [BuilderAdapter] * Заставляет указанное свойство быть проигнорированным при автоматическом создании [BuilderAdapter]
*/ */
@Target(AnnotationTarget.PROPERTY) @Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class JsonIgnoreProperty annotation class JsonIgnore
/** /**
* Выставляет флаги данному свойству при автоматическом создании [BuilderAdapter] * Указывает, что данное свойство или аргумент является компонующим, а не потомственным значением.
* *
* @see BuilderAdapter.Builder.add * Заставляет адаптеры распаковать данное свойство или аргумент на том же уровне, что и родителя
*/ */
@Target(AnnotationTarget.PROPERTY) @Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class JsonPropertyConfig( annotation class JsonFlat
val isFlat: Boolean = false,
val write: Boolean = true, @Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class JsonAlias(vararg val aliases: String)
val mustBePresent: Int = 0, /**
) * Указывает, что данное свойство может отсутствовать в структуре
*/
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class JsonOptional
val JsonPropertyConfig.realMustBePresent get() = mustBePresent.toBool() /**
* Указывает, что данное свойство обязано присутствовать в структуре
*/
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class JsonRequired
/**
* Указывает, что для данного класса можно автоматически создать [BuilderAdapter] для всех его свойств,
* которые не указаны как [JsonIgnore]
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class JsonBuilder
/** /**
* Указывает, что для данного класса можно автоматически создать [FactoryAdapter] * Указывает, что для данного класса можно автоматически создать [FactoryAdapter]
* *
* В подавляющем большинстве случаев это работает исключительно с data классами * Чаще всего, это используется только для data классов
* *
* С технической точки зрения, у класса определяются все свойства и происходит поиск *главного* конструктора, а * @see JsonIgnore
* затем аргументы конструктора отражаются на свойства класса
*
* @see JsonIgnoreProperty
* @see JsonPropertyConfig
*/ */
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
@ -88,11 +63,6 @@ annotation class JsonFactory(
*/ */
val storesJson: Boolean = false, val storesJson: Boolean = false,
/**
* @see FactoryAdapter.Builder.logMisses
*/
val logMisses: Boolean = true,
/** /**
* @see FactoryAdapter.Builder.inputAsList * @see FactoryAdapter.Builder.inputAsList
* @see FactoryAdapter.Builder.inputAsMap * @see FactoryAdapter.Builder.inputAsMap

View File

@ -1,7 +1,6 @@
package ru.dbotthepony.kstarbound.io.json.builder package ru.dbotthepony.kstarbound.io.json.builder
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import com.github.benmanes.caffeine.cache.Interner import com.github.benmanes.caffeine.cache.Interner
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonObject import com.google.gson.JsonObject
@ -14,14 +13,11 @@ import com.google.gson.reflect.TypeToken
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.ObjectArraySet
import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.defs.FreezableDefintionBuilder
import ru.dbotthepony.kstarbound.defs.util.flattenJsonElement import ru.dbotthepony.kstarbound.defs.util.flattenJsonElement
import ru.dbotthepony.kstarbound.io.json.builder.BuilderAdapter.Builder
import ru.dbotthepony.kstarbound.util.INotNullDelegate import ru.dbotthepony.kstarbound.util.INotNullDelegate
import kotlin.properties.Delegates import kotlin.properties.Delegates
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -43,21 +39,6 @@ class BuilderAdapter<T : Any> private constructor(
*/ */
val properties: ImmutableMap<String, IResolvedMutableProperty<T, *>>, val properties: ImmutableMap<String, IResolvedMutableProperty<T, *>>,
/**
* Ключи, которые необходимо игнорировать при чтении JSON
*/
val ignoreKeys: ImmutableSet<String>,
/**
* @see Builder.extraPropertiesAreFatal
*/
val extraPropertiesAreFatal: Boolean,
/**
* @see Builder.logMisses
*/
val logMisses: Boolean,
val stringInterner: Interner<String> = Interner { it }, val stringInterner: Interner<String> = Interner { it },
) : TypeAdapter<T>() { ) : TypeAdapter<T>() {
private val loggedMisses = ObjectOpenHashSet<String>() private val loggedMisses = ObjectOpenHashSet<String>()
@ -92,21 +73,11 @@ class BuilderAdapter<T : Any> private constructor(
} }
// загружаем указатели на стек // загружаем указатели на стек
val logMisses = logMisses
val extraPropertiesAreFatal = extraPropertiesAreFatal
val loggedMisses = loggedMisses
val ignoreKeys = ignoreKeys
reader.beginObject() reader.beginObject()
while (reader.hasNext()) { while (reader.hasNext()) {
val name = reader.nextName() val name = reader.nextName()
if (ignoreKeys.contains(name)) {
reader.skipValue()
continue
}
val property = properties[name] val property = properties[name]
if (property != null) { if (property != null) {
@ -131,11 +102,7 @@ class BuilderAdapter<T : Any> private constructor(
throw JsonSyntaxException("Reading property ${property.name} of ${instance::class.qualifiedName} near ${reader.path}", err) throw JsonSyntaxException("Reading property ${property.name} of ${instance::class.qualifiedName} near ${reader.path}", err)
} }
} else { } else {
if (extraPropertiesAreFatal) { if (loggedMisses.add(name)) {
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") LOGGER.warn("${instance::class.qualifiedName} has no property for storing $name")
} }
@ -186,25 +153,18 @@ class BuilderAdapter<T : Any> private constructor(
} }
} }
if (instance is FreezableDefintionBuilder) {
instance.freeze()
}
return instance return instance
} }
class Builder<T : Any>(val factory: () -> T, vararg fields: KMutableProperty1<T, *>) : TypeAdapterFactory { class Builder<T : Any>(val factory: () -> T, vararg fields: KMutableProperty1<T, *>) : TypeAdapterFactory {
private val properties = ArrayList<IResolvableMutableProperty<T, *>>() private val properties = ArrayList<IResolvableMutableProperty<T, *>>()
private val ignoreKeys = ObjectArraySet<String>()
var extraPropertiesAreFatal = false
var logMisses: Boolean? = null
private val factoryReturnType by lazy { factory.invoke()::class.java } private val factoryReturnType by lazy { factory.invoke()::class.java }
var stringInterner: Interner<String> = Interner { it } var stringInterner: Interner<String> = Interner { it }
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? { override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (type.rawType == factoryReturnType) { if (type.rawType == factoryReturnType)
return build(gson) as TypeAdapter<T> return build(gson) as TypeAdapter<T>
}
return null return null
} }
@ -219,102 +179,18 @@ class BuilderAdapter<T : Any> private constructor(
factory = factory, factory = factory,
properties = map.build(), properties = map.build(),
stringInterner = stringInterner, stringInterner = stringInterner,
ignoreKeys = ImmutableSet.copyOf(ignoreKeys),
extraPropertiesAreFatal = extraPropertiesAreFatal,
logMisses = logMisses ?: properties.none { it.isFlat },
) )
} }
@Deprecated("Используйте как TypeAdapterFactory")
fun build(): BuilderAdapter<T> {
val map = ImmutableMap.Builder<String, IResolvedMutableProperty<T, *>>()
for (property in properties)
map.put(property.property.name, property.resolve(null))
return BuilderAdapter(
factory = factory,
properties = map.build(),
stringInterner = stringInterner,
ignoreKeys = ImmutableSet.copyOf(ignoreKeys),
extraPropertiesAreFatal = extraPropertiesAreFatal,
logMisses = logMisses ?: properties.none { it.isFlat },
)
}
/**
* Являются ли "лишние" ключи в JSON структуре ошибкой.
*
* Если "лишние" ключи являются ошибкой и известны некоторые лишние ключи, которые не нужны,
* то [extraPropertiesAreFatal] можно скомбинировать с [ignoreKey].
*/
fun extraPropertiesAreFatal(flag: Boolean = true): Builder<T> {
check(properties.none { it.isFlat } || !flag) { "Can't have both flattened properties and extraPropertiesAreFatal" }
extraPropertiesAreFatal = flag
return this
}
/**
* Логировать ли несуществующие свойства у класса когда они попадаются в исходной JSON структуре
*/
fun logMisses(flag: Boolean = true): Builder<T> {
logMisses = flag
return this
}
init { init {
for (field in fields) for (field in fields) add(field)
auto(field)
} }
/** fun <V> add(property: KMutableProperty1<T, V>, isFlat: Boolean = false, mustBePresent: Boolean? = null): Builder<T> {
* Добавляет указанное свойство в будущий адаптер с указанным [adapter]
*
* Если указан [isFlat] как true, то данное свойство будет обработано как на одном уровне с данным объектом.
* Пример:
* ```json
* {
* "prop_belong_to_a_1": ...,
* "prop_belong_to_a_2": ...,
* "prop_belong_to_b_1": ...,
* }
* ```
*
* В данном случае, можно указать `b` как плоский класс внутри `a`.
*
* Данный подход позволяет избавиться от постоянного наследования и реализации одного и того же интерфейса во множестве других классов.
* Флаг [extraPropertiesAreFatal] не поддерживается с данными свойствами.
* Если [logMisses] не указан явно, то он будет выставлен на false.
*/
fun <V> add(property: KMutableProperty1<T, V>, adapter: TypeAdapter<V>, isFlat: Boolean = false, mustBePresent: Boolean? = null): Builder<T> {
if (properties.any { it.property == property }) { if (properties.any { it.property == property }) {
throw IllegalArgumentException("Property $property is defined twice") throw IllegalArgumentException("Property $property is defined twice")
} }
ignoreKeys.remove(property.name)
properties.add(ResolvedMutableProperty(
property = property,
adapter = adapter,
mustBePresent = mustBePresent,
isFlat = isFlat,
))
return this
}
/**
* Автоматически определяет тип свойства и необходимый [TypeAdapter]
*
* Для флагов смотрите [add]
*
* @see add
*/
fun <V> auto(property: KMutableProperty1<T, V>, isFlat: Boolean = false, mustBePresent: Boolean? = null): Builder<T> {
if (properties.any { it.property == property }) {
throw IllegalArgumentException("Property $property is defined twice")
}
ignoreKeys.remove(property.name)
properties.add(ResolvableMutableProperty( properties.add(ResolvableMutableProperty(
property = property, property = property,
mustBePresent = mustBePresent, mustBePresent = mustBePresent,
@ -323,14 +199,35 @@ class BuilderAdapter<T : Any> private constructor(
return this return this
} }
}
fun ignoreKey(name: String): Builder<T> { class Factory(val stringInterner: Interner<String> = Interner { it }) : TypeAdapterFactory {
if (properties.any { it.property.name == name }) { override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
throw IllegalArgumentException("Can not ignore key $name because we have property with this name!") val raw = type.rawType
if (raw.isAnnotationPresent(JsonBuilder::class.java)) {
val kclass = raw.kotlin
val builder = Builder(kclass.constructors.first { it.parameters.isEmpty() } as () -> T)
builder.stringInterner = stringInterner
val declarations = LinkedHashMap<String, KMutableProperty1<*, *>>()
collectDecl(kclass, declarations)
for (decl in declarations.values) {
if (decl.annotations.none { it is JsonIgnore }) {
builder.add(
decl as KMutableProperty1<T, *>,
isFlat = decl.annotations.any { it.annotationClass == JsonFlat::class },
mustBePresent = if (decl.annotations.any { it.annotationClass == JsonRequired::class }) true else if (decl.annotations.any { it.annotationClass == JsonOptional::class }) false else null
)
}
}
return builder.build(gson)
} }
ignoreKeys.add(name) return null
return this
} }
} }
@ -373,47 +270,4 @@ class BuilderAdapter<T : Any> private constructor(
return list return list
} }
} }
class Factory(val stringInterner: Interner<String> = Interner { it }) : TypeAdapterFactory {
override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
val raw = type.rawType
if (raw.isAnnotationPresent(JsonBuilder::class.java)) {
val first = raw.getAnnotationsByType(JsonBuilder::class.java)
require(first.size == 1) { "Multiple JsonBuilder defined: ${first.joinToString(", ")}" }
val bconfig = first[0] as JsonBuilder
val kclass = raw.kotlin
val builder = Builder(kclass.constructors.first { it.parameters.isEmpty() && !it.returnType.isMarkedNullable } as () -> T)
builder.logMisses = bconfig.realLogMisses
builder.extraPropertiesAreFatal = bconfig.extraPropertiesAreFatal
builder.stringInterner = stringInterner
for (name in bconfig.ignoreKeys) {
builder.ignoreKey(name)
}
val declarations = LinkedHashMap<String, KMutableProperty1<*, *>>()
collectDecl(kclass, declarations)
for (decl in declarations.values) {
if (decl.annotations.none { it is JsonIgnoreProperty }) {
val config = decl.annotations.firstOrNull { it is JsonPropertyConfig }
if (config == null) {
builder.auto(decl as KMutableProperty1<T, *>)
} else {
config as JsonPropertyConfig
builder.auto(decl as KMutableProperty1<T, *>, isFlat = config.isFlat, mustBePresent = config.realMustBePresent)
}
}
}
return builder.build(gson)
}
return null
}
}
} }

View File

@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableMap
import com.github.benmanes.caffeine.cache.Interner import com.github.benmanes.caffeine.cache.Interner
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonNull
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonParseException import com.google.gson.JsonParseException
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
@ -16,14 +17,16 @@ import com.google.gson.reflect.TypeToken
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.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2IntArrayMap import it.unimi.dsi.fastutil.objects.Object2IntArrayMap
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.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.defs.util.enrollList import ru.dbotthepony.kstarbound.defs.util.enrollList
import ru.dbotthepony.kstarbound.defs.util.enrollMap import ru.dbotthepony.kstarbound.defs.util.enrollMap
import ru.dbotthepony.kstarbound.defs.util.flattenJsonElement import ru.dbotthepony.kstarbound.defs.util.flattenJsonElement
import ru.dbotthepony.kstarbound.io.json.consumeNull import ru.dbotthepony.kstarbound.io.json.consumeNull
import ru.dbotthepony.kstarbound.io.json.ifString import ru.dbotthepony.kstarbound.io.json.value
import java.lang.reflect.Constructor import java.lang.reflect.Constructor
import kotlin.jvm.internal.DefaultConstructorMarker import kotlin.jvm.internal.DefaultConstructorMarker
import kotlin.properties.Delegates import kotlin.properties.Delegates
@ -37,28 +40,38 @@ import kotlin.reflect.full.primaryConstructor
* [TypeAdapter] для классов, которые имеют все свои свойства в главном конструкторе. * [TypeAdapter] для классов, которые имеют все свои свойства в главном конструкторе.
*/ */
class FactoryAdapter<T : Any> private constructor( class FactoryAdapter<T : Any> private constructor(
val bound: KClass<T>, val clazz: KClass<T>,
val types: ImmutableList<IResolvedProperty<T, *>>, val types: ImmutableList<IResolvedProperty<T, *>>,
aliases: Map<String, String>,
val asJsonArray: Boolean, val asJsonArray: Boolean,
val storesJson: Boolean, val storesJson: Boolean,
val logMisses: Boolean,
val stringInterner: Interner<String> val stringInterner: Interner<String>
) : TypeAdapter<T>() { ) : TypeAdapter<T>() {
private val name2index = Object2IntArrayMap<String>() private val name2index = Object2IntArrayMap<String>()
private val loggedMisses = ObjectArraySet<String>() private val loggedMisses = ObjectArraySet<String>()
init { init {
if (asJsonArray && types.any { it.isFlat }) {
throw IllegalArgumentException("Can't have both flat properties and input be as json array")
}
name2index.defaultReturnValue(-1) name2index.defaultReturnValue(-1)
for ((i, pair) in types.withIndex()) { for ((i, pair) in types.withIndex()) {
name2index[pair.property.name] = i name2index[pair.property.name] = i
aliases.entries.forEach {
if (it.value == pair.property.name) {
name2index[it.key] = i
}
}
} }
} }
/** /**
* Обычный конструктор класса (без флагов "значения по умолчанию") * Обычный конструктор класса (без флагов "значения по умолчанию")
*/ */
private val regularFactory: KFunction<T> = bound.constructors.firstOrNull first@{ private val regularFactory: KFunction<T> = clazz.constructors.firstOrNull first@{
var requiredSize = types.size var requiredSize = types.size
if (storesJson) if (storesJson)
@ -98,7 +111,7 @@ class FactoryAdapter<T : Any> private constructor(
} }
return@first false return@first false
} ?: throw NoSuchElementException("Unable to determine constructor for ${bound.qualifiedName} matching (${types.joinToString(", ")})") } ?: throw NoSuchElementException("Unable to determine constructor for ${clazz.qualifiedName} matching (${types.joinToString(", ")})")
/** /**
* Синтетический конструктор класса, который создаётся Kotlin'ном, для создания классов со значениями по умолчанию * Синтетический конструктор класса, который создаётся Kotlin'ном, для создания классов со значениями по умолчанию
@ -117,19 +130,15 @@ class FactoryAdapter<T : Any> private constructor(
typelist.add(DefaultConstructorMarker::class.java) typelist.add(DefaultConstructorMarker::class.java)
bound.java.getDeclaredConstructor(*typelist.toTypedArray()) clazz.java.getDeclaredConstructor(*typelist.toTypedArray())
} catch(_: NoSuchMethodException) { } catch(_: NoSuchMethodException) {
null null
} }
private val syntheticPrimitives: Array<Any?>? private val syntheticPrimitives = Int2ObjectOpenHashMap<Any>()
init { init {
if (syntheticFactory == null) { if (syntheticFactory != null) {
syntheticPrimitives = null
} else {
syntheticPrimitives = arrayOfNulls(syntheticFactory.parameters.size)
for ((i, param) in syntheticFactory.parameters.withIndex()) { for ((i, param) in syntheticFactory.parameters.withIndex()) {
val type = param.parameterizedType as? Class<*> ?: continue val type = param.parameterizedType as? Class<*> ?: continue
@ -158,10 +167,24 @@ class FactoryAdapter<T : Any> private constructor(
out.beginObject() out.beginObject()
for ((field, adapter) in types) { for (type in types) {
out.name(field.name) if (type.isFlat) {
@Suppress("unchecked_cast") val (field, adapter) = type
(adapter as TypeAdapter<Any>).write(out, (field as KProperty1<T, Any>).get(value)) val result = (adapter as TypeAdapter<Any>).toJsonTree((field as KProperty1<T, Any>).get(value))
if (result != null && result != JsonNull.INSTANCE) {
if (result !is JsonObject) {
throw JsonSyntaxException("Expected JsonObject from adapter of ${type.name}, but got ${result::class.qualifiedName}")
}
out.value(result)
}
} else {
val (field, adapter) = type
out.name(field.name)
@Suppress("unchecked_cast")
(adapter as TypeAdapter<Any>).write(out, (field as KProperty1<T, Any>).get(value))
}
} }
out.endObject() out.endObject()
@ -173,7 +196,7 @@ class FactoryAdapter<T : Any> private constructor(
// таблица присутствия значений (если значение true то на i было значение внутри json) // таблица присутствия значений (если значение true то на i было значение внутри json)
val presentValues = BooleanArray(types.size + (if (storesJson) 1 else 0)) val presentValues = BooleanArray(types.size + (if (storesJson) 1 else 0))
val readValues = arrayOfNulls<Any>(types.size + (if (storesJson) 1 else 0)) var readValues = arrayOfNulls<Any>(types.size + (if (storesJson) 1 else 0))
if (storesJson) if (storesJson)
presentValues[presentValues.size - 1] = true presentValues[presentValues.size - 1] = true
@ -183,28 +206,26 @@ class FactoryAdapter<T : Any> private constructor(
// Если нам необходимо читать объект как набор данных массива, то давай // Если нам необходимо читать объект как набор данных массива, то давай
if (asJsonArray) { if (asJsonArray) {
val iterator = types.iterator()
var fieldId = 0
if (storesJson) { if (storesJson) {
val readArray = TypeAdapters.JSON_ELEMENT.read(reader) val readArray = TypeAdapters.JSON_ELEMENT.read(reader)
if (readArray !is JsonArray) { if (readArray !is JsonArray)
throw JsonParseException("Expected JSON element to be an Array, ${readArray::class.qualifiedName} given") throw JsonParseException("Expected JSON element to be an Array, ${readArray::class.qualifiedName} given")
}
reader = JsonTreeReader(readArray) reader = JsonTreeReader(readArray)
readValues[readValues.size - 1] = enrollList(flattenJsonElement(readArray) as List<Any>, stringInterner::intern) readValues[readValues.size - 1] = enrollList(flattenJsonElement(readArray, stringInterner) as List<Any>, stringInterner)
} }
reader.beginArray() reader.beginArray()
val iterator = types.iterator()
var fieldId = 0
while (reader.peek() != JsonToken.END_ARRAY) { while (reader.peek() != JsonToken.END_ARRAY) {
if (!iterator.hasNext()) { if (!iterator.hasNext()) {
val name = fieldId.toString() val name = fieldId.toString()
if (!storesJson && loggedMisses.add(name)) { if (!storesJson && loggedMisses.add(name)) {
LOGGER.warn("${bound.qualifiedName} has no property for storing $name") LOGGER.warn("${clazz.qualifiedName} has no property for storing $name")
} }
reader.skipValue() reader.skipValue()
@ -213,14 +234,13 @@ class FactoryAdapter<T : Any> private constructor(
continue continue
} }
val tuple = iterator.next() val (field, adapter) = iterator.next()
val (field, adapter) = tuple
try { try {
readValues[fieldId] = adapter.read(reader) readValues[fieldId] = adapter.read(reader)
presentValues[fieldId] = true presentValues[fieldId] = true
} catch(err: Throwable) { } catch(err: Throwable) {
throw JsonSyntaxException("Reading field \"${field.name}\" near ${reader.path} for ${bound.qualifiedName}", err) throw JsonSyntaxException("Reading field \"${field.name}\" near ${reader.path} for ${clazz.qualifiedName}", err)
} }
fieldId++ fieldId++
@ -228,18 +248,18 @@ class FactoryAdapter<T : Any> private constructor(
// иначе - читаем как json object // иначе - читаем как json object
} else { } else {
var json: JsonObject by Delegates.notNull() var json: JsonObject by Delegates.notNull()
val hasFlatValues = types.any { it.isFlat }
if (storesJson || hasFlatValues) { if (storesJson || types.any { it.isFlat }) {
val readMap = TypeAdapters.JSON_ELEMENT.read(reader) val readMap = TypeAdapters.JSON_ELEMENT.read(reader)
if (readMap !is JsonObject) { if (readMap !is JsonObject)
throw JsonParseException("Expected JSON element to be a Map, ${readMap::class.qualifiedName} given") throw JsonParseException("Expected JSON element to be a Map, ${readMap::class.qualifiedName} given")
}
json = readMap json = readMap
reader = JsonTreeReader(readMap) reader = JsonTreeReader(readMap)
readValues[readValues.size - 1] = enrollMap(flattenJsonElement(readMap) as Map<String, Any>, stringInterner::intern)
if (storesJson)
readValues[readValues.size - 1] = enrollMap(flattenJsonElement(readMap, stringInterner) as Map<String, Any>, stringInterner)
} }
reader.beginObject() reader.beginObject()
@ -249,8 +269,8 @@ class FactoryAdapter<T : Any> private constructor(
val fieldId = name2index.getInt(name) val fieldId = name2index.getInt(name)
if (fieldId == -1) { if (fieldId == -1) {
if (!storesJson && !hasFlatValues && logMisses && loggedMisses.add(name)) { if (!storesJson && loggedMisses.add(name)) {
LOGGER.warn("${bound.qualifiedName} has no property for storing $name") LOGGER.warn("${clazz.qualifiedName} has no property for storing $name")
} }
reader.skipValue() reader.skipValue()
@ -268,7 +288,7 @@ class FactoryAdapter<T : Any> private constructor(
readValues[fieldId] = adapter.read(reader) readValues[fieldId] = adapter.read(reader)
presentValues[fieldId] = true presentValues[fieldId] = true
} catch(err: Throwable) { } catch(err: Throwable) {
throw JsonSyntaxException("Reading field \"${field.name}\" near ${reader.path} for ${bound.qualifiedName}", err) throw JsonSyntaxException("Reading field \"${field.name}\" near ${reader.path} for ${clazz.qualifiedName}", err)
} }
} }
} }
@ -283,7 +303,7 @@ class FactoryAdapter<T : Any> private constructor(
readValues[i] = read readValues[i] = read
} }
} catch(err: Throwable) { } catch(err: Throwable) {
throw JsonSyntaxException("Reading flat field \"${property.property.name}\" near ${reader.path} for ${bound.qualifiedName}", err) throw JsonSyntaxException("Reading flat field \"${property.property.name}\" for ${clazz.qualifiedName}", err)
} }
} }
} }
@ -303,11 +323,9 @@ class FactoryAdapter<T : Any> private constructor(
if (readValues[i] == null) { if (readValues[i] == null) {
if (!tuple.isMarkedNullable) { if (!tuple.isMarkedNullable) {
throw JsonSyntaxException("Field ${field.name} of ${bound.qualifiedName} does not accept nulls") throw JsonSyntaxException("Field ${field.name} of ${clazz.qualifiedName} does not accept nulls")
} } else if (!regularFactory.parameters[i].isOptional && !presentValues[i]) {
throw JsonSyntaxException("Field ${field.name} of ${clazz.qualifiedName} must be defined")
if (!regularFactory.parameters[i].isOptional && !presentValues[i]) {
throw JsonSyntaxException("Field ${field.name} of ${bound.qualifiedName} must be defined (even just as null)")
} }
} }
} }
@ -316,86 +334,59 @@ class FactoryAdapter<T : Any> private constructor(
return regularFactory.call(*readValues as Array<out Any>) return regularFactory.call(*readValues as Array<out Any>)
// иначе - в бой вступает синтетический конструктор, с флагами "значения по умолчанию" // иначе - в бой вступает синтетический конструктор, с флагами "значения по умолчанию"
} else { } else {
// количество bitflag'ов значений по умолчанию var argumentFlagCount = presentValues.size / 31 + 1 /* DefaultConstructorMarker */
val ints = if (presentValues.size % 31 == 0) presentValues.size / 31 else presentValues.size / 31 + 1 if (presentValues.size % 31 != 0) argumentFlagCount++
// максимум считанных значений + bitflag аргументы значений по умолчанию + null типа DefaultConstructorMarker readValues = readValues.copyOf(readValues.size + argumentFlagCount)
val copied = readValues.copyOf(readValues.size + ints + 1)
var intIndex = readValues.size
var target = 0
var targetMove = 0
for (bool in presentValues) { var flagIndex = readValues.size - argumentFlagCount
if (!bool) { var flags = 0
target = target.or(1.shl(targetMove)) var flagBit = 0
}
targetMove++ for (isPresent in presentValues) {
if (!isPresent) flags = flags.or(1.shl(flagBit))
flagBit++
if (targetMove >= 32) { if (flagBit >= 32) {
copied[intIndex++] = target readValues[flagIndex++] = flags
target = 0 flags = 0
targetMove = 0 flagBit = 0
} }
} }
if (targetMove != 0) { if (flagBit != 0) {
copied[intIndex] = target readValues[flagIndex] = flags
} }
val syntheticPrimitives = syntheticPrimitives!! for ((i, field) in types.withIndex()) {
if (readValues[i] != null) continue
for ((i, tuple) in types.withIndex()) {
if (copied[i] != null) {
continue
}
val (field) = tuple
val param = regularFactory.parameters[i] val param = regularFactory.parameters[i]
if (!param.isOptional && !presentValues[i]) {
throw JsonSyntaxException("Field ${field.name} of ${bound.qualifiedName} is missing")
}
if (tuple.isMarkedNullable) {
continue
}
if (param.isOptional && !presentValues[i]) { if (param.isOptional && !presentValues[i]) {
copied[i] = syntheticPrimitives[i] readValues[i] = syntheticPrimitives[i]
continue } else if (!param.isOptional) {
if (!presentValues[i]) throw JsonSyntaxException("Field ${field.name} of ${clazz.qualifiedName} is missing")
if (!param.type.isMarkedNullable) throw JsonSyntaxException("Field ${field.name} of ${clazz.qualifiedName} does not accept nulls")
} }
throw JsonSyntaxException("Field \"${field.name}\" of ${bound.qualifiedName} does not accept nulls near ${reader.path}")
} }
return syntheticFactory.newInstance(*copied) return syntheticFactory.newInstance(*readValues)
} }
} }
/** /**
* Позволяет построить класс [FactoryAdapter] на основе заданных параметров * Позволяет построить класс [FactoryAdapter] на основе заданных параметров
*/ */
class Builder<T : Any>(val clazz: KClass<T>) : TypeAdapterFactory { class Builder<T : Any>(val clazz: KClass<T>, vararg fields: KProperty1<T, *>) : TypeAdapterFactory {
constructor(clazz: KClass<T>, vararg fields: KProperty1<T, *>) : this(clazz) { private var asList = false
for (field in fields) {
auto(field)
}
}
private var storesJson = false private var storesJson = false
private var logMisses = true
private val types = ArrayList<IResolvableProperty<T, *>>() private val types = ArrayList<IResolvableProperty<T, *>>()
private var stringTransformer: ((String) -> T)? = null private val aliases = Object2ObjectArrayMap<String, String>()
var stringInterner: Interner<String> = Interner { it } var stringInterner: Interner<String> = Interner { it }
fun stringInterner(interner: Interner<String>): Builder<T> { init {
this.stringInterner = interner for (field in fields) {
return this add(field)
} }
fun ifString(transformer: (String) -> T): Builder<T> {
stringTransformer = transformer
return this
} }
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? { override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
@ -413,44 +404,13 @@ class FactoryAdapter<T : Any> private constructor(
check(!asList || types.none { it.isFlat }) { "Can't have both flat properties and json data array layout" } check(!asList || types.none { it.isFlat }) { "Can't have both flat properties and json data array layout" }
return FactoryAdapter( return FactoryAdapter(
bound = clazz, clazz = clazz,
types = ImmutableList.copyOf(types.map { it.resolve(gson) }), types = ImmutableList.copyOf(types.map { it.resolve(gson) }),
asJsonArray = asList, asJsonArray = asList,
storesJson = storesJson, storesJson = storesJson,
logMisses = logMisses,
stringInterner = stringInterner, stringInterner = stringInterner,
).let { aliases = aliases
if (stringTransformer != null) )
it.ifString(stringTransformer!!)
else
it
}
}
/**
* Собирает этот [FactoryAdapter] без GSON объекта
*
* Не рекомендуется использовать, лучше всего использовать как [TypeAdapterFactory]
*
* Несмотря на @Deprecated, данный вариант метода удалён не будет
*/
@Deprecated("Используйте как TypeAdapterFactory")
fun build(): TypeAdapter<T> {
check(!asList || types.none { it.isFlat }) { "Can't have both flat properties and json data array layout" }
return FactoryAdapter(
bound = clazz,
types = ImmutableList.copyOf(types.map { it.resolve(null) }),
asJsonArray = asList,
storesJson = storesJson,
logMisses = logMisses,
stringInterner = stringInterner,
).let {
if (stringTransformer != null)
it.ifString(stringTransformer!!)
else
it
}
} }
/** /**
@ -466,34 +426,15 @@ class FactoryAdapter<T : Any> private constructor(
return this return this
} }
/** fun <V> add(field: KProperty1<T, V>, isFlat: Boolean = false, transform: (TypeAdapter<V>) -> TypeAdapter<V> = { it }): Builder<T> {
* Логировать ли json значения которые нет в списке свойств
*/
fun logMisses(flag: Boolean = true): Builder<T> {
logMisses = flag
return this
}
/**
* Добавляет свойство с определённым [adapter]
*/
fun <V> add(field: KProperty1<T, V>, adapter: TypeAdapter<V>, isFlat: Boolean = false): Builder<T> {
types.add(ResolvedProperty(field, adapter, isFlat = isFlat))
return this
}
/**
* Автоматически определяет необходимый адаптер типа к свойству при сборке данного адаптера внутри Gson
*
* Можно указать [transform] для изменения определённого адаптера
*/
@Suppress("unchecked_cast")
fun <V> auto(field: KProperty1<T, V>, isFlat: Boolean = false, transform: (TypeAdapter<V>) -> TypeAdapter<V> = { it }): Builder<T> {
types.add(ResolvableProperty(field, isFlat = isFlat, transform = transform)) types.add(ResolvableProperty(field, isFlat = isFlat, transform = transform))
return this return this
} }
private var asList = false fun alias(alias: String, canonical: String): Builder<T> {
aliases[alias] = canonical
return this
}
/** /**
* При выставлении данного флага в качестве исходной структуры будет приниматься Json объект: * При выставлении данного флага в качестве исходной структуры будет приниматься Json объект:
@ -542,7 +483,6 @@ class FactoryAdapter<T : Any> private constructor(
} }
builder.storesJson(config.storesJson) builder.storesJson(config.storesJson)
builder.logMisses(config.logMisses)
builder.stringInterner = stringInterner builder.stringInterner = stringInterner
if (properties.isEmpty()) { if (properties.isEmpty()) {
@ -551,20 +491,20 @@ class FactoryAdapter<T : Any> private constructor(
val foundConstructor = kclass.primaryConstructor ?: throw NoSuchElementException("Can't determine primary constructor for ${kclass.qualifiedName}") val foundConstructor = kclass.primaryConstructor ?: throw NoSuchElementException("Can't determine primary constructor for ${kclass.qualifiedName}")
if (!config.storesJson) { val params = foundConstructor.parameters
for (argument in foundConstructor.parameters) { val lastIndex = if (config.storesJson) params.size - 1 else params.size
val property = properties.first { it.name == argument.name && it.returnType.isSupertypeOf(argument.type) }
val config = property.annotations.firstOrNull { it.annotationClass == JsonPropertyConfig::class } as JsonPropertyConfig?
builder.auto(property, isFlat = config?.isFlat ?: false)
}
} else {
val params = foundConstructor.parameters
for (i in 0 until params.size - 1) { for (i in 0 until lastIndex) {
val argument = params[i] val argument = params[i]
val property = properties.first { it.name == argument.name && it.returnType.isSupertypeOf(argument.type) } val property = properties.first { it.name == argument.name && it.returnType.isSupertypeOf(argument.type) }
val config = property.annotations.firstOrNull { it.annotationClass == JsonPropertyConfig::class } as JsonPropertyConfig? builder.add(property, isFlat = property.annotations.any { it.annotationClass == JsonFlat::class })
builder.auto(property, isFlat = config?.isFlat ?: false)
property.annotations.firstOrNull { it.annotationClass == JsonAlias::class }?.let {
it as JsonAlias
for (name in it.aliases) {
builder.alias(name, property.name)
}
} }
} }