From 7c1122ae3e9edcd4bd61b4e1b4f9d21a3268917e Mon Sep 17 00:00:00 2001 From: Ben Woo <30431861+benwoo1110@users.noreply.github.com> Date: Thu, 16 Feb 2023 22:35:53 +0800 Subject: [PATCH] Add Node and Settings feature (#1) * Add Node and Settings feature * Split node and typed node * Remove validation * Add a base CommentedNode without value * Update package to io.github.townyadvanced.commentedconfiguration * Add tests and build settings --- pom.xml | 84 ++- .../CommentedConfiguration.java | 666 +++++++++--------- .../setting/CommentedNode.java | 22 + .../setting/Settings.java | 170 +++++ .../setting/SimpleNode.java | 139 ++++ .../setting/TypedValueNode.java | 25 + .../setting/ValueNode.java | 15 + .../commentedconfiguration/TestNodes.java | 53 ++ .../TestTypedValueNodesSettings.java | 123 ++++ src/test/resources/edited_config.yml | 22 + src/test/resources/og_config.yml | 10 + src/test/resources/saved_config.yml | 23 + 12 files changed, 1030 insertions(+), 322 deletions(-) rename src/main/java/io/github/townyadvanced/{ => commentedconfiguration}/CommentedConfiguration.java (90%) create mode 100644 src/main/java/io/github/townyadvanced/commentedconfiguration/setting/CommentedNode.java create mode 100644 src/main/java/io/github/townyadvanced/commentedconfiguration/setting/Settings.java create mode 100644 src/main/java/io/github/townyadvanced/commentedconfiguration/setting/SimpleNode.java create mode 100644 src/main/java/io/github/townyadvanced/commentedconfiguration/setting/TypedValueNode.java create mode 100644 src/main/java/io/github/townyadvanced/commentedconfiguration/setting/ValueNode.java create mode 100644 src/test/java/io/github/townyadvanced/commentedconfiguration/TestNodes.java create mode 100644 src/test/java/io/github/townyadvanced/commentedconfiguration/TestTypedValueNodesSettings.java create mode 100644 src/test/resources/edited_config.yml create mode 100644 src/test/resources/og_config.yml create mode 100644 src/test/resources/saved_config.yml diff --git a/pom.xml b/pom.xml index c151450..6d53537 100644 --- a/pom.xml +++ b/pom.xml @@ -1,8 +1,9 @@ 4.0.0 - CommentedConfiguration + io.github.townyadvanced.commentedconfiguration CommentedConfiguration - 1.0.0 + 1.0.0-SNAPSHOT + YAML Configuration for Bukkit with support for comments. 1.8 @@ -16,13 +17,92 @@ https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + clean package + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.10.1 + + 8 + 8 + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.0.1 + + + + se.eris + notnull-instrumenter-maven-plugin + 1.1.1 + + + + instrument + tests-instrument + + + + org.jetbrains.annotations.NotNull + javax.validation.constraints.NotNull + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M8 + + + + org.spigotmc spigot-api 1.18.1-R0.1-SNAPSHOT provided + + + org.jetbrains + annotations + 24.0.0 + compile + + + + org.junit.jupiter + junit-jupiter-engine + 5.9.2 + test + + + commons-io + commons-io + 2.11.0 + test + diff --git a/src/main/java/io/github/townyadvanced/CommentedConfiguration.java b/src/main/java/io/github/townyadvanced/commentedconfiguration/CommentedConfiguration.java similarity index 90% rename from src/main/java/io/github/townyadvanced/CommentedConfiguration.java rename to src/main/java/io/github/townyadvanced/commentedconfiguration/CommentedConfiguration.java index 34a2a13..78f80ba 100644 --- a/src/main/java/io/github/townyadvanced/CommentedConfiguration.java +++ b/src/main/java/io/github/townyadvanced/commentedconfiguration/CommentedConfiguration.java @@ -1,320 +1,346 @@ -package io.github.townyadvanced; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.logging.Logger; - -import org.bukkit.configuration.InvalidConfigurationException; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.plugin.Plugin; -import com.google.common.base.Strings; - -/** - * @author dumptruckman - * @author Articdive - * @author LlmDl - */ -public class CommentedConfiguration extends YamlConfiguration { - private final HashMap comments = new HashMap<>(); - private final Path path; - private final Logger logger; - private final String newLine = System.getProperty("line.separator"); - private int depth; - - /** - * Create a new CommentedConfiguration using the file at the given path, for the - * given plugin. Plugin's own Logger will be used for any error messages. - * - * @param path The Path where the config will be/is saved. - * @param plugin The plugin which supplies a logger. - */ - public CommentedConfiguration(Path path, Plugin plugin) { - super(); - this.path = path; - this.logger = plugin.getLogger(); - setWidth(); - } - - /** - * Load the yaml configuration file into memory. - * - * @return true if file is able to load. - */ - public boolean load() { - return loadFile(); - } - - private boolean loadFile() { - try { - this.load(path.toFile()); - return true; - } catch (InvalidConfigurationException | IOException e) { - logger.warning(String.format("Loading error: Failed to load file %s (does it pass a yaml parser?).", path)); - logger.warning("https://jsonformatter.org/yaml-parser"); - logger.warning(e.getMessage()); - return false; - } - } - - /** - * Save the yaml configuration file from memory to file. - */ - public void save() { - - // Save the config like normal. - boolean saved = saveFile(); - - // If there's comments to add and it saved fine, we need to add comments. - if (!comments.isEmpty() && saved) { - - // String list of each line in the config file. - List yamlContents = readYaml(); - - // Generate new config strings, ignoring existing comments and parsing in our - // up-to-date comments from the ConfigNodes enum. - StringBuilder newContents = readConfigToString(yamlContents); - - // Write newContents to file. - writeYaml(newContents); - } - } - - /** - * Read the file found at the path into a list of Strings. - * - * @return yamlContents a List of Strings which represent the raw lines of the - * file at the given path. - */ - private List readYaml() { - - List yamlContents = new ArrayList<>(); - try { - yamlContents = Files.readAllLines(path, StandardCharsets.UTF_8); - } catch (IOException e) { - logger.warning(String.format("Failed to read file %s.", path)); - logger.warning(e.getMessage()); - } - return yamlContents; - } - - /** - * Write the file to the given path, in valid yaml format, with the comments - * added in. - */ - private void writeYaml(StringBuilder newContents) { - try { - Files.write(path, newContents.toString().getBytes(StandardCharsets.UTF_8)); - } catch (IOException e) { - logger.warning(String.format("Saving error: Failed to write to file %s.", path)); - logger.warning(e.getMessage()); - } - } - - /** - * Attempts to save the file via YamlConfiguration's usual methods. - * - * @return true if the file was able to be saved. - */ - private boolean saveFile() { - try { - this.save(path.toFile()); - return true; - } catch (Exception e) { - return false; - } - } - - /** - * Read through the contents of the old config and return an up to date new - * config, complete with comments generated from the ConfigNodes enum. - * - * @param oldContents List of Strings which represent the config being loaded - * from the server. - * @return newContents StringBuilder. - */ - private StringBuilder readConfigToString(List oldContents) { - // This will hold the newly formatted line - StringBuilder newContents = new StringBuilder(); - // This holds the current path the lines are at in the config - String currentPath = ""; - // The depth of the path. (number of words separated by periods - 1) - depth = 0; - - // Loop through the old config lines. - for (String line : oldContents) { - // Spigot's addition of native SnakeYAML comment support in MC 1.18.1, requires - // us to ignore the comments in our own file, which will be replaced later on - // with up-to-date comments from the ConfigNodes enum. - // TODO: This comment above is relevant to Towny's use. - if (line.trim().startsWith("#") || line.isEmpty() || line.trim().isEmpty()) - continue; - - // If the line is a node (and not something like a list value) - if (line.contains(": ") || (line.length() > 1 && line.charAt(line.length() - 1) == ':')) { - - // Build the new line, allowing us to get the comments made in the ConfigNodes - // enum. - // ie: new_world_settings.pvp.force_pvp_on - currentPath = getCurrentPath(line, currentPath); - - // Grab any available comments for the current path. - String comment = comments.get(currentPath); - - // If there are comments, add them to the beginning of the current line. - if (comment != null) - line = comment + newLine + line; - } - - // Add the line to what will be written in the new config. - newContents.append(line).append(newLine); - } - - return newContents; - } - - /** - * Creates a Configuration path from a raw yaml file's lines. - * - * @param line String line from the old configuration file. - * @param currentPath What has been built thus far as a Configuration path. - * @return currentPath The next Configuration path to save into the new config. - */ - private String getCurrentPath(String line, String currentPath) { - // Grab the index of the end of the node name - int index; - index = line.indexOf(": "); - if (index < 0) { - index = line.length() - 1; - } - // The first line of the file, store the node name as the currentPath. - if (currentPath.isEmpty()) { - currentPath = line.substring(0, index); - } else { - // Calculate the whitespace preceding the node name, allowing us to determine - // depth. - int whiteSpace = getWhiteSpaceFromLine(line); - // Get the current node we're adding. - String nodeName = line.substring(whiteSpace, index); - // Find out if the current depth (whitespace * 2) is greater/lesser/equal to the - // previous depth. - if (whiteSpace / 2 > depth) { - // Path is deeper. Add a . and the node name. - currentPath += "." + nodeName; - depth++; - } else if (whiteSpace / 2 < depth) { - // Path is shallower, calculate current depth from whitespace (whitespace / 2) - // and subtract that many levels from the currentPath - int newDepth = whiteSpace / 2; - // Shrink the currentPath, removing nodes with no more children. - currentPath = shrinkCurrentPath(currentPath, newDepth); - // Add the nodeName to the currentPath. - currentPath = addNodeNameToCurrentPath(currentPath, nodeName); - // Reset the depth - depth = newDepth; - } else { - // Path is same depth, replace the last path node name to the current node name. - // Add the nodeName to the currentPath. - currentPath = addNodeNameToCurrentPath(currentPath, nodeName); - } - } - - return currentPath; - } - - /** - * Get the whitespace in front of the current line's text. - * - * @param line String line from the old config. - * @return number of empty spaces preceding the current line's text. - */ - private int getWhiteSpaceFromLine(String line) { - int whiteSpace = 0; - for (int n = 0; n < line.length(); n++) - if (line.charAt(n) == ' ') - whiteSpace++; - else - break; - return whiteSpace; - } - - /** - * Shrink the currentPath, scaling the path back, removing completed child - * nodes. - * - * @param currentPath String representing the Configuration path. - * @param newDepth int representing the depth we're going to get back to. - * @return currentPath with finished child nodes removed. - */ - private String shrinkCurrentPath(String currentPath, int newDepth) { - for (int i = 0; i < depth - newDepth; i++) - currentPath = currentPath.replace(currentPath.substring(currentPath.lastIndexOf(".")), ""); - return currentPath; - } - - /** - * Add the nodeName to the currentPath, either at the same depth or shallower. - * - * @param currentPath String representing the Configuration path being added. - * @param nodeName String representing the new node being added to the - * currentPath. - * @return currentPath after it has been adjusted and had the new nodeName - * added. - */ - private String addNodeNameToCurrentPath(String currentPath, String nodeName) { - // Grab the index of the final period - int lastIndex = currentPath.lastIndexOf("."); - if (lastIndex < 0) { - // If there isn't a final period, set the current path to nothing because we're - // at root - currentPath = ""; - } else { - // If there is a final period, replace everything after it with nothing - currentPath = currentPath.replace(currentPath.substring(lastIndex), ""); - currentPath += "."; - } - return currentPath += nodeName; - } - - /** - * Stores a comment for the specified Configuration path. The comment can be - * multiple lines. An empty string will indicate a blank line. - * - * @param path Configuration path to add comment. - * @param commentLines Comments to add. One String per line. - */ - public void addComment(String path, String... commentLines) { - - StringBuilder commentBlock = new StringBuilder(); - // Get the preceding spaces based on how many .'s are in the path. - String leadingSpaces = Strings.repeat(" ", (int) path.chars().filter(ch -> ch == '.').count()); - // Parse over all of the comment lines. - for (String commentLine : commentLines) { - // Add the leading spaces if commentLine isn't empty. - commentLine = !commentLine.isEmpty() ? leadingSpaces + commentLine : " "; - // Add a new line if this comment block already has more than one line. - if (commentBlock.length() > 0) - commentBlock.append(newLine); - // Add the line to the commentBlock. - commentBlock.append(commentLine); - } - // Put the comment block into the comments HashMap to be parsed into the config - // later. - comments.put(path, commentBlock.toString()); - } - - /** - * Width became an option with MC 1.18.1. Setting it wider will allow - * configurations to not break things into multi-lines. - */ - private void setWidth() { - try { - this.options().width(10000); - } catch (NoSuchMethodError ignored) { - } - } -} \ No newline at end of file +package io.github.townyadvanced.commentedconfiguration; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.logging.Logger; + +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; +import com.google.common.base.Strings; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A YamlConfiguration that supports comments. + * + * @author dumptruckman + * @author Articdive + * @author LlmDl + */ +public class CommentedConfiguration extends YamlConfiguration { + private static final Logger DEFAULT_LOGGER = Logger.getLogger("CommentedConfiguration"); + + private final HashMap comments = new HashMap<>(); + private final Path path; + private final Logger logger; + private final String newLine = System.getProperty("line.separator"); + private int depth; + + /** + * Create a new CommentedConfiguration using the file at the given path. + * + * @param path The Path where the config will be/is saved. + */ + public CommentedConfiguration(@NotNull Path path) { + this(path, (Logger) null); + } + + /** + * Create a new CommentedConfiguration using the file at the given path, for the + * given plugin. Plugin's own Logger will be used for any error messages. + * + * @param path The Path where the config will be/is saved. + * @param plugin The Plugin to get the logger from. + */ + public CommentedConfiguration(@NotNull Path path, @NotNull Plugin plugin) { + this(path, plugin.getLogger()); + } + + /** + * Create a new CommentedConfiguration using the file at the given path, for the + * given plugin. Provide a Logger to use for any error messages. + * + * @param path The Path where the config will be/is saved. + * @param logger The Logger to use for error messages. + */ + public CommentedConfiguration(@NotNull Path path, @Nullable Logger logger) { + super(); + this.path = path; + this.logger = logger == null ? DEFAULT_LOGGER : logger; + setWidth(); + } + + /** + * Load the yaml configuration file into memory. + * + * @return true if file is able to load. + */ + public boolean load() { + return loadFile(); + } + + private boolean loadFile() { + try { + this.load(path.toFile()); + return true; + } catch (InvalidConfigurationException | IOException e) { + logger.warning(String.format("Loading error: Failed to load file %s (does it pass a yaml parser?).", path)); + logger.warning("https://jsonformatter.org/yaml-parser"); + logger.warning(e.getMessage()); + return false; + } + } + + /** + * Save the yaml configuration file from memory to file. + */ + public void save() { + + // Save the config like normal. + boolean saved = saveFile(); + + // If there's comments to add and it saved fine, we need to add comments. + if (!comments.isEmpty() && saved) { + + // String list of each line in the config file. + List yamlContents = readYaml(); + + // Generate new config strings, ignoring existing comments and parsing in our + // up-to-date comments from the ConfigNodes enum. + StringBuilder newContents = readConfigToString(yamlContents); + + // Write newContents to file. + writeYaml(newContents); + } + } + + /** + * Read the file found at the path into a list of Strings. + * + * @return yamlContents a List of Strings which represent the raw lines of the + * file at the given path. + */ + private List readYaml() { + + List yamlContents = new ArrayList<>(); + try { + yamlContents = Files.readAllLines(path, StandardCharsets.UTF_8); + } catch (IOException e) { + logger.warning(String.format("Failed to read file %s.", path)); + logger.warning(e.getMessage()); + } + return yamlContents; + } + + /** + * Write the file to the given path, in valid yaml format, with the comments + * added in. + */ + private void writeYaml(StringBuilder newContents) { + try { + Files.write(path, newContents.toString().getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + logger.warning(String.format("Saving error: Failed to write to file %s.", path)); + logger.warning(e.getMessage()); + } + } + + /** + * Attempts to save the file via YamlConfiguration's usual methods. + * + * @return true if the file was able to be saved. + */ + private boolean saveFile() { + try { + this.save(path.toFile()); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * Read through the contents of the old config and return an up to date new + * config, complete with comments generated from the ConfigNodes enum. + * + * @param oldContents List of Strings which represent the config being loaded + * from the server. + * @return newContents StringBuilder. + */ + private StringBuilder readConfigToString(List oldContents) { + // This will hold the newly formatted line + StringBuilder newContents = new StringBuilder(); + // This holds the current path the lines are at in the config + String currentPath = ""; + // The depth of the path. (number of words separated by periods - 1) + depth = 0; + + // Loop through the old config lines. + for (String line : oldContents) { + // Spigot's addition of native SnakeYAML comment support in MC 1.18.1, requires + // us to ignore the comments in our own file, which will be replaced later on + // with up-to-date comments from the ConfigNodes enum. + // TODO: This comment above is relevant to Towny's use. + if (line.trim().startsWith("#") || line.isEmpty() || line.trim().isEmpty()) + continue; + + // If the line is a node (and not something like a list value) + if (line.contains(": ") || (line.length() > 1 && line.charAt(line.length() - 1) == ':')) { + + // Build the new line, allowing us to get the comments made in the ConfigNodes + // enum. + // ie: new_world_settings.pvp.force_pvp_on + currentPath = getCurrentPath(line, currentPath); + + // Grab any available comments for the current path. + String comment = comments.get(currentPath); + + // If there are comments, add them to the beginning of the current line. + if (comment != null) + line = comment + newLine + line; + } + + // Add the line to what will be written in the new config. + newContents.append(line).append(newLine); + } + + return newContents; + } + + /** + * Creates a Configuration path from a raw yaml file's lines. + * + * @param line String line from the old configuration file. + * @param currentPath What has been built thus far as a Configuration path. + * @return currentPath The next Configuration path to save into the new config. + */ + private String getCurrentPath(String line, String currentPath) { + // Grab the index of the end of the node name + int index; + index = line.indexOf(": "); + if (index < 0) { + index = line.length() - 1; + } + // The first line of the file, store the node name as the currentPath. + if (currentPath.isEmpty()) { + currentPath = line.substring(0, index); + } else { + // Calculate the whitespace preceding the node name, allowing us to determine + // depth. + int whiteSpace = getWhiteSpaceFromLine(line); + // Get the current node we're adding. + String nodeName = line.substring(whiteSpace, index); + // Find out if the current depth (whitespace * 2) is greater/lesser/equal to the + // previous depth. + if (whiteSpace / 2 > depth) { + // Path is deeper. Add a . and the node name. + currentPath += "." + nodeName; + depth++; + } else if (whiteSpace / 2 < depth) { + // Path is shallower, calculate current depth from whitespace (whitespace / 2) + // and subtract that many levels from the currentPath + int newDepth = whiteSpace / 2; + // Shrink the currentPath, removing nodes with no more children. + currentPath = shrinkCurrentPath(currentPath, newDepth); + // Add the nodeName to the currentPath. + currentPath = addNodeNameToCurrentPath(currentPath, nodeName); + // Reset the depth + depth = newDepth; + } else { + // Path is same depth, replace the last path node name to the current node name. + // Add the nodeName to the currentPath. + currentPath = addNodeNameToCurrentPath(currentPath, nodeName); + } + } + + return currentPath; + } + + /** + * Get the whitespace in front of the current line's text. + * + * @param line String line from the old config. + * @return number of empty spaces preceding the current line's text. + */ + private int getWhiteSpaceFromLine(String line) { + int whiteSpace = 0; + for (int n = 0; n < line.length(); n++) + if (line.charAt(n) == ' ') + whiteSpace++; + else + break; + return whiteSpace; + } + + /** + * Shrink the currentPath, scaling the path back, removing completed child + * nodes. + * + * @param currentPath String representing the Configuration path. + * @param newDepth int representing the depth we're going to get back to. + * @return currentPath with finished child nodes removed. + */ + private String shrinkCurrentPath(String currentPath, int newDepth) { + for (int i = 0; i < depth - newDepth; i++) + currentPath = currentPath.replace(currentPath.substring(currentPath.lastIndexOf(".")), ""); + return currentPath; + } + + /** + * Add the nodeName to the currentPath, either at the same depth or shallower. + * + * @param currentPath String representing the Configuration path being added. + * @param nodeName String representing the new node being added to the + * currentPath. + * @return currentPath after it has been adjusted and had the new nodeName + * added. + */ + private String addNodeNameToCurrentPath(String currentPath, String nodeName) { + // Grab the index of the final period + int lastIndex = currentPath.lastIndexOf("."); + if (lastIndex < 0) { + // If there isn't a final period, set the current path to nothing because we're + // at root + currentPath = ""; + } else { + // If there is a final period, replace everything after it with nothing + currentPath = currentPath.replace(currentPath.substring(lastIndex), ""); + currentPath += "."; + } + return currentPath += nodeName; + } + + /** + * Stores a comment for the specified Configuration path. The comment can be + * multiple lines. An empty string will indicate a blank line. + * + * @param path Configuration path to add comment. + * @param commentLines Comments to add. One String per line. + */ + public void addComment(String path, String... commentLines) { + + StringBuilder commentBlock = new StringBuilder(); + // Get the preceding spaces based on how many .'s are in the path. + String leadingSpaces = Strings.repeat(" ", (int) path.chars().filter(ch -> ch == '.').count()); + // Parse over all of the comment lines. + for (String commentLine : commentLines) { + // Add the leading spaces if commentLine isn't empty. + commentLine = !commentLine.isEmpty() ? leadingSpaces + commentLine : " "; + // Add a new line if this comment block already has more than one line. + if (commentBlock.length() > 0) + commentBlock.append(newLine); + // Add the line to the commentBlock. + commentBlock.append(commentLine); + } + // Put the comment block into the comments HashMap to be parsed into the config + // later. + comments.put(path, commentBlock.toString()); + } + + /** + * Width became an option with MC 1.18.1. Setting it wider will allow + * configurations to not break things into multi-lines. + */ + private void setWidth() { + try { + this.options().width(10000); + } catch (NoSuchMethodError ignored) { + } + } +} diff --git a/src/main/java/io/github/townyadvanced/commentedconfiguration/setting/CommentedNode.java b/src/main/java/io/github/townyadvanced/commentedconfiguration/setting/CommentedNode.java new file mode 100644 index 0000000..ab7676b --- /dev/null +++ b/src/main/java/io/github/townyadvanced/commentedconfiguration/setting/CommentedNode.java @@ -0,0 +1,22 @@ +package io.github.townyadvanced.commentedconfiguration.setting; + +import org.jetbrains.annotations.NotNull; + +/** + * Represents a node that has comments in a configuration file. + */ +public interface CommentedNode { + /** + * Gets the YAML path of the node. + * + * @return The YAML path of the node. + */ + @NotNull String getPath(); + + /** + * Gets the comment of the node. + * + * @return The comment of the node. + */ + @NotNull String[] getComments(); +} diff --git a/src/main/java/io/github/townyadvanced/commentedconfiguration/setting/Settings.java b/src/main/java/io/github/townyadvanced/commentedconfiguration/setting/Settings.java new file mode 100644 index 0000000..633b549 --- /dev/null +++ b/src/main/java/io/github/townyadvanced/commentedconfiguration/setting/Settings.java @@ -0,0 +1,170 @@ +package io.github.townyadvanced.commentedconfiguration.setting; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.logging.Logger; + +import io.github.townyadvanced.commentedconfiguration.CommentedConfiguration; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A class that handles loading and saving of a CommentedConfiguration with {@link TypedValueNode}s. + */ +public class Settings { + private final CommentedConfiguration config; + private final Path configPath; + private final List defaultNodes; + + /** + * Creates a new CommentedSettings instance that makes use of CommentedConfiguration. + * + * @param configPath The path to the configuration file. + * @param plugin The Plugin to get the logger from. + * @param defaultNodes The default node values to add to the configuration. + */ + public Settings(@NotNull Path configPath, @NotNull Plugin plugin, @Nullable List defaultNodes) { + this.config = new CommentedConfiguration(configPath, plugin); + this.configPath = configPath; + this.defaultNodes = defaultNodes; + } + + /** + * Creates a new CommentedSettings instance that makes use of CommentedConfiguration. + * + * @param configPath The path to the configuration file. + * @param logger The Logger to use for error messages. + * @param defaultNodes The default node values to add to the configuration. + */ + public Settings(@NotNull Path configPath, @Nullable Logger logger, @Nullable List defaultNodes) { + this.config = new CommentedConfiguration(configPath, logger); + this.configPath = configPath; + this.defaultNodes = defaultNodes; + } + + /** + * Loads the configuration. + * + * @return True if the configuration was loaded successfully, false otherwise. + */ + public boolean load() { + if (!createConfigFile()) { + return false; + } + if (!config.load()) { + return false; + } + addDefaultNodes(); + return true; + } + + /** + * Create a new config file if file does not exist + * + * @return True if file exist or created successfully, otherwise false. + */ + private boolean createConfigFile() { + File configFile = configPath.toFile(); + if (configFile.exists()) { + return true; + } + try { + if (!configFile.createNewFile()) { + return false; + } + } catch (IOException e) { + return false; + } + return true; + } + + /** + * Adds default node values to the configuration if they are not already present. + */ + private void addDefaultNodes() { + if (defaultNodes == null || defaultNodes.isEmpty()) { + return; + } + for (CommentedNode node : defaultNodes) { + if (node.getComments().length > 0) { + config.addComment(node.getPath(), node.getComments()); + } + if (node instanceof ValueNode && config.get(node.getPath()) == null) { + config.set(node.getPath(), ((ValueNode)node).getDefaultValue()); + } + } + } + + /** + * Saves the configuration. + */ + public void save() { + config.save(); + } + + /** + * Gets the value of a node, if the node has a default value, it will be returned if the node is not found. + * + * @param node The node to get the value of. + * @return The value of the node. + */ + public Object get(@NotNull ValueNode node) { + return config.get(node.getPath(), node.getDefaultValue()); + } + + /** + * Gets the value of a node, if the node has a default value, it will be returned if the node is not found. + * + * @param node The node to get the value of. + * @param type The type of the node value. + * @return The value of the node. + * @param The type of the node value. + */ + public T get(@NotNull ValueNode node, Class type) { + return config.getObject(node.getPath(), type, (T) node.getDefaultValue()); + } + + /** + * Gets the value of a node, if the node has a default value, it will be returned if the node is not found. + * + * @param node The node to get the value of. + * @return The value of the node. + * @param The type of the node value. + */ + public T get(@NotNull TypedValueNode node) { + return config.getObject(node.getPath(), node.getType(), node.getDefaultValue()); + } + + /** + * Sets the value of a node, if the validator is not null, it will be tested first. + * + * @param node The node to set the value of. + * @param value The value to set. + */ + public void set(@NotNull ValueNode node, Object value) { + config.set(node.getPath(), value); + } + + /** + * Sets the value of a node, if the validator is not null, it will be tested first. + * + * @param node The node to set the value of. + * @param value The value to set. + * @param The type of the node value. + */ + public void set(@NotNull TypedValueNode node, T value) { + config.set(node.getPath(), value); + } + + /** + * Gets the configuration object. + * + * @return The configuration object. + */ + public @NotNull CommentedConfiguration getConfig() { + return config; + } +} diff --git a/src/main/java/io/github/townyadvanced/commentedconfiguration/setting/SimpleNode.java b/src/main/java/io/github/townyadvanced/commentedconfiguration/setting/SimpleNode.java new file mode 100644 index 0000000..be14f0a --- /dev/null +++ b/src/main/java/io/github/townyadvanced/commentedconfiguration/setting/SimpleNode.java @@ -0,0 +1,139 @@ +package io.github.townyadvanced.commentedconfiguration.setting; + +import java.util.ArrayList; +import java.util.List; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Basic implementation of {@link TypedValueNode} with a builder. + * + * @param The type of the node value. + */ +public class SimpleNode implements TypedValueNode { + + /** + * A builder for {@link SimpleNode}. + * + * @param path The path of the node. + * @param type The class type of the node value. + * @return A new builder. + * @param The type of the node value. + */ + public static Builder builder(@NotNull String path, @NotNull Class type) { + return new Builder<>(path, type); + } + + private final String path; + private final Class type; + private final T defaultValue; + private final String[] comments; + + /** + * Creates a new node with the given path, type, default value, comment, and validator. + * + * @param path The path of the node. + * @param type The class type of the node value. + * @param defaultValue The default value of the node. + * @param comments The comments of the node. + */ + public SimpleNode(@NotNull String path, @NotNull Class type, @Nullable T defaultValue, @NotNull String[] comments) { + this.path = path; + this.type = type; + this.defaultValue = defaultValue; + this.comments = comments; + } + + /** + * {@inheritDoc} + */ + @Override + public @NotNull String getPath() { + return path; + } + + /** + * {@inheritDoc} + */ + @Override + public @NotNull Class getType() { + return type; + } + + /** + * {@inheritDoc} + */ + @Override + public @Nullable T getDefaultValue() { + return defaultValue; + } + + /** + * {@inheritDoc} + */ + @Override + public @NotNull String[] getComments() { + return comments; + } + + /** + * A builder for {@link SimpleNode}s. + * + * @param The type of the node value. + */ + public static class Builder { + private final String path; + private final Class type; + private T defaultValue; + private final List comments; + + /** + * Creates a new builder with the given path and type. + * + * @param path The path of the node. + * @param type The class type of the node value. + */ + protected Builder(@NotNull String path, @NotNull Class type) { + this.path = path; + this.type = type; + this.comments = new ArrayList<>(); + } + + /** + * Sets the default value of the node. + * + * @param defaultValue The default value of the node. + * @return This builder. + */ + public Builder defaultValue(@Nullable T defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + /** + * Adds a comment line to the node. + * Automatically adds a comment prefix "#" if the comment doesn't start with one. + * + * @param comment The comment to add. + * @return This builder. + */ + public Builder comment(@NotNull String comment) { + if (!comment.isEmpty() && !comment.startsWith("#")) { + // Automatically add a comment prefix if the comment doesn't start with one. + comment = "# " + comment; + } + this.comments.add(comment); + return this; + } + + /** + * Builds the node. + * + * @return The node. + */ + public SimpleNode build() { + return new SimpleNode<>(path, type, defaultValue, comments.toArray(new String[0])); + } + } +} diff --git a/src/main/java/io/github/townyadvanced/commentedconfiguration/setting/TypedValueNode.java b/src/main/java/io/github/townyadvanced/commentedconfiguration/setting/TypedValueNode.java new file mode 100644 index 0000000..e025575 --- /dev/null +++ b/src/main/java/io/github/townyadvanced/commentedconfiguration/setting/TypedValueNode.java @@ -0,0 +1,25 @@ +package io.github.townyadvanced.commentedconfiguration.setting; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Represents a node with specific type of value in a configuration file. + * + * @param The type of the node value. + */ +public interface TypedValueNode extends ValueNode { + /** + * Gets the class type {@link T} of the node value. + * + * @return The class type of the node value. + */ + @NotNull Class getType(); + + /** + * Gets the default value with type {@link T} of the node. + * + * @return The default value of the node. + */ + @Nullable T getDefaultValue(); +} diff --git a/src/main/java/io/github/townyadvanced/commentedconfiguration/setting/ValueNode.java b/src/main/java/io/github/townyadvanced/commentedconfiguration/setting/ValueNode.java new file mode 100644 index 0000000..5f83a0d --- /dev/null +++ b/src/main/java/io/github/townyadvanced/commentedconfiguration/setting/ValueNode.java @@ -0,0 +1,15 @@ +package io.github.townyadvanced.commentedconfiguration.setting; + +import org.jetbrains.annotations.Nullable; + +/** + * Represents a node that holds value in a configuration file. + */ +public interface ValueNode extends CommentedNode { + /** + * Gets the default value of the node. + * + * @return The default value of the node. + */ + @Nullable Object getDefaultValue(); +} diff --git a/src/test/java/io/github/townyadvanced/commentedconfiguration/TestNodes.java b/src/test/java/io/github/townyadvanced/commentedconfiguration/TestNodes.java new file mode 100644 index 0000000..61e6c37 --- /dev/null +++ b/src/test/java/io/github/townyadvanced/commentedconfiguration/TestNodes.java @@ -0,0 +1,53 @@ +package io.github.townyadvanced.commentedconfiguration; + +import java.util.ArrayList; +import java.util.List; + +import com.google.common.collect.Lists; +import io.github.townyadvanced.commentedconfiguration.setting.CommentedNode; +import io.github.townyadvanced.commentedconfiguration.setting.SimpleNode; +import io.github.townyadvanced.commentedconfiguration.setting.TypedValueNode; +import org.bukkit.Location; + +public class TestNodes { + private static final List nodes = new ArrayList<>(); + + private static TypedValueNode node(TypedValueNode node) { + nodes.add(node); + return node; + } + + private static final TypedValueNode HEADER_NODE = node(SimpleNode.builder("test", Object.class) + .comment("This config is for testing") + .comment("# It is not intended for use in production") + .build()); + + public static final TypedValueNode BOOLEAN_NODE = node(SimpleNode.builder("test.boolean", Boolean.class) + .defaultValue(false) + .comment("This is a boolean") + .build()); + + public static final TypedValueNode STRING_NODE = node(SimpleNode.builder("test.string", String.class) + .defaultValue("default") + .comment("This is a string") + .build()); + + public static final TypedValueNode INTEGER_NODE = node(SimpleNode.builder("test.integer", Integer.class) + .defaultValue(1234) + .comment("This is an integer") + .build()); + + public static final TypedValueNode LOCATION_NODE = node(SimpleNode.builder("test.location", Location.class) + .defaultValue(new Location(null, 0, 0, 0)) + .comment("This is a location") + .build()); + + public static final TypedValueNode LIST_NODE = node(SimpleNode.builder("test.nested.list", List.class) + .defaultValue(Lists.newArrayList("one", "two", "three")) + .comment("This is a list") + .build()); + + public static List getAllNodes() { + return nodes; + } +} diff --git a/src/test/java/io/github/townyadvanced/commentedconfiguration/TestTypedValueNodesSettings.java b/src/test/java/io/github/townyadvanced/commentedconfiguration/TestTypedValueNodesSettings.java new file mode 100644 index 0000000..f3553f5 --- /dev/null +++ b/src/test/java/io/github/townyadvanced/commentedconfiguration/TestTypedValueNodesSettings.java @@ -0,0 +1,123 @@ +package io.github.townyadvanced.commentedconfiguration; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; +import java.util.logging.Logger; + +import com.google.common.collect.Lists; +import io.github.townyadvanced.commentedconfiguration.setting.Settings; +import io.github.townyadvanced.commentedconfiguration.setting.TypedValueNode; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.bukkit.Location; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TestTypedValueNodesSettings { + private static final File testdir = new File("bin/test/"); + + private final File configFile = new File("bin/test/config.yml"); + private List ogConfigFile; + private List savedConfigFile; + private List editedConfigFile; + private Settings settings; + + @BeforeAll + public static void setUpAll() { + if (testdir.exists()) { + testdir.delete(); + } + testdir.mkdirs(); + } + + @AfterAll + public static void tearDownAll() { + if (testdir.exists()) { + testdir.delete(); + testdir.getParentFile().delete(); + } + } + + @BeforeEach + public void setUp() throws IOException { + if (configFile.exists()) { + configFile.delete(); + } + configFile.createNewFile(); + + ogConfigFile = IOUtils.readLines(Objects.requireNonNull(this.getClass().getResourceAsStream("/og_config.yml")), StandardCharsets.UTF_8); + savedConfigFile = IOUtils.readLines(Objects.requireNonNull(this.getClass().getResourceAsStream("/saved_config.yml")), StandardCharsets.UTF_8); + editedConfigFile = IOUtils.readLines(Objects.requireNonNull(this.getClass().getResourceAsStream("/edited_config.yml")), StandardCharsets.UTF_8); + + FileUtils.writeLines(configFile, ogConfigFile); + + settings = new Settings(configFile.toPath(), Logger.getLogger("TestTypedValueNodesSettings"), TestNodes.getAllNodes()); + assertTrue(settings.load()); + } + + @AfterEach + public void tearDown() { + if (configFile.exists()) { + configFile.delete(); + } + } + + @Test + @DisplayName("Save the configuration file with the comments and new values.") + public void saveFile() throws IOException { + settings.save(); + assertEquals(FileUtils.readLines(configFile, StandardCharsets.UTF_8), savedConfigFile); + } + + @Test + @DisplayName("Get nodes that were not set in the configuration file.") + public void getDefaultNodes() { + assertEquals(settings.get(TestNodes.INTEGER_NODE), 1234); + assertEquals(Lists.newArrayList("one", "two", "three"), settings.get(TestNodes.LIST_NODE)); + } + + @Test + @DisplayName("Get nodes that were set in the configuration file.") + public void getNodes() { + assertEquals(settings.get(TestNodes.STRING_NODE), "test"); + assertEquals(settings.get(TestNodes.BOOLEAN_NODE), true); + assertEquals(settings.get(TestNodes.LOCATION_NODE), new Location(null, 1.0, 1.0, 1.0)); + } + + @Test + @DisplayName("Set new values for nodes in the configuration file, and save the file.") + public void setNodes() throws IOException { + settings.set(TestNodes.STRING_NODE, "new string"); + settings.set(TestNodes.BOOLEAN_NODE, true); + settings.set(TestNodes.LOCATION_NODE, new Location(null, 2.0, 2.0, 2.0)); + settings.set(TestNodes.INTEGER_NODE, 4321); + settings.set(TestNodes.LIST_NODE, Lists.newArrayList("four", "five")); + + assertEquals(settings.get(TestNodes.STRING_NODE), "new string"); + assertEquals(settings.get(TestNodes.BOOLEAN_NODE), true); + assertEquals(settings.get(TestNodes.LOCATION_NODE), new Location(null, 2.0, 2.0, 2.0)); + assertEquals(settings.get(TestNodes.INTEGER_NODE), 4321); + assertEquals(Lists.newArrayList("four", "five"), settings.get(TestNodes.LIST_NODE)); + + settings.save(); + assertEquals(FileUtils.readLines(configFile, StandardCharsets.UTF_8), editedConfigFile); + } + + @Test + @DisplayName("Test Nullable and NonNull annotations") + public void instrumenter() { + assertThrows(IllegalArgumentException.class, () -> settings.get(null)); + assertThrows(IllegalArgumentException.class, () -> settings.set(null, "test")); + } +} diff --git a/src/test/resources/edited_config.yml b/src/test/resources/edited_config.yml new file mode 100644 index 0000000..36eb180 --- /dev/null +++ b/src/test/resources/edited_config.yml @@ -0,0 +1,22 @@ +# This config is for testing +# It is not intended for use in production +test: + # This is a boolean + boolean: true + # This is a string + string: new string + # This is a location + location: + ==: org.bukkit.Location + x: 2.0 + y: 2.0 + z: 2.0 + pitch: 0.0 + yaw: 0.0 + # This is an integer + integer: 4321 + nested: + # This is a list + list: + - four + - five diff --git a/src/test/resources/og_config.yml b/src/test/resources/og_config.yml new file mode 100644 index 0000000..e2e3cee --- /dev/null +++ b/src/test/resources/og_config.yml @@ -0,0 +1,10 @@ +test: + boolean: true + string: 'test' + location: + ==: org.bukkit.Location + x: 1.0 + y: 1.0 + z: 1.0 + pitch: 0.0 + yaw: 0.0 diff --git a/src/test/resources/saved_config.yml b/src/test/resources/saved_config.yml new file mode 100644 index 0000000..22f1560 --- /dev/null +++ b/src/test/resources/saved_config.yml @@ -0,0 +1,23 @@ +# This config is for testing +# It is not intended for use in production +test: + # This is a boolean + boolean: true + # This is a string + string: test + # This is a location + location: + ==: org.bukkit.Location + x: 1.0 + y: 1.0 + z: 1.0 + pitch: 0.0 + yaw: 0.0 + # This is an integer + integer: 1234 + nested: + # This is a list + list: + - one + - two + - three