aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/gui/config/storage
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin/gui/config/storage')
-rw-r--r--src/main/kotlin/gui/config/storage/ConfigLoadContext.kt65
-rw-r--r--src/main/kotlin/gui/config/storage/ConfigStorageClass.kt8
-rw-r--r--src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt204
-rw-r--r--src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt83
-rw-r--r--src/main/kotlin/gui/config/storage/LegacyImporter.kt63
5 files changed, 423 insertions, 0 deletions
diff --git a/src/main/kotlin/gui/config/storage/ConfigLoadContext.kt b/src/main/kotlin/gui/config/storage/ConfigLoadContext.kt
new file mode 100644
index 0000000..59afaa1
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/ConfigLoadContext.kt
@@ -0,0 +1,65 @@
+package moe.nea.firmament.gui.config.storage
+
+import java.io.PrintWriter
+import java.nio.file.Path
+import org.apache.commons.io.output.StringBuilderWriter
+import kotlin.io.path.Path
+import kotlin.io.path.createParentDirectories
+import kotlin.io.path.writeText
+import moe.nea.firmament.Firmament
+
+data class ConfigLoadContext(
+ val loadId: String,
+) : AutoCloseable {
+ val logFile = Path("logs")
+ .resolve(Firmament.MOD_ID)
+ .resolve("config-$loadId.log")
+ .toAbsolutePath()
+ val logBuffer = StringBuilder()
+
+ var shouldSaveLogBuffer = false
+ fun markShouldSaveLogBuffer() {
+ shouldSaveLogBuffer = true
+ }
+
+ fun logDebug(message: String) {
+ logBuffer.append("[DEBUG] ").append(message).appendLine()
+ }
+
+ fun logInfo(message: String) {
+ Firmament.logger.info("[ConfigUpgrade] $message")
+ logBuffer.append("[INFO] ").append(message).appendLine()
+ }
+
+ fun logError(message: String, exception: Throwable) {
+ markShouldSaveLogBuffer()
+ Firmament.logger.error("[ConfigUpgrade] $message", exception)
+ logBuffer.append("[ERROR] ").append(message).appendLine()
+ PrintWriter(StringBuilderWriter(logBuffer)).use {
+ exception.printStackTrace(it)
+ }
+ logBuffer.appendLine()
+ }
+
+ fun logError(message: String) {
+ markShouldSaveLogBuffer()
+ Firmament.logger.error("[ConfigUpgrade] $message")
+ logBuffer.append("[ERROR] ").append(message).appendLine()
+ }
+
+ fun ensureWritable(path: Path) {
+ path.createParentDirectories()
+ }
+
+ override fun close() {
+ logInfo("Closing out config load.")
+ if (shouldSaveLogBuffer) {
+ try {
+ ensureWritable(logFile)
+ logFile.writeText(logBuffer.toString())
+ } catch (ex: Exception) {
+ logError("Could not save config load log", ex)
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/gui/config/storage/ConfigStorageClass.kt b/src/main/kotlin/gui/config/storage/ConfigStorageClass.kt
new file mode 100644
index 0000000..8258fe7
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/ConfigStorageClass.kt
@@ -0,0 +1,8 @@
+package moe.nea.firmament.gui.config.storage
+
+enum class ConfigStorageClass { // TODO: make this encode type info somehow
+ PROFILE,
+ STORAGE,
+ CONFIG,
+}
+
diff --git a/src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt b/src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt
new file mode 100644
index 0000000..22cba2c
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt
@@ -0,0 +1,204 @@
+package moe.nea.firmament.gui.config.storage
+
+import java.util.UUID
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.jsonObject
+import kotlin.io.path.Path
+import kotlin.io.path.exists
+import kotlin.io.path.forEachDirectoryEntry
+import kotlin.io.path.isDirectory
+import kotlin.io.path.listDirectoryEntries
+import kotlin.io.path.name
+import kotlin.io.path.readText
+import kotlin.io.path.writeText
+import moe.nea.firmament.util.data.IConfigProvider
+import moe.nea.firmament.util.data.IDataHolder
+import moe.nea.firmament.util.data.ProfileKeyedConfig
+import moe.nea.firmament.util.json.intoGson
+import moe.nea.firmament.util.json.intoKotlinJson
+
+object FirmamentConfigLoader {
+ val currentConfigVersion = 1000
+ val configFolder = Path("config/firmament")
+ .toAbsolutePath()
+ val storageFolder = configFolder.resolve("storage")
+ val profilePath = configFolder.resolve("profiles")
+ val tagLines = listOf(
+ "<- your config version here",
+ "I'm a teapot",
+ "mail.example.com ESMTP",
+ "Apples"
+ )
+ val configVersionFile = configFolder.resolve("config.version")
+
+ fun loadConfig() {
+ if (configFolder.exists()) {
+ if (!configVersionFile.exists()) {
+ LegacyImporter.importFromLegacy()
+ }
+ updateConfigs()
+ }
+
+ ConfigLoadContext("load-${System.currentTimeMillis()}").use { loadContext ->
+ val configData = FirstLevelSplitJsonFolder(loadContext, configFolder).load()
+ loadConfigFromData(configData, Unit, ConfigStorageClass.CONFIG)
+ val storageData = FirstLevelSplitJsonFolder(loadContext, storageFolder).load()
+ loadConfigFromData(storageData, Unit, ConfigStorageClass.STORAGE)
+ val profileData =
+ profilePath.listDirectoryEntries()
+ .filter { it.isDirectory() }
+ .associate {
+ UUID.fromString(it.name) to FirstLevelSplitJsonFolder(loadContext, it).load()
+ }
+ profileData.forEach { (key, value) ->
+ loadConfigFromData(value, key, ConfigStorageClass.PROFILE)
+ }
+ }
+ }
+
+ fun <T> loadConfigFromData(
+ configData: JsonObject,
+ key: T,
+ storageClass: ConfigStorageClass
+ ) {
+ for (holder in allConfigs) {
+ if (holder.storageClass == storageClass) {
+ (holder as IDataHolder<T>).loadFrom(key, configData)
+ }
+ }
+ }
+
+ fun <T> collectConfigFromData(
+ key: T,
+ storageClass: ConfigStorageClass,
+ ): JsonObject {
+ var json = JsonObject(mapOf())
+ for (holder in allConfigs) {
+ if (holder.storageClass == storageClass) {
+ json = mergeJson(json, (holder as IDataHolder<T>).saveTo(key))
+ }
+ }
+ return json
+ }
+
+ fun <T> saveStorage(
+ storageClass: ConfigStorageClass,
+ key: T,
+ firstLevelSplitJsonFolder: FirstLevelSplitJsonFolder,
+ ) {
+ firstLevelSplitJsonFolder.save(
+ collectConfigFromData(key, storageClass)
+ )
+ }
+
+ fun collectAllProfileIds(): Set<UUID> {
+ return allConfigs
+ .filter { it.storageClass == ConfigStorageClass.PROFILE }
+ .flatMapTo(mutableSetOf()) {
+ (it as ProfileKeyedConfig<*>).keys()
+ }
+ }
+
+ fun saveAll() {
+ ConfigLoadContext("load-${System.currentTimeMillis()}").use { context ->
+ saveStorage(
+ ConfigStorageClass.CONFIG,
+ Unit,
+ FirstLevelSplitJsonFolder(context, configFolder)
+ )
+ saveStorage(
+ ConfigStorageClass.STORAGE,
+ Unit,
+ FirstLevelSplitJsonFolder(context, storageFolder)
+ )
+ collectAllProfileIds().forEach { profileId ->
+ saveStorage(
+ ConfigStorageClass.PROFILE,
+ profileId,
+ FirstLevelSplitJsonFolder(context, profilePath.resolve(profileId.toString()))
+ )
+ }
+ }
+ }
+
+ fun mergeJson(a: JsonObject, b: JsonObject): JsonObject {
+ fun mergeInner(a: JsonElement?, b: JsonElement?): JsonElement {
+ if (a == null)
+ return b!!
+ if (b == null)
+ return a
+ a as JsonObject
+ b as JsonObject
+ return buildJsonObject {
+ (a.keys + b.keys)
+ .forEach {
+ put(it, mergeInner(a[it], b[it]))
+ }
+ }
+ }
+ return mergeInner(a, b) as JsonObject
+ }
+
+ val allConfigs: List<IDataHolder<*>> = IConfigProvider.providers.allValidInstances.flatMap { it.configs }
+
+ fun updateConfigs() {
+ val startVersion = configVersionFile.readText()
+ .substringBefore(' ')
+ .trim()
+ .toInt()
+ ConfigLoadContext("update-from-$startVersion-to-$currentConfigVersion-${System.currentTimeMillis()}")
+ .use { loadContext ->
+ updateOneConfig(
+ loadContext,
+ startVersion,
+ ConfigStorageClass.CONFIG,
+ FirstLevelSplitJsonFolder(loadContext, configFolder)
+ )
+ updateOneConfig(
+ loadContext,
+ startVersion,
+ ConfigStorageClass.STORAGE,
+ FirstLevelSplitJsonFolder(loadContext, storageFolder)
+ )
+ profilePath.forEachDirectoryEntry {
+ updateOneConfig(
+ loadContext,
+ startVersion,
+ ConfigStorageClass.PROFILE,
+ FirstLevelSplitJsonFolder(loadContext, it)
+ )
+ }
+ configVersionFile.writeText("$currentConfigVersion ${tagLines.random()}")
+ }
+ }
+
+ private fun updateOneConfig(
+ loadContext: ConfigLoadContext,
+ startVersion: Int,
+ storageClass: ConfigStorageClass,
+ firstLevelSplitJsonFolder: FirstLevelSplitJsonFolder
+ ) {
+ loadContext.logInfo("Starting upgrade from at ${firstLevelSplitJsonFolder.folder} ($storageClass) to $startVersion")
+ var data = firstLevelSplitJsonFolder.load()
+ for (nextVersion in (startVersion + 1)..currentConfigVersion) {
+ data = updateOneConfigOnce(nextVersion, storageClass, data)
+ }
+ firstLevelSplitJsonFolder.save(data)
+ }
+
+ private fun updateOneConfigOnce(
+ nextVersion: Int,
+ storageClass: ConfigStorageClass,
+ data: JsonObject
+ ): JsonObject {
+ return ConfigFixEvent.publish(ConfigFixEvent(storageClass, nextVersion, data.intoGson().asJsonObject))
+ .data.intoKotlinJson().jsonObject
+ }
+
+ fun markDirty(holder: IDataHolder<*>) {
+ TODO("Not yet implemented")
+ }
+
+}
diff --git a/src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt b/src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt
new file mode 100644
index 0000000..ff544d5
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt
@@ -0,0 +1,83 @@
+@file:OptIn(ExperimentalSerializationApi::class)
+
+package moe.nea.firmament.gui.config.storage
+
+import java.nio.file.Path
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.decodeFromStream
+import kotlinx.serialization.json.encodeToStream
+import kotlin.io.path.deleteExisting
+import kotlin.io.path.inputStream
+import kotlin.io.path.listDirectoryEntries
+import kotlin.io.path.nameWithoutExtension
+import kotlin.io.path.outputStream
+import moe.nea.firmament.Firmament
+
+class FirstLevelSplitJsonFolder(
+ val context: ConfigLoadContext,
+ val folder: Path
+) {
+ fun load(): JsonObject {
+ context.logInfo("Loading FLSJF from $folder")
+ return folder.listDirectoryEntries("*.json")
+ .mapNotNull(::loadIndividualFile)
+ .toMap()
+ .let(::JsonObject)
+ .also { context.logInfo("FLSJF from $folder - Voller Erfolg!") }
+ }
+
+ fun loadIndividualFile(path: Path): Pair<String, JsonElement>? {
+ context.logInfo("Loading partial file from $path")
+ return try {
+ path.inputStream().use {
+ path.nameWithoutExtension to Firmament.json.decodeFromStream(JsonElement.serializer(), it)
+ }
+ } catch (ex: Exception) {
+ context.logError("Could not load file from $path", ex)
+ null
+ }
+ }
+
+ fun save(value: JsonObject) {
+ context.logInfo("Saving FLSJF to $folder")
+ context.logDebug("Current value:\n$value")
+ val entries = folder.listDirectoryEntries("*.json")
+ .toMutableList()
+ for ((name, element) in value) {
+ val path = saveIndividualFile(name, element)
+ if (path != null) {
+ entries.remove(path)
+ }
+ }
+ if (entries.isNotEmpty()) {
+ context.logInfo("Deleting additional files.")
+ for (path in entries) {
+ context.logInfo("Deleting $path")
+// context.backup(path)
+ try {
+ path.deleteExisting()
+ } catch (ex: Exception) {
+ context.logError("Could not delete $path", ex)
+ }
+ }
+ }
+ context.logInfo("FLSJF to $folder - Voller Erfolg!")
+ }
+
+ fun saveIndividualFile(name: String, element: JsonElement): Path? {
+ try {
+ context.logInfo("Saving partial file with name $name")
+ val path = folder.resolve("$name.json")
+ context.ensureWritable(path)
+ path.outputStream().use {
+ Firmament.json.encodeToStream(JsonElement.serializer(), element, it)
+ }
+ return path
+ } catch (ex: Exception) {
+ context.logError("Could not save $name with value $element", ex)
+ return null
+ }
+ }
+}
diff --git a/src/main/kotlin/gui/config/storage/LegacyImporter.kt b/src/main/kotlin/gui/config/storage/LegacyImporter.kt
new file mode 100644
index 0000000..8915c17
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/LegacyImporter.kt
@@ -0,0 +1,63 @@
+package moe.nea.firmament.gui.config.storage
+
+import java.nio.file.Path
+import javax.xml.namespace.QName
+import kotlin.io.path.Path
+import kotlin.io.path.copyTo
+import kotlin.io.path.copyToRecursively
+import kotlin.io.path.createDirectories
+import kotlin.io.path.createParentDirectories
+import kotlin.io.path.exists
+import kotlin.io.path.forEachDirectoryEntry
+import kotlin.io.path.listDirectoryEntries
+import kotlin.io.path.moveTo
+import kotlin.io.path.name
+import kotlin.io.path.nameWithoutExtension
+import kotlin.io.path.writeText
+import moe.nea.firmament.gui.config.storage.FirmamentConfigLoader.configFolder
+import moe.nea.firmament.gui.config.storage.FirmamentConfigLoader.configVersionFile
+import moe.nea.firmament.gui.config.storage.FirmamentConfigLoader.storageFolder
+
+object LegacyImporter {
+ val legacyConfigVersion = 995
+ val backupPath = configFolder.resolveSibling("firmament-legacy-config-${System.currentTimeMillis()}")
+
+ fun copyIf(from: Path, to: Path) {
+ if (from.exists()) {
+ to.createParentDirectories()
+ from.copyTo(to)
+ }
+ }
+
+ fun importFromLegacy() {
+ configFolder.moveTo(backupPath)
+ configFolder.createDirectories()
+
+ copyIf(
+ backupPath.resolve("inventory-buttons.json"),
+ storageFolder.resolve("inventory-buttons.json")
+ )
+
+ backupPath.listDirectoryEntries("*.json")
+ .forEach { path ->
+ val name = path.name
+ if (name == "inventory-buttons.json")
+ return@forEach
+ path.copyTo(configFolder.resolve(name))
+ }
+
+ backupPath.resolve("profiles")
+ .forEachDirectoryEntry { category ->
+ category.forEachDirectoryEntry { profile ->
+ copyIf(
+ profile,
+ FirmamentConfigLoader.profilePath
+ .resolve(profile.nameWithoutExtension)
+ .resolve(category.name + ".json")
+ )
+ }
+ }
+
+ configVersionFile.writeText(legacyConfigVersion.toString())
+ }
+}