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.commentedconfigurationCommentedConfiguration
- 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
+
+
+
+
+ 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.spigotmcspigot-api1.18.1-R0.1-SNAPSHOTprovided
+
+
+ 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