diff options
author | Dico200 <dico.karssiens@gmail.com> | 2018-07-23 02:23:46 +0200 |
---|---|---|
committer | Dico200 <dico.karssiens@gmail.com> | 2018-07-23 02:23:46 +0200 |
commit | 42026191ec3a1f6468d8a46304d6ce5cd2d0689c (patch) | |
tree | 9af249ea52a7485e665828ca8654f846d55ec204 /src/main/kotlin/io/dico/parcels2/storage | |
parent | 13b73dad61e8624322df7fb9ddf9bab90db9cc95 (diff) |
Initial exposed backing implementation
Diffstat (limited to 'src/main/kotlin/io/dico/parcels2/storage')
7 files changed, 356 insertions, 113 deletions
diff --git a/src/main/kotlin/io/dico/parcels2/storage/Backing.kt b/src/main/kotlin/io/dico/parcels2/storage/Backing.kt index cd33b3d..0f8829d 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/Backing.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/Backing.kt @@ -3,7 +3,6 @@ package io.dico.parcels2.storage import io.dico.parcels2.Parcel import io.dico.parcels2.ParcelData import io.dico.parcels2.ParcelOwner -import io.dico.parcels2.storage.SerializableParcel import kotlinx.coroutines.experimental.channels.ProducerScope import java.util.* @@ -15,23 +14,26 @@ interface Backing { suspend fun shutdown() + /** - * This producer function is capable of constantly reading plots from a potentially infinite sequence, - * and provide plotdata for it as read from the database. + * This producer function is capable of constantly reading parcels from a potentially infinite sequence, + * and provide parcel data for it as read from the database. */ - suspend fun ProducerScope<Pair<Parcel, ParcelData?>>.produceParcelData(parcels: Sequence<Parcel>) - suspend fun readParcelData(plotFor: Parcel): ParcelData? + suspend fun readParcelData(parcelFor: Parcel): ParcelData? suspend fun getOwnedParcels(user: ParcelOwner): List<SerializableParcel> - suspend fun setParcelOwner(plotFor: Parcel, owner: ParcelOwner?) - suspend fun setParcelPlayerState(plotFor: Parcel, player: UUID, state: Boolean?) + suspend fun setParcelData(parcelFor: Parcel, data: ParcelData?) + + suspend fun setParcelOwner(parcelFor: Parcel, owner: ParcelOwner?) + + suspend fun setParcelPlayerState(parcelFor: Parcel, player: UUID, state: Boolean?) - suspend fun setParcelAllowsInteractInventory(plot: Parcel, value: Boolean) + suspend fun setParcelAllowsInteractInventory(parcel: Parcel, value: Boolean) - suspend fun setParcelAllowsInteractInputs(plot: Parcel, value: Boolean) + suspend fun setParcelAllowsInteractInputs(parcel: Parcel, value: Boolean) }
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/storage/Exposed.kt b/src/main/kotlin/io/dico/parcels2/storage/Exposed.kt deleted file mode 100644 index cbb5887..0000000 --- a/src/main/kotlin/io/dico/parcels2/storage/Exposed.kt +++ /dev/null @@ -1,90 +0,0 @@ -package io.dico.parcels2.storage - -import com.zaxxer.hikari.HikariDataSource -import io.dico.parcels2.Parcel -import io.dico.parcels2.ParcelData -import io.dico.parcels2.ParcelOwner -import kotlinx.coroutines.experimental.channels.ProducerScope -import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.ReferenceOption -import org.jetbrains.exposed.sql.Table -import org.jetbrains.exposed.sql.transactions.transaction -import java.util.* -import javax.sql.DataSource -import org.jetbrains.exposed.sql.SchemaUtils.create - -object ParcelsTable : Table() { - val id = integer("id").autoIncrement().primaryKey() - val px = integer("px") - val pz = integer("pz") - val world_uuid = binary("world_uuid", 16).also { uniqueIndex("location", it, px, pz) } - val world = varchar("world", 32).nullable() - val owner_uuid = binary("owner_uuid", 16).nullable() - val owner = varchar("owner", 16).nullable() -} - -object ParcelsAddedTable : Table() { - val id = integer("id").references(ParcelsTable.id, ReferenceOption.CASCADE) - val player_uuid = binary("player_uuid", 16).also { uniqueIndex("pair", id, it) } - val allowed_flag = bool("allowed_flag") -} - -object PlayerAddedTable : Table() { - val owner_uuid = binary("owner_uuid", 16) - val player_uuid = binary("player_uuid", 16).also { uniqueIndex("pair", owner_uuid, it) } - val allowed_flag = bool("allowed_flag") -} - -class ExposedBacking(val dataSource: DataSource) : Backing { - override val name get() = "Exposed" - lateinit var database: Database - - override suspend fun init() { - database = Database.connect(dataSource) - transaction(database) { - create(ParcelsTable, ParcelsAddedTable) - } - } - - override suspend fun shutdown() { - if (dataSource is HikariDataSource) { - dataSource.close() - } - } - - override suspend fun ProducerScope<Pair<Parcel, ParcelData?>>.produceParcelData(parcels: Sequence<Parcel>) { - TODO() - } - - override suspend fun readParcelData(plotFor: Parcel): ParcelData? { - TODO() - } - - override suspend fun getOwnedParcels(user: ParcelOwner): List<SerializableParcel> { - TODO() - } - - override suspend fun setParcelOwner(plotFor: Parcel, owner: ParcelOwner?) { - TODO() - } - - override suspend fun setParcelPlayerState(plotFor: Parcel, player: UUID, state: Boolean?) { - TODO() - } - - override suspend fun setParcelAllowsInteractInventory(plot: Parcel, value: Boolean) { - TODO() - } - - override suspend fun setParcelAllowsInteractInputs(plot: Parcel, value: Boolean) { - TODO() - } - -} - - - - - - - diff --git a/src/main/kotlin/io/dico/parcels2/storage/ExposedBacking.kt b/src/main/kotlin/io/dico/parcels2/storage/ExposedBacking.kt new file mode 100644 index 0000000..e79c7e0 --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/storage/ExposedBacking.kt @@ -0,0 +1,281 @@ +package io.dico.parcels2.storage + +import com.zaxxer.hikari.HikariDataSource +import io.dico.parcels2.* +import io.dico.parcels2.math.Vec2i +import io.dico.parcels2.util.toByteArray +import io.dico.parcels2.util.toUUID +import kotlinx.coroutines.experimental.channels.ProducerScope +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SchemaUtils.create +import org.jetbrains.exposed.sql.transactions.transaction +import java.util.* +import javax.sql.DataSource + +object WorldsT : Table("worlds") { + val id = integer("id").autoIncrement().primaryKey() + val name = varchar("name", 50) + val uid = binary("uid", 16) + .also { uniqueIndex("index_uid", it) } +} + +object ParcelsT : Table("parcels") { + val id = integer("id").autoIncrement().primaryKey() + val px = integer("px") + val pz = integer("pz") + val world_id = integer("id") + .also { uniqueIndex("index_location", it, px, pz) } + .references(WorldsT.id) + val owner_uuid = binary("owner_uuid", 16).nullable() + val owner_name = varchar("owner_name", 16).nullable() +} + +object AddedLocalT : Table("parcels_added_local") { + val parcel_id = integer("parcel_id") + .references(ParcelsT.id, ReferenceOption.CASCADE) + val player_uuid = binary("player_uuid", 16) + .also { uniqueIndex("index_pair", parcel_id, it) } + val allowed_flag = bool("allowed_flag") +} + +object AddedGlobalT : Table("parcels_added_global") { + val owner_uuid = binary("owner_uuid", 16) + val player_uuid = binary("player_uuid", 16) + .also { uniqueIndex("index_pair", owner_uuid, it) } + val allowed_flag = bool("allowed_flag") +} + +object ParcelOptionsT : Table("parcel_options") { + val parcel_id = integer("parcel_id") + .also { uniqueIndex("index_parcel_id", it) } + .references(ParcelsT.id, ReferenceOption.CASCADE) + val interact_inventory = bool("interact_inventory").default(false) + val interact_inputs = bool("interact_inputs").default(false) +} + +private class ExposedDatabaseException(message: String? = null) : Exception(message) + +@Suppress("NOTHING_TO_INLINE") +class ExposedBacking(val dataSource: DataSource) : Backing { + override val name get() = "Exposed" + lateinit var database: Database + + override suspend fun init() { + database = Database.connect(dataSource) + transaction(database) { + create(ParcelsT, AddedLocalT) + } + } + + override suspend fun shutdown() { + if (dataSource is HikariDataSource) { + dataSource.close() + } + } + + private fun <T> transaction(statement: Transaction.() -> T) = transaction(database, statement) + + private inline fun Transaction.getWorldId(binaryUid: ByteArray): Int? { + return WorldsT.select { WorldsT.uid eq binaryUid }.firstOrNull()?.let { it[WorldsT.id] } + } + + private inline fun Transaction.getWorldId(worldUid: UUID): Int? { + return getWorldId(worldUid.toByteArray()!!) + } + + private inline fun Transaction.getOrInitWorldId(worldUid: UUID, worldName: String): Int { + val binaryUid = worldUid.toByteArray()!! + return getWorldId(binaryUid) + ?: WorldsT.insertIgnore { it[uid] = binaryUid; it[name] = worldName }.get(WorldsT.id) + ?: throw ExposedDatabaseException("This should not happen - failed to insert world named $worldName and get its id") + } + + private inline fun Transaction.getParcelId(worldId: Int, parcelX: Int, parcelZ: Int): Int? { + return ParcelsT.select { (ParcelsT.world_id eq worldId) and (ParcelsT.px eq parcelX) and (ParcelsT.pz eq parcelZ) } + .firstOrNull()?.let { it[ParcelsT.id] } + } + + private inline fun Transaction.getParcelId(worldUid: UUID, parcelX: Int, parcelZ: Int): Int? { + return getWorldId(worldUid)?.let { getParcelId(it, parcelX, parcelZ) } + } + + private inline fun Transaction.getOrInitParcelId(worldUid: UUID, worldName: String, parcelX: Int, parcelZ: Int): Int { + val worldId = getOrInitWorldId(worldUid, worldName) + return getParcelId(worldId, parcelX, parcelZ) + ?: ParcelsT.insertIgnore { it[world_id] = worldId; it[px] = parcelX; it[pz] = parcelZ }.get(ParcelsT.id) + ?: throw ExposedDatabaseException("This should not happen - failed to insert parcel at $worldName($parcelX, $parcelZ)") + } + + private inline fun Transaction.getParcelRow(id: Int): ResultRow? { + return ParcelsT.select { ParcelsT.id eq id }.firstOrNull() + } + + fun Transaction.getWorldId(world: ParcelWorld): Int? { + return getWorldId(world.world.uid) + } + + fun Transaction.getOrInitWorldId(world: ParcelWorld): Int { + return world.world.let { getOrInitWorldId(it.uid, it.name) } + } + + fun Transaction.getParcelId(parcel: Parcel): Int? { + return getParcelId(parcel.world.world.uid, parcel.pos.x, parcel.pos.z) + } + + fun Transaction.getOrInitParcelId(parcel: Parcel): Int { + return parcel.world.world.let { getOrInitParcelId(it.uid, it.name, parcel.pos.x, parcel.pos.z) } + } + + fun Transaction.getParcelRow(parcel: Parcel): ResultRow? { + return getParcelId(parcel)?.let { getParcelRow(it) } + } + + override suspend fun ProducerScope<Pair<Parcel, ParcelData?>>.produceParcelData(parcels: Sequence<Parcel>) { + for (parcel in parcels) { + val data = readParcelData(parcel) + channel.send(parcel to data) + } + channel.close() + } + + override suspend fun readParcelData(parcelFor: Parcel): ParcelData? = transaction { + val row = getParcelRow(parcelFor) ?: return@transaction null + + ParcelDataHolder().apply { + + owner = ParcelOwner.create( + uuid = row[ParcelsT.owner_uuid]?.toUUID(), + name = row[ParcelsT.owner_name] + ) + + val parcelId = row[ParcelsT.id] + AddedLocalT.select { AddedLocalT.parcel_id eq parcelId }.forEach { + val uuid = it[AddedLocalT.player_uuid].toUUID()!! + val status = if (it[AddedLocalT.allowed_flag]) AddedStatus.ALLOWED else AddedStatus.BANNED + setAddedStatus(uuid, status) + } + + ParcelOptionsT.select { ParcelOptionsT.parcel_id eq parcelId }.firstOrNull()?.let { + allowInteractInputs = it[ParcelOptionsT.interact_inputs] + allowInteractInventory = it[ParcelOptionsT.interact_inventory] + } + + } + + } + + override suspend fun getOwnedParcels(user: ParcelOwner): List<SerializableParcel> = transaction { + val where: SqlExpressionBuilder.() -> Op<Boolean> + + if (user.uuid != null) { + val binaryUuid = user.uuid.toByteArray() + where = { ParcelsT.owner_uuid eq binaryUuid } + } else { + val name = user.name + where = { ParcelsT.owner_name eq name } + } + + ParcelsT.select(where) + .map { parcelRow -> + val worldId = parcelRow[ParcelsT.world_id] + val worldRow = WorldsT.select({ WorldsT.id eq worldId }).firstOrNull() + ?: return@map null + + val world = SerializableWorld(worldRow[WorldsT.name], worldRow[WorldsT.uid].toUUID()) + SerializableParcel(world, Vec2i(parcelRow[ParcelsT.px], parcelRow[ParcelsT.pz])) + } + .filterNotNull() + .toList() + } + + + override suspend fun setParcelData(parcelFor: Parcel, data: ParcelData?) { + if (data == null) { + transaction { + getParcelId(parcelFor)?.let { id -> + ParcelsT.deleteIgnoreWhere(limit = 1) { ParcelsT.id eq id } + + // Below should cascade automatically + /* + AddedLocalT.deleteIgnoreWhere { AddedLocalT.parcel_id eq id } + ParcelOptionsT.deleteIgnoreWhere(limit = 1) { ParcelOptionsT.parcel_id eq id } + */ + } + + } + return + } + + val id = transaction { + val id = getOrInitParcelId(parcelFor) + AddedLocalT.deleteIgnoreWhere { AddedLocalT.parcel_id eq id } + id + } + + setParcelOwner(parcelFor, data.owner) + + for ((uuid, status) in data.added) { + val state = status.asBoolean + setParcelPlayerState(parcelFor, uuid, state) + } + + setParcelAllowsInteractInputs(parcelFor, data.allowInteractInputs) + setParcelAllowsInteractInventory(parcelFor, data.allowInteractInventory) + } + + override suspend fun setParcelOwner(parcelFor: Parcel, owner: ParcelOwner?) = transaction { + val binaryUuid = owner?.uuid?.toByteArray() + val name = owner?.name + + val id = if (owner == null) + getParcelId(parcelFor) ?: return@transaction + else + getOrInitParcelId(parcelFor) + + ParcelsT.update({ ParcelsT.id eq id }, limit = 1) { + it[ParcelsT.owner_uuid] = binaryUuid + it[ParcelsT.owner_name] = name + } + } + + override suspend fun setParcelPlayerState(parcelFor: Parcel, player: UUID, state: Boolean?) = transaction { + val binaryUuid = player.toByteArray()!! + + if (state == null) { + getParcelId(parcelFor)?.let { id -> + AddedLocalT.deleteWhere { (AddedLocalT.parcel_id eq id) and (AddedLocalT.player_uuid eq binaryUuid) } + } + return@transaction + } + + val id = getOrInitParcelId(parcelFor) + AddedLocalT.insertOrUpdate(AddedLocalT.allowed_flag) { + it[AddedLocalT.parcel_id] = id + it[AddedLocalT.player_uuid] = binaryUuid + } + } + + override suspend fun setParcelAllowsInteractInventory(parcel: Parcel, value: Boolean): Unit = transaction { + val id = getOrInitParcelId(parcel) + ParcelOptionsT.insertOrUpdate(ParcelOptionsT.interact_inventory) { + it[ParcelOptionsT.parcel_id] = id + it[ParcelOptionsT.interact_inventory] = value + } + } + + override suspend fun setParcelAllowsInteractInputs(parcel: Parcel, value: Boolean): Unit = transaction { + val id = getOrInitParcelId(parcel) + ParcelOptionsT.insertOrUpdate(ParcelOptionsT.interact_inputs) { + it[ParcelOptionsT.parcel_id] = id + it[ParcelOptionsT.interact_inputs] = value + } + } + +} + + + + + + + diff --git a/src/main/kotlin/io/dico/parcels2/storage/ExposedExtensions.kt b/src/main/kotlin/io/dico/parcels2/storage/ExposedExtensions.kt new file mode 100644 index 0000000..f429d7e --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/storage/ExposedExtensions.kt @@ -0,0 +1,36 @@ +package io.dico.parcels2.storage + +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.Transaction +import org.jetbrains.exposed.sql.statements.InsertStatement +import org.jetbrains.exposed.sql.transactions.TransactionManager + +/* + * insertOrUpdate from https://github.com/JetBrains/Exposed/issues/167#issuecomment-403837917 + */ +inline fun <T : Table> T.insertOrUpdate(vararg onDuplicateUpdateKeys: Column<*>, body: T.(InsertStatement<Number>) -> Unit) = + InsertOrUpdate<Number>(onDuplicateUpdateKeys, this).apply { + body(this) + execute(TransactionManager.current()) + } + +class InsertOrUpdate<Key : Any>( + private val onDuplicateUpdateKeys: Array<out Column<*>>, + table: Table, + isIgnore: Boolean = false +) : InsertStatement<Key>(table, isIgnore) { + override fun prepareSQL(transaction: Transaction): String { + val onUpdateSQL = if (onDuplicateUpdateKeys.isNotEmpty()) { + " ON DUPLICATE KEY UPDATE " + onDuplicateUpdateKeys.joinToString { "${transaction.identity(it)}=VALUES(${transaction.identity(it)})" } + } else "" + return super.prepareSQL(transaction) + onUpdateSQL + } +} + + + + + + + diff --git a/src/main/kotlin/io/dico/parcels2/storage/Jackson.kt b/src/main/kotlin/io/dico/parcels2/storage/Jackson.kt index b5bdbeb..08ca810 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/Jackson.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/Jackson.kt @@ -22,12 +22,15 @@ val yamlObjectMapper = ObjectMapper(YAMLFactory()).apply { with(kotlinModule) { setSerializerModifier(object : BeanSerializerModifier() { @Suppress("UNCHECKED_CAST") - override fun modifySerializer(config: SerializationConfig?, beanDesc: BeanDescription?, serializer: JsonSerializer<*>?): JsonSerializer<*> { - if (GeneratorOptions::class.isSuperclassOf(beanDesc?.beanClass?.kotlin as KClass<*>)) { - return GeneratorOptionsSerializer(serializer as JsonSerializer<GeneratorOptions>) + override fun modifySerializer(config: SerializationConfig?, beanDesc: BeanDescription, serializer: JsonSerializer<*>): JsonSerializer<*> { + + val newSerializer = if (GeneratorOptions::class.isSuperclassOf(beanDesc.beanClass.kotlin)) { + GeneratorOptionsSerializer(serializer as JsonSerializer<GeneratorOptions>) + } else { + serializer } - return super.modifySerializer(config, beanDesc, serializer) + return super.modifySerializer(config, beanDesc, newSerializer) } }) diff --git a/src/main/kotlin/io/dico/parcels2/storage/SerializableTypes.kt b/src/main/kotlin/io/dico/parcels2/storage/SerializableTypes.kt index 4e467b1..121e251 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/SerializableTypes.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/SerializableTypes.kt @@ -22,7 +22,7 @@ data class SerializableWorld(val name: String? = null, * Used by storage backing options to encompass the location of a parcel */ data class SerializableParcel(val world: SerializableWorld, - val coord: Vec2i) { + val pos: Vec2i) { val parcel: Parcel? by lazy { TODO() } }
\ No newline at end of file diff --git a/src/main/kotlin/io/dico/parcels2/storage/Storage.kt b/src/main/kotlin/io/dico/parcels2/storage/Storage.kt index 36f5400..67c4b05 100644 --- a/src/main/kotlin/io/dico/parcels2/storage/Storage.kt +++ b/src/main/kotlin/io/dico/parcels2/storage/Storage.kt @@ -19,12 +19,16 @@ interface Storage { fun shutdown(): Deferred<Unit> + fun readParcelData(parcelFor: Parcel): Deferred<ParcelData?> fun readParcelData(parcelsFor: Sequence<Parcel>, channelCapacity: Int): ReceiveChannel<Pair<Parcel, ParcelData?>> fun getOwnedParcels(user: ParcelOwner): Deferred<List<SerializableParcel>> + + fun setParcelData(parcelFor: Parcel, data: ParcelData?): Deferred<Unit> + fun setParcelOwner(parcelFor: Parcel, owner: ParcelOwner?): Deferred<Unit> fun setParcelPlayerState(parcelFor: Parcel, player: UUID, state: Boolean?): Deferred<Unit> @@ -41,25 +45,32 @@ class StorageWithCoroutineBacking internal constructor(val backing: Backing) : S val poolSize: Int get() = 4 override val asyncDispatcher = Executors.newFixedThreadPool(poolSize) { Thread(it, "Parcels2_StorageThread") }.asCoroutineDispatcher() - private fun <T> future(block: suspend CoroutineScope.() -> T) = async(context = asyncDispatcher, start = CoroutineStart.ATOMIC, block = block) + @Suppress("NOTHING_TO_INLINE") + private inline fun <T> defer(noinline block: suspend CoroutineScope.() -> T): Deferred<T> { + return async(context = asyncDispatcher, start = CoroutineStart.ATOMIC, block = block) + } - override fun init() = future { backing.init() } + override fun init() = defer { backing.init() } - override fun shutdown() = future { backing.shutdown() } + override fun shutdown() = defer { backing.shutdown() } - override fun readParcelData(parcelFor: Parcel) = future { backing.readParcelData(parcelFor) } + + override fun readParcelData(parcelFor: Parcel) = defer { backing.readParcelData(parcelFor) } override fun readParcelData(parcelsFor: Sequence<Parcel>, channelCapacity: Int) = produce(asyncDispatcher, capacity = channelCapacity) { with(backing) { produceParcelData(parcelsFor) } } - override fun getOwnedParcels(user: ParcelOwner) = future { backing.getOwnedParcels(user) } - override fun setParcelOwner(parcelFor: Parcel, owner: ParcelOwner?) = future { backing.setParcelOwner(parcelFor, owner) } + override fun setParcelData(parcelFor: Parcel, data: ParcelData?) = defer { backing.setParcelData(parcelFor, data) } + + override fun getOwnedParcels(user: ParcelOwner) = defer { backing.getOwnedParcels(user) } + + override fun setParcelOwner(parcelFor: Parcel, owner: ParcelOwner?) = defer { backing.setParcelOwner(parcelFor, owner) } - override fun setParcelPlayerState(parcelFor: Parcel, player: UUID, state: Boolean?) = future { backing.setParcelPlayerState(parcelFor, player, state) } + override fun setParcelPlayerState(parcelFor: Parcel, player: UUID, state: Boolean?) = defer { backing.setParcelPlayerState(parcelFor, player, state) } - override fun setParcelAllowsInteractInventory(parcel: Parcel, value: Boolean) = future { backing.setParcelAllowsInteractInventory(parcel, value) } + override fun setParcelAllowsInteractInventory(parcel: Parcel, value: Boolean) = defer { backing.setParcelAllowsInteractInventory(parcel, value) } - override fun setParcelAllowsInteractInputs(parcel: Parcel, value: Boolean) = future { backing.setParcelAllowsInteractInputs(parcel, value) } + override fun setParcelAllowsInteractInputs(parcel: Parcel, value: Boolean) = defer { backing.setParcelAllowsInteractInputs(parcel, value) } } |