diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index b6d56a9..0dbda5e 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -43,7 +43,7 @@ jobs: - name: Upload JAR artifact uses: actions/upload-artifact@v4 with: - name: ConfigurationAPI + name: ConfigurationProvider path: target/*.jar # Optional: GitHub security / dependency tracking diff --git a/README.md b/README.md index 3a26c7e..2cd0e39 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,56 @@ -[![Java CI with Maven](https://github.com/devspexx/ConfigurationAPI/actions/workflows/maven.yml/badge.svg)](https://github.com/devspexx/CentralDatabase/actions/workflows/maven.yml) +[![Java CI with Maven](https://github.com/devspexx/ConfigurationAPI/actions/workflows/maven.yml/badge.svg)](https://github.com/devspexx/ConfigurationAPI/actions/workflows/maven.yml) [![Code Factor](https://img.shields.io/codefactor/grade/github/devspexx/ConfigurationAPI?label=Code%20Factor&style=flat)](https://www.codefactor.io/repository/github/devspexx/ConfigurationAPI) ![Latest Release](https://img.shields.io/github/v/release/devspexx/ConfigurationAPI?label=Latest%20Release&style=flat) +## 1. Overview -## ConfigurationAPI +ConfigurationAPI is a Paper-based plugin that handles configuration files for you. -This is a simple yet somewhat robust plugin I wrote in my free time. -It allows developers to not worry about config changes and implementing /reload commands in their java plugins. -It is a Paper based API, well, a Paper based plugin. It will only work on Paper servers. Don't try to install it on spigot -servers, it won't work. I might make a fork of it in the near future, to also support spigot. -

-This plugin automatically tracks any configs you configure it to track, and reloads them, when -they were manually edited (without code), and you can also write to them by code, of course. So it works -both ways. +It automatically watches, reloads, and synchronizes configs when they +change — no `/reload` commands needed. Designed to be fast, safe, +and developer-friendly. -## Features +> This plugin is Paper-only and will not work on Spigot. -- Simple API -- File watching (WatchService) -- checksum validation (no unnecessary reloads) -- Debounce protection -- Single watcher (no per-file threads) -- A cool sync event! (`ConfigReloadEvent`) +### 1.1 Features -## Installation +- No need for `/reload` commands — configs update instantly when files change. +- Efficiently monitors files at the system level with a single watcher thread. +- Reloads only when actual content changes — avoids unnecessary operations. +- Prevents duplicate reloads caused by rapid file system events. +- React to changes with built-in events: + - `ConfigReloadedEvent` + - `ConfigDeletedEvent` + - `ConfigRegisteredEvent` +- Access everything through `ConfigurationProvider` — no boilerplate. +- Create, register, and manage configs at runtime — not just on startup. +- Normalized paths ensure reliable lookups across environments. +- Designed for concurrent environments with minimal overhead. +- Direct access to Bukkit’s `YamlConfiguration` for full control when needed. -#### Use jar: -```xml - - dev.spexx - ConfigurationAPI - 1.3.0 - -``` +### 1.2 Additional Features + +- Load configs directly from your plugin JAR: + - `registerFromJar(File target, String resourcePath, JavaPlugin plugin)` + +- Create custom config files: + - `register(File file)` + +- Register configs with default values (only applied if missing): + - `registerWithDefaults(File file, Map defaults)` + +- Retrieve configs safely: + - `get(File file)` + - `getByPath(String path)` + - `isRegistered(File file)` -#### or Jitpack + +## 2. Installation + +Add ConfigurationAPI to your project using one of the following methods: + +### 2.1 JitPack (Recommended) +Add this to your `pom.xml`: ```xml @@ -46,100 +62,239 @@ both ways. com.github.devspexx ConfigurationAPI - v1.3.0 + v1.3.2 ``` -### Usage -#### Full sample (with comments) +### 2.2 Manual JAR build +If you prefer building the project yourself: +```bash +git clone https://github.com/devspexx/ConfigurationAPI.git +cd ConfigurationAPI +mvn clean install +```` +This installs the artifact into your local Maven repository. +You can then add it as a dependency using the version defined in the project's `pom.xml` (e.g. `1.3.2`). + +#### 2.2.1 Runtime +ConfigurationAPI must be available at runtime. You can choose one of the following: +- Shade it into your plugin (recommended for standalone plugins) +- Install it as a plugin on the server and depend on it + +> 💡 If you do not shade it, make sure the ConfigurationAPI plugin is present on the server. + +### 3. API Usage +How do I integrate ConfigurationAPI into my plugin? + +#### 3.1 Initialize the API +You should initialize the API inside your plugin's `onEnable()` method. ```java -import dev.spexx.configurationAPI.api.manager.ConfigManager; +import dev.spexx.configurationAPI.api.ConfigurationProvider; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; -// ConfigManager takes a plugin instance, should only -// be initialized once! -ConfigManager configManager = new ConfigManager(this); +public final class MyPlugin extends JavaPlugin { -// Sample 1 - registering files from the plugin jar itself -// Which means you should NOT use saveDefaultConfig() / saveConfig() -// anywhere in your plugin. + private final ConfigurationProvider configurationProvider; - // This is where you want the config from the plugin resources to be saved. - File myPluginConfigDestination = new File(getDataFolder(), "config.yml"); + public @NotNull ConfigManager getConfigurationProvider() { + return configurationProvider.api(); + } -// 2nd parameter, "config.yml" is the path inside the jar. Should really leave that as it is, only -// change your file name. 3rd parameter is your plugin instance - so we know from which plugin we're -// pulling the config. -configManager. + @Override + public void onEnable() { + setupConfigurationProvider(); + } - registerFromJar(myPluginConfigDestination, "config.yml",this); + public void setupConfigurationProvider() { -// I recommend putting all of this in your onEnable, at the end don't forget to -// actually start the watch service. Note that you can also dynamically create files during -// runtime, you don't have to do everything in onEnable. -manager. + // Initialize ConfigurationProvider for this plugin. + // Passing "this" binds the provider to this plugin's lifecycle. + configurationProvider = new ConfigurationProvider(this); + configurationProvider.api().startFileWatcher(); + } +} +``` - start(); +> 💡 You can register configs even after the file watcher has started. +> Newly registered configs will be picked up and tracked automatically. - // Sample 2 - registering custom yaml files, except not from plugin jar -// This is fairly simple! - YamlConfig myCustomConfig = manager.register( - new File(getDataFolder(), "data.yml") - ); +#### 3.2 Working with Configs +Once the API is initialized, you can create, access, and modify configuration files. + +##### 3.2.1 Register a custom config + +```java +@NotNull YamlConfig dataConfig = getConfigurationProvider().register( + new File(getDataFolder(), "data.yml") +); +``` +- Creates the file if it does not exist +- Loads and registers it +- Automatically tracked by the watcher + +#### 3.2.2 Register with default values +```java +Map defaults = Map.of( + "settings.enabled", true, + "motd.line1", "Hello world" +); + +getConfigurationProvider().registerWithDefaults( + new File(getDataFolder(), "config.yml"), + defaults +); +``` +- Only missing values are added +- Existing values are never overwritten -// What you're doing here is creating a file, in your plugin folder - getDataFolder(), -// and you're naming it 'data.yml', that's about it. -// You can write to it: -myCustomConfig. +#### 3.2.3 Access a config + +```java +import dev.spexx.configurationAPI.api.config.yaml.YamlConfig; - get(). +YamlConfig config = getConfigurationProvider().get( + new File(getDataFolder(), "config.yml") +); - set("hello","world"); +String value = config.get().getString("path.to.value"); +``` +> 💡 Prefer get(File) over getByPath(String) for better reliability. - // Read from it (get() - returns the latest internally cached config, -// basically, the latest config that matches the file on your disk) -// with get(), you get raw YamlConfiguration access, so you're able to do -// anything. If you do change anything in the file don't forget to -// save it in the end: myCustomConfig.save(), api will handle the rest. - @Nullable - String myValue = myCustomConfig.get().getString("something.here"); +#### 3.2.4 Modify and save +```java +config.get().set("path.to.value", "newValue"); -// --- -// If you find this confusing, there is javadoc at every method / public variable. So you won't be lost! +// Always save after modifying +config.save(); ``` -#### Listen for reload (If you need to) +#### 3.2.5 React to config changes ```java +import dev.spexx.configurationAPI.api.event.ConfigReloadedEvent; import org.jetbrains.annotations.NotNull; @EventHandler -public void onReload(@NotNull ConfigReloadEvent event) { +public void onReload(@NotNull ConfigReloadedEvent event) { getLogger().info("Reloaded: " + event.getConfigName()); } ``` +- Fired only when file content actually changes +- Runs on the main thread -#### What this event provides +##### 3.2.6 Recommended pattern (cache values) + +```java +import dev.spexx.configurationAPI.api.event.ConfigReloadedEvent; +import org.bukkit.event.Listener; +import org.jetbrains.annotations.NotNull; + +public class MyListener implements Listener { + + private String cachedValue; + + public MyListener(MyPlugin plugin) { + update(plugin); + } + + public void update(@NotNull MyPlugin plugin) { + YamlConfig config = plugin.getConfigurationProvider().get( + new File(plugin.getDataFolder(), "config.yml") + ); + + cachedValue = config.get().getString("path.to.value"); + } + + @EventHandler + public void onReload(ConfigReloadedEvent event) { + update(plugin); + } +} ``` -## ConfigReloadEvent - -| Method | Description | -|--------------------|------------------------------------------| -| `getConfigName()` | Full name of the file (e.g. `config.yml`)| -| `getNewConfig()` | Updated configuration (latest state) | -| `getOldChecksum()` | Checksum before reload | -| `getNewChecksum()` | Checksum after reload | +- Avoid reading config on every event +- Cache values and refresh on reload + +### 4 Events +ConfigurationAPI provides built-in events that allow you to react to configuration changes in real-time. +> 💡 All events are fired on the **main server thread**. + +#### 4.1 ConfigReloadedEvent +Fired when a configuration file is modified and successfully reloaded. + +| Method | Description | +|--------------------|----------------------------------------------| +| `getConfigName()` | Name of the config file (e.g. `config.yml`) | +| `getNewConfig()` | Updated `FileConfiguration` instance | +| `getOldChecksum()` | Checksum before reload (may be `null`) | +| `getNewChecksum()` | Checksum after reload (may be `null`) | + +- Triggered only when file content actually changes (checksum-based). + + +#### 4.2 ConfigDeletedEvent +Fired when a tracked configuration file is deleted. + +| Method | Description | +|--------------------|----------------------------------------------| +| `getConfigName()` | Name of the deleted config file | +| `getPath()` | Absolute path of the deleted file | + +- Fired when the file is removed from disk +- Config is automatically untracked + +#### 4.3 ConfigRegisteredEvent +Fired when a configuration is registered and loaded. + +| Method | Description | +|-------------------|----------------------------------------------| +| `getConfigName()` | Name of the config file | +| `getConfig()` | Loaded `FileConfiguration` instance | +| `getChecksum()` | Initial checksum (may be `null`) | + +- Fired after registration and initial load +- Useful for initialization logic + +#### 4.4 Example usage of events + +```java +import dev.spexx.configurationAPI.api.event.ConfigDeletedEvent; +import dev.spexx.configurationAPI.api.event.ConfigRegisteredEvent; +import dev.spexx.configurationAPI.api.event.ConfigReloadedEvent; +import org.jetbrains.annotations.NotNull; + +public class ConfigListener implements Listener { + + @EventHandler + public void onReload(@NotNull ConfigReloadedEvent event) { + Bukkit.getLogger().info("Reloaded: " + event.getConfigName()); + } + + @EventHandler + public void onDelete(@NotNull ConfigDeletedEvent event) { + Bukkit.getLogger().warning("Deleted: " + event.getConfigName()); + } + + @EventHandler + public void onRegister(@NotNull ConfigRegisteredEvent event) { + Bukkit.getLogger().info("Registered: " + event.getConfigName()); + } +} ``` +- Events are synchronous (main thread) +- Reload events are checksum-based (no duplicate triggers) +- Delete events automatically stop tracking the file +- Register events fire once per config registration + +### 5. Contributing + +Pull requests are welcome! + +If you find a bug or have a feature request, feel free to open an issue. -#### Event behavior -When file changes: -- Change is detected internally in watcher -- Checksum is computed - - If checksum is different, reload event is fired, and new file is cached - - New file overrides the latest file (if any) in the cache - - If checksum isn't different, nothing happens, as there were no changes in the file -- Event is fired on main thread +## 6. Final Notes ---- +ConfigurationAPI is designed to remove boilerplate and let you focus on building features. -### Thank you for reading! +If you find it useful, consider ⭐ starring the repository. I would really appreciate it! \ No newline at end of file diff --git a/pom.xml b/pom.xml index e125ec2..7509333 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ dev.spexx ConfigurationAPI - 1.3.1 + 1.3.2 jar ConfigurationAPI diff --git a/src/main/java/dev/spexx/configurationAPI/ConfigurationAPI.java b/src/main/java/dev/spexx/configurationAPI/ConfigurationAPI.java index 4806403..8f0924e 100644 --- a/src/main/java/dev/spexx/configurationAPI/ConfigurationAPI.java +++ b/src/main/java/dev/spexx/configurationAPI/ConfigurationAPI.java @@ -1,19 +1,24 @@ package dev.spexx.configurationAPI; import org.bukkit.plugin.java.JavaPlugin; + /** - * Main plugin entry point for the ConfigurationAPI. + * Main plugin entry point for ConfigurationProvider. + * + *

This class is managed by the Bukkit/Paper plugin system and is responsible + * for initializing the plugin lifecycle.

* - *

Initializes and demonstrates usage of the configuration system.

+ *

It does not expose the API directly. Use the API classes from the + * {@code dev.spexx.configurationAPI.api} package instead.

* - * @since 1.0.0 + * @since 1.3.1 */ public final class ConfigurationAPI extends JavaPlugin { /** - * Creates a new plugin instance. + * Creates the plugin instance. * - * @since 1.0.0 + *

This constructor is called by the server and should not be used manually.

*/ public ConfigurationAPI() { } diff --git a/src/main/java/dev/spexx/configurationAPI/api/ConfigurationProvider.java b/src/main/java/dev/spexx/configurationAPI/api/ConfigurationProvider.java new file mode 100644 index 0000000..3fadd59 --- /dev/null +++ b/src/main/java/dev/spexx/configurationAPI/api/ConfigurationProvider.java @@ -0,0 +1,46 @@ +package dev.spexx.configurationAPI.api; + +import dev.spexx.configurationAPI.api.manager.ConfigManager; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +/** + * Facade for accessing configuration API components. + * + * @since 1.3.2 + */ +public class ConfigurationProvider { + + /** + * The internal configuration manager instance. + * + * @since 1.3.2 + */ + private final @NotNull ConfigManager configManager; + + /** + * Creates a new {@link dev.spexx.configurationAPI.ConfigurationAPI} instance. + * + *

The provided {@link JavaPlugin} is used internally for scheduling tasks + * and interacting with the Bukkit API.

+ * + * @param javaPlugin the owning plugin instance + * @since 1.3.2 + */ + public ConfigurationProvider(@NotNull JavaPlugin javaPlugin) { + this.configManager = new ConfigManager(javaPlugin); + } + + /** + * Returns the core {@link ConfigManager} API. + * + *

This provides access to configuration registration, retrieval, + * and file watching functionality.

+ * + * @return the {@link ConfigManager} instance + * @since 1.3.2 + */ + public @NotNull ConfigManager api() { + return this.configManager; + } +} diff --git a/src/main/java/dev/spexx/configurationAPI/api/config/yaml/YamlConfig.java b/src/main/java/dev/spexx/configurationAPI/api/config/yaml/YamlConfig.java index 87ea766..0787085 100644 --- a/src/main/java/dev/spexx/configurationAPI/api/config/yaml/YamlConfig.java +++ b/src/main/java/dev/spexx/configurationAPI/api/config/yaml/YamlConfig.java @@ -102,7 +102,7 @@ public YamlConfig(@NotNull File file) throws ConfigException { try { this.cachedChecksum = FileChecksum.computeSha256(file); } catch (Exception e) { - e.printStackTrace(); // log the exception + e.printStackTrace(); this.cachedChecksum = null; } diff --git a/src/main/java/dev/spexx/configurationAPI/api/config/yaml/YamlConfigWatcher.java b/src/main/java/dev/spexx/configurationAPI/api/config/yaml/YamlConfigWatcher.java index ec587d4..bd6bbd3 100644 --- a/src/main/java/dev/spexx/configurationAPI/api/config/yaml/YamlConfigWatcher.java +++ b/src/main/java/dev/spexx/configurationAPI/api/config/yaml/YamlConfigWatcher.java @@ -1,6 +1,8 @@ package dev.spexx.configurationAPI.api.config.yaml; -import dev.spexx.configurationAPI.api.event.ConfigReloadEvent; +import dev.spexx.configurationAPI.api.event.ConfigDeletedEvent; +import dev.spexx.configurationAPI.api.event.ConfigRegisteredEvent; +import dev.spexx.configurationAPI.api.event.ConfigReloadedEvent; import dev.spexx.configurationAPI.api.exceptions.ConfigException; import dev.spexx.configurationAPI.api.utils.FileChecksum; import org.bukkit.Bukkit; @@ -12,67 +14,35 @@ import java.nio.file.*; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; /** - * Watches {@link YamlConfig} files for changes. + * Watches registered {@link YamlConfig} files for changes. * - *

Registered configurations are monitored for file system modifications. - * Files located in the same directory share a single {@link WatchKey}.

+ *

Only files registered through this watcher are monitored. + * File system events for unrelated files are ignored.

* - *

Modification events are debounced to prevent duplicate reloads.

+ *

Events are dispatched on the main server thread.

* * @since 1.3.0 */ public class YamlConfigWatcher { + private static final long DEBOUNCE_MS = 200; private final @NotNull JavaPlugin javaPlugin; - private final @NotNull WatchService watchService; - - /** - * Maps directories to their {@link WatchKey}. - * - *

Each directory is registered only once, even if multiple files within it are watched.

- * - * @since 1.3.0 - */ private final @NotNull Map directories = new ConcurrentHashMap<>(); - - /** - * Maps absolute file paths to their corresponding {@link YamlConfig}. - * - * @since 1.3.0 - */ - private final Map watchedFiles = new ConcurrentHashMap<>(); - - /** - * Tracks last modification timestamps used for debounce logic. - * - * @since 1.3.0 - */ - private final Map lastModified = new ConcurrentHashMap<>(); - - /** - * Maps {@link WatchKey} instances to their corresponding directory {@link Path}. - * - *

This reverse mapping allows constant-time (O(1)) resolution of a directory - * from a {@link WatchKey}, avoiding linear scans over registered directories.

- * - *

The map is populated when directories are registered and cleaned up when - * {@link WatchKey}s become invalid.

- * - * @since 1.3.0 - */ + private final @NotNull Map watchedFiles = new ConcurrentHashMap<>(); + private final @NotNull Map lastModified = new ConcurrentHashMap<>(); private final @NotNull Map watchKeys = new ConcurrentHashMap<>(); - + private Thread watcherThread; private volatile boolean running = false; /** * Creates a new watcher instance. * - * @param javaPlugin the plugin instance used for scheduling synchronous tasks - * @throws ConfigException if the watcher cannot be initialized - * @since 1.3.0 + * @param javaPlugin the plugin used for scheduling and logging + * @throws ConfigException if initialization fails */ public YamlConfigWatcher(@NotNull JavaPlugin javaPlugin) throws ConfigException { this.javaPlugin = javaPlugin; @@ -84,17 +54,15 @@ public YamlConfigWatcher(@NotNull JavaPlugin javaPlugin) throws ConfigException } /** - * Registers a configuration for watching. + * Registers a configuration for monitoring. * - *

If multiple configurations are located in the same directory, - * the directory is registered only once.

+ *

Only registered configurations will produce events.

* * @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(); + Path path = config.getFile().toPath().toAbsolutePath().normalize(); if (watchedFiles.containsKey(path)) { throw new ConfigException("File is already being watched: " + path); @@ -111,13 +79,13 @@ public void watch(@NotNull YamlConfig config) throws ConfigException { WatchKey key = dir.register( watchService, StandardWatchEventKinds.ENTRY_MODIFY, - StandardWatchEventKinds.ENTRY_DELETE + StandardWatchEventKinds.ENTRY_DELETE, + StandardWatchEventKinds.ENTRY_CREATE ); - // for reverse mapping watchKeys.put(key, dir); - return key; + } catch (IOException e) { throw new RuntimeException(e); } @@ -125,6 +93,17 @@ public void watch(@NotNull YamlConfig config) throws ConfigException { watchedFiles.put(path, config); + // REGISTER EVENT + Bukkit.getScheduler().runTask(javaPlugin, () -> + Bukkit.getPluginManager().callEvent( + new ConfigRegisteredEvent( + config.getFile().getName(), + config.get(), + config.getCachedChecksum() + ) + ) + ); + } catch (RuntimeException e) { throw new ConfigException("Failed to register file watcher: " + path, e); } @@ -134,7 +113,6 @@ public void watch(@NotNull YamlConfig config) throws ConfigException { * Starts the watcher thread. * * @throws ConfigException if already running - * @since 1.3.0 */ public void start() throws ConfigException { if (running) { @@ -143,15 +121,13 @@ public void start() throws ConfigException { running = true; - Thread thread = new Thread(this::run, "YamlConfigWatcher"); - thread.setDaemon(true); - thread.start(); + watcherThread = new Thread(this::run, "YamlConfigWatcher"); + watcherThread.setDaemon(true); + watcherThread.start(); } /** - * Stops the watcher thread. - * - * @since 1.3.0 + * Stops the watcher and releases resources. */ public void stop() { running = false; @@ -160,16 +136,21 @@ public void stop() { watchService.close(); } catch (IOException ignored) { } + + if (watcherThread != null) { + watcherThread.interrupt(); + } } /** - * Internal watcher loop. - * - *

Resolves {@link WatchKey} instances back to their corresponding directory - * and processes events for registered files only.

+ * Returns whether the watcher is running. * - * @since 1.3.0 + * @return true if running */ + public boolean isRunning() { + return running; + } + private void run() { var scheduler = Bukkit.getScheduler(); var pluginManager = Bukkit.getPluginManager(); @@ -190,20 +171,41 @@ private void run() { } for (WatchEvent event : key.pollEvents()) { - Path changed = dir.resolve((Path) event.context()).toAbsolutePath(); + Path changed = dir.resolve((Path) event.context()) + .toAbsolutePath() + .normalize(); - // null safety YamlConfig config = watchedFiles.get(changed); if (config == null) { continue; } + // DELETE if (event.kind() == StandardWatchEventKinds.ENTRY_DELETE) { + + String name = config.getFile().getName(); + watchedFiles.remove(changed); + + scheduler.runTask(javaPlugin, () -> + pluginManager.callEvent( + new ConfigDeletedEvent(name, changed) + ) + ); + continue; } - if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) { + // MODIFY / CREATE + if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY + || event.kind() == StandardWatchEventKinds.ENTRY_CREATE) { + + Path filePath = config.getFile().toPath(); + + // skip if file was deleted + if (!Files.exists(filePath)) { + continue; + } @Nullable String oldChecksum = config.getCachedChecksum(); @Nullable String newChecksum; @@ -211,19 +213,18 @@ private void run() { try { newChecksum = FileChecksum.computeSha256(config.getFile()); } catch (Exception e) { - e.printStackTrace(); + javaPlugin.getLogger().log(Level.SEVERE, + "Failed to compute checksum for " + config.getFile(), 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) { + if (now - last < DEBOUNCE_MS) { continue; } @@ -236,7 +237,7 @@ private void run() { scheduler.runTask(javaPlugin, () -> pluginManager.callEvent( - new ConfigReloadEvent( + new ConfigReloadedEvent( config.getFile().getName(), config.get(), oldChecksum, @@ -246,7 +247,8 @@ private void run() { ); } catch (Exception e) { - e.printStackTrace(); + javaPlugin.getLogger().log(Level.SEVERE, + "Failed to reload config: " + config.getFile(), e); } } } diff --git a/src/main/java/dev/spexx/configurationAPI/api/event/ConfigDeletedEvent.java b/src/main/java/dev/spexx/configurationAPI/api/event/ConfigDeletedEvent.java new file mode 100644 index 0000000..9e1283e --- /dev/null +++ b/src/main/java/dev/spexx/configurationAPI/api/event/ConfigDeletedEvent.java @@ -0,0 +1,67 @@ +package dev.spexx.configurationAPI.api.event; + +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; + +/** + * Fired when a watched configuration file is deleted. + * + *

This event is triggered when the underlying file is removed + * from the file system.

+ * + * @since 1.3.2 + */ +public class ConfigDeletedEvent extends Event { + + private static final HandlerList HANDLERS = new HandlerList(); + + private final @NotNull String configName; + private final @NotNull Path path; + + /** + * Creates a new deletion event. + * + * @param configName the configuration file name + * @param path the absolute path of the deleted file + */ + public ConfigDeletedEvent(@NotNull String configName, + @NotNull Path path) { + this.configName = configName; + this.path = path; + } + + /** + * Required handler list for Bukkit events. + * + * @return the handler list + */ + public static @NotNull HandlerList getHandlerList() { + return HANDLERS; + } + + /** + * Returns the configuration file name. + * + * @return the file name + */ + public @NotNull String getConfigName() { + return configName; + } + + /** + * Returns the absolute path of the deleted file. + * + * @return the file path + */ + public @NotNull Path getPath() { + return path; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } +} \ No newline at end of file diff --git a/src/main/java/dev/spexx/configurationAPI/api/event/ConfigRegisteredEvent.java b/src/main/java/dev/spexx/configurationAPI/api/event/ConfigRegisteredEvent.java new file mode 100644 index 0000000..c813e53 --- /dev/null +++ b/src/main/java/dev/spexx/configurationAPI/api/event/ConfigRegisteredEvent.java @@ -0,0 +1,78 @@ +package dev.spexx.configurationAPI.api.event; + +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * Fired when a configuration is registered. + * + *

This event is triggered after the file is loaded and tracked by the watcher.

+ * + * @since 1.3.0 + */ +public class ConfigRegisteredEvent extends Event { + + private static final HandlerList HANDLERS = new HandlerList(); + + private final @NotNull String configName; + private final @NotNull FileConfiguration config; + private final String checksum; + + /** + * Creates a new registration event. + * + * @param configName the configuration file name + * @param config the loaded configuration + * @param checksum the current checksum + */ + public ConfigRegisteredEvent(@NotNull String configName, + @NotNull FileConfiguration config, + String checksum) { + this.configName = configName; + this.config = config; + this.checksum = checksum; + } + + /** + * Required handler list for Bukkit events. + * + * @return the handler list + */ + public static @NotNull HandlerList getHandlerList() { + return HANDLERS; + } + + /** + * Returns the configuration name. + * + * @return the configuration file name + */ + public @NotNull String getConfigName() { + return configName; + } + + /** + * Returns the loaded configuration. + * + * @return the configuration instance + */ + public @NotNull FileConfiguration getConfig() { + return config; + } + + /** + * Returns the checksum. + * + * @return checksum or null if unavailable + */ + public String getChecksum() { + return checksum; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } +} \ No newline at end of file diff --git a/src/main/java/dev/spexx/configurationAPI/api/event/ConfigReloadEvent.java b/src/main/java/dev/spexx/configurationAPI/api/event/ConfigReloadedEvent.java similarity index 90% rename from src/main/java/dev/spexx/configurationAPI/api/event/ConfigReloadEvent.java rename to src/main/java/dev/spexx/configurationAPI/api/event/ConfigReloadedEvent.java index 78cd22b..b603eba 100644 --- a/src/main/java/dev/spexx/configurationAPI/api/event/ConfigReloadEvent.java +++ b/src/main/java/dev/spexx/configurationAPI/api/event/ConfigReloadedEvent.java @@ -13,7 +13,7 @@ * * @since 1.3.0 */ -public class ConfigReloadEvent extends Event { +public class ConfigReloadedEvent extends Event { private static final HandlerList HANDLERS = new HandlerList(); @@ -31,10 +31,10 @@ public class ConfigReloadEvent extends Event { * @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) { + public ConfigReloadedEvent(@NotNull String configName, + @NotNull FileConfiguration newConfig, + String oldChecksum, + String newChecksum) { this.configName = configName; this.newConfig = newConfig; this.oldChecksum = oldChecksum; diff --git a/src/main/java/dev/spexx/configurationAPI/api/manager/ConfigManager.java b/src/main/java/dev/spexx/configurationAPI/api/manager/ConfigManager.java index 46516d6..014ffca 100644 --- a/src/main/java/dev/spexx/configurationAPI/api/manager/ConfigManager.java +++ b/src/main/java/dev/spexx/configurationAPI/api/manager/ConfigManager.java @@ -64,9 +64,9 @@ public ConfigManager(@NotNull JavaPlugin javaPlugin) throws ConfigException { */ public @NotNull YamlConfig register(@NotNull File file) throws ConfigException { - String key = file.getAbsolutePath(); + String key = getNormalizedPath(file); - if (configs.containsKey(key)) { + if (isRegistered(file)) { throw new ConfigException("Config already registered: " + key); } @@ -80,24 +80,34 @@ public ConfigManager(@NotNull JavaPlugin javaPlugin) throws ConfigException { } /** - * Registers a configuration file using a resource from the plugin JAR. + * Registers a configuration file backed by a resource inside the plugin JAR. * - *

If the file does not exist, it is copied from the specified resource path - * before being loaded.

+ *

If the target file does not exist, it is first created by copying the specified + * resource from the plugin JAR. The file is then initialized, loaded, and registered + * within this manager.

* - * @param file the target configuration file - * @param resourcePath the path inside the plugin JAR + *

Once registered, the configuration is tracked and automatically monitored for + * changes if the internal watcher is running.

+ * + * @param file the target configuration file on disk + * @param resourcePath the path to the resource inside the plugin JAR * @param plugin the plugin used to access the resource - * @throws ConfigException if already registered or copy/load fails + * @return the managed {@link YamlConfig} instance + * @throws ConfigException if: + *
    + *
  • the configuration is already registered
  • + *
  • the resource cannot be found in the JAR
  • + *
  • the file cannot be copied or initialized
  • + *
* @since 1.3.0 */ - public void registerFromJar(@NotNull File file, - @NotNull String resourcePath, - @NotNull JavaPlugin plugin) throws ConfigException { + public @NotNull YamlConfig registerFromJar(@NotNull File file, + @NotNull String resourcePath, + @NotNull JavaPlugin plugin) throws ConfigException { - String key = file.getAbsolutePath(); + String key = getNormalizedPath(file); - if (configs.containsKey(key)) { + if (isRegistered(file)) { throw new ConfigException("Config already registered: " + key); } @@ -110,6 +120,60 @@ public void registerFromJar(@NotNull File file, configs.put(key, config); watcher.watch(config); + + return config; + } + + /** + * Registers a configuration file and applies default values if missing. + * + *

If the file does not exist, it is created and loaded. The provided default + * values are then applied only to keys that are not already present in the file.

+ * + *

Existing values are never overwritten.

+ * + * @param file the configuration file + * @param defaults a map of default key-value pairs to apply if missing + * @return the managed {@link YamlConfig} instance + * + * @throws ConfigException if: + *
    + *
  • the configuration is already registered
  • + *
  • initialization or saving fails
  • + *
+ * + * @since 1.3.3 + */ + public @NotNull YamlConfig registerWithDefaults( + @NotNull File file, + @NotNull Map defaults + ) throws ConfigException { + + String key = getNormalizedPath(file); + + if (isRegistered(file)) { + throw new ConfigException("Config already registered: " + key); + } + + YamlConfig config = initialize(file); + + boolean changed = false; + + for (Map.Entry entry : defaults.entrySet()) { + if (!config.get().contains(entry.getKey())) { + config.get().set(entry.getKey(), entry.getValue()); + changed = true; + } + } + + if (changed) { + config.save(); + } + + configs.put(key, config); + watcher.watch(config); + + return config; } /** @@ -120,9 +184,7 @@ public void registerFromJar(@NotNull File file, * * @param file the configuration file to initialize * @return the initialized {@link YamlConfig} - * * @throws ConfigException if file creation fails or the path is invalid - * * @since 1.3.0 */ private @NotNull YamlConfig initialize(File file) { @@ -175,7 +237,8 @@ private void copyResource(@NotNull JavaPlugin plugin, * @since 1.3.0 */ public @NotNull YamlConfig get(@NotNull File file) throws ConfigException { - String key = file.getAbsolutePath(); + + String key = getNormalizedPath(file); YamlConfig config = configs.get(key); @@ -196,13 +259,11 @@ private void copyResource(@NotNull JavaPlugin plugin, * @param path the raw file path (relative or absolute) * @return the registered {@link YamlConfig} * @throws ConfigException if no config is registered for the resolved path - * * @since 1.3.0 */ - @ApiStatus.Experimental public @NotNull YamlConfig getByPath(@NotNull String path) throws ConfigException { File file = new File(path); - String key = file.getAbsolutePath(); + String key = getNormalizedPath(file); YamlConfig config = configs.get(key); @@ -213,13 +274,26 @@ private void copyResource(@NotNull JavaPlugin plugin, return config; } + /** + * Checks whether a configuration is registered. + * + *

Lookup is performed using the file's absolute path.

+ * + * @param file the configuration file + * @return {@code true} if registered, {@code false} otherwise + * @since 1.3.2 + */ + public boolean isRegistered(@NotNull File file) { + return configs.containsKey(getNormalizedPath(file)); + } + /** * Starts the internal watcher. * * @throws ConfigException if already running or fails * @since 1.3.0 */ - public void start() throws ConfigException { + public void startFileWatcher() throws ConfigException { watcher.start(); } @@ -228,7 +302,21 @@ public void start() throws ConfigException { * * @since 1.3.0 */ - public void stop() { + public void stopFileWatcher() { watcher.stop(); } + + /** + * Resolves a normalized key for the given file. + * + *

This ensures consistent lookup regardless of relative paths, + * redundant segments, or platform differences.

+ * + * @param file the file + * @return normalized absolute path string + * @since 1.3.2 + */ + private @NotNull String getNormalizedPath(@NotNull File file) { + return file.toPath().toAbsolutePath().normalize().toString(); + } } diff --git a/src/main/java/dev/spexx/configurationAPI/bootstrap/ConfigurationBootstrap.java b/src/main/java/dev/spexx/configurationAPI/bootstrap/ConfigurationBootstrap.java deleted file mode 100644 index 0806160..0000000 --- a/src/main/java/dev/spexx/configurationAPI/bootstrap/ConfigurationBootstrap.java +++ /dev/null @@ -1,40 +0,0 @@ -package dev.spexx.configurationAPI.bootstrap; - -import io.papermc.paper.plugin.bootstrap.BootstrapContext; -import io.papermc.paper.plugin.bootstrap.PluginBootstrap; -import org.jetbrains.annotations.NotNull; - -/** - * Bootstrap entry point for the ConfigurationAPI plugin. - * - *

This class is invoked during the early plugin bootstrap phase, - * before the plugin is fully loaded and enabled.

- * - *

It can be used to perform early initialization logic such as - * preparing resources, validating environment state, or influencing - * plugin loading behavior.

- * - *

Note that the Bukkit API is not fully available at this stage.

- * - * @since 1.3.0 - */ -public class ConfigurationBootstrap implements PluginBootstrap { - - /** - * Creates a new ConfigurationBootstrap instance. - * - * @since 1.3.0 - */ - public ConfigurationBootstrap() { - } - - /** - * Called during the bootstrap phase of plugin initialization. - * - * @param context the bootstrap context providing access to plugin metadata and logging - */ - @Override - public void bootstrap(@NotNull BootstrapContext context) { - // no-op (reserved for future use) - } -} \ No newline at end of file diff --git a/src/main/java/dev/spexx/configurationAPI/loader/ConfigurationLoader.java b/src/main/java/dev/spexx/configurationAPI/loader/ConfigurationLoader.java deleted file mode 100644 index 1505580..0000000 --- a/src/main/java/dev/spexx/configurationAPI/loader/ConfigurationLoader.java +++ /dev/null @@ -1,39 +0,0 @@ -package dev.spexx.configurationAPI.loader; - -import io.papermc.paper.plugin.loader.PluginClasspathBuilder; -import io.papermc.paper.plugin.loader.PluginLoader; -import org.jetbrains.annotations.NotNull; - -/** - * Classpath loader for the ConfigurationAPI plugin. - * - *

This class is responsible for modifying the plugin's classpath - * during the loading phase.

- * - *

It can be used to add external libraries or dependencies to the - * plugin classloader before the plugin is initialized.

- * - *

Currently, no additional libraries are injected.

- * - * @since 1.3.0 - */ -public class ConfigurationLoader implements PluginLoader { - - /** - * Creates a new ConfigurationLoader instance. - * - * @since 1.3.0 - */ - public ConfigurationLoader() { - } - - /** - * Called when the plugin classloader is being constructed. - * - * @param builder the classpath builder used to modify the plugin classpath - */ - @Override - public void classloader(@NotNull PluginClasspathBuilder builder) { - // no-op (reserved for future use) - } -} \ No newline at end of file diff --git a/src/main/resources/paper-plugin.yml b/src/main/resources/paper-plugin.yml index c2f7a85..fd86284 100644 --- a/src/main/resources/paper-plugin.yml +++ b/src/main/resources/paper-plugin.yml @@ -1,13 +1,11 @@ name: ConfigurationAPI description: "Lightweight YAML config API with automatic reload and event-driven updates." -version: '1.3.1' +version: '1.3.2' main: dev.spexx.configurationAPI.ConfigurationAPI -bootstrapper: dev.spexx.configurationAPI.bootstrap.ConfigurationBootstrap -loader: dev.spexx.configurationAPI.bootstrap.ConfigurationLoader -api-version: '21.1.1' -load: POSTWORLD +api-version: '1.21.11' +load: STARTUP authors: [ Spexx ] website: www.spexx.dev \ No newline at end of file