mirror of https://github.com/hykilpikonna/AquaDX
[M] Generalize artemis import code
parent
7fd7e17d1d
commit
d338809750
|
@ -82,6 +82,7 @@ fun Str.isValidEmail(): Bool = emailRegex.matches(this)
|
||||||
// JSON
|
// JSON
|
||||||
val ACCEPTABLE_FALSE = setOf("0", "false", "no", "off", "False", "None", "null")
|
val ACCEPTABLE_FALSE = setOf("0", "false", "no", "off", "False", "None", "null")
|
||||||
val ACCEPTABLE_TRUE = setOf("1", "true", "yes", "on", "True")
|
val ACCEPTABLE_TRUE = setOf("1", "true", "yes", "on", "True")
|
||||||
|
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "UNCHECKED_CAST")
|
||||||
val jackson = ObjectMapper().apply {
|
val jackson = ObjectMapper().apply {
|
||||||
registerModule(SimpleModule().addDeserializer(Boolean::class.java, object : JsonDeserializer<Boolean>() {
|
registerModule(SimpleModule().addDeserializer(Boolean::class.java, object : JsonDeserializer<Boolean>() {
|
||||||
override fun deserialize(parser: JsonParser, context: DeserializationContext) = when(parser.text) {
|
override fun deserialize(parser: JsonParser, context: DeserializationContext) = when(parser.text) {
|
||||||
|
@ -143,6 +144,7 @@ operator fun <K, V> MutableMap<K, V>.plusAssign(map: Map<K, V>) { putAll(map) }
|
||||||
operator fun Str.get(range: IntRange) = substring(range.first, (range.last + 1).coerceAtMost(length))
|
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))
|
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.center(width: Int, padChar: Char = ' ') = padStart((length + width) / 2, padChar).padEnd(width, padChar)
|
||||||
|
fun Str.splitLines() = replace("\r\n", "\n").split('\n')
|
||||||
|
|
||||||
// Coroutine
|
// Coroutine
|
||||||
suspend fun <T> async(block: suspend kotlinx.coroutines.CoroutineScope.() -> T): T = withContext(Dispatchers.IO) { block() }
|
suspend fun <T> async(block: suspend kotlinx.coroutines.CoroutineScope.() -> T): T = withContext(Dispatchers.IO) { block() }
|
||||||
|
|
|
@ -44,25 +44,3 @@ fun findTrend(log: List<TrendLog>): List<TrendOut> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun List<IGenericGamePlaylog>.acc() = if (isEmpty()) 0.0 else sumOf { it.achievement }.toDouble() / size / 10000.0
|
fun List<IGenericGamePlaylog>.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<String, String>)
|
|
||||||
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<String>()
|
|
||||||
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())
|
|
||||||
}
|
|
|
@ -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<T : Any>(
|
||||||
|
val type: KClass<T>,
|
||||||
|
val renames: Map<String, String?>? = null,
|
||||||
|
val name: String = type.simpleName!!.lowercase()
|
||||||
|
)
|
||||||
|
|
||||||
|
abstract class ImportController<T: Any>(
|
||||||
|
val exportFields: Map<String, Field>,
|
||||||
|
val renameTable: Map<String, ImportClass<*>>
|
||||||
|
) {
|
||||||
|
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<String>()
|
||||||
|
val warnings = ArrayList<String>()
|
||||||
|
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<Any> }
|
||||||
|
|
||||||
|
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 <T : Any> ImportClass<T>.mapTo(rawDict: Map<String, String>): 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<String, String>)
|
||||||
|
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<String>()
|
||||||
|
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())
|
||||||
|
}
|
|
@ -113,10 +113,3 @@ interface GenericPlaylogRepo<T: IGenericGamePlaylog> : JpaRepository<T, Long> {
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ImportResult(val errors: List<String>, val warnings: List<String>, val json: String)
|
data class ImportResult(val errors: List<String>, val warnings: List<String>, val json: String)
|
||||||
|
|
||||||
interface GameDataImport {
|
|
||||||
/**
|
|
||||||
* Read an artemis SQL dump file and return Aqua JSON
|
|
||||||
*/
|
|
||||||
fun importArtemisSql(sql: String): ImportResult
|
|
||||||
}
|
|
|
@ -1,40 +1,15 @@
|
||||||
package icu.samnyan.aqua.net.games.mai2
|
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.api.model.resp.sega.maimai2.external.Maimai2DataExport
|
||||||
import icu.samnyan.aqua.net.games.GameDataImport
|
import icu.samnyan.aqua.net.games.ImportClass
|
||||||
import icu.samnyan.aqua.net.games.ImportResult
|
import icu.samnyan.aqua.net.games.ImportController
|
||||||
import icu.samnyan.aqua.net.games.asSqlInsert
|
|
||||||
import icu.samnyan.aqua.sega.maimai2.model.userdata.*
|
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<T : Any>(
|
class Mai2Import : ImportController<Maimai2DataExport>(
|
||||||
val type: KClass<T>,
|
exportFields = Maimai2DataExport::class.java.declaredFields.associateBy {
|
||||||
val renames: Map<String, String?>? = null,
|
|
||||||
val name: String = type.simpleName!!.lowercase()
|
|
||||||
)
|
|
||||||
val exportFields = Maimai2DataExport::class.java.declaredFields.associateBy {
|
|
||||||
it.name.replace("List", "").lowercase()
|
it.name.replace("List", "").lowercase()
|
||||||
}
|
},
|
||||||
|
renameTable = mapOf(
|
||||||
class Mai2Import : GameDataImport {
|
|
||||||
fun <T : Any> ImportClass<T>.mapTo(rawDict: Map<String, String>): 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(
|
|
||||||
"mai2_item_character" to ImportClass(UserCharacter::class),
|
"mai2_item_character" to ImportClass(UserCharacter::class),
|
||||||
"mai2_item_charge" to ImportClass(UserCharge::class),
|
"mai2_item_charge" to ImportClass(UserCharge::class),
|
||||||
"mai2_item_friend_season_ranking" to ImportClass(UserFriendSeasonRanking::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_rating" to ImportClass(UserRating::class),
|
||||||
// "mai2_profile_region" to ImportClass(UserRegion::class),
|
// "mai2_profile_region" to ImportClass(UserRegion::class),
|
||||||
)
|
)
|
||||||
|
) {
|
||||||
init {
|
override fun createEmpty() = Maimai2DataExport("SDEZ", UserDetail(), UserExtend(), UserOption(),
|
||||||
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(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(),
|
ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(),
|
||||||
ArrayList(), UserUdemae())
|
ArrayList(), UserUdemae())
|
||||||
val errors = ArrayList<String>()
|
|
||||||
val warnings = ArrayList<String>()
|
|
||||||
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<Any> }
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue