Skip to content

Commit b2c5f5a

Browse files
Merge pull request #17 from simplecloudapp/refactor/ConfigFactory
feat: ConfigFactory.kt
2 parents 18cdeef + 0966944 commit b2c5f5a

File tree

2 files changed

+92
-50
lines changed

2 files changed

+92
-50
lines changed
Lines changed: 88 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,41 @@
11
package app.simplecloud.plugin.api.shared.config
22

3+
import app.simplecloud.plugin.api.shared.exception.ConfigurationException
34
import 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.*
106
import org.spongepowered.configurate.ConfigurationOptions
117
import org.spongepowered.configurate.kotlin.objectMapperFactory
12-
import org.spongepowered.configurate.kotlin.toNode
138
import org.spongepowered.configurate.yaml.NodeStyle
149
import org.spongepowered.configurate.yaml.YamlConfigurationLoader
1510
import 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
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package app.simplecloud.plugin.api.shared.exception
2+
3+
class ConfigurationException(message: String, cause: Throwable? = null) :
4+
Exception(message, cause)

0 commit comments

Comments
 (0)