11package app.simplecloud.plugin.api.shared.config
22
3+ import app.simplecloud.plugin.api.shared.exception.ConfigurationException
34import app.simplecloud.plugin.api.shared.repository.GenericEnumSerializer
4- import kotlinx.coroutines.CoroutineScope
5- import kotlinx.coroutines.Dispatchers
6- import kotlinx.coroutines.Job
7- import kotlinx.coroutines.delay
8- import kotlinx.coroutines.isActive
9- import kotlinx.coroutines.launch
5+ import kotlinx.coroutines.*
106import org.spongepowered.configurate.ConfigurationOptions
117import org.spongepowered.configurate.kotlin.objectMapperFactory
12- import org.spongepowered.configurate.kotlin.toNode
138import org.spongepowered.configurate.yaml.NodeStyle
149import org.spongepowered.configurate.yaml.YamlConfigurationLoader
1510import java.io.File
16- import java.nio.file.FileSystems
17- import java.nio.file.Files
18- import java.nio.file.Path
19- import java.nio.file.StandardWatchEventKinds
11+ import java.nio.file.*
12+ import kotlin.coroutines.CoroutineContext
2013
2114/* *
22- * @author Niklas Nieberler
15+ * A configuration factory that loads, saves and watches configuration files.
16+ * The factory automatically reloads the configuration when the file changes.
17+ *
18+ * Usage:
19+ * ```
20+ * // Using create
21+ * val factory = ConfigFactory.create<MyConfig>(File("config.yaml"))
22+ *
23+ * // Using create with custom coroutineContext
24+ * val factory = ConfigFactory.create<MyConfig>(File("config.json"), Dispatchers.Default)
25+ * ```
2326 */
24-
25- class ConfigFactory <E >(
27+ class ConfigFactory <T >(
2628 private val file : File ,
27- private val defaultConfig : E
28- ) {
29+ private val configClass : Class <T >,
30+ private val coroutineContext : CoroutineContext = Dispatchers .IO
31+ ) : AutoCloseable {
2932
30- private var config = defaultConfig
31- private val path = file.toPath()
33+ private var config: T ? = null
34+ private val path: Path = file.toPath()
35+ private var watchJob: Job ? = null
3236
3337 private val configurationLoader = YamlConfigurationLoader .builder()
34- .path(this . path)
38+ .path(path)
3539 .nodeStyle(NodeStyle .BLOCK )
3640 .defaultOptions { options ->
3741 options.serializers { builder ->
@@ -41,60 +45,93 @@ class ConfigFactory<E>(
4145 }
4246 .build()
4347
44- fun loadOrCreate () {
45- registerWatcher()
46- if (this .file.exists()) {
48+ fun loadOrCreate (defaultConfig : T ) {
49+ if (! configClass.isInstance(defaultConfig)) {
50+ throw IllegalArgumentException (" Default config must be an instance of ${configClass.name} " )
51+ }
52+
53+ if (file.exists()) {
4754 loadConfig()
48- return
55+ } else {
56+ createDefaultConfig(defaultConfig)
4957 }
50- createDefaultConfig()
58+
59+ registerWatcher()
5160 }
5261
53- private fun createDefaultConfig () {
54- this . path.parent?.let { Files .createDirectories(it) }
55- Files .createFile(this . path)
62+ private fun createDefaultConfig (defaultConfig : T ) {
63+ path.parent?.let { Files .createDirectories(it) }
64+ Files .createFile(path)
5665
57- val configurationNode = this .configurationLoader.load(ConfigurationOptions .defaults())
58- this .defaultConfig!! .toNode(configurationNode)
59- this .configurationLoader.save(configurationNode)
66+ val node = configurationLoader.createNode()
67+ node.set(configClass, defaultConfig)
68+ configurationLoader.save(node)
69+ config = defaultConfig
6070 }
6171
62- fun getConfig (): E = this .config
72+ fun getConfig (): T {
73+ return config ? : throw IllegalStateException (" Configuration not loaded or invalid type" )
74+ }
6375
76+ @Throws(ConfigurationException ::class )
6477 private fun loadConfig () {
65- val configurationNode = this .configurationLoader.load(ConfigurationOptions .defaults())
66- this .config = configurationNode.get(this .defaultConfig!! ::class .java)
67- ? : throw IllegalStateException (" Config could not be loaded" )
78+ try {
79+ val node = configurationLoader.load(ConfigurationOptions .defaults())
80+ config = node.get(configClass)
81+ ? : throw ConfigurationException (" Failed to parse configuration file" )
82+ } catch (e: Exception ) {
83+ throw ConfigurationException (" Failed to load configuration" , e)
84+ }
6885 }
6986
7087 private fun registerWatcher (): Job {
7188 val watchService = FileSystems .getDefault().newWatchService()
72- this . path.register(
89+ path.parent? .register(
7390 watchService,
7491 StandardWatchEventKinds .ENTRY_CREATE ,
7592 StandardWatchEventKinds .ENTRY_MODIFY
7693 )
7794
78- return CoroutineScope (Dispatchers .IO ).launch {
79- while (isActive) {
80- val key = watchService.take()
81-
82- key.pollEvents().forEach { event ->
83- val path = event.context() as ? Path ? : return @forEach
84- if (! file.name.contains(path.toString())) return @launch
85-
86- when (event.kind()) {
87- StandardWatchEventKinds .ENTRY_CREATE ,
88- StandardWatchEventKinds .ENTRY_MODIFY -> {
89- delay(100 )
90- loadConfig()
91- }
95+ return CoroutineScope (coroutineContext).launch {
96+ watchService.use { watchService ->
97+ while (isActive) {
98+ val key = watchService.take()
99+ key.pollEvents().forEach { event ->
100+ handleWatchEvent(event)
101+ }
102+ if (! key.reset()) {
103+ break
92104 }
93105 }
106+ }
107+ }.also { watchJob = it }
108+ }
94109
95- key.reset()
110+ private suspend fun handleWatchEvent (event : WatchEvent <* >) {
111+ val path = event.context() as ? Path ? : return
112+ if (! file.name.contains(path.toString())) return
113+
114+ when (event.kind()) {
115+ StandardWatchEventKinds .ENTRY_CREATE ,
116+ StandardWatchEventKinds .ENTRY_MODIFY -> {
117+ delay(100 )
118+ try {
119+ loadConfig()
120+ } catch (e: ConfigurationException ) {
121+ println (" Failed to reload configuration: ${e.message} " )
122+ }
96123 }
97124 }
98125 }
99126
127+ override fun close () {
128+ watchJob?.cancel()
129+ }
130+
131+ companion object {
132+ inline fun <reified T : Any > create (
133+ file : File ,
134+ coroutineContext : CoroutineContext = Dispatchers .IO
135+ ): ConfigFactory <T > = ConfigFactory (file, T ::class .java, coroutineContext)
136+ }
100137}
0 commit comments