If multiple configurations are located in the same directory,
+ * the directory is registered only once.
+ *
+ * @param config the configuration to watch
+ * @throws ConfigException if already registered or invalid
+ * @since 1.3.0
+ */
+ public void watch(@NotNull YamlConfig config) throws ConfigException {
+ Path path = config.getFile().toPath().toAbsolutePath();
+
+ if (watchedFiles.containsKey(path)) {
+ throw new ConfigException("File is already being watched: " + path);
+ }
+
+ Path directory = path.getParent();
+ if (directory == null) {
+ throw new ConfigException("File has no parent directory: " + path);
+ }
+
+ try {
+ directories.computeIfAbsent(directory, dir -> {
+ try {
+ return dir.register(
+ watchService,
+ StandardWatchEventKinds.ENTRY_MODIFY,
+ StandardWatchEventKinds.ENTRY_DELETE
+ );
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ watchedFiles.put(path, config);
+
+ } catch (RuntimeException e) {
+ throw new ConfigException("Failed to register file watcher: " + path, e);
+ }
+ }
+
+ /**
+ * Starts the watcher thread.
+ *
+ * @throws ConfigException if already running
+ * @since 1.3.0
+ */
+ public void start() throws ConfigException {
+ if (running) {
+ throw new ConfigException("Watcher is already running");
+ }
+
+ running = true;
+
+ Thread thread = new Thread(this::run, "YamlConfigWatcher");
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+ /**
+ * Stops the watcher thread.
+ *
+ * @since 1.3.0
+ */
+ public void stop() {
+ running = false;
+
+ try {
+ watchService.close();
+ } catch (IOException ignored) {
+ }
+ }
+
+ /**
+ * Internal watcher loop.
+ *
+ * Resolves {@link WatchKey} instances back to their corresponding directory
+ * and processes events for registered files only.
+ *
+ * @since 1.3.0
+ */
+ private void run() {
+ while (running) {
+ WatchKey key;
+
+ try {
+ key = watchService.take();
+ } catch (InterruptedException | ClosedWatchServiceException e) {
+ return;
+ }
+
+ Path dir = null;
+
+ for (Map.Entry entry : directories.entrySet()) {
+ if (entry.getValue().equals(key)) {
+ dir = entry.getKey();
+ break;
+ }
+ }
+
+ if (dir == null) {
+ key.reset();
+ continue;
+ }
+
+ for (WatchEvent> event : key.pollEvents()) {
+ Path changed = dir.resolve((Path) event.context()).toAbsolutePath();
+
+ // null safety
+ YamlConfig config = watchedFiles.get(changed);
+ if (config == null) {
+ continue;
+ }
+
+ if (event.kind() == StandardWatchEventKinds.ENTRY_DELETE) {
+ watchedFiles.remove(changed);
+ continue;
+ }
+
+ if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
+
+ @Nullable String oldChecksum = config.getCachedChecksum();
+ @Nullable String newChecksum;
+
+ try {
+ newChecksum = FileChecksum.getSha256Checksum(config.getFile());
+ } catch (Exception e) {
+ continue;
+ }
+
+ // skip if nothing actually changed
+ if (oldChecksum != null && oldChecksum.equals(newChecksum)) {
+ continue;
+ }
+
+ // debounce
+ long now = System.currentTimeMillis();
+ long last = lastModified.getOrDefault(changed, 0L);
+ if (now - last < 200) {
+ continue;
+ }
+
+ lastModified.put(changed, now);
+
+ try {
+ config.reload();
+
+ // get updated checksum
+ String updatedChecksum = config.getCachedChecksum();
+
+ // Fire event on main thread
+ Bukkit.getScheduler().runTask(javaPlugin, () -> {
+ Bukkit.getPluginManager().callEvent(
+ new ConfigReloadEvent(
+ config.getFile().getName(),
+ config.get(),
+ oldChecksum,
+ updatedChecksum
+ )
+ );
+ });
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ key.reset();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/dev/spexx/configurationAPI/events/ConfigReloadEvent.java b/src/main/java/dev/spexx/configurationAPI/events/ConfigReloadEvent.java
new file mode 100644
index 0000000..b0f68ee
--- /dev/null
+++ b/src/main/java/dev/spexx/configurationAPI/events/ConfigReloadEvent.java
@@ -0,0 +1,106 @@
+package dev.spexx.configurationAPI.events;
+
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Event fired when a configuration has been reloaded.
+ *
+ * This event provides access to the updated configuration state
+ * as well as checksum information for change comparison.
+ *
+ * @since 1.3.0
+ */
+public class ConfigReloadEvent extends Event {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ private final @NotNull String configName;
+ private final @NotNull FileConfiguration newConfig;
+ private final String oldChecksum;
+ private final String newChecksum;
+
+ /**
+ * Creates a new configuration reload event.
+ *
+ * @param configName the name of the configuration file (including extension)
+ * @param newConfig the updated {@link FileConfiguration} instance
+ * @param oldChecksum the previous checksum, or {@code null} if not available
+ * @param newChecksum the new checksum, or {@code null} if generation failed
+ * @since 1.3.0
+ */
+ public ConfigReloadEvent(@NotNull String configName,
+ @NotNull FileConfiguration newConfig,
+ String oldChecksum,
+ String newChecksum) {
+ this.configName = configName;
+ this.newConfig = newConfig;
+ this.oldChecksum = oldChecksum;
+ this.newChecksum = newChecksum;
+ }
+
+ /**
+ * Returns the list of handlers for this event.
+ *
+ * This method is required by the Bukkit event system.
+ *
+ * @return the static {@link HandlerList} for this event
+ * @since 1.3.0
+ */
+ public static @NotNull HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+
+ /**
+ * Returns the name of the configuration file.
+ *
+ * @return the configuration file name (including extension)
+ * @since 1.3.0
+ */
+ public @NotNull String getConfigName() {
+ return configName;
+ }
+
+ /**
+ * Returns the updated configuration.
+ *
+ * @return the reloaded {@link FileConfiguration}
+ * @since 1.3.0
+ */
+ public @NotNull FileConfiguration getNewConfig() {
+ return newConfig;
+ }
+
+ /**
+ * Returns the checksum before the reload.
+ *
+ * @return the previous checksum, or {@code null} if not available
+ * @since 1.3.0
+ */
+ public String getOldChecksum() {
+ return oldChecksum;
+ }
+
+ /**
+ * Returns the checksum after the reload.
+ *
+ * @return the new checksum, or {@code null} if generation failed
+ * @since 1.3.0
+ */
+ public String getNewChecksum() {
+ return newChecksum;
+ }
+
+ /**
+ * Returns the handlers for this event instance.
+ *
+ * @return the {@link HandlerList}
+ * @since 1.3.0
+ */
+ @Override
+ public @NotNull HandlerList getHandlers() {
+ return HANDLERS;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/dev/spexx/configurationAPI/events/ConfigReloadedEvent.java b/src/main/java/dev/spexx/configurationAPI/events/ConfigReloadedEvent.java
deleted file mode 100644
index f51d3cf..0000000
--- a/src/main/java/dev/spexx/configurationAPI/events/ConfigReloadedEvent.java
+++ /dev/null
@@ -1,204 +0,0 @@
-package dev.spexx.configurationAPI.events;
-
-import dev.spexx.configurationAPI.config.YamlConfig;
-import org.bukkit.event.Event;
-import org.bukkit.event.HandlerList;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * Event fired when a configuration file has been reloaded.
- *
- * This event is dispatched after a successful configuration reload
- * triggered by a file system change detected by the configuration watcher.
- *
- * The event provides both the previous and updated {@link YamlConfig}
- * instances, along with checksum information and the total time required
- * to perform the reload operation.
- *
- * Threading
- * This event is always fired synchronously on the main server thread.
- * Consumers may safely interact with the Bukkit API within event handlers.
- *
- * Immutability
- * All fields are immutable and represent a complete snapshot of the reload
- * operation at the time the event was created.
- *
- * Event Guarantees
- *
- * - Both {@link #getOldConfig()} and {@link #getNewConfig()} are non-null
- * - Checksums represent valid SHA-256 hashes of the file contents
- * - The event is only fired when a real content change is detected
- *
- *
- * @apiNote
- * Consumers should treat {@link YamlConfig} instances as read-only snapshots.
- * To obtain the most recent configuration state, query the managing component
- * rather than storing references long-term.
- *
- * @since 1.0.5
- */
-public final class ConfigReloadedEvent extends Event {
-
- /**
- * Static handler list required by the Bukkit event system.
- *
- * @since 1.0.5
- */
- private static final HandlerList HANDLERS = new HandlerList();
-
- /**
- * Previous configuration snapshot before reload.
- *
- * @since 1.0.5
- */
- private final @NotNull YamlConfig oldConfig;
-
- /**
- * Updated configuration snapshot after reload.
- *
- * @since 1.0.5
- */
- private final @NotNull YamlConfig newConfig;
-
- /**
- * SHA-256 checksum of the configuration file before reload.
- *
- * @since 1.0.5
- */
- private final @NotNull String oldChecksum;
-
- /**
- * SHA-256 checksum of the configuration file after reload.
- *
- * @since 1.0.5
- */
- private final @NotNull String newChecksum;
-
- /**
- * Time required to reload the configuration, measured in milliseconds.
- *
- * @since 1.0.5
- */
- private final int reloadTimeMs;
-
- /**
- * Constructs a new {@code ConfigReloadedEvent}.
- *
- * This constructor is typically invoked internally by the configuration
- * watcher after detecting and applying a file change.
- *
- * All parameters are required and must represent a valid transition
- * from one configuration state to another.
- *
- * @param oldConfig previous configuration snapshot, must not be {@code null}
- * @param newConfig updated configuration snapshot, must not be {@code null}
- * @param oldChecksum checksum of the file before reload, must not be {@code null}
- * @param newChecksum checksum of the file after reload, must not be {@code null}
- * @param reloadTimeMs time taken to reload the configuration in milliseconds
- *
- * @throws NullPointerException if any non-null parameter is {@code null}
- *
- * @since 1.0.5
- */
- public ConfigReloadedEvent(
- @NotNull YamlConfig oldConfig,
- @NotNull YamlConfig newConfig,
- @NotNull String oldChecksum,
- @NotNull String newChecksum,
- int reloadTimeMs
- ) {
- this.oldConfig = oldConfig;
- this.newConfig = newConfig;
- this.oldChecksum = oldChecksum;
- this.newChecksum = newChecksum;
- this.reloadTimeMs = reloadTimeMs;
- }
-
- /**
- * Returns the configuration snapshot prior to reload.
- *
- * This represents the last known valid state before the file change
- * was applied.
- *
- * @return previous configuration snapshot, never {@code null}
- *
- * @since 1.0.5
- */
- public @NotNull YamlConfig getOldConfig() {
- return oldConfig;
- }
-
- /**
- * Returns the configuration snapshot after reload.
- *
- * This represents the newly loaded state reflecting the current
- * contents of the configuration file.
- *
- * @return updated configuration snapshot, never {@code null}
- *
- * @since 1.0.5
- */
- public @NotNull YamlConfig getNewConfig() {
- return newConfig;
- }
-
- /**
- * Returns the checksum of the configuration file before reload.
- *
- * @return previous SHA-256 checksum, never {@code null}
- *
- * @since 1.0.5
- */
- public @NotNull String getOldChecksum() {
- return oldChecksum;
- }
-
- /**
- * Returns the checksum of the configuration file after reload.
- *
- * @return new SHA-256 checksum, never {@code null}
- *
- * @since 1.0.5
- */
- public @NotNull String getNewChecksum() {
- return newChecksum;
- }
-
- /**
- * Returns the time required to reload the configuration.
- *
- * The value is measured in milliseconds and represents the duration
- * between the start of the reload process and completion of the new
- * configuration snapshot.
- *
- * @return reload duration in milliseconds
- *
- * @since 1.0.5
- */
- public int getReloadTimeMs() {
- return reloadTimeMs;
- }
-
- /**
- * Returns the handler list for this event instance.
- *
- * @return handler list, never {@code null}
- *
- * @since 1.0.5
- */
- @Override
- public @NotNull HandlerList getHandlers() {
- return HANDLERS;
- }
-
- /**
- * Returns the static handler list required by the Bukkit event system.
- *
- * @return static handler list, never {@code null}
- *
- * @since 1.0.5
- */
- public static @NotNull HandlerList getHandlerList() {
- return HANDLERS;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/dev/spexx/configurationAPI/exceptions/ConfigException.java b/src/main/java/dev/spexx/configurationAPI/exceptions/ConfigException.java
new file mode 100644
index 0000000..103feac
--- /dev/null
+++ b/src/main/java/dev/spexx/configurationAPI/exceptions/ConfigException.java
@@ -0,0 +1,58 @@
+package dev.spexx.configurationAPI.exceptions;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Base exception for all configuration-related errors.
+ *
+ * This exception acts as the root of the configuration exception hierarchy.
+ * All configuration-specific exceptions extend this class.
+ *
+ * It extends {@link RuntimeException}, making it unchecked. This allows
+ * configuration operations to remain concise while still providing the option
+ * to handle errors explicitly when needed.
+ *
+ * @since 1.3.0
+ */
+public class ConfigException extends RuntimeException {
+
+ /**
+ * Constructs a new {@link ConfigException} with no detail message.
+ *
+ * @since 1.3.0
+ */
+ public ConfigException() {
+ super();
+ }
+
+ /**
+ * Constructs a new {@link ConfigException} with a detail message.
+ *
+ * @param message the detail message describing the error
+ * @since 1.3.0
+ */
+ public ConfigException(@NotNull String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new {@link ConfigException} with a detail message and cause.
+ *
+ * @param message the detail message describing the error
+ * @param cause the underlying cause of the exception
+ * @since 1.3.0
+ */
+ public ConfigException(@NotNull String message, @NotNull Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructs a new {@link ConfigException} with a cause.
+ *
+ * @param cause the underlying cause of the exception
+ * @since 1.3.0
+ */
+ public ConfigException(@NotNull Throwable cause) {
+ super(cause);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/dev/spexx/configurationAPI/exceptions/ConfigFileException.java b/src/main/java/dev/spexx/configurationAPI/exceptions/ConfigFileException.java
new file mode 100644
index 0000000..dc07cb3
--- /dev/null
+++ b/src/main/java/dev/spexx/configurationAPI/exceptions/ConfigFileException.java
@@ -0,0 +1,77 @@
+package dev.spexx.configurationAPI.exceptions;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
+/**
+ * Exception related to configuration file operations.
+ *
+ * This exception is used when an error occurs while interacting with a file,
+ * such as missing files, invalid paths, or general I/O-related issues.
+ *
+ * It may optionally carry a reference to the file that caused the failure.
+ *
+ * @since 1.3.0
+ */
+public class ConfigFileException extends ConfigException {
+
+ /**
+ * The file associated with the exception, if available.
+ *
+ * @since 1.3.0
+ */
+ private final File file;
+
+ /**
+ * Constructs a new {@link ConfigFileException} with a detail message.
+ *
+ * @param message the detail message describing the error
+ * @since 1.3.0
+ */
+ public ConfigFileException(@NotNull String message) {
+ super(message);
+ this.file = null;
+ }
+
+ /**
+ * Constructs a new {@link ConfigFileException} for a specific file.
+ *
+ * The file path is appended to the message for easier debugging.
+ *
+ * @param file the file related to the error
+ * @param message the detail message describing the error
+ * @since 1.3.0
+ */
+ public ConfigFileException(@NotNull File file, @NotNull String message) {
+ super(message + ": " + file.getAbsolutePath());
+ this.file = file;
+ }
+
+ /**
+ * Constructs a new {@link ConfigFileException} for a specific file with a cause.
+ *
+ * The file path is appended to the message for easier debugging.
+ *
+ * @param file the file related to the error
+ * @param message the detail message describing the error
+ * @param cause the underlying cause of the exception
+ * @since 1.3.0
+ */
+ public ConfigFileException(@NotNull File file,
+ @NotNull String message,
+ @NotNull Throwable cause) {
+ super(message + ": " + file.getAbsolutePath(), cause);
+ this.file = file;
+ }
+
+ /**
+ * Returns the file associated with this exception.
+ *
+ * @return the file, or {@code null} if not specified
+ * @since 1.3.0
+ */
+ public File getFile() {
+ return file;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/dev/spexx/configurationAPI/exceptions/ConfigParseException.java b/src/main/java/dev/spexx/configurationAPI/exceptions/ConfigParseException.java
new file mode 100644
index 0000000..f11b05b
--- /dev/null
+++ b/src/main/java/dev/spexx/configurationAPI/exceptions/ConfigParseException.java
@@ -0,0 +1,44 @@
+package dev.spexx.configurationAPI.exceptions;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
+/**
+ * Exception thrown when a configuration file cannot be parsed.
+ *
+ * This occurs when the file content is syntactically invalid or does not
+ * conform to the expected format.
+ *
+ * Typical causes include malformed structure, invalid indentation,
+ * or unexpected tokens.
+ *
+ * @since 1.3.0
+ */
+public class ConfigParseException extends ConfigFileException {
+
+ /**
+ * Constructs a new {@link ConfigParseException} for a specific file.
+ *
+ * @param file the file that failed to parse
+ * @param message the detail message describing the parsing error
+ * @since 1.3.0
+ */
+ public ConfigParseException(@NotNull File file, @NotNull String message) {
+ super(file, "Failed to parse configuration: " + message);
+ }
+
+ /**
+ * Constructs a new {@link ConfigParseException} for a specific file with a cause.
+ *
+ * @param file the file that failed to parse
+ * @param message the detail message describing the parsing error
+ * @param cause the underlying cause of the parsing failure
+ * @since 1.3.0
+ */
+ public ConfigParseException(@NotNull File file,
+ @NotNull String message,
+ @NotNull Throwable cause) {
+ super(file, "Failed to parse configuration: " + message, cause);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/dev/spexx/configurationAPI/exceptions/ConfigPermissionException.java b/src/main/java/dev/spexx/configurationAPI/exceptions/ConfigPermissionException.java
new file mode 100644
index 0000000..fa7f006
--- /dev/null
+++ b/src/main/java/dev/spexx/configurationAPI/exceptions/ConfigPermissionException.java
@@ -0,0 +1,28 @@
+package dev.spexx.configurationAPI.exceptions;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
+/**
+ * Exception thrown when a configuration file lacks the required permission
+ * for an operation.
+ *
+ * This is typically used to validate file access before performing actions
+ * such as reading or writing.
+ *
+ * @since 1.3.0
+ */
+public class ConfigPermissionException extends ConfigFileException {
+
+ /**
+ * Constructs a new {@link ConfigPermissionException} for a specific file.
+ *
+ * @param file the file that failed permission validation
+ * @param permission the missing permission (e.g. "read", "write", "execute")
+ * @since 1.3.0
+ */
+ public ConfigPermissionException(@NotNull File file, @NotNull String permission) {
+ super(file, "Missing " + permission + " permission");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/dev/spexx/configurationAPI/file/PermissionChecker.java b/src/main/java/dev/spexx/configurationAPI/file/PermissionChecker.java
new file mode 100644
index 0000000..ddcf5c6
--- /dev/null
+++ b/src/main/java/dev/spexx/configurationAPI/file/PermissionChecker.java
@@ -0,0 +1,50 @@
+package dev.spexx.configurationAPI.file;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
+/**
+ * Utility for checking file system permissions on a {@link File}.
+ *
+ * Provides direct access to the file's readable, writable, and executable
+ * states using the standard {@link File} API.
+ *
+ * This is typically used for validation before performing file operations
+ * such as loading or saving configurations.
+ *
+ * @param file the file to check, must not be {@code null}
+ * @since 1.3.0
+ */
+public record PermissionChecker(@NotNull File file) {
+
+ /**
+ * Returns whether the file is readable.
+ *
+ * @return {@code true} if the file can be read, otherwise {@code false}
+ * @since 1.3.0
+ */
+ public boolean canRead() {
+ return file.canRead();
+ }
+
+ /**
+ * Returns whether the file is writable.
+ *
+ * @return {@code true} if the file can be written to, otherwise {@code false}
+ * @since 1.3.0
+ */
+ public boolean canWrite() {
+ return file.canWrite();
+ }
+
+ /**
+ * Returns whether the file is executable.
+ *
+ * @return {@code true} if the file can be executed, otherwise {@code false}
+ * @since 1.3.0
+ */
+ public boolean canExecute() {
+ return file.canExecute();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/dev/spexx/configurationAPI/manager/ConfigManager.java b/src/main/java/dev/spexx/configurationAPI/manager/ConfigManager.java
index 7059cec..d351fd7 100644
--- a/src/main/java/dev/spexx/configurationAPI/manager/ConfigManager.java
+++ b/src/main/java/dev/spexx/configurationAPI/manager/ConfigManager.java
@@ -1,289 +1,191 @@
package dev.spexx.configurationAPI.manager;
-import dev.spexx.configurationAPI.config.ConfigLoadResult;
-import dev.spexx.configurationAPI.config.ConfigLoadStatus;
-import dev.spexx.configurationAPI.config.YamlConfig;
-import dev.spexx.configurationAPI.watcher.GlobalConfigWatcher;
-import org.bukkit.configuration.file.YamlConfiguration;
+import dev.spexx.configurationAPI.configuration.yaml.YamlConfig;
+import dev.spexx.configurationAPI.configuration.yaml.YamlConfigWatcher;
+import dev.spexx.configurationAPI.exceptions.ConfigException;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
-import java.nio.file.Path;
-import java.util.Collection;
-import java.util.Objects;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
/**
- * Central access point for configuration files managed by the system.
+ * Manages multiple {@link YamlConfig} instances.
*
- * This class is responsible for:
- *
- * - Loading configuration files from disk
- * - Registering configurations with the global watcher
- * - Providing access to the latest immutable configuration snapshots
- *
+ * Provides a centralized registry for configuration files, ensuring that each file
+ * is registered only once and accessed consistently.
*
- * Architecture
- * The {@link GlobalConfigWatcher} serves as the single source of truth for all
- * configuration state. This class delegates all state management to the watcher
- * and does not maintain its own cache.
+ * Integrates with {@link YamlConfigWatcher} to optionally enable automatic reload
+ * when files are modified.
*
- * Thread Safety
- * This class is thread-safe. The underlying watcher uses concurrent data structures
- * and atomic replacement of {@link YamlConfig} instances.
- *
- * @apiNote {@link YamlConfig} instances are immutable snapshots. Consumers should
- * retrieve them on demand instead of caching long-term references.
- *
- * @since 1.1.0
+ * @since 1.3.0
*/
-public final class ConfigManager {
-
- private final @NotNull JavaPlugin plugin;
- private final @NotNull GlobalConfigWatcher watcher;
+public class ConfigManager {
/**
- * Creates a new {@code ConfigManager}.
- *
- * Initializes and starts the {@link GlobalConfigWatcher}, which begins
- * monitoring registered configuration files for changes.
- *
- * @param plugin owning plugin instance, must not be {@code null}
+ * Stores registered configurations keyed by their absolute file path.
*
- * @throws NullPointerException if {@code plugin} is {@code null}
- * @throws RuntimeException if watcher initialization fails
+ * Using a {@link String} key avoids inconsistencies caused by different
+ * {@link File} instances referring to the same path.
*
- * @since 1.1.0
+ * @since 1.3.0
*/
- public ConfigManager(@NotNull JavaPlugin plugin) {
- this.plugin = Objects.requireNonNull(plugin, "plugin");
+ private final @NotNull Map configs = new ConcurrentHashMap<>();
- try {
- this.watcher = new GlobalConfigWatcher(plugin);
- this.watcher.start();
- } catch (IOException e) {
- throw new RuntimeException("Failed to initialize GlobalConfigWatcher", e);
- }
- }
+ private final @NotNull YamlConfigWatcher watcher;
/**
- * Returns the latest configuration snapshot for the given file.
- *
- * This method does not attempt to load the file. The configuration must
- * already be registered.
+ * Creates a new configuration manager.
*
- * @param file configuration file, must not be {@code null}
- * @return latest {@link YamlConfig} snapshot, never {@code null}
+ * An internal {@link YamlConfigWatcher} is created for tracking file changes.
*
- * @throws NullPointerException if {@code file} is {@code null}
- * @throws IllegalStateException if the configuration is not loaded
- *
- * @since 1.1.0
+ * @param javaPlugin the plugin instance used for scheduling and event dispatching
+ * @throws ConfigException if watcher initialization fails
+ * @since 1.3.0
*/
- public @NotNull YamlConfig get(@NotNull File file) {
-
- Path path = normalize(file.toPath());
-
- YamlConfig config = watcher.get(path);
- if (config == null) {
- throw new IllegalStateException("Config not loaded: " + path);
- }
-
- return config;
+ public ConfigManager(@NotNull JavaPlugin javaPlugin) throws ConfigException {
+ this.watcher = new YamlConfigWatcher(javaPlugin);
}
/**
- * Returns an existing configuration or loads it if not already tracked.
- *
- * If the configuration is not registered, it is loaded and registered
- * with the watcher.
+ * Registers and initializes a configuration file.
*
- * @param file configuration file, must not be {@code null}
- * @return latest {@link YamlConfig} snapshot, never {@code null}
+ * If the file does not exist, it is created before loading.
*
- * @since 1.1.0
+ * @param file the configuration file
+ * @return the managed {@link YamlConfig}
+ * @throws ConfigException if the file is already registered or initialization fails
+ * @since 1.3.0
*/
- public @NotNull YamlConfig getOrLoad(@NotNull File file) {
+ public @NotNull YamlConfig register(@NotNull File file) throws ConfigException {
- Path path = normalize(file.toPath());
+ String key = file.getAbsolutePath();
- YamlConfig existing = watcher.get(path);
- if (existing != null) {
- return existing;
+ if (configs.containsKey(key)) {
+ throw new ConfigException("Config already registered: " + key);
}
- return loadInternal(file);
+ YamlConfig config = new YamlConfig(file);
+
+ config.create();
+ config.load();
+
+ configs.put(key, config);
+
+ watcher.watch(config);
+
+ return config;
}
/**
- * Loads a configuration file intended for internal plugin usage.
- *
- * This method guarantees that the file exists by copying it from the
- * plugin JAR if missing.
- *
- * Behavior:
- *
- * - Validates that the resource exists in the plugin JAR
- * - Copies the resource if the file does not exist
- * - Loads and registers the configuration
- *
- *
- * @param resourceName resource name (e.g. {@code "config.yml"})
- * @return loaded configuration snapshot
+ * Registers a configuration file using a resource from the plugin JAR.
*
- * @throws IllegalArgumentException if resource is missing in JAR
+ * If the file does not exist, it is copied from the specified resource path
+ * before being loaded.
*
- * @since 1.1.0
+ * @param file the target configuration file
+ * @param resourcePath the path inside the plugin JAR
+ * @param plugin the plugin used to access the resource
+ * @throws ConfigException if already registered or copy/load fails
+ * @since 1.3.0
*/
- public @NotNull YamlConfig getInternal(@NotNull String resourceName) {
+ public void registerFromJar(@NotNull File file,
+ @NotNull String resourcePath,
+ @NotNull JavaPlugin plugin) throws ConfigException {
- Objects.requireNonNull(resourceName, "resourceName");
+ String key = file.getAbsolutePath();
- // Ensure resource exists inside JAR
- if (plugin.getResource(resourceName) == null) {
- throw new IllegalArgumentException("Resource not found in plugin JAR: " + resourceName);
+ if (configs.containsKey(key)) {
+ throw new ConfigException("Config already registered: " + key);
}
- File file = new File(plugin.getDataFolder(), resourceName);
-
- // Copy resource if missing
if (!file.exists()) {
- plugin.saveResource(resourceName, false);
+ copyResource(plugin, resourcePath, file);
}
- return getOrLoad(file);
- }
+ YamlConfig config = new YamlConfig(file);
+ config.create();
+ config.load();
- /**
- * Attempts to load a configuration file safely.
- *
- * Never throws exceptions. Instead, returns a structured result.
- *
- * @param file configuration file
- * @return result describing outcome
- *
- * @since 1.1.0
- */
- public @NotNull ConfigLoadResult tryLoad(@NotNull File file) {
- try {
- return new ConfigLoadResult(ConfigLoadStatus.LOADED, loadInternal(file), null);
- } catch (Exception e) {
- return new ConfigLoadResult(ConfigLoadStatus.IO_ERROR, null, e);
- }
+ configs.put(key, config);
+
+ watcher.watch(config);
}
/**
- * Attempts to create and load a configuration safely.
+ * Copies a resource from the plugin JAR to the specified file.
*
- * @param file configuration file
- * @return result describing outcome
+ * Parent directories are created if necessary.
*
- * @since 1.1.0
+ * @param plugin the plugin providing the resource
+ * @param resourcePath the path inside the plugin JAR
+ * @param target the target file location
+ * @throws ConfigException if the resource is missing or copy fails
+ * @since 1.3.0
*/
- public @NotNull ConfigLoadResult tryCreate(@NotNull File file) {
+ private void copyResource(@NotNull JavaPlugin plugin,
+ @NotNull String resourcePath,
+ @NotNull File target) throws ConfigException {
- if (file.exists()) {
- return new ConfigLoadResult(ConfigLoadStatus.ALREADY_EXISTS, get(file), null);
- }
+ try (InputStream in = plugin.getResource(resourcePath)) {
- try {
- ensureParentDirectories(file);
- return new ConfigLoadResult(ConfigLoadStatus.CREATED, loadInternal(file), null);
- } catch (Exception e) {
- return new ConfigLoadResult(ConfigLoadStatus.IO_ERROR, null, e);
- }
- }
+ if (in == null) {
+ throw new ConfigException("Resource not found in jar: " + resourcePath);
+ }
- /**
- * Stops tracking a configuration file.
- *
- * @param file configuration file
- *
- * @since 1.1.0
- */
- public void unload(@NotNull File file) {
- watcher.unregister(normalize(file.toPath()));
- }
+ if (target.getParentFile() != null) {
+ Files.createDirectories(target.getParentFile().toPath());
+ }
- /**
- * Returns all tracked configurations.
- *
- * @return collection of configurations
- *
- * @since 1.1.0
- */
- public @NotNull Collection getAll() {
- return watcher.getAll();
- }
+ Files.copy(in, target.toPath());
- /**
- * Stops the underlying watcher.
- *
- * Must be called during plugin shutdown to avoid thread leaks.
- *
- * @since 1.1.0
- */
- public void shutdown() {
- watcher.stop();
+ } catch (IOException e) {
+ throw new ConfigException("Failed to copy resource: " + resourcePath, e);
+ }
}
/**
- * Internal load implementation.
+ * Returns a registered configuration.
*
- * @param file configuration file
- * @return loaded configuration
+ * Lookup is performed using the file's absolute path to ensure consistency.
*
- * @since 1.1.0
+ * @param file the configuration file
+ * @return the {@link YamlConfig}
+ * @throws ConfigException if the config is not registered
+ * @since 1.3.0
*/
- private @NotNull YamlConfig loadInternal(@NotNull File file) {
+ public @NotNull YamlConfig get(@NotNull File file) throws ConfigException {
+ String key = file.getAbsolutePath();
- // Ensure directory structure exists
- ensureParentDirectories(file);
+ YamlConfig config = configs.get(key);
- // Load YAML into memory
- YamlConfig config = new YamlConfig(
- file,
- YamlConfiguration.loadConfiguration(file)
- );
-
- try {
- // Register with watcher for live updates
- watcher.register(config);
- } catch (IOException e) {
- throw new RuntimeException("Failed to register watcher for " + file, e);
+ if (config == null) {
+ throw new ConfigException("Config not registered: " + key);
}
return config;
}
/**
- * Ensures parent directories exist.
- *
- * @param file file whose parent directories should exist
+ * Starts the internal watcher.
*
- * @since 1.1.0
+ * @throws ConfigException if already running or fails
+ * @since 1.3.0
*/
- private void ensureParentDirectories(@NotNull File file) {
-
- File parent = file.getParentFile();
-
- if (parent != null && !parent.exists()) {
- if (!parent.mkdirs() && !parent.exists()) {
- throw new IllegalStateException("Failed to create directories: " + parent);
- }
- }
+ public void start() throws ConfigException {
+ watcher.start();
}
/**
- * Normalizes a file path.
+ * Stops the internal watcher.
*
- * @param path raw path
- * @return normalized absolute path
- *
- * @since 1.1.0
+ * @since 1.3.0
*/
- private static @NotNull Path normalize(@NotNull Path path) {
- return path.toAbsolutePath().normalize();
+ public void stop() {
+ watcher.stop();
}
}
\ No newline at end of file
diff --git a/src/main/java/dev/spexx/configurationAPI/util/FileChecksum.java b/src/main/java/dev/spexx/configurationAPI/util/FileChecksum.java
deleted file mode 100644
index 7dacec3..0000000
--- a/src/main/java/dev/spexx/configurationAPI/util/FileChecksum.java
+++ /dev/null
@@ -1,112 +0,0 @@
-package dev.spexx.configurationAPI.util;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.security.MessageDigest;
-
-/**
- * Utility class for computing cryptographic checksums of files.
- *
- * This class provides methods for generating hash values that can be used
- * to detect changes in file contents. The primary use case is configuration
- * change detection where file content equality must be verified reliably.
- *
- * Performance Considerations
- * Checksum computation requires reading the entire file. This operation
- * may be expensive for large files and should not be performed excessively
- * in performance-critical code paths without appropriate caching or throttling.
- *
- * Thread Safety
- * This class is stateless and thread-safe. All methods are static and do not
- * maintain internal state.
- *
- * @apiNote
- * This utility is designed for correctness over performance. Consumers should
- * cache results where appropriate if repeated checksum calculations are required.
- *
- * @implSpec
- * The {@link #sha256(File)} method computes a SHA-256 hash using a buffered
- * stream and returns the result as a lowercase hexadecimal string.
- *
- * @implNote
- * A fixed buffer size of 8192 bytes is used for reading file data. The method
- * relies on {@link MessageDigest} and standard Java I/O without using
- * memory-mapped files or other advanced optimizations.
- *
- * @since 1.0.0
- */
-public final class FileChecksum {
-
- /**
- * Private constructor to prevent instantiation.
- *
- * This class is not intended to be instantiated.
- *
- * @since 1.0.0
- */
- private FileChecksum() {
- }
-
- /**
- * Computes the SHA-256 checksum of the specified file.
- *
- * The file is read in full using a buffered input stream and processed
- * through a {@link MessageDigest} instance configured for SHA-256.
- *
- * The resulting hash is returned as a lowercase hexadecimal string.
- *
- * Behavior:
- *
- * - Reads the file sequentially in fixed-size chunks
- * - Feeds each chunk into the message digest
- * - Produces a deterministic SHA-256 hash
- *
- *
- * Failure Conditions:
- * If the file cannot be read or the hashing algorithm is unavailable,
- * a {@link RuntimeException} is thrown wrapping the original cause.
- *
- * @param file the file to compute the checksum for, must not be {@code null}
- * @return the SHA-256 checksum as a lowercase hexadecimal string, never {@code null}
- *
- * @throws NullPointerException if {@code file} is {@code null}
- * @throws RuntimeException if an error occurs while reading the file or
- * computing the checksum
- *
- * @since 1.0.0
- */
- public static @NotNull String sha256(@NotNull File file) {
-
- try (FileInputStream fis = new FileInputStream(file)) {
-
- // Initialize SHA-256 digest instance
- MessageDigest digest = MessageDigest.getInstance("SHA-256");
-
- // Buffer used for chunked reading (8 KB)
- byte[] buffer = new byte[8192];
- int read;
-
- // Read file in chunks and update digest incrementally
- while ((read = fis.read(buffer)) != -1) {
- digest.update(buffer, 0, read);
- }
-
- // Finalize hash computation
- byte[] hash = digest.digest();
-
- // Convert byte array to hexadecimal string representation
- StringBuilder hex = new StringBuilder(hash.length * 2);
- for (byte b : hash) {
- hex.append(String.format("%02x", b));
- }
-
- return hex.toString();
-
- } catch (Exception e) {
- // Wrap checked exceptions into runtime exception for API simplicity
- throw new RuntimeException("Failed to compute checksum for " + file, e);
- }
- }
-}
\ No newline at end of file
diff --git a/src/main/java/dev/spexx/configurationAPI/utils/FileChecksum.java b/src/main/java/dev/spexx/configurationAPI/utils/FileChecksum.java
new file mode 100644
index 0000000..f6a237c
--- /dev/null
+++ b/src/main/java/dev/spexx/configurationAPI/utils/FileChecksum.java
@@ -0,0 +1,83 @@
+package dev.spexx.configurationAPI.utils;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.security.MessageDigest;
+
+/**
+ * Utility class for generating file checksums.
+ *
+ * This class provides methods to compute cryptographic hashes of files,
+ * which can be used for change detection, integrity verification, and caching mechanisms.
+ *
+ * The primary use case within this API is to detect configuration file changes
+ * efficiently by comparing previously stored checksums with newly computed ones.
+ *
+ * Implementation Details
+ *
+ * - Uses {@code SHA-256} as the hashing algorithm
+ * - Reads files in buffered chunks (8 KB) for memory efficiency
+ * - Outputs checksum as a lowercase hexadecimal string
+ *
+ *
+ * Thread Safety
+ * This class is stateless and thread-safe.
+ *
+ * @since 1.3.0
+ */
+public final class FileChecksum {
+
+ /**
+ * Private constructor to prevent instantiation.
+ *
+ * This class is intended to be used as a static utility holder.
+ *
+ * @since 1.3.0
+ */
+ private FileChecksum() {
+ throw new UnsupportedOperationException("Utility class");
+ }
+
+ /**
+ * Computes the SHA-256 checksum of the given file.
+ *
+ * The file is read in chunks to avoid loading the entire file into memory,
+ * making this method suitable for both small and large files.
+ *
+ * The resulting checksum is returned as a lowercase hexadecimal string.
+ *
+ * @param file the file to compute the checksum for (must not be {@code null})
+ * @return the SHA-256 checksum as a hexadecimal string
+ * @throws Exception if:
+ *
+ * - the file cannot be read
+ * - the hashing algorithm is not available
+ * - an I/O error occurs during reading
+ *
+ */
+ public static @NotNull String getSha256Checksum(File file) throws Exception {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+
+ try (InputStream fis = new FileInputStream(file)) {
+ byte[] buffer = new byte[8192];
+ int bytesRead;
+
+ while ((bytesRead = fis.read(buffer)) != -1) {
+ digest.update(buffer, 0, bytesRead);
+ }
+ }
+
+ byte[] hashBytes = digest.digest();
+
+ // Convert to hex
+ StringBuilder hex = new StringBuilder();
+ for (byte b : hashBytes) {
+ hex.append(String.format("%02x", b));
+ }
+
+ return hex.toString();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/dev/spexx/configurationAPI/watcher/GlobalConfigWatcher.java b/src/main/java/dev/spexx/configurationAPI/watcher/GlobalConfigWatcher.java
deleted file mode 100644
index 32bdb7c..0000000
--- a/src/main/java/dev/spexx/configurationAPI/watcher/GlobalConfigWatcher.java
+++ /dev/null
@@ -1,418 +0,0 @@
-package dev.spexx.configurationAPI.watcher;
-
-import dev.spexx.configurationAPI.config.YamlConfig;
-import dev.spexx.configurationAPI.events.ConfigReloadedEvent;
-import dev.spexx.configurationAPI.util.FileChecksum;
-import org.bukkit.configuration.file.YamlConfiguration;
-import org.bukkit.plugin.java.JavaPlugin;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.*;
-import java.util.Collection;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.logging.Level;
-
-import static java.nio.file.StandardWatchEventKinds.*;
-
-/**
- * Global watcher responsible for monitoring multiple configuration files.
- *
- * This class maintains a thread-safe registry of configuration snapshots and
- * automatically reloads them when changes are detected on disk.
- *
- * Reload operations are validated before being applied. If a configuration
- * fails to load (for example due to invalid YAML), the previous snapshot is
- * preserved and no reload event is fired.
- *
- * @since 1.1.0
- */
-public final class GlobalConfigWatcher {
-
- /**
- * Minimum delay between reload attempts for the same file.
- *
- * This prevents excessive reloads caused by rapid file system events
- * (for example, editors writing files in multiple steps).
- */
- private static final long DEBOUNCE_MS = 300;
-
- /**
- * Owning plugin instance used for scheduling and logging.
- */
- private final @NotNull JavaPlugin plugin;
-
- /**
- * Underlying {@link WatchService} used to monitor file system changes.
- */
- private final @NotNull WatchService watchService;
-
- /**
- * Active configuration snapshots indexed by normalized file path.
- *
- * Each entry represents the latest known valid configuration state.
- */
- private final Map configs = new ConcurrentHashMap<>();
-
- /**
- * Cached checksums used to detect content changes.
- */
- private final Map checksums = new ConcurrentHashMap<>();
-
- /**
- * Tracks the last reload time for each file to apply debounce protection.
- */
- private final Map lastReload = new ConcurrentHashMap<>();
-
- /**
- * Set of directories currently registered with the watch service.
- *
- * Directories are registered once to avoid redundant registrations.
- */
- private final Set watchedDirs = ConcurrentHashMap.newKeySet();
-
- /**
- * Indicates whether the watcher thread is currently running.
- */
- private final AtomicBoolean running = new AtomicBoolean(false);
-
- /**
- * Background thread responsible for processing file system events.
- */
- private Thread thread;
-
- /**
- * Creates a new watcher instance.
- *
- * @param plugin owning plugin instance, must not be {@code null}
- * @throws IOException if the watch service cannot be initialized
- *
- * @since 1.1.0
- */
- public GlobalConfigWatcher(@NotNull JavaPlugin plugin) throws IOException {
- this.plugin = plugin;
- this.watchService = FileSystems.getDefault().newWatchService();
- }
-
- /**
- * Registers a configuration file for monitoring.
- *
- * This method performs the following actions:
- *
- * - Normalizes the file path for consistent identity
- * - Adds the configuration snapshot to the registry
- * - Computes and stores its checksum
- * - Registers the parent directory with the watch service if needed
- *
- *
- * If the configuration is already registered, the method returns
- * without performing any additional work.
- *
- * @param config configuration snapshot to register, must not be {@code null}
- * @throws IOException if directory registration fails
- *
- * @since 1.1.0
- */
- public void register(@NotNull YamlConfig config) throws IOException {
-
- // Normalize path to ensure consistent lookup
- Path path = normalize(config.file().toPath());
- Path dir = path.getParent();
-
- // Prevent duplicate registration
- if (configs.putIfAbsent(path, config) != null) {
- return;
- }
-
- // Store checksum for change detection
- checksums.put(path, FileChecksum.sha256(config.file()));
-
- // Register directory only once
- if (watchedDirs.add(dir)) {
- dir.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);
- }
- }
-
- /**
- * Unregisters a configuration file from monitoring.
- *
- * This removes all associated state, including:
- *
- * - Configuration snapshot
- * - Checksum cache
- * - Debounce tracking
- *
- *
- * @param path configuration file path, must not be {@code null}
- *
- * @since 1.1.0
- */
- public void unregister(@NotNull Path path) {
- Path normalized = normalize(path);
-
- configs.remove(normalized);
- checksums.remove(normalized);
- lastReload.remove(normalized);
- }
-
- /**
- * Starts the watcher thread.
- *
- * This method is idempotent. Calling it multiple times will not create
- * additional threads.
- *
- * @since 1.1.0
- */
- public void start() {
- if (running.get()) {
- return;
- }
-
- running.set(true);
-
- thread = new Thread(this::run, "GlobalConfigWatcher");
- thread.setDaemon(true);
- thread.start();
- }
-
- /**
- * Stops the watcher and releases system resources.
- *
- * This method:
- *
- * - Stops the watcher loop
- * - Closes the {@link WatchService}
- * - Interrupts the watcher thread
- *
- *
- * @since 1.1.0
- */
- public void stop() {
- running.set(false);
-
- try {
- watchService.close();
- } catch (IOException ignored) {
- }
-
- if (thread != null) {
- thread.interrupt();
- }
- }
-
- /**
- * Handles a file system event affecting a tracked configuration file.
- *
- * This method performs the following steps:
- *
- * - Validates that the file is still tracked
- * - Applies debounce logic to prevent excessive reloads
- * - Computes a checksum to detect actual content changes
- * - Loads and validates the YAML configuration
- * - Applies rollback protection if the configuration is invalid
- * - Replaces the previous snapshot atomically
- * - Dispatches a {@link ConfigReloadedEvent} on successful reload
- *
- *
- * Validation
- * If the YAML file is syntactically invalid or results in an empty
- * configuration while the file is non-empty, the reload is aborted and the
- * previous configuration snapshot is preserved.
- *
- * Event Dispatch
- * The reload event is only fired if both the previous configuration and
- * checksum are available. This ensures that events always represent a valid
- * transition from one known state to another.
- *
- * @param path the affected file path, must not be {@code null}
- * @param kind the type of file system event, must not be {@code null}
- *
- * @since 1.1.0
- */
- private void handleFileEvent(@NotNull Path path, WatchEvent.@NotNull Kind> kind) {
-
- // Ignore files that are not currently tracked
- if (!configs.containsKey(path)) {
- return;
- }
-
- File file = path.toFile();
-
- // Handle file deletion
- if (kind == ENTRY_DELETE) {
- configs.remove(path);
- checksums.remove(path);
- lastReload.remove(path);
- return;
- }
-
- // Ignore events for files that no longer exist
- if (!file.exists()) {
- return;
- }
-
- try {
- // Compute new checksum for change detection
- String newChecksum = FileChecksum.sha256(file);
-
- // Retrieve previous checksum (may be null if not initialized properly)
- String oldChecksum = checksums.get(path);
-
- // Skip reload if content has not changed
- if (oldChecksum != null && oldChecksum.equals(newChecksum)) {
- return;
- }
-
- // Load YAML configuration from disk
- YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
-
- // Validate YAML: prevent silent failures from replacing valid config
- if (yaml.getKeys(false).isEmpty() && file.length() > 0) {
- plugin.getLogger().warning(
- "[ConfigWatcher] Invalid YAML detected, keeping previous configuration: " + file.getName()
- );
- return;
- }
-
- long start = System.nanoTime();
-
- // Retrieve previous snapshot (may be null on first load)
- YamlConfig oldConfig = configs.get(path);
-
- // Create new immutable snapshot
- YamlConfig newConfig = new YamlConfig(file, yaml);
-
- // Atomically replace configuration
- configs.put(path, newConfig);
- checksums.put(path, newChecksum);
-
- int timeMs = (int) ((System.nanoTime() - start) / 1_000_000);
-
- /*
- * Only fire event if we have a valid previous state.
- * This avoids passing null values into the event and ensures
- * a meaningful state transition.
- */
- if (oldConfig != null && oldChecksum != null) {
-
- plugin.getServer().getScheduler().runTask(plugin, () ->
- plugin.getServer().getPluginManager().callEvent(
- new ConfigReloadedEvent(
- oldConfig,
- newConfig,
- oldChecksum,
- newChecksum,
- timeMs
- )
- )
- );
- }
-
- } catch (Exception e) {
- // Log full stack trace for debugging
- plugin.getLogger().log(
- Level.WARNING,
- "[ConfigWatcher] Failed to reload configuration: " + file.getName(),
- e
- );
- }
- }
-
- /**
- * Main watcher loop responsible for processing file system events.
- *
- * This method continuously listens for file system events and dispatches
- * them to {@link #handleFileEvent(Path, WatchEvent.Kind)}.
- *
- * It applies debounce logic to prevent excessive reloads and ensures that
- * unexpected errors do not terminate the watcher thread.
- *
- * @since 1.1.0
- */
- private void run() {
- while (running.get()) {
- try {
- // Wait for next file system event
- WatchKey key = watchService.take();
- Path dir = (Path) key.watchable();
-
- for (WatchEvent> event : key.pollEvents()) {
-
- if (event.kind() == OVERFLOW) {
- continue;
- }
-
- // Resolve full path of affected file
- Path path = normalize(dir.resolve((Path) event.context()));
-
- long now = System.currentTimeMillis();
- long last = lastReload.getOrDefault(path, 0L);
-
- // Apply debounce protection
- if (now - last < DEBOUNCE_MS) {
- continue;
- }
-
- lastReload.put(path, now);
-
- // Delegate to handler
- handleFileEvent(path, event.kind());
- }
-
- key.reset();
-
- } catch (Exception e) {
- plugin.getLogger().log(Level.WARNING, "[ConfigWatcher] Error", e);
- }
- }
- }
-
- /**
- * Returns the current configuration snapshot for the given path.
- *
- * The returned value represents the latest known valid configuration
- * state. If the configuration is not tracked, {@code null} is returned.
- *
- * @param path configuration path, must not be {@code null}
- * @return configuration snapshot or {@code null} if not tracked
- *
- * @since 1.1.0
- */
- public @Nullable YamlConfig get(@NotNull Path path) {
- return configs.get(normalize(path));
- }
-
- /**
- * Returns all currently tracked configuration snapshots.
- *
- * The returned collection is backed by the internal data structure and
- * reflects the current state at the time of invocation.
- *
- * @return collection of configuration snapshots, never {@code null}
- *
- * @since 1.1.0
- */
- public @NotNull Collection getAll() {
- return configs.values();
- }
-
- /**
- * Normalizes a file path to ensure consistent identity.
- *
- * This method converts the path to an absolute path and removes any
- * redundant elements such as {@code "."} or {@code ".."}.
- *
- * @param path raw path, must not be {@code null}
- * @return normalized absolute path
- *
- * @since 1.1.0
- */
- private static @NotNull Path normalize(@NotNull Path path) {
- return path.toAbsolutePath().normalize();
- }
-}
\ No newline at end of file
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
new file mode 100644
index 0000000..4cd6175
--- /dev/null
+++ b/src/main/resources/config.yml
@@ -0,0 +1 @@
+test: "some text"
\ No newline at end of file
diff --git a/src/main/resources/paper-plugin.yml b/src/main/resources/paper-plugin.yml
index cbdb3e0..1536674 100644
--- a/src/main/resources/paper-plugin.yml
+++ b/src/main/resources/paper-plugin.yml
@@ -1,6 +1,6 @@
name: ConfigurationAPI
description: $description
-version: '1.2.0'
+version: '1.3.0'
main: dev.spexx.configurationAPI.ConfigurationAPI
api-version: '1.21.11'