@@ -7,22 +7,23 @@ import com.zaxxer.hikari.HikariConfig
7
7
import com.zaxxer.hikari.HikariDataSource
8
8
import dev.racci.minix.api.annotations.MappedConfig
9
9
import dev.racci.minix.api.annotations.MappedExtension
10
- import dev.racci.minix.api.data.IConfig
10
+ import dev.racci.minix.api.data.MinixConfig
11
11
import dev.racci.minix.api.exceptions.MissingAnnotationException
12
+ import dev.racci.minix.api.exceptions.MissingPluginException
12
13
import dev.racci.minix.api.plugin.Minix
13
14
import dev.racci.minix.api.plugin.MinixPlugin
15
+ import dev.racci.minix.api.plugin.logger.MinixLogger
14
16
import dev.racci.minix.api.serializables.Serializer
15
17
import dev.racci.minix.api.services.DataService
16
18
import dev.racci.minix.api.updater.providers.UpdateProvider
19
+ import dev.racci.minix.api.utils.Closeable
17
20
import dev.racci.minix.api.utils.getKoin
18
21
import dev.racci.minix.api.utils.kotlin.ifInitialized
19
- import dev.racci.minix.api.utils.kotlin.ifTrue
20
22
import dev.racci.minix.api.utils.safeCast
21
23
import dev.racci.minix.api.utils.unsafeCast
22
- import io.leangen.geantyref.TypeToken
23
- import kotlinx.coroutines.Dispatchers
24
- import kotlinx.coroutines.runBlocking
25
- import kotlinx.coroutines.withContext
24
+ import kotlinx.coroutines.DelicateCoroutinesApi
25
+ import kotlinx.coroutines.ExecutorCoroutineDispatcher
26
+ import kotlinx.coroutines.newSingleThreadContext
26
27
import net.kyori.adventure.serializer.configurate4.ConfigurateComponentSerializer
27
28
import org.bukkit.plugin.Plugin
28
29
import org.jetbrains.exposed.dao.Entity
@@ -35,44 +36,39 @@ import org.koin.core.component.KoinComponent
35
36
import org.spongepowered.configurate.CommentedConfigurationNode
36
37
import org.spongepowered.configurate.ConfigurateException
37
38
import org.spongepowered.configurate.hocon.HoconConfigurationLoader
39
+ import org.spongepowered.configurate.kotlin.extensions.get
40
+ import org.spongepowered.configurate.kotlin.extensions.set
38
41
import org.spongepowered.configurate.kotlin.objectMapperFactory
39
42
import org.spongepowered.configurate.objectmapping.ConfigSerializable
40
43
import org.spongepowered.configurate.serialize.TypeSerializer
41
44
import org.spongepowered.configurate.serialize.TypeSerializerCollection
45
+ import org.spongepowered.configurate.transformation.ConfigurationTransformation
42
46
import java.io.File
43
- import java.io.IOException
44
47
import kotlin.reflect.KClass
45
48
import kotlin.reflect.full.createInstance
46
49
import kotlin.reflect.full.findAnnotation
47
- import kotlin.reflect.full.superclasses
50
+ import kotlin.reflect.full.hasAnnotation
48
51
49
52
@MappedExtension(Minix ::class , " Data Service" , bindToKClass = DataService ::class )
50
53
class DataServiceImpl (override val plugin : Minix ) : DataService() {
51
- private val configClasses: LoadingCache <KClass <* >, ConfigClass > = Caffeine .newBuilder().build(::ConfigClass )
52
-
53
- override val configurateLoaders: LoadingCache <KClass <* >, HoconConfigurationLoader > = Caffeine .newBuilder()
54
- .build() {
55
- val config = configClasses[it]
56
- runBlocking { getConfigurateLoader(it, config.file) }
57
- }
54
+ @OptIn(DelicateCoroutinesApi ::class )
55
+ private val threadContext = object : Closeable <ExecutorCoroutineDispatcher >() {
56
+ override fun create () = newSingleThreadContext(" Data Service Thread" )
57
+ override fun onClose () { value.value?.close() }
58
+ }
58
59
59
- override val configurations: LoadingCache <KClass <* >, Pair <Any , CommentedConfigurationNode >> = Caffeine .newBuilder()
60
- .removalListener<KClass <* >, Pair <Any , CommentedConfigurationNode >> { key, value, cause ->
60
+ val configDataHolder: LoadingCache <KClass <MinixConfig <MinixPlugin >>, ConfigData <MinixPlugin , MinixConfig <MinixPlugin >>> = Caffeine .newBuilder()
61
+ .executor(threadContext.get().executor)
62
+ .removalListener<KClass <* >, ConfigData <* , * >> { key, value, cause ->
61
63
if (key == null || value == null || cause == RemovalCause .REPLACED ) return @removalListener
62
- log.info { " Saving and disposing configurate class ${key.simpleName} " }
63
-
64
- val (config, node) = value
65
- val loader = configurateLoaders[key]
66
- config.safeCast<IConfig >()?.unloadCallback()
67
- if (loader.canSave()) {
68
- node.set(key.java, config)
69
- loader.save(node)
70
- }
64
+ log.info(scope = SCOPE ) { " Saving and disposing configurate class ${key.simpleName} " }
71
65
72
- configurateLoaders.invalidate(key)
73
- configClasses.invalidate(key)
66
+ value.configInstance.handleUnload()
67
+ if (value.configLoader.canSave()) {
68
+ value.save()
69
+ }
74
70
}
75
- .build { clazz -> runBlocking { loadFrom( ConfigClass (clazz), clazz) } }
71
+ .build(:: ConfigData )
76
72
77
73
private val dataSource = lazy {
78
74
HikariConfig ().apply {
@@ -87,78 +83,73 @@ class DataServiceImpl(override val plugin: Minix) : DataService() {
87
83
88
84
override suspend fun handleLoad () {
89
85
if (! plugin.dataFolder.exists() && ! plugin.dataFolder.mkdirs()) {
90
- log.error { " Failed to create data folder!" }
86
+ log.error(scope = SCOPE ) { " Failed to create data folder!" }
91
87
}
92
88
}
93
89
94
90
override suspend fun handleUnload () {
95
91
dataSource.ifInitialized(HikariDataSource ::close)
96
- configurations .invalidateAll()
92
+ configDataHolder .invalidateAll()
97
93
}
98
94
99
- class ConfigClass (kClass : KClass <* >) {
100
- val mappedConfig: MappedConfig
101
- val plugin: MinixPlugin
102
- val file: File
95
+ override fun <P : MinixPlugin , T : MinixConfig <P >> getConfig (kClass : KClass <T >): T ? = configDataHolder[kClass.unsafeCast()].configInstance as ? T
103
96
104
- init {
105
- val annotations = kClass.annotations
106
- mappedConfig = annotations.filterIsInstance<MappedConfig >().firstOrNull() ? : throw MissingAnnotationException (" Class ${kClass.qualifiedName} is not annotated with @MappedConfig" )
107
- annotations.all { it !is ConfigSerializable }.ifTrue { throw MissingAnnotationException (" Class ${kClass.qualifiedName} is not annotated with @ConfigSerializable" ) }
97
+ class ConfigData <P : MinixPlugin , T : MinixConfig <P >>(val kClass : KClass <T >) {
98
+ val mappedConfig: MappedConfig = this .kClass.findAnnotation() ? : throw MissingAnnotationException (this .kClass, MappedConfig ::class .unsafeCast())
99
+ val configInstance: T
100
+ val file: File
101
+ val node: CommentedConfigurationNode
102
+ val configLoader: HoconConfigurationLoader
108
103
109
- if (MinixPlugin ::class !in mappedConfig.parent.superclasses) {
110
- throw IllegalArgumentException (" Class ${mappedConfig.parent.qualifiedName} is not subclass of MinixPlugin" )
111
- }
112
- plugin = getKoin().getOrNull<MinixPlugin >(mappedConfig.parent) ? : throw IllegalStateException (" Could not find plugin instance for ${mappedConfig.parent} " )
113
- file = plugin.dataFolder.resolve(mappedConfig.file)
104
+ fun save () {
105
+ this .node.set(this .kClass, this .configInstance)
106
+ this .configLoader.save(updateNode())
114
107
}
115
- }
116
-
117
- object PluginData : IdTable<String>(" plugin" ) {
118
-
119
- override val id: Column <EntityID <String >> = text(" name" ).entityId()
120
- var newVersion = text(" new_version" )
121
- var oldVersion = text(" old_version" )
122
- }
123
-
124
- class DataHolder (plugin : EntityID <String >) : Entity<String>(plugin) {
125
108
126
- companion object : EntityClass <String , DataHolder >(PluginData ), KoinComponent {
109
+ private fun updateNode (): CommentedConfigurationNode {
110
+ if (! node.virtual()) { // we only want to migrate existing data
111
+ val trans = createVersionBuilder()
112
+ val startVersion = trans.version(node)
127
113
128
- fun getOrNull ( id : String ): DataHolder ? = find { PluginData .id eq id }.firstOrNull( )
114
+ trans. apply (node )
129
115
130
- fun getOrNull (plugin : Plugin ): DataHolder ? = getOrNull(plugin.name)
116
+ val endVersion = trans.version(node)
117
+ if (startVersion != endVersion) { // we might not have made any changes
118
+ getKoin().get<MinixLogger >().info { " Updated config schema from $startVersion to $endVersion " }
119
+ }
120
+ }
131
121
132
- operator fun get ( plugin : Plugin ): DataHolder = get(plugin.name)
122
+ return node
133
123
}
134
124
135
- var newVersion by PluginData .newVersion
136
- var oldVersion by PluginData .oldVersion
137
- }
138
-
139
- override suspend fun <T : Any > getConfigurateLoader (
140
- clazz : KClass <T >,
141
- file : File
142
- ): HoconConfigurationLoader = HoconConfigurationLoader .builder()
143
- .file(file)
144
- .prettyPrinting(true )
145
- .defaultOptions { options ->
146
- options.acceptsType(clazz.java)
147
- options.shouldCopyDefaults(true )
148
- options.serializers { serializerBuilder ->
149
- serializerBuilder.registerAnnotatedObjects(objectMapperFactory())
150
- .registerAll(TypeSerializerCollection .defaults())
151
- .registerAll(ConfigurateComponentSerializer .builder().build().serializers())
152
- .registerAll(UpdateProvider .UpdateProviderSerializer .serializers)
153
- .registerAll(Serializer .serializers)
154
- .also { getSerializerCollection(clazz)?.let (it::registerAll) } // User defined serializers
125
+ private fun createVersionBuilder (): ConfigurationTransformation .Versioned {
126
+ val builder = ConfigurationTransformation .versionedBuilder()
127
+ for ((version, transformation) in configInstance.versionTransformations) {
128
+ builder.versionKey()
129
+ builder.addVersion(version, transformation)
155
130
}
156
- }.build()
157
131
158
- private fun getSerializerCollection (clazz : KClass <* >): TypeSerializerCollection ? {
159
- val annotation = clazz.findAnnotation<MappedConfig >()
160
- return if (annotation != null ) {
161
- val extraSerializers = annotation.serializers.asList().listIterator()
132
+ return builder.build()
133
+ }
134
+
135
+ private fun buildConfigLoader () = HoconConfigurationLoader .builder()
136
+ .file(file)
137
+ .prettyPrinting(true )
138
+ .defaultOptions { options ->
139
+ options.acceptsType(kClass.java)
140
+ options.shouldCopyDefaults(true )
141
+ options.serializers { serializerBuilder ->
142
+ serializerBuilder.registerAnnotatedObjects(objectMapperFactory())
143
+ .registerAll(TypeSerializerCollection .defaults())
144
+ .registerAll(ConfigurateComponentSerializer .builder().build().serializers())
145
+ .registerAll(UpdateProvider .UpdateProviderSerializer .serializers)
146
+ .registerAll(Serializer .serializers)
147
+ .also { getSerializerCollection()?.let (it::registerAll) } // User defined serializers
148
+ }
149
+ }.build()
150
+
151
+ private fun getSerializerCollection (): TypeSerializerCollection ? {
152
+ val extraSerializers = mappedConfig.serializers.asList().listIterator()
162
153
val collection = TypeSerializerCollection .builder()
163
154
while (extraSerializers.hasNext()) {
164
155
val nextClazz = extraSerializers.next()
@@ -169,45 +160,66 @@ class DataServiceImpl(override val plugin: Minix) : DataService() {
169
160
}.getOrNull() ? : continue
170
161
collection.register(nextClazz.java, serializer.safeCast())
171
162
}
172
- collection.build()
173
- } else null
174
- }
175
163
176
- @Suppress(" kotlin:S6307" )
177
- @Throws(IOException ::class , MissingAnnotationException ::class , IllegalArgumentException ::class ) // uwu dangerous
178
- suspend inline fun <reified T : Any > loadFrom (config : ConfigClass = ConfigClass (T ::class)): T ? = loadFrom<T >(config, T ::class )?.first
164
+ return collection.build()
165
+ }
179
166
180
- @Throws(IOException ::class , MissingAnnotationException ::class , IllegalArgumentException ::class ) // uwu dangerous
181
- suspend fun <T > loadFrom (config : ConfigClass , clazz : KClass <* >): Pair <T , CommentedConfigurationNode >? = withContext(Dispatchers .IO ) {
182
- if (! config.plugin.dataFolder.exists() && ! config.plugin.dataFolder.mkdirs()) {
183
- config.plugin.log.warn { " Failed to create directory: ${config.plugin.dataFolder.absolutePath} " }
184
- return @withContext null
167
+ private fun ensureDirectory () {
168
+ if (! configInstance.plugin.dataFolder.exists() && ! configInstance.plugin.dataFolder.mkdirs()) {
169
+ getKoin().get<MinixLogger >().warn { " Failed to create directory: ${configInstance.plugin.dataFolder.absolutePath} " }
170
+ }
185
171
}
186
172
187
- val loader = configurateLoaders[clazz]
173
+ init {
174
+ println (" Building new config on thread: " + Thread .currentThread().name)
175
+
176
+ if (! this .kClass.hasAnnotation<ConfigSerializable >()) throw MissingAnnotationException (this .kClass, ConfigSerializable ::class .unsafeCast())
177
+
178
+ val plugin = getKoin().getOrNull<MinixPlugin >(this .mappedConfig.parent) ? : throw MissingPluginException (" Could not find plugin instance for ${this .mappedConfig.parent} " )
179
+ this .file = plugin.dataFolder.resolve(this .mappedConfig.file)
180
+
181
+ ensureDirectory()
182
+ this .configLoader = buildConfigLoader()
183
+
184
+ try {
185
+ this .node = this .configLoader.load()
186
+ this .configInstance = this .node.get(kClass) ? : throw RuntimeException (" Could not load configurate class ${this .kClass.simpleName} " )
188
187
189
- return @withContext try {
190
- val node = loader.load()
191
- val configNode = node.get(TypeToken .get(clazz.java))
192
- if (! config.file.exists()) {
193
- node.set(clazz.java, configNode)
194
- loader.save(node)
188
+ if (! this .file.exists()) {
189
+ this .save()
190
+ }
191
+
192
+ this .configInstance.load()
193
+ } catch (e: ConfigurateException ) {
194
+ getKoin().get<MinixLogger >().error(e) { " Failed to load configurate file ${this .file.name} " }
195
+ throw e
195
196
}
196
- configNode.safeCast<IConfig >()?.loadCallback()
197
- configNode.unsafeCast<T >() to node
198
- } catch (e: ConfigurateException ) {
199
- config.plugin.log.error(e) { " Failed to load configurate file ${config.file.name} " }
200
- null
201
197
}
202
198
}
203
199
204
- private inline fun <reified T : Any > save (clazz : KClass <T > = T : :class) {
205
- configurateLoaders[clazz].let { loader ->
206
- val (data, node) = configurations[clazz]
207
- node.set(clazz.java, data)
208
- loader.save(node)
200
+ object PluginData : IdTable<String>(" plugin" ) {
201
+
202
+ override val id: Column <EntityID <String >> = text(" name" ).entityId()
203
+ var newVersion = text(" new_version" )
204
+ var oldVersion = text(" old_version" )
205
+ }
206
+
207
+ class DataHolder (plugin : EntityID <String >) : Entity<String>(plugin) {
208
+
209
+ companion object : EntityClass <String , DataHolder >(PluginData ), KoinComponent {
210
+
211
+ fun getOrNull (id : String ): DataHolder ? = find { PluginData .id eq id }.firstOrNull()
212
+
213
+ fun getOrNull (plugin : Plugin ): DataHolder ? = getOrNull(plugin.name)
214
+
215
+ operator fun get (plugin : Plugin ): DataHolder = get(plugin.name)
209
216
}
217
+
218
+ var newVersion by PluginData .newVersion
219
+ var oldVersion by PluginData .oldVersion
210
220
}
211
221
212
- companion object : ExtensionCompanion <DataServiceImpl >()
222
+ companion object : ExtensionCompanion <DataServiceImpl >() {
223
+ const val SCOPE = " data"
224
+ }
213
225
}
0 commit comments