diff --git a/pom.xml b/pom.xml index 0b25067..d48aa7f 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ dev.spexx ConfigurationAPI - 1.1 + 1.1.0 jar ConfigurationAPI diff --git a/src/main/java/dev/spexx/configurationAPI/config/ConfigLoadResult.java b/src/main/java/dev/spexx/configurationAPI/config/ConfigLoadResult.java new file mode 100644 index 0000000..a70711e --- /dev/null +++ b/src/main/java/dev/spexx/configurationAPI/config/ConfigLoadResult.java @@ -0,0 +1,70 @@ +package dev.spexx.configurationAPI.config; + +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +/** + * Represents the result of a configuration load or creation operation. + * + *

This class encapsulates:

+ * + * + *

This allows safe, non-throwing APIs for configuration handling.

+ * + * @since 1.1.0 + */ +public final class ConfigLoadResult { + + private final ConfigLoadStatus status; + private final YamlConfig config; + private final Exception error; + + /** + * Creates a new result instance. + * + * @param status operation status, must not be {@code null} + * @param config resulting configuration, may be {@code null} + * @param error exception if an error occurred, may be {@code null} + */ + public ConfigLoadResult( + @NotNull ConfigLoadStatus status, + YamlConfig config, + Exception error + ) { + this.status = status; + this.config = config; + this.error = error; + } + + /** + * Returns the operation status. + * + * @return status value, never {@code null} + */ + public @NotNull ConfigLoadStatus status() { + return status; + } + + /** + * Returns the loaded configuration if present. + * + * @return optional configuration + */ + public @NotNull Optional config() { + return Optional.ofNullable(config); + } + + /** + * Returns the error if one occurred. + * + * @return optional exception + */ + public @NotNull Optional error() { + return Optional.ofNullable(error); + } +} \ No newline at end of file diff --git a/src/main/java/dev/spexx/configurationAPI/config/ConfigLoadStatus.java b/src/main/java/dev/spexx/configurationAPI/config/ConfigLoadStatus.java new file mode 100644 index 0000000..5c18f65 --- /dev/null +++ b/src/main/java/dev/spexx/configurationAPI/config/ConfigLoadStatus.java @@ -0,0 +1,32 @@ +package dev.spexx.configurationAPI.config; + +/** + * Represents the result status of a configuration load or creation operation. + * + *

This enum is used in conjunction with {@link ConfigLoadResult} to provide + * structured feedback about configuration handling outcomes.

+ * + * @since 1.1.0 + */ +public enum ConfigLoadStatus { + + /** + * Configuration was successfully loaded from disk. + */ + LOADED, + + /** + * Configuration file was newly created and loaded. + */ + CREATED, + + /** + * Configuration already existed and was not created again. + */ + ALREADY_EXISTS, + + /** + * An I/O error occurred during read/write operations. + */ + IO_ERROR +} \ No newline at end of file diff --git a/src/main/java/dev/spexx/configurationAPI/config/YamlConfig.java b/src/main/java/dev/spexx/configurationAPI/config/YamlConfig.java index 1987621..0142344 100644 --- a/src/main/java/dev/spexx/configurationAPI/config/YamlConfig.java +++ b/src/main/java/dev/spexx/configurationAPI/config/YamlConfig.java @@ -1,161 +1,329 @@ -package dev.spexx.configurationAPI.config; +package dev.spexx.configurationAPI.manager; -import org.bukkit.configuration.file.FileConfiguration; +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 org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.File; -import java.util.List; -import java.util.Optional; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Objects; /** - * Immutable snapshot of a YAML configuration file. + * Central access point for configuration files managed by the system. * - *

This class encapsulates the following components:

+ *

This class is responsible for:

* * - *

Each instance represents a point-in-time view of a configuration file. - * When the file is reloaded, a new {@code YamlConfig} instance is created - * and atomically replaces the previous instance.

+ *

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.

* - *

Immutability

- *

This class is immutable. All fields are {@code final} and no mutator - * methods are provided. This ensures thread-safe access without requiring - * synchronization.

+ *

Lifecycle

+ *

This class provides multiple levels of control over configuration handling:

+ * * - *

Usage

- *

Instances should be treated as read-only snapshots. Consumers should not - * cache instances long-term if they require access to the most up-to-date - * configuration state.

+ *

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 */ -public record YamlConfig(@NotNull File file, @NotNull FileConfiguration config) { +public final class ConfigManager { + + private final @NotNull JavaPlugin plugin; + private final @NotNull GlobalConfigWatcher watcher; /** - * Returns the underlying configuration file. + * Creates a new {@code ConfigManager}. + * + *

This initializes and starts the {@link GlobalConfigWatcher}, which begins + * monitoring registered configuration files for changes.

+ * + * @param plugin owning plugin instance, must not be {@code null} + * + * @throws NullPointerException if {@code plugin} is {@code null} + * @throws RuntimeException if watcher initialization fails */ - @Override - public @NotNull File file() { - return file; + public ConfigManager(@NotNull JavaPlugin plugin) { + this.plugin = Objects.requireNonNull(plugin, "plugin"); + + try { + this.watcher = new GlobalConfigWatcher(plugin); + this.watcher.start(); + } catch (IOException e) { + throw new RuntimeException("Failed to initialize GlobalConfigWatcher", e); + } } /** - * Returns the parsed configuration snapshot. + * Returns the latest configuration snapshot for the given file. * - *

This object should be treated as read-only.

+ *

The configuration must have been previously loaded using one of the + * loading methods. This method does not attempt to load the file.

+ * + * @param file configuration file, must not be {@code null} + * @return latest {@link YamlConfig} snapshot, never {@code null} + * + * @throws NullPointerException if {@code file} is {@code null} + * @throws IllegalStateException if the configuration is not loaded */ - @Override - public @NotNull FileConfiguration config() { + 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; } - // ========================================================= - // Optional-based getters (safe access) - // ========================================================= + /** + * Returns an existing configuration or loads it if not already tracked. + * + *

If the configuration is not registered, it will be loaded from disk + * and registered with the watcher.

+ * + * @param file configuration file, must not be {@code null} + * @return latest {@link YamlConfig} snapshot, never {@code null} + * + * @throws NullPointerException if {@code file} is {@code null} + */ + public @NotNull YamlConfig getOrLoad(@NotNull File file) { + Path path = normalize(file.toPath()); + + YamlConfig existing = watcher.get(path); + if (existing != null) { + return existing; + } + + return loadInternal(file); + } /** - * Returns a string value at the given path. + * Loads a configuration file intended for internal plugin usage. + * + *

This method guarantees that the configuration file exists by copying it + * from the plugin JAR if it is not already present in the plugin's data folder.

+ * + *

If the file already exists, it is simply loaded and returned.

* - * @param path config path - * @return Optional containing the value, or empty if not present + *

Behavior:

+ * + * + *

Failure Conditions:

+ *

This method will throw an exception if the resource does not exist inside + * the plugin JAR. It is intended strictly for internal configuration files that + * are guaranteed to be packaged with the plugin.

+ * + *

Example usage:

+ *
+     * YamlConfig config = manager.getInternal("config.yml");
+     * 
+ * + * @param resourceName name of the resource inside the plugin JAR (e.g. {@code "config.yml"}), must not be {@code null} + * @return loaded {@link YamlConfig} snapshot, never {@code null} + * + * @throws NullPointerException if {@code resourceName} is {@code null} + * @throws IllegalArgumentException if the resource does not exist in the plugin JAR + * @throws RuntimeException if loading or registration fails */ - public @NotNull Optional getString(@NotNull String path) { - return Optional.ofNullable(config.getString(path)); + public @NotNull YamlConfig getInternal(@NotNull String resourceName) { + + Objects.requireNonNull(resourceName, "resourceName"); + + // Validate resource exists in JAR + if (plugin.getResource(resourceName) == null) { + throw new IllegalArgumentException( + "Resource not found in plugin JAR: " + resourceName + ); + } + + File file = new File(plugin.getDataFolder(), resourceName); + + // Copy if missing + if (!file.exists()) { + plugin.saveResource(resourceName, false); + } + + return getOrLoad(file); } /** - * Returns an integer value at the given path. + * Explicitly loads a configuration file and registers it with the watcher. + * + *

This method always attempts to load the file regardless of its current + * registration state.

+ * + * @param file configuration file, must not be {@code null} + * @return loaded {@link YamlConfig} snapshot, never {@code null} * - *

This method avoids Bukkit's default fallback (0) by checking presence.

+ * @throws NullPointerException if {@code file} is {@code null} + * @throws RuntimeException if loading or registration fails */ - public @NotNull Optional getInt(@NotNull String path) { - return config.contains(path) - ? Optional.of(config.getInt(path)) - : Optional.empty(); + public @NotNull YamlConfig load(@NotNull File file) { + return loadInternal(file); } /** - * Returns a boolean value at the given path. + * Ensures the configuration file exists, creating parent directories if needed, + * and then loads and registers it. + * + * @param file configuration file, must not be {@code null} + * @return loaded {@link YamlConfig} snapshot, never {@code null} + * + * @throws NullPointerException if {@code file} is {@code null} + * @throws RuntimeException if loading or registration fails */ - public @NotNull Optional getBoolean(@NotNull String path) { - return config.contains(path) - ? Optional.of(config.getBoolean(path)) - : Optional.empty(); + public @NotNull YamlConfig createIfMissing(@NotNull File file) { + if (!file.exists()) { + ensureParentDirectories(file); + } + return loadInternal(file); } /** - * Returns a double value at the given path. + * Attempts to load a configuration file safely. + * + *

This method does not throw exceptions. Instead, it returns a structured + * {@link ConfigLoadResult} describing the outcome.

+ * + * @param file configuration file, must not be {@code null} + * @return result object containing status, configuration, or error */ - public @NotNull Optional getDouble(@NotNull String path) { - return config.contains(path) - ? Optional.of(config.getDouble(path)) - : Optional.empty(); + public @NotNull ConfigLoadResult tryLoad(@NotNull File file) { + try { + YamlConfig config = loadInternal(file); + return new ConfigLoadResult(ConfigLoadStatus.LOADED, config, null); + } catch (Exception e) { + return new ConfigLoadResult(ConfigLoadStatus.IO_ERROR, null, e); + } } /** - * Returns a float value at the given path. + * Attempts to create and load a configuration file safely. + * + *

If the file already exists, the existing configuration is returned.

+ * + * @param file configuration file, must not be {@code null} + * @return result object describing the outcome */ - public @NotNull Optional getFloat(@NotNull String path) { - return config.contains(path) - ? Optional.of((float) config.getDouble(path)) - : Optional.empty(); + public @NotNull ConfigLoadResult tryCreate(@NotNull File file) { + + if (file.exists()) { + try { + return new ConfigLoadResult( + ConfigLoadStatus.ALREADY_EXISTS, + get(file), + null + ); + } catch (Exception e) { + return new ConfigLoadResult(ConfigLoadStatus.IO_ERROR, null, e); + } + } + + try { + ensureParentDirectories(file); + YamlConfig config = loadInternal(file); + return new ConfigLoadResult(ConfigLoadStatus.CREATED, config, null); + } catch (Exception e) { + return new ConfigLoadResult(ConfigLoadStatus.IO_ERROR, null, e); + } } /** - * Returns a string list at the given path. + * Stops tracking the specified configuration file. + * + * @param file configuration file, must not be {@code null} */ - public @NotNull Optional> getStringList(@NotNull String path) { - return config.contains(path) - ? Optional.of(config.getStringList(path)) - : Optional.empty(); + public void unload(@NotNull File file) { + watcher.unregister(normalize(file.toPath())); } /** - * Returns a raw object at the given path. + * Returns all currently tracked configuration snapshots. + * + * @return collection of configurations, never {@code null} */ - public @NotNull Optional get(@NotNull String path) { - return Optional.ofNullable(config.get(path)); + public @NotNull Collection getAll() { + return watcher.getAll(); } - // ========================================================= - // Default-based getters (most commonly used) - // ========================================================= + /** + * Internal method responsible for loading and registering configurations. + * + * @param file configuration file, must not be {@code null} + * @return loaded {@link YamlConfig} snapshot + */ + private @NotNull YamlConfig loadInternal(@NotNull File file) { - public @NotNull String getStringOrDefault(@NotNull String path, @NotNull String def) { - String value = config.getString(path); - return value != null ? value : def; - } + ensureParentDirectories(file); - public int getIntOrDefault(@NotNull String path, int def) { - return config.getInt(path, def); - } + YamlConfig config = new YamlConfig( + file, + YamlConfiguration.loadConfiguration(file) + ); - public boolean getBooleanOrDefault(@NotNull String path, boolean def) { - return config.getBoolean(path, def); - } + try { + watcher.register(config); + } catch (IOException e) { + throw new RuntimeException("Failed to register watcher for " + file, e); + } - public double getDoubleOrDefault(@NotNull String path, double def) { - return config.getDouble(path, def); + return config; } - public float getFloatOrDefault(@NotNull String path, float def) { - return (float) config.getDouble(path, def); - } + /** + * Ensures that the parent directories of the file exist. + * + * @param file file whose parent directories should be verified + * + * @throws IllegalStateException if directory creation fails + */ + private void ensureParentDirectories(@NotNull File file) { + + File parent = file.getParentFile(); - // ========================================================= - // Utility - // ========================================================= + if (parent != null && !parent.exists()) { + if (!parent.mkdirs() && !parent.exists()) { + throw new IllegalStateException("Failed to create directories: " + parent); + } + } + } /** - * Checks whether a value exists at the given path. + * Normalizes a path to ensure consistent identity. * - * @param path config path - * @return true if present + * @param path raw path, must not be {@code null} + * @return normalized absolute path */ - public boolean has(@NotNull String path) { - return config.contains(path); + private static @NotNull Path normalize(@NotNull Path path) { + return path.toAbsolutePath().normalize(); } } \ 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 5f1dc6e..0142344 100644 --- a/src/main/java/dev/spexx/configurationAPI/manager/ConfigManager.java +++ b/src/main/java/dev/spexx/configurationAPI/manager/ConfigManager.java @@ -1,5 +1,7 @@ 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; @@ -16,29 +18,37 @@ /** * Central access point for configuration files managed by the system. * - *

This class is responsible for: + *

This class is responsible for:

*
    - *
  • Initial loading of configuration files
  • + *
  • Loading configuration files from disk
  • *
  • Registering configurations with the global watcher
  • - *
  • Providing access to the latest configuration snapshots
  • + *
  • Providing access to the latest immutable configuration snapshots
  • *
* *

Architecture

- *

The {@link GlobalConfigWatcher} is the single source of truth for all - * configuration state. This class does not maintain its own cache.

+ *

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.

* - *

All calls to {@link #get(File)} delegate directly to the watcher, - * ensuring that consumers always receive the most up-to-date configuration.

+ *

Lifecycle

+ *

This class provides multiple levels of control over configuration handling:

+ *
    + *
  • {@link #get(File)} — retrieve an already loaded configuration
  • + *
  • {@link #getOrLoad(File)} — retrieve or load if missing
  • + *
  • {@link #load(File)} — explicitly load and register
  • + *
  • {@link #createIfMissing(File)} — create file if needed, then load
  • + *
  • {@link #tryLoad(File)} — safe load with structured result
  • + *
  • {@link #tryCreate(File)} — safe create with structured result
  • + *
* *

Thread Safety

- *

This class is thread-safe. The underlying watcher uses concurrent - * data structures and atomic replacement of {@link YamlConfig} instances.

+ *

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 rather than caching references long-term. + * @apiNote {@link YamlConfig} instances are immutable snapshots. Consumers should + * retrieve them on demand instead of caching long-term references. * - * @since 1.0.5 + * @since 1.1.0 */ public final class ConfigManager { @@ -48,10 +58,12 @@ public final class ConfigManager { /** * Creates a new {@code ConfigManager}. * - *

This initializes and starts the {@link GlobalConfigWatcher}.

+ *

This initializes and starts the {@link GlobalConfigWatcher}, which begins + * monitoring registered configuration files for changes.

* * @param plugin owning plugin instance, must not be {@code null} * + * @throws NullPointerException if {@code plugin} is {@code null} * @throws RuntimeException if watcher initialization fails */ public ConfigManager(@NotNull JavaPlugin plugin) { @@ -68,12 +80,13 @@ public ConfigManager(@NotNull JavaPlugin plugin) { /** * Returns the latest configuration snapshot for the given file. * - *

The configuration must have been previously loaded using - * {@link #getOrLoad(File)} or {@link #getOrLoadResource(String)}.

+ *

The configuration must have been previously loaded using one of the + * loading methods. This method does not attempt to load the file.

* * @param file configuration file, must not be {@code null} * @return latest {@link YamlConfig} snapshot, never {@code null} * + * @throws NullPointerException if {@code file} is {@code null} * @throws IllegalStateException if the configuration is not loaded */ public @NotNull YamlConfig get(@NotNull File file) { @@ -90,14 +103,13 @@ public ConfigManager(@NotNull JavaPlugin plugin) { /** * Returns an existing configuration or loads it if not already tracked. * - *

If the configuration is not already registered, it is: - *

    - *
  1. Loaded from disk
  2. - *
  3. Registered with the watcher
  4. - *
+ *

If the configuration is not registered, it will be loaded from disk + * and registered with the watcher.

* * @param file configuration file, must not be {@code null} - * @return latest {@link YamlConfig} snapshot + * @return latest {@link YamlConfig} snapshot, never {@code null} + * + * @throws NullPointerException if {@code file} is {@code null} */ public @NotNull YamlConfig getOrLoad(@NotNull File file) { Path path = normalize(file.toPath()); @@ -107,73 +119,151 @@ public ConfigManager(@NotNull JavaPlugin plugin) { return existing; } - return load(file); + return loadInternal(file); } /** - * Returns the latest configuration snapshot for a file located - * relative to the plugin's data folder. + * Loads a configuration file intended for internal plugin usage. * - *

This is a convenience method that resolves the provided path - * against {@link JavaPlugin#getDataFolder()}.

+ *

This method guarantees that the configuration file exists by copying it + * from the plugin JAR if it is not already present in the plugin's data folder.

+ * + *

If the file already exists, it is simply loaded and returned.

+ * + *

Behavior:

+ *
    + *
  • If the file does not exist, it is copied from the plugin resources
  • + *
  • If the file exists, it is loaded normally
  • + *
  • The configuration is always registered with the watcher
  • + *
+ * + *

Failure Conditions:

+ *

This method will throw an exception if the resource does not exist inside + * the plugin JAR. It is intended strictly for internal configuration files that + * are guaranteed to be packaged with the plugin.

* *

Example usage:

*
-     * YamlConfig config = manager.getByPath("configs/example.yml");
+     * YamlConfig config = manager.getInternal("config.yml");
      * 
* - * @param path relative file path using forward slashes, must not be {@code null} - * @return latest {@link YamlConfig} snapshot + * @param resourceName name of the resource inside the plugin JAR (e.g. {@code "config.yml"}), must not be {@code null} + * @return loaded {@link YamlConfig} snapshot, never {@code null} * - * @throws IllegalStateException if the configuration is not loaded + * @throws NullPointerException if {@code resourceName} is {@code null} + * @throws IllegalArgumentException if the resource does not exist in the plugin JAR + * @throws RuntimeException if loading or registration fails */ - public @NotNull YamlConfig getByPath(@NotNull String path) { - File file = resolvePath(path); - return get(file); + public @NotNull YamlConfig getInternal(@NotNull String resourceName) { + + Objects.requireNonNull(resourceName, "resourceName"); + + // Validate resource exists in JAR + if (plugin.getResource(resourceName) == null) { + throw new IllegalArgumentException( + "Resource not found in plugin JAR: " + resourceName + ); + } + + File file = new File(plugin.getDataFolder(), resourceName); + + // Copy if missing + if (!file.exists()) { + plugin.saveResource(resourceName, false); + } + + return getOrLoad(file); } /** - * Returns an existing configuration or loads it if not already tracked, - * using a path relative to the plugin's data folder. + * Explicitly loads a configuration file and registers it with the watcher. * - *

Example usage:

- *
-     * YamlConfig config = manager.getOrLoadByPath("configs/example.yml");
-     * 
+ *

This method always attempts to load the file regardless of its current + * registration state.

+ * + * @param file configuration file, must not be {@code null} + * @return loaded {@link YamlConfig} snapshot, never {@code null} * - * @param path relative file path using forward slashes, must not be {@code null} - * @return latest {@link YamlConfig} snapshot + * @throws NullPointerException if {@code file} is {@code null} + * @throws RuntimeException if loading or registration fails */ - public @NotNull YamlConfig getOrLoadByPath(@NotNull String path) { - File file = resolvePath(path); - return getOrLoad(file); + public @NotNull YamlConfig load(@NotNull File file) { + return loadInternal(file); } /** - * Loads a configuration file from plugin resources if necessary. + * Ensures the configuration file exists, creating parent directories if needed, + * and then loads and registers it. * - *

If the file does not exist in the plugin data folder, it is copied - * from the plugin JAR. The file is then loaded and registered.

+ * @param file configuration file, must not be {@code null} + * @return loaded {@link YamlConfig} snapshot, never {@code null} * - * @param name resource name (for example {@code "config.yml"}), must not be {@code null} - * @return latest {@link YamlConfig} snapshot + * @throws NullPointerException if {@code file} is {@code null} + * @throws RuntimeException if loading or registration fails */ - public @NotNull YamlConfig getOrLoadResource(@NotNull String name) { - plugin.saveResource(name, false); - return getOrLoad(new File(plugin.getDataFolder(), name)); + public @NotNull YamlConfig createIfMissing(@NotNull File file) { + if (!file.exists()) { + ensureParentDirectories(file); + } + return loadInternal(file); } /** - * Stops tracking the specified configuration file. + * Attempts to load a configuration file safely. * - *

This removes the configuration from the watcher. If the file still - * exists, it may be reloaded again if explicitly registered.

+ *

This method does not throw exceptions. Instead, it returns a structured + * {@link ConfigLoadResult} describing the outcome.

+ * + * @param file configuration file, must not be {@code null} + * @return result object containing status, configuration, or error + */ + public @NotNull ConfigLoadResult tryLoad(@NotNull File file) { + try { + YamlConfig config = loadInternal(file); + return new ConfigLoadResult(ConfigLoadStatus.LOADED, config, null); + } catch (Exception e) { + return new ConfigLoadResult(ConfigLoadStatus.IO_ERROR, null, e); + } + } + + /** + * Attempts to create and load a configuration file safely. + * + *

If the file already exists, the existing configuration is returned.

+ * + * @param file configuration file, must not be {@code null} + * @return result object describing the outcome + */ + public @NotNull ConfigLoadResult tryCreate(@NotNull File file) { + + if (file.exists()) { + try { + return new ConfigLoadResult( + ConfigLoadStatus.ALREADY_EXISTS, + get(file), + null + ); + } catch (Exception e) { + return new ConfigLoadResult(ConfigLoadStatus.IO_ERROR, null, e); + } + } + + try { + ensureParentDirectories(file); + YamlConfig config = loadInternal(file); + return new ConfigLoadResult(ConfigLoadStatus.CREATED, config, null); + } catch (Exception e) { + return new ConfigLoadResult(ConfigLoadStatus.IO_ERROR, null, e); + } + } + + /** + * Stops tracking the specified configuration file. * * @param file configuration file, must not be {@code null} */ public void unload(@NotNull File file) { - Path path = normalize(file.toPath()); - watcher.unregister(path); + watcher.unregister(normalize(file.toPath())); } /** @@ -186,16 +276,14 @@ public void unload(@NotNull File file) { } /** - * Loads a configuration file and registers it with the watcher. + * Internal method responsible for loading and registering configurations. * * @param file configuration file, must not be {@code null} * @return loaded {@link YamlConfig} snapshot - * - * @throws RuntimeException if watcher registration fails */ - private @NotNull YamlConfig load(@NotNull File file) { + private @NotNull YamlConfig loadInternal(@NotNull File file) { - ensureFileExists(file); + ensureParentDirectories(file); YamlConfig config = new YamlConfig( file, @@ -214,15 +302,11 @@ public void unload(@NotNull File file) { /** * Ensures that the parent directories of the file exist. * - * @param file file to verify, must not be {@code null} + * @param file file whose parent directories should be verified * * @throws IllegalStateException if directory creation fails */ - private void ensureFileExists(@NotNull File file) { - - if (file.exists()) { - return; - } + private void ensureParentDirectories(@NotNull File file) { File parent = file.getParentFile(); @@ -233,24 +317,6 @@ private void ensureFileExists(@NotNull File file) { } } - /** - * Resolves a relative path against the plugin's data folder. - * - *

Backslashes are normalized to forward slashes to ensure - * cross-platform compatibility.

- * - * @param path relative path, must not be {@code null} - * @return resolved file - */ - private @NotNull File resolvePath(@NotNull String path) { - Objects.requireNonNull(path, "path"); - - // Normalize separators (Windows → Unix style) - String normalized = path.replace("\\", "/"); - - return new File(plugin.getDataFolder(), normalized); - } - /** * Normalizes a path to ensure consistent identity. * diff --git a/src/main/resources/paper-plugin.yml b/src/main/resources/paper-plugin.yml index fbd112a..2d92960 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.1' +version: '1.1.0' main: dev.spexx.configurationAPI.ConfigurationAPI api-version: '1.21.11'