@@ -3,36 +3,61 @@ package app.simplecloud.plugin.api.shared.config
33import app.simplecloud.plugin.api.shared.exception.ConfigurationException
44import app.simplecloud.plugin.api.shared.repository.GenericEnumSerializer
55import kotlinx.coroutines.*
6+ import org.slf4j.LoggerFactory
67import org.spongepowered.configurate.ConfigurationOptions
78import org.spongepowered.configurate.kotlin.objectMapperFactory
89import org.spongepowered.configurate.yaml.NodeStyle
910import org.spongepowered.configurate.yaml.YamlConfigurationLoader
1011import java.io.File
1112import java.nio.file.*
13+ import java.util.concurrent.atomic.AtomicReference
1214import kotlin.coroutines.CoroutineContext
1315
1416/* *
1517 * A configuration factory that loads, saves and watches configuration files.
1618 * The factory automatically reloads the configuration when the file changes.
1719 *
20+ * Features:
21+ * - Thread-safe configuration handling
22+ * - File watching with automatic reloading
23+ * - Configuration change callbacks
24+ * - Validation support
25+ *
1826 * Usage:
1927 * ```
2028 * // Using create
2129 * val factory = ConfigFactory.create<MyConfig>(File("config.yaml"))
2230 *
2331 * // Using create with custom coroutineContext
2432 * val factory = ConfigFactory.create<MyConfig>(File("config.json"), Dispatchers.Default)
33+ *
34+ * // Add change listener
35+ * factory.onConfigChanged { oldConfig, newConfig ->
36+ * println("Config updated from $oldConfig to $newConfig")
37+ * }
38+ *
39+ * // Save the modified config
40+ * factory.save(modifiedConfig)
41+ *
42+ * // Optionally with validation
43+ * factory.save(modifiedConfig) { config ->
44+ * config.someField.isNotEmpty() // return true/false for validation
45+ * }
2546 * ```
2647 */
27- class ConfigFactory <T >(
48+ class ConfigFactory <T : Any >(
2849 private val file : File ,
2950 private val configClass : Class <T >,
3051 private val coroutineContext : CoroutineContext = Dispatchers .IO
3152) : AutoCloseable {
3253
33- private var config: T ? = null
54+ private val logger = LoggerFactory .getLogger(ConfigFactory ::class .java)
55+ private val configRef = AtomicReference <T >()
3456 private val path: Path = file.toPath()
3557 private var watchJob: Job ? = null
58+ private val changeListeners = mutableListOf< suspend (T ? , T ) -> Unit > ()
59+ private val saveLock = Object ()
60+ private val scope = CoroutineScope (coroutineContext + SupervisorJob ())
3661
3762 private val configurationLoader = YamlConfigurationLoader .builder()
3863 .path(path)
@@ -45,45 +70,145 @@ class ConfigFactory<T>(
4570 }
4671 .build()
4772
48- fun loadOrCreate (defaultConfig : T ) {
73+ /* *
74+ * Loads existing configuration or creates a new one with default values.
75+ * @param defaultConfig The default configuration to use if no file exists
76+ * @param validator Optional validation function for the configuration
77+ */
78+ fun loadOrCreate (defaultConfig : T , validator : ((T ) -> Boolean )? = null) {
4979 if (! configClass.isInstance(defaultConfig)) {
5080 throw IllegalArgumentException (" Default config must be an instance of ${configClass.name} " )
5181 }
5282
5383 if (file.exists()) {
54- loadConfig()
84+ loadConfig(validator )
5585 } else {
56- createDefaultConfig(defaultConfig)
86+ createDefaultConfig(defaultConfig, validator )
5787 }
5888
5989 registerWatcher()
6090 }
6191
62- private fun createDefaultConfig (defaultConfig : T ) {
92+ /* *
93+ * Checks if the configuration file exists.
94+ * @return true if the configuration file exists
95+ */
96+ fun exists (): Boolean = file.exists()
97+
98+ /* *
99+ * Adds a listener that will be called whenever the configuration changes.
100+ * @param listener The listener function that receives old and new config
101+ */
102+ fun onConfigChanged (listener : suspend (T ? , T ) -> Unit ) {
103+ synchronized(changeListeners) {
104+ changeListeners.add(listener)
105+ }
106+ }
107+
108+ private fun createDefaultConfig (defaultConfig : T , validator : ((T ) -> Boolean )? ) {
63109 path.parent?.let { Files .createDirectories(it) }
64110 Files .createFile(path)
65111
66- val node = configurationLoader.createNode()
67- node.set(configClass, defaultConfig)
68- configurationLoader.save(node)
69- config = defaultConfig
112+ if (validator?.invoke(defaultConfig) == false ) {
113+ throw ConfigurationException (" Default configuration failed validation" )
114+ }
115+
116+ synchronized(saveLock) {
117+ try {
118+ val node = configurationLoader.createNode()
119+ node.set(configClass, defaultConfig)
120+ configurationLoader.save(node)
121+ runBlocking(coroutineContext) {
122+ updateConfig(defaultConfig)
123+ }
124+ } catch (e: Exception ) {
125+ throw ConfigurationException (" Failed to save default configuration" , e)
126+ }
127+ }
70128 }
71129
130+ /* *
131+ * Gets the current configuration.
132+ * @throws IllegalStateException if configuration is not loaded
133+ */
72134 fun getConfig (): T {
73- return config ? : throw IllegalStateException (" Configuration not loaded or invalid type" )
135+ return configRef.get() ? : throw IllegalStateException (" Configuration not loaded or invalid type" )
74136 }
75137
138+ /* *
139+ * Manually reloads the configuration from disk.
140+ * @param validator Optional validation function for the loaded configuration
141+ * @throws ConfigurationException if loading or validation fails
142+ */
76143 @Throws(ConfigurationException ::class )
77- private fun loadConfig () {
144+ fun reloadConfig (validator : ((T ) -> Boolean )? = null) {
145+ loadConfig(validator)
146+ }
147+
148+ @Throws(ConfigurationException ::class )
149+ private fun loadConfig (validator : ((T ) -> Boolean )? = null) {
78150 try {
79151 val node = configurationLoader.load(ConfigurationOptions .defaults())
80- config = node.get(configClass)
152+ val loadedConfig = node.get(configClass)
81153 ? : throw ConfigurationException (" Failed to parse configuration file" )
154+
155+ if (validator?.invoke(loadedConfig) == false ) {
156+ throw ConfigurationException (" Configuration failed validation" )
157+ }
158+
159+ runBlocking(coroutineContext) {
160+ updateConfig(loadedConfig)
161+ }
82162 } catch (e: Exception ) {
83163 throw ConfigurationException (" Failed to load configuration" , e)
84164 }
85165 }
86166
167+ /* *
168+ * Saves the provided configuration to disk.
169+ * @param config The configuration to save
170+ * @param validator Optional validation function to run before saving
171+ * @throws ConfigurationException if saving or validation fails
172+ */
173+ @Throws(ConfigurationException ::class )
174+ fun save (config : T , validator : ((T ) -> Boolean )? = null) {
175+ if (validator?.invoke(config) == false ) {
176+ throw ConfigurationException (" Configuration failed validation" )
177+ }
178+
179+ synchronized(saveLock) {
180+ try {
181+ val node = configurationLoader.createNode()
182+ node.set(configClass, config)
183+ configurationLoader.save(node)
184+ runBlocking(coroutineContext) {
185+ updateConfig(config)
186+ }
187+ } catch (e: Exception ) {
188+ throw ConfigurationException (" Failed to save configuration" , e)
189+ }
190+ }
191+ }
192+
193+ private suspend fun updateConfig (newConfig : T ) {
194+ val oldConfig = configRef.get()
195+ configRef.set(newConfig)
196+
197+ val listeners = synchronized(changeListeners) {
198+ changeListeners.toList()
199+ }
200+
201+ listeners.forEach { listener ->
202+ try {
203+ withContext(coroutineContext) {
204+ listener(oldConfig, newConfig)
205+ }
206+ } catch (e: Exception ) {
207+ logger.error(" Error in config change listener" , e)
208+ }
209+ }
210+ }
211+
87212 private fun registerWatcher (): Job {
88213 val watchService = FileSystems .getDefault().newWatchService()
89214 path.parent?.register(
@@ -92,7 +217,7 @@ class ConfigFactory<T>(
92217 StandardWatchEventKinds .ENTRY_MODIFY
93218 )
94219
95- return CoroutineScope (coroutineContext) .launch {
220+ return scope .launch {
96221 watchService.use { watchService ->
97222 while (isActive) {
98223 val key = watchService.take()
@@ -118,17 +243,25 @@ class ConfigFactory<T>(
118243 try {
119244 loadConfig()
120245 } catch (e: ConfigurationException ) {
121- println (" Failed to reload configuration: ${e.message} " )
246+ logger.error (" Failed to reload configuration: ${e.message} " , e )
122247 }
123248 }
249+
250+ else -> {}
124251 }
125252 }
126253
127254 override fun close () {
255+ scope.cancel()
128256 watchJob?.cancel()
129257 }
130258
131259 companion object {
260+ /* *
261+ * Creates a new ConfigFactory instance.
262+ * @param file The configuration file
263+ * @param coroutineContext The coroutine context to use for async operations
264+ */
132265 inline fun <reified T : Any > create (
133266 file : File ,
134267 coroutineContext : CoroutineContext = Dispatchers .IO
0 commit comments