diff --git a/build.gradle.kts b/build.gradle.kts index 1e0b2db7..5bee9a7b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,7 +14,6 @@ plugins { id("maven-publish") id("java") kotlin("jvm") version "1.9.21" - kotlin("plugin.serialization") version "1.9.21" } dependencies { @@ -41,7 +40,6 @@ allprojects { apply(plugin = "maven-publish") apply(plugin = "io.github.goooler.shadow") apply(plugin = "kotlin") - apply(plugin = "org.jetbrains.kotlin.plugin.serialization") repositories { mavenCentral() @@ -212,7 +210,6 @@ tasks { //relocate("com.mysql", "com.willfp.eco.libs.mysql") relocate("com.mongodb", "com.willfp.eco.libs.mongodb") relocate("org.bson", "com.willfp.eco.libs.bson") - relocate("org.litote", "com.willfp.eco.libs.litote") relocate("org.reactivestreams", "com.willfp.eco.libs.reactivestreams") relocate("reactor.", "com.willfp.eco.libs.reactor.") // Dot in name to be safe relocate("com.moandjiezana.toml", "com.willfp.eco.libs.toml") diff --git a/eco-core/core-plugin/build.gradle.kts b/eco-core/core-plugin/build.gradle.kts index 124a4864..7cbb676b 100644 --- a/eco-core/core-plugin/build.gradle.kts +++ b/eco-core/core-plugin/build.gradle.kts @@ -9,16 +9,14 @@ dependencies { // Libraries implementation("com.github.WillFP:Crunch:1.1.3") - implementation("mysql:mysql-connector-java:8.0.25") - implementation("org.jetbrains.exposed:exposed-core:0.37.3") - implementation("org.jetbrains.exposed:exposed-dao:0.37.3") - implementation("org.jetbrains.exposed:exposed-jdbc:0.37.3") - implementation("com.zaxxer:HikariCP:5.0.0") + implementation("mysql:mysql-connector-java:8.0.28") + implementation("org.jetbrains.exposed:exposed-core:0.53.0") + implementation("org.jetbrains.exposed:exposed-jdbc:0.53.0") + implementation("com.zaxxer:HikariCP:5.1.0") implementation("net.kyori:adventure-platform-bukkit:4.1.0") implementation("org.javassist:javassist:3.29.2-GA") - implementation("org.mongodb:mongodb-driver-kotlin-coroutine:5.0.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1") - implementation("org.mongodb:bson-kotlinx:5.0.0") + implementation("org.mongodb:mongodb-driver-kotlin-coroutine:5.1.2") + implementation("org.mongodb:bson-kotlinx:5.1.2") implementation("com.moandjiezana.toml:toml4j:0.7.2") { exclude(group = "com.google.code.gson", module = "gson") } @@ -76,7 +74,6 @@ dependencies { tasks { shadowJar { minimize { - exclude(dependency("org.litote.kmongo:kmongo-coroutine:.*")) exclude(dependency("org.jetbrains.exposed:.*:.*")) exclude(dependency("com.willfp:ModelEngineBridge:.*")) } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/LegacyMySQLPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/LegacyMySQLPersistentDataHandler.kt index 037a6163..0723a572 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/LegacyMySQLPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/LegacyMySQLPersistentDataHandler.kt @@ -13,7 +13,10 @@ import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import org.jetbrains.exposed.dao.id.UUIDTable import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.SqlExpressionBuilder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction @@ -37,7 +40,7 @@ class LegacyMySQLPersistentDataHandler( private val database = Database.connect(dataSource) private val table = object : UUIDTable("eco_data") { - val data = text("json_data") + val data = text("json_data", eagerLoading = true) } init { @@ -65,7 +68,8 @@ class LegacyMySQLPersistentDataHandler( private inner class LegacySerializer : DataTypeSerializer() { override fun readAsync(uuid: UUID, key: PersistentDataKey): T? { val json = transaction(database) { - table.select { table.id eq uuid } + table.selectAll() + .where { table.id eq uuid } .limit(1) .singleOrNull() ?.get(table.data) diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MongoPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MongoPersistentDataHandler.kt index d78894b8..bc93f345 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MongoPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MongoPersistentDataHandler.kt @@ -1,5 +1,6 @@ package com.willfp.eco.internal.spigot.data.handlers.impl +import com.mongodb.MongoClientSettings import com.mongodb.client.model.Filters import com.mongodb.client.model.ReplaceOptions import com.mongodb.kotlin.client.coroutine.MongoClient @@ -9,95 +10,184 @@ import com.willfp.eco.core.data.handlers.DataTypeSerializer import com.willfp.eco.core.data.handlers.PersistentDataHandler import com.willfp.eco.core.data.keys.PersistentDataKey import com.willfp.eco.core.data.keys.PersistentDataKeyType -import com.willfp.eco.internal.spigot.EcoSpigotPlugin import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking -import kotlinx.serialization.Contextual -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable +import org.bson.BsonArray +import org.bson.BsonBoolean +import org.bson.BsonDecimal128 +import org.bson.BsonDocument +import org.bson.BsonDouble +import org.bson.BsonInt32 +import org.bson.BsonObjectId +import org.bson.BsonString +import org.bson.BsonValue +import org.bson.codecs.configuration.CodecRegistries +import org.bson.codecs.pojo.PojoCodecProvider +import org.bson.types.Decimal128 import java.math.BigDecimal import java.util.UUID class MongoPersistentDataHandler( config: Config ) : PersistentDataHandler("mongo") { + private val codecRegistry = CodecRegistries.fromRegistries( + MongoClientSettings.getDefaultCodecRegistry(), + CodecRegistries.fromProviders(PojoCodecProvider.builder().automatic(true).build()) + ) + private val client = MongoClient.create(config.getString("url")) private val database = client.getDatabase(config.getString("database")) // Collection name is set for backwards compatibility - private val collection = database.getCollection("uuidprofile") + private val collection = database.getCollection(config.getString("collection")) + .withCodecRegistry(codecRegistry) init { - PersistentDataKeyType.STRING.registerSerializer(this, MongoSerializer()) - PersistentDataKeyType.BOOLEAN.registerSerializer(this, MongoSerializer()) - PersistentDataKeyType.INT.registerSerializer(this, MongoSerializer()) - PersistentDataKeyType.DOUBLE.registerSerializer(this, MongoSerializer()) - PersistentDataKeyType.STRING_LIST.registerSerializer(this, MongoSerializer>()) - PersistentDataKeyType.BIG_DECIMAL.registerSerializer(this, MongoSerializer()) + PersistentDataKeyType.STRING.registerSerializer(this, object : MongoSerializer() { + override fun serialize(value: String): BsonValue { + return BsonString(value) + } + + override fun deserialize(value: BsonValue): String { + return value.asString().value + } + }) + + PersistentDataKeyType.BOOLEAN.registerSerializer(this, object : MongoSerializer() { + override fun serialize(value: Boolean): BsonValue { + return BsonBoolean(value) + } + + override fun deserialize(value: BsonValue): Boolean { + return value.asBoolean().value + } + }) + + PersistentDataKeyType.INT.registerSerializer(this, object : MongoSerializer() { + override fun serialize(value: Int): BsonValue { + return BsonInt32(value) + } + + override fun deserialize(value: BsonValue): Int { + return value.asInt32().value + } + }) + + PersistentDataKeyType.DOUBLE.registerSerializer(this, object : MongoSerializer() { + override fun serialize(value: Double): BsonValue { + return BsonDouble(value) + } + + override fun deserialize(value: BsonValue): Double { + return value.asDouble().value + } + }) + + PersistentDataKeyType.STRING_LIST.registerSerializer(this, object : MongoSerializer>() { + override fun serialize(value: List): BsonValue { + return BsonArray(value.map { BsonString(it) }) + } + + override fun deserialize(value: BsonValue): List { + return value.asArray().values.map { it.asString().value } + } + }) + + PersistentDataKeyType.BIG_DECIMAL.registerSerializer(this, object : MongoSerializer() { + override fun serialize(value: BigDecimal): BsonValue { + return BsonDecimal128(Decimal128(value)) + } + + override fun deserialize(value: BsonValue): BigDecimal { + return value.asDecimal128().value.bigDecimalValue() + } + }) PersistentDataKeyType.CONFIG.registerSerializer(this, object : MongoSerializer() { - override fun convertToMongo(value: Config): Any { - return value.toMap() + private fun deserializeConfigValue(value: BsonValue): Any { + return when (value) { + is BsonString -> value.value + is BsonInt32 -> value.value + is BsonDouble -> value.value + is BsonBoolean -> value.value + is BsonDecimal128 -> value.value.bigDecimalValue() + is BsonArray -> value.values.map { deserializeConfigValue(it) } + is BsonDocument -> value.mapValues { (_, v) -> deserializeConfigValue(v) } + + else -> throw IllegalArgumentException("Could not deserialize config value type ${value::class.simpleName}") + } + } + + private fun serializeConfigValue(value: Any): BsonValue { + return when (value) { + is String -> BsonString(value) + is Int -> BsonInt32(value) + is Double -> BsonDouble(value) + is Boolean -> BsonBoolean(value) + is BigDecimal -> BsonDecimal128(Decimal128(value)) + is List<*> -> BsonArray(value.map { serializeConfigValue(it!!) }) + is Map<*, *> -> BsonDocument().apply { + value.forEach { (k, v) -> append(k.toString(), serializeConfigValue(v!!)) } + } + + else -> throw IllegalArgumentException("Could not serialize config value type ${value::class.simpleName}") + } } - @Suppress("UNCHECKED_CAST") - override fun convertFromMongo(value: Any): Config { - return Configs.fromMap(value as Map) + override fun serialize(value: Config): BsonValue { + return serializeConfigValue(value.toMap()) + } + + override fun deserialize(value: BsonValue): Config { + @Suppress("UNCHECKED_CAST") + return Configs.fromMap(deserializeConfigValue(value.asDocument()) as Map) } }) } override fun getSavedUUIDs(): Set { return runBlocking { - collection.find().toList().map { UUID.fromString(it.uuid) }.toSet() + collection.find().toList().map { + UUID.fromString(it.getString("uuid").value) + }.toSet() } } - private open inner class MongoSerializer : DataTypeSerializer() { + private abstract inner class MongoSerializer : DataTypeSerializer() { override fun readAsync(uuid: UUID, key: PersistentDataKey): T? { return runBlocking { - val profile = collection.find(Filters.eq("uuid", uuid.toString())).firstOrNull() - ?: return@runBlocking null + val filter = Filters.eq("uuid", uuid.toString()) - val value = profile.data[key.key.toString()] - ?: return@runBlocking null + val profile = collection.find(filter) + .firstOrNull() ?: return@runBlocking null - convertFromMongo(value) - } - } + val value = profile[key.key.toString()] ?: return@runBlocking null - protected open fun convertToMongo(value: T): Any { - return value + deserialize(value) + } } override fun writeAsync(uuid: UUID, key: PersistentDataKey, value: T) { runBlocking { - val profile = collection.find(Filters.eq("uuid", uuid.toString())).firstOrNull() - ?: UUIDProfile(uuid.toString(), mutableMapOf()) + val filter = Filters.eq("uuid", uuid.toString()) - profile.data[key.key.toString()] = convertToMongo(value) + val profile = collection.find(filter).firstOrNull() + ?: BsonDocument() + .append("_id", BsonObjectId()) + .append("uuid", BsonString(uuid.toString())) + + profile.append(key.key.toString(), serialize(value)) collection.replaceOne( - Filters.eq("uuid", uuid.toString()), + filter, profile, ReplaceOptions().upsert(true) ) } } - protected open fun convertFromMongo(value: Any): T { - @Suppress("UNCHECKED_CAST") - return value as T - } + protected abstract fun serialize(value: T): BsonValue + protected abstract fun deserialize(value: BsonValue): T } - - @Serializable - private data class UUIDProfile( - // Storing UUID as strings for serialization - @SerialName("_id") val uuid: String, - - // Storing NamespacedKeys as strings for serialization - val data: MutableMap - ) } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt index 13dd0930..680360ee 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt @@ -12,14 +12,21 @@ import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.SqlExpressionBuilder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.batchInsert import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.replace import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.update +import org.jetbrains.exposed.sql.upsert import java.math.BigDecimal import java.util.UUID @@ -44,7 +51,7 @@ class MySQLPersistentDataHandler( init { PersistentDataKeyType.STRING.registerSerializer(this, object : DirectStoreSerializer() { override val table = object : KeyTable("string") { - override val value = varchar("value", 128) + override val value = varchar("value", 256) } }.createTable()) @@ -94,7 +101,7 @@ class MySQLPersistentDataHandler( PersistentDataKeyType.STRING_LIST.registerSerializer(this, object : MultiValueSerializer() { override val table = object : ListKeyTable("string_list") { - override val value = varchar("value", 128) + override val value = varchar("value", 256) } }.createTable()) } @@ -138,7 +145,8 @@ class MySQLPersistentDataHandler( override fun readAsync(uuid: UUID, key: PersistentDataKey): T? { val stored = transaction(database) { - table.select { (table.uuid eq uuid) and (table.key eq key.key.toString()) } + table.selectAll() + .where { (table.uuid eq uuid) and (table.key eq key.key.toString()) } .limit(1) .singleOrNull() ?.get(table.value) @@ -150,9 +158,7 @@ class MySQLPersistentDataHandler( override fun writeAsync(uuid: UUID, key: PersistentDataKey, value: T) { withRetries { transaction(database) { - table.deleteWhere { (table.uuid eq uuid) and (table.key eq key.key.toString()) } - - table.insert { + table.upsert { it[table.uuid] = uuid it[table.key] = key.key.toString() it[table.value] = convertToStored(value) @@ -177,7 +183,8 @@ class MySQLPersistentDataHandler( override fun readAsync(uuid: UUID, key: PersistentDataKey>): List? { val stored = transaction(database) { - table.select { (table.uuid eq uuid) and (table.key eq key.key.toString()) } + table.selectAll() + .where { (table.uuid eq uuid) and (table.key eq key.key.toString()) } .orderBy(table.index) .map { it[table.value] } } @@ -190,8 +197,10 @@ class MySQLPersistentDataHandler( transaction(database) { table.deleteWhere { (table.uuid eq uuid) and (table.key eq key.key.toString()) } + // Can't get batch inserts to work, would like to fix value.forEachIndexed { index, t -> - table.insert { + // Using replace instead of insert to avoid any deadlock issues + table.replace { it[table.uuid] = uuid it[table.key] = key.key.toString() it[table.index] = index diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileHandler.kt index fb5b9903..8ac795ac 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileHandler.kt @@ -21,8 +21,8 @@ class ProfileHandler( private val handlerId = plugin.configYml.getString("data-handler") val localHandler = YamlPersistentDataHandler(plugin) - val defaultHandler = PersistentDataHandlers[handlerId] - ?.create(plugin) ?: throw IllegalArgumentException("Invalid data handler ($handlerId)") + val defaultHandler = PersistentDataHandlers[handlerId]?.create(plugin) + ?: throw IllegalArgumentException("Invalid data handler ($handlerId)") val profileWriter = ProfileWriter(plugin, this) diff --git a/eco-core/core-plugin/src/main/resources/config.yml b/eco-core/core-plugin/src/main/resources/config.yml index 4a6640f5..8c422be2 100644 --- a/eco-core/core-plugin/src/main/resources/config.yml +++ b/eco-core/core-plugin/src/main/resources/config.yml @@ -16,8 +16,12 @@ perform-data-migration: true mongodb: # The full MongoDB connection URL. url: "" + # The name of the database to use. - database: "eco" + database: eco + + # The collection to use for player data. + collection: profiles mysql: # The table prefix to use for all tables.