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 (
2628 private val file : File ,
27- private val defaultConfig : E
28- ) {
29+ private val configClass : Class <* >,
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: Any? = 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,94 @@ class ConfigFactory<E>(
4145 }
4246 .build()
4347
44- fun loadOrCreate () {
45- registerWatcher()
46- if (this .file.exists()) {
48+ fun loadOrCreate (defaultConfig : Any ) {
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 : Any ) {
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+ @Suppress(" UNCHECKED_CAST" )
73+ fun <T > getConfig (): T {
74+ return config as ? T ? : throw IllegalStateException (" Configuration not loaded or invalid type" )
75+ }
6376
77+ @Throws(ConfigurationException ::class )
6478 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" )
79+ try {
80+ val node = configurationLoader.load(ConfigurationOptions .defaults())
81+ config = node.get(configClass)
82+ ? : throw ConfigurationException (" Failed to parse configuration file" )
83+ } catch (e: Exception ) {
84+ throw ConfigurationException (" Failed to load configuration" , e)
85+ }
6886 }
6987
7088 private fun registerWatcher (): Job {
7189 val watchService = FileSystems .getDefault().newWatchService()
72- this . path.register(
90+ path.parent? .register(
7391 watchService,
7492 StandardWatchEventKinds .ENTRY_CREATE ,
7593 StandardWatchEventKinds .ENTRY_MODIFY
7694 )
7795
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- }
96+ return CoroutineScope (coroutineContext).launch {
97+ watchService.use { watchService ->
98+ while (isActive) {
99+ val key = watchService.take()
100+ key.pollEvents().forEach { event ->
101+ handleWatchEvent(event)
102+ }
103+ if (! key.reset()) {
104+ break
92105 }
93106 }
107+ }
108+ }.also { watchJob = it }
109+ }
94110
95- key.reset()
111+ private suspend fun handleWatchEvent (event : WatchEvent <* >) {
112+ val path = event.context() as ? Path ? : return
113+ if (! file.name.contains(path.toString())) return
114+
115+ when (event.kind()) {
116+ StandardWatchEventKinds .ENTRY_CREATE ,
117+ StandardWatchEventKinds .ENTRY_MODIFY -> {
118+ delay(100 )
119+ try {
120+ loadConfig()
121+ } catch (e: ConfigurationException ) {
122+ println (" Failed to reload configuration: ${e.message} " )
123+ }
96124 }
97125 }
98126 }
99127
128+ override fun close () {
129+ watchJob?.cancel()
130+ }
131+
132+ companion object {
133+ inline fun <reified T : Any > create (
134+ file : File ,
135+ coroutineContext : CoroutineContext = Dispatchers .IO
136+ ): ConfigFactory = ConfigFactory (file, T ::class .java, coroutineContext)
137+ }
100138}
0 commit comments