diff --git a/src/main/java/ext/Ext.kt b/src/main/java/ext/Ext.kt index 9f12783a..03fa8ba0 100644 --- a/src/main/java/ext/Ext.kt +++ b/src/main/java/ext/Ext.kt @@ -82,6 +82,7 @@ fun Str.isValidEmail(): Bool = emailRegex.matches(this) // JSON val ACCEPTABLE_FALSE = setOf("0", "false", "no", "off", "False", "None", "null") val ACCEPTABLE_TRUE = setOf("1", "true", "yes", "on", "True") +@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "UNCHECKED_CAST") val jackson = ObjectMapper().apply { registerModule(SimpleModule().addDeserializer(Boolean::class.java, object : JsonDeserializer() { override fun deserialize(parser: JsonParser, context: DeserializationContext) = when(parser.text) { @@ -143,6 +144,7 @@ operator fun MutableMap.plusAssign(map: Map) { putAll(map) } operator fun Str.get(range: IntRange) = substring(range.first, (range.last + 1).coerceAtMost(length)) operator fun Str.get(start: Int, end: Int) = substring(start, end.coerceAtMost(length)) fun Str.center(width: Int, padChar: Char = ' ') = padStart((length + width) / 2, padChar).padEnd(width, padChar) +fun Str.splitLines() = replace("\r\n", "\n").split('\n') // Coroutine suspend fun async(block: suspend kotlinx.coroutines.CoroutineScope.() -> T): T = withContext(Dispatchers.IO) { block() } diff --git a/src/main/java/icu/samnyan/aqua/net/games/GameHelper.kt b/src/main/java/icu/samnyan/aqua/net/games/GameHelper.kt index faf4ea03..fe99ee9f 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/GameHelper.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/GameHelper.kt @@ -44,25 +44,3 @@ fun findTrend(log: List): List { } fun List.acc() = if (isEmpty()) 0.0 else sumOf { it.achievement }.toDouble() / size / 10000.0 - -val insertPattern = """INSERT INTO\s+(\w+)\s*\(([^)]+)\)\s*VALUES\s*\(([^)]+)\);""".toRegex() -data class SqlInsert(val table: String, val mapping: Map) -fun String.asSqlInsert(): SqlInsert { - val match = insertPattern.matchEntire(this) ?: error("Does not match insert pattern") - val (table, rawCols, rawVals) = match.destructured - val cols = rawCols.split(',').map { it.trim(' ', '"') } - - // Parse values with proper quote handling - val vals = mutableListOf() - var startI = 0 - var insideQuote = false - rawVals.forEachIndexed { i, c -> - if (c == ',' && !insideQuote) { - vals.add(rawVals.substring(startI, i).trim(' ', '"')) - startI = i + 1 - } else if (c == '"') insideQuote = !insideQuote - } - - assert(cols.size == vals.size) { "Column and value count mismatch" } - return SqlInsert(table, cols.zip(vals).toMap()) -} \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/net/games/ImportController.kt b/src/main/java/icu/samnyan/aqua/net/games/ImportController.kt new file mode 100644 index 00000000..44b5ad85 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/net/games/ImportController.kt @@ -0,0 +1,98 @@ +package icu.samnyan.aqua.net.games + +import ext.jackson +import ext.splitLines +import java.lang.reflect.Field +import kotlin.reflect.KClass + +// Import class with renaming +data class ImportClass( + val type: KClass, + val renames: Map? = null, + val name: String = type.simpleName!!.lowercase() +) + +abstract class ImportController( + val exportFields: Map, + val renameTable: Map> +) { + abstract fun createEmpty(): T + + init { + renameTable.values.forEach { + if (it.name !in exportFields) error("Code error! Export fields incomplete") + } + } + + /** + * Read an artemis SQL dump file and return Aqua JSON + */ + @Suppress("UNCHECKED_CAST") + fun importArtemisSql(sql: String): ImportResult { + val data = createEmpty() + val errors = ArrayList() + val warnings = ArrayList() + fun err(msg: String) { errors.add(msg) } + fun warn(msg: String) { warnings.add(msg) } + + val lists = exportFields.filter { it.value.type == List::class.java } + .mapValues { it.value.get(data) as ArrayList } + + val statements = sql.splitLines().mapNotNull { + try { it.asSqlInsert() } + catch (e: Exception) { err("Failed to parse insert: $it\n${e.message}"); null } + } + + // For each insert statement, we will try to parse the values + statements.forEachIndexed fi@{ i, insert -> + // Try to map tables + val tb = renameTable[insert.table] ?: return@fi warn("Unknown table ${insert.table} in insert $i") + val field = exportFields[tb.name]!! + val obj = tb.mapTo(insert.mapping) + + // Add value to list or set field + lists[tb.name]?.add(obj) ?: field.set(data, obj) + } + + return ImportResult(errors, warnings, jackson.writeValueAsString(data)) + } + + companion object + { + // Map a dictionary to a class + fun ImportClass.mapTo(rawDict: Map): T { + // Process renaming + var dict = renames?.let { rawDict + .filter { (k, _) -> if (k in it) it[k] != null else true } + .mapKeys { (k, _) -> it[k] ?: k } } ?: rawDict + + // Process Nones + dict = dict.filterValues { it != "None" } + + return jackson.convertValue(dict, type.java) + } + } +} + +// Read SQL dump and convert to dictionary +val insertPattern = """INSERT INTO\s+(\w+)\s*\(([^)]+)\)\s*VALUES\s*\(([^)]+)\);""".toRegex() +data class SqlInsert(val table: String, val mapping: Map) +fun String.asSqlInsert(): SqlInsert { + val match = insertPattern.matchEntire(this) ?: error("Does not match insert pattern") + val (table, rawCols, rawVals) = match.destructured + val cols = rawCols.split(',').map { it.trim(' ', '"') } + + // Parse values with proper quote handling + val vals = mutableListOf() + var startI = 0 + var insideQuote = false + rawVals.forEachIndexed { i, c -> + if (c == ',' && !insideQuote) { + vals.add(rawVals.substring(startI, i).trim(' ', '"')) + startI = i + 1 + } else if (c == '"') insideQuote = !insideQuote + } + + assert(cols.size == vals.size) { "Column and value count mismatch" } + return SqlInsert(table, cols.zip(vals).toMap()) +} diff --git a/src/main/java/icu/samnyan/aqua/net/games/Models.kt b/src/main/java/icu/samnyan/aqua/net/games/Models.kt index b601e1b0..605f6808 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/Models.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/Models.kt @@ -113,10 +113,3 @@ interface GenericPlaylogRepo : JpaRepository { } data class ImportResult(val errors: List, val warnings: List, val json: String) - -interface GameDataImport { - /** - * Read an artemis SQL dump file and return Aqua JSON - */ - fun importArtemisSql(sql: String): ImportResult -} \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/net/games/mai2/Mai2Import.kt b/src/main/java/icu/samnyan/aqua/net/games/mai2/Mai2Import.kt index 4e7093df..2a17c058 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/mai2/Mai2Import.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/mai2/Mai2Import.kt @@ -1,40 +1,15 @@ package icu.samnyan.aqua.net.games.mai2 -import ext.jackson import icu.samnyan.aqua.api.model.resp.sega.maimai2.external.Maimai2DataExport -import icu.samnyan.aqua.net.games.GameDataImport -import icu.samnyan.aqua.net.games.ImportResult -import icu.samnyan.aqua.net.games.asSqlInsert +import icu.samnyan.aqua.net.games.ImportClass +import icu.samnyan.aqua.net.games.ImportController import icu.samnyan.aqua.sega.maimai2.model.userdata.* -import kotlin.io.path.Path -import kotlin.io.path.readText -import kotlin.io.path.writeText -import kotlin.reflect.KClass -import kotlin.time.measureTime -data class ImportClass( - val type: KClass, - val renames: Map? = null, - val name: String = type.simpleName!!.lowercase() -) -val exportFields = Maimai2DataExport::class.java.declaredFields.associateBy { - it.name.replace("List", "").lowercase() -} - -class Mai2Import : GameDataImport { - fun ImportClass.mapTo(rawDict: Map): T { - // Process renaming - var dict = renames?.let { rawDict - .filter { (k, _) -> if (k in it) it[k] != null else true } - .mapKeys { (k, _) -> it[k] ?: k } } ?: rawDict - - // Process Nones - dict = dict.filterValues { it != "None" } - - return jackson.convertValue(dict, type.java) - } - - val renameTable = mapOf( +class Mai2Import : ImportController( + exportFields = Maimai2DataExport::class.java.declaredFields.associateBy { + it.name.replace("List", "").lowercase() + }, + renameTable = mapOf( "mai2_item_character" to ImportClass(UserCharacter::class), "mai2_item_charge" to ImportClass(UserCharge::class), "mai2_item_friend_season_ranking" to ImportClass(UserFriendSeasonRanking::class), @@ -54,45 +29,9 @@ class Mai2Import : GameDataImport { // "mai2_profile_rating" to ImportClass(UserRating::class), // "mai2_profile_region" to ImportClass(UserRegion::class), ) - - init { - renameTable.values.forEach { - if (it.name !in exportFields) error("Code error! Export fields incomplete") - } - } - - @Suppress("UNCHECKED_CAST") - override fun importArtemisSql(sql: String): ImportResult { - val data = Maimai2DataExport("SDEZ", UserDetail(), UserExtend(), UserOption(), - ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), - ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), - ArrayList(), UserUdemae()) - val errors = ArrayList() - val warnings = ArrayList() - fun err(msg: String) { errors.add(msg) } - fun warn(msg: String) { warnings.add(msg) } - - sql.lines() - - val lists = exportFields.filter { it.value.type == List::class.java } - .mapValues { it.value.get(data) as ArrayList } - - val statements = sql.replace("\r\n", "\n").split('\n', '\r').mapNotNull { - try { it.asSqlInsert() } - catch (e: Exception) { err("Failed to parse insert: $it\n${e.message}"); null } - } - - // For each insert statement, we will try to parse the values - statements.forEachIndexed fi@{ i, insert -> - // Try to map tables - val tb = renameTable[insert.table] ?: return@fi warn("Unknown table ${insert.table} in insert $i") - val field = exportFields[tb.name]!! - val obj = tb.mapTo(insert.mapping) - - // Add value to list or set field - lists[tb.name]?.add(obj) ?: field.set(data, obj) - } - - return ImportResult(errors, warnings, jackson.writeValueAsString(data)) - } +) { + override fun createEmpty() = Maimai2DataExport("SDEZ", UserDetail(), UserExtend(), UserOption(), + ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), + ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), + ArrayList(), UserUdemae()) } \ No newline at end of file