From 6d6fa7210ad0204aea98f8728ce09450c5fc5d86 Mon Sep 17 00:00:00 2001 From: Christian Koop Date: Wed, 27 Apr 2022 21:40:21 +0200 Subject: [PATCH] Replace Songoda's YAML Configuration wrapper with an own implementation Because Spigot 1.18 still hasn't fixed a critical bug like PaperMC did, I recoded the current YAML Configuration classes and access SnakeYaml directly instead of using the Spigot wrapper. This implementation approach also allows for adding node comments using the lib instead of some woodo string manipulation. #41 // I might move this into my own library in the future, lets see :p --- .../songoda/core/configuration/Comment.java | 114 --- .../songoda/core/configuration/Config.java | 793 ------------------ .../ConfigFileConfigurationAdapter.java | 272 ------ .../configuration/ConfigFormattingRules.java | 96 --- .../configuration/ConfigOptionsAdapter.java | 72 -- .../core/configuration/ConfigSection.java | 761 ----------------- .../core/configuration/ConfigSetting.java | 135 --- .../core/configuration/DataStoreObject.java | 30 - .../core/configuration/HeaderCommentable.java | 18 + .../core/configuration/IConfiguration.java | 98 +++ .../core/configuration/NodeCommentable.java | 16 + .../core/configuration/SimpleDataStore.java | 284 ------- .../yaml/YamlCommentRepresenter.java | 71 ++ .../configuration/yaml/YamlConfiguration.java | 336 ++++++++ .../yaml/YamlConfigurationTest.java | 591 +++++++++++++ 15 files changed, 1130 insertions(+), 2557 deletions(-) delete mode 100644 Core/src/main/java/com/songoda/core/configuration/Comment.java delete mode 100644 Core/src/main/java/com/songoda/core/configuration/Config.java delete mode 100644 Core/src/main/java/com/songoda/core/configuration/ConfigFileConfigurationAdapter.java delete mode 100644 Core/src/main/java/com/songoda/core/configuration/ConfigFormattingRules.java delete mode 100644 Core/src/main/java/com/songoda/core/configuration/ConfigOptionsAdapter.java delete mode 100644 Core/src/main/java/com/songoda/core/configuration/ConfigSection.java delete mode 100644 Core/src/main/java/com/songoda/core/configuration/ConfigSetting.java delete mode 100644 Core/src/main/java/com/songoda/core/configuration/DataStoreObject.java create mode 100644 Core/src/main/java/com/songoda/core/configuration/HeaderCommentable.java create mode 100644 Core/src/main/java/com/songoda/core/configuration/IConfiguration.java create mode 100644 Core/src/main/java/com/songoda/core/configuration/NodeCommentable.java delete mode 100644 Core/src/main/java/com/songoda/core/configuration/SimpleDataStore.java create mode 100644 Core/src/main/java/com/songoda/core/configuration/yaml/YamlCommentRepresenter.java create mode 100644 Core/src/main/java/com/songoda/core/configuration/yaml/YamlConfiguration.java create mode 100644 Core/src/test/java/com/songoda/core/configuration/yaml/YamlConfigurationTest.java diff --git a/Core/src/main/java/com/songoda/core/configuration/Comment.java b/Core/src/main/java/com/songoda/core/configuration/Comment.java deleted file mode 100644 index e04f2137..00000000 --- a/Core/src/main/java/com/songoda/core/configuration/Comment.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.songoda.core.configuration; - -import com.songoda.core.configuration.ConfigFormattingRules.CommentStyle; - -import java.io.IOException; -import java.io.Writer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; -import java.util.stream.Collectors; - -/** - * A comment for a configuration key - */ -public class Comment { - final List lines = new ArrayList<>(); - CommentStyle commentStyle = null; - - public Comment() { - } - - public Comment(String... lines) { - this(null, Arrays.asList(lines)); - } - - public Comment(List lines) { - this(null, lines); - } - - public Comment(CommentStyle commentStyle, String... lines) { - this(commentStyle, Arrays.asList(lines)); - } - - public Comment(CommentStyle commentStyle, List lines) { - this.commentStyle = commentStyle; - - if (lines != null) { - lines.forEach(s -> this.lines.addAll(Arrays.asList(s.split("\n")))); - } - } - - public CommentStyle getCommentStyle() { - return commentStyle; - } - - public void setCommentStyle(CommentStyle commentStyle) { - this.commentStyle = commentStyle; - } - - public List getLines() { - return lines; - } - - @Override - public String toString() { - return lines.isEmpty() ? "" : String.join("\n", lines); - } - - public static Comment loadComment(List lines) { - CommentStyle style = ConfigFormattingRules.parseStyle(lines); - - int linePad = (style.drawBorder ? 1 : 0) + (style.drawSpace ? 1 : 0); - int prefix = style.commentPrefix.length(); - int suffix = style.commentSuffix.length(); - - return new Comment(style, lines.subList(linePad, lines.size() - linePad).stream().map(s -> s.substring(prefix, s.length() - suffix).trim()).collect(Collectors.toList())); - } - - public void writeComment(Writer output, int offset, CommentStyle defaultStyle) throws IOException { - CommentStyle style = commentStyle != null ? commentStyle : defaultStyle; - int minSpacing = 0, borderSpacing = 0; - - // first draw the top of the comment - if (style.drawBorder) { - // grab the longest line in the list of lines - minSpacing = lines.stream().max(Comparator.comparingInt(String::length)).orElse("").length(); - borderSpacing = minSpacing + style.commentPrefix.length() + style.commentSuffix.length(); - - // draw the first line - output.write((new String(new char[offset])).replace('\0', ' ') + (new String(new char[borderSpacing + 2])).replace('\0', '#') + "\n"); - - if (style.drawSpace) { - output.write((new String(new char[offset])).replace('\0', ' ') - + "#" + style.spacePrefixTop - + (new String(new char[borderSpacing - style.spacePrefixTop.length() - style.spaceSuffixTop.length()])).replace('\0', style.spaceCharTop) - + style.spaceSuffixTop + "#\n"); - } - } else if (style.drawSpace) { - output.write((new String(new char[offset])).replace('\0', ' ') + "#\n"); - } - - // then the actual comment lines - for (String line : lines) { - // todo? should we auto-wrap comment lines that are longer than 80 characters? - output.write((new String(new char[offset])).replace('\0', ' ') + "#" + style.commentPrefix - + (minSpacing == 0 ? line : line + (new String(new char[minSpacing - line.length()])).replace('\0', ' ')) + style.commentSuffix + (style.drawBorder ? "#\n" : "\n")); - } - - // now draw the bottom of the comment border - if (style.drawBorder) { - if (style.drawSpace) { - output.write((new String(new char[offset])).replace('\0', ' ') - + "#" + style.spacePrefixBottom - + (new String(new char[borderSpacing - style.spacePrefixBottom.length() - style.spaceSuffixBottom.length()])).replace('\0', style.spaceCharBottom) - + style.spaceSuffixBottom + "#\n"); - } - - output.write((new String(new char[offset])).replace('\0', ' ') + (new String(new char[borderSpacing + 2])).replace('\0', '#') + "\n"); - } else if (style.drawSpace) { - output.write((new String(new char[offset])).replace('\0', ' ') + "#\n"); - } - } -} diff --git a/Core/src/main/java/com/songoda/core/configuration/Config.java b/Core/src/main/java/com/songoda/core/configuration/Config.java deleted file mode 100644 index 6445382d..00000000 --- a/Core/src/main/java/com/songoda/core/configuration/Config.java +++ /dev/null @@ -1,793 +0,0 @@ -package com.songoda.core.configuration; - -import com.songoda.core.utils.TextUtils; -import org.apache.commons.lang.Validate; -import org.bukkit.Bukkit; -import org.bukkit.configuration.InvalidConfigurationException; -import org.bukkit.configuration.file.YamlConstructor; -import org.bukkit.configuration.file.YamlRepresenter; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.yaml.snakeyaml.DumperOptions; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.error.YAMLException; -import org.yaml.snakeyaml.representer.Representer; - -import java.io.BufferedInputStream; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.io.Reader; -import java.io.StringReader; -import java.io.StringWriter; -import java.io.Writer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Timer; -import java.util.TimerTask; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** - * Configuration settings for a plugin - */ -public class Config extends ConfigSection { - /* - Serialization notes: - // implements ConfigurationSerializable: - //public Map serialize(); - - // Class must contain one of: - // public static Object deserialize(@NotNull Map args); - // public static valueOf(Map args); - // public new (Map args) - */ - protected static final String BLANK_CONFIG = "{}\n"; - - protected File file; - protected final ConfigFileConfigurationAdapter config = new ConfigFileConfigurationAdapter(this); - protected Comment headerComment = null; - protected Comment footerComment = null; - final String dirName, fileName; - final Plugin plugin; - final DumperOptions yamlOptions = new DumperOptions(); - final Representer yamlRepresenter = new YamlRepresenter(); - final Yaml yaml = new Yaml(new YamlConstructor(), yamlRepresenter, yamlOptions); - Charset defaultCharset = StandardCharsets.UTF_8; - SaveTask saveTask; - Timer autosaveTimer; - - ////////////// Config settings //////////////// - /** - * save file whenever a change is made - */ - boolean autosave = false; - - /** - * time in seconds to start a save after a change is made - */ - int autosaveInterval = 60; - - /** - * remove nodes not defined in defaults - */ - boolean autoremove = false; - - /** - * load comments when loading the file - */ - boolean loadComments = true; - - /** - * Default comment applied to config nodes - */ - ConfigFormattingRules.CommentStyle defaultNodeCommentFormat = ConfigFormattingRules.CommentStyle.SIMPLE; - - /** - * Default comment applied to section nodes - */ - ConfigFormattingRules.CommentStyle defaultSectionCommentFormat = ConfigFormattingRules.CommentStyle.SPACED; - - /** - * Extra lines to put between root nodes - */ - int rootNodeSpacing = 1; - - /** - * Extra lines to put in front of comments.
- * This is separate from rootNodeSpacing, if applicable. - */ - int commentSpacing = 1; - - public Config() { - this.plugin = null; - this.file = null; - - dirName = null; - fileName = null; - } - - public Config(@NotNull File file) { - this.plugin = null; - this.file = file.getAbsoluteFile(); - - dirName = null; - fileName = null; - } - - public Config(@NotNull Plugin plugin) { - this.plugin = plugin; - - dirName = null; - fileName = null; - } - - public Config(@NotNull Plugin plugin, @NotNull String file) { - this.plugin = plugin; - - dirName = null; - fileName = file; - } - - public Config(@NotNull Plugin plugin, @Nullable String directory, @NotNull String file) { - this.plugin = plugin; - - dirName = directory; - fileName = file; - } - - @NotNull - public ConfigFileConfigurationAdapter getFileConfig() { - return config; - } - - @NotNull - public File getFile() { - if (file == null) { - if (dirName != null) { - this.file = new File(plugin.getDataFolder() + dirName, fileName != null ? fileName : "config.yml"); - } else { - this.file = new File(plugin.getDataFolder(), fileName != null ? fileName : "config.yml"); - } - } - - return file; - } - - public Charset getDefaultCharset() { - return defaultCharset; - } - - /** - * Set the Charset that will be used to save this config - * - * @param defaultCharset Charset to use - * - * @return this class - */ - public Config setDefaultCharset(Charset defaultCharset) { - this.defaultCharset = defaultCharset; - return this; - } - - /** - * Set the default charset to use UTF-16 - * - * @return this class - */ - public Config setUseUTF16() { - this.defaultCharset = StandardCharsets.UTF_16; - return this; - } - - public boolean getLoadComments() { - return loadComments; - } - - /** - * Should comments from the config file be loaded when loading? - * - * @param loadComments set to false if you do not want to preserve node comments - */ - public void setLoadComments(boolean loadComments) { - this.loadComments = loadComments; - } - - public boolean getAutosave() { - return autosave; - } - - /** - * Should the configuration automatically save whenever it's been changed?
- * All saves are done asynchronously, so this should not impact server performance. - * - * @param autosave set to true if autosaving is enabled. - * - * @return this class - */ - @NotNull - public Config setAutosave(boolean autosave) { - this.autosave = autosave; - return this; - } - - public int getAutosaveInterval() { - return autosaveInterval; - } - - /** - * If autosave is enabled, this is the delay between a change and when the save is started.
- * If the configuration is changed within this period, the timer is not reset. - * - * @param autosaveInterval time in seconds - * - * @return this class - */ - @NotNull - public Config setAutosaveInterval(int autosaveInterval) { - this.autosaveInterval = autosaveInterval; - return this; - } - - public boolean getAutoremove() { - return autoremove; - } - - /** - * This setting is used to prevent users to from adding extraneous settings - * to the config and to remove deprecated settings.
- * If this is enabled, the config will delete any nodes that are not defined - * as a default setting. - * - * @param autoremove Remove settings that don't exist as defaults - * - * @return this class - */ - @NotNull - public Config setAutoremove(boolean autoremove) { - this.autoremove = autoremove; - return this; - } - - /** - * Default comment applied to config nodes - */ - @Nullable - public ConfigFormattingRules.CommentStyle getDefaultNodeCommentFormat() { - return defaultNodeCommentFormat; - } - - /** - * Default comment applied to config nodes - * - * @return this config - */ - @NotNull - public Config setDefaultNodeCommentFormat(@Nullable ConfigFormattingRules.CommentStyle defaultNodeCommentFormat) { - this.defaultNodeCommentFormat = defaultNodeCommentFormat; - return this; - } - - /** - * Default comment applied to section nodes - */ - @Nullable - public ConfigFormattingRules.CommentStyle getDefaultSectionCommentFormat() { - return defaultSectionCommentFormat; - } - - /** - * Default comment applied to section nodes - * - * @return this config - */ - @NotNull - public Config setDefaultSectionCommentFormat(@Nullable ConfigFormattingRules.CommentStyle defaultSectionCommentFormat) { - this.defaultSectionCommentFormat = defaultSectionCommentFormat; - return this; - } - - /** - * Extra lines to put between root nodes - */ - public int getRootNodeSpacing() { - return rootNodeSpacing; - } - - /** - * Extra lines to put between root nodes - * - * @return this config - */ - @NotNull - public Config setRootNodeSpacing(int rootNodeSpacing) { - this.rootNodeSpacing = rootNodeSpacing; - return this; - } - - /** - * Extra lines to put in front of comments.
- * This is separate from rootNodeSpacing, if applicable. - */ - public int getCommentSpacing() { - return commentSpacing; - } - - /** - * Extra lines to put in front of comments.
- * This is separate from rootNodeSpacing, if applicable. - * - * @return this config - */ - @NotNull - public Config setCommentSpacing(int commentSpacing) { - this.commentSpacing = commentSpacing; - return this; - } - - @NotNull - public Config setHeader(@NotNull String... description) { - if (description.length == 0) { - headerComment = null; - } else { - headerComment = new Comment(description); - } - - return this; - } - - @NotNull - public Config setHeader(@Nullable ConfigFormattingRules.CommentStyle commentStyle, @NotNull String... description) { - if (description.length == 0) { - headerComment = null; - } else { - headerComment = new Comment(commentStyle, description); - } - - return this; - } - - @NotNull - public Config setHeader(@Nullable List description) { - if (description == null || description.isEmpty()) { - headerComment = null; - } else { - headerComment = new Comment(description); - } - - return this; - } - - @NotNull - public Config setHeader(@Nullable ConfigFormattingRules.CommentStyle commentStyle, @Nullable List description) { - if (description == null || description.isEmpty()) { - headerComment = null; - } else { - headerComment = new Comment(commentStyle, description); - } - - return this; - } - - @NotNull - public List getHeader() { - if (headerComment != null) { - return headerComment.getLines(); - } - - return Collections.emptyList(); - } - - public Config clearConfig(boolean clearDefaults) { - root.values.clear(); - root.configComments.clear(); - - if (clearDefaults) { - root.defaultComments.clear(); - root.defaults.clear(); - } - - return this; - } - - public Config clearDefaults() { - root.defaultComments.clear(); - root.defaults.clear(); - - return this; - } - - public boolean load() { - return load(getFile()); - } - - public boolean load(@NotNull File file) { - Validate.notNull(file, "File cannot be null"); - if (file.exists()) { - try (BufferedInputStream stream = new BufferedInputStream(new FileInputStream(file))) { - Charset charset = TextUtils.detectCharset(stream, StandardCharsets.UTF_8); - - // upgrade charset if file was saved in a more complex format - if (charset == StandardCharsets.UTF_16BE || charset == StandardCharsets.UTF_16LE) { - defaultCharset = StandardCharsets.UTF_16; - } - - this.load(new InputStreamReader(stream, charset)); - - return true; - } catch (IOException | InvalidConfigurationException ex) { - (plugin != null ? plugin.getLogger() : Bukkit.getLogger()).log(Level.SEVERE, "Failed to load config file: " + file.getName(), ex); - } - - return false; - } - - return true; - } - - public void load(@NotNull Reader reader) throws IOException, InvalidConfigurationException { - StringBuilder builder = new StringBuilder(); - - try (BufferedReader input = reader instanceof BufferedReader ? (BufferedReader) reader : new BufferedReader(reader)) { - String line; - boolean firstLine = true; - while ((line = input.readLine()) != null) { - if (firstLine) { - line = line.replaceAll("[\uFEFF\uFFFE\u200B]", ""); // clear BOM markers - firstLine = false; - } - builder.append(line).append('\n'); - } - } - - this.loadFromString(builder.toString()); - } - - public void loadFromString(@NotNull String contents) throws InvalidConfigurationException { - Map input; - - try { - input = this.yaml.load(contents); - } catch (YAMLException e2) { - throw new InvalidConfigurationException(e2); - } catch (ClassCastException e3) { - throw new InvalidConfigurationException("Top level is not a Map."); - } - - if (input != null) { - if (loadComments) { - this.parseComments(contents, input); - } - - this.convertMapsToSections(input, this); - } - } - - protected void convertMapsToSections(@NotNull Map input, @NotNull ConfigSection section) { - // TODO: make this non-recursive - for (Map.Entry entry : input.entrySet()) { - String key = entry.getKey().toString(); - Object value = entry.getValue(); - - if (value instanceof Map) { - this.convertMapsToSections((Map) value, section.createSection(key)); - continue; - } - - section.set(key, value); - } - } - - protected void parseComments(@NotNull String contents, @NotNull Map input) { - // if starts with a comment, load all nonbreaking comments as a header - // then load all comments and assign to the next valid node loaded - // (Only load comments that are on their own line) - - BufferedReader in = new BufferedReader(new StringReader(contents)); - String line; - boolean insideScalar = false; - boolean firstNode = true; - int index = 0; - LinkedList currentPath = new LinkedList<>(); - ArrayList commentBlock = new ArrayList<>(); - - try { - while ((line = in.readLine()) != null) { - if (line.isEmpty()) { - if (firstNode && !commentBlock.isEmpty()) { - // header comment - firstNode = false; - headerComment = Comment.loadComment(commentBlock); - commentBlock.clear(); - } - continue; - } else if (line.trim().startsWith("#")) { - // only load full-line comments - commentBlock.add(line.trim()); - continue; - } - - // check to see if this is a line that we can process - int lineOffset = getOffset(line); - insideScalar &= lineOffset <= index; - Matcher m; - if (!insideScalar && (m = yamlNode.matcher(line)).find()) { - // we found a config node! ^.^ - // check to see what the full path is - int depth = (m.group(1).length() / indentation); - while (depth < currentPath.size()) { - currentPath.removeLast(); - } - currentPath.add(m.group(2)); - - // do we have a comment for this node? - if (!commentBlock.isEmpty()) { - String path = currentPath.stream().collect(Collectors.joining(String.valueOf(pathChar))); - Comment comment = Comment.loadComment(commentBlock); - commentBlock.clear(); - setComment(path, comment); - } - - firstNode = false; // we're no longer on the first node - - // ignore scalars - index = lineOffset; - if (m.group(3).trim().equals("|") || m.group(3).trim().equals(">")) { - insideScalar = true; - } - } - } - - if (!commentBlock.isEmpty()) { - footerComment = Comment.loadComment(commentBlock); - commentBlock.clear(); - } - } catch (IOException ex) { - Logger.getLogger(Config.class.getName()).log(Level.SEVERE, "Error parsing config comment", ex); - } - } - - public void deleteNonDefaultSettings() { - // Delete old config values (thread-safe) - List defaultKeys = Arrays.asList(defaults.keySet().toArray(new String[0])); - - for (String key : values.keySet().toArray(new String[0])) { - if (!defaultKeys.contains(key)) { - values.remove(key); - } - } - } - - @Override - protected void onChange() { - if (autosave) { - delaySave(); - } - } - - public void delaySave() { - // save async even if no plugin or if plugin disabled - if (saveTask == null && (changed || hasNewDefaults())) { - autosaveTimer = new Timer((plugin != null ? plugin.getName() + "-ConfigSave-" : "ConfigSave-") + getFile().getName()); - autosaveTimer.schedule(saveTask = new SaveTask(), autosaveInterval * 1000L); - } - } - - public boolean saveChanges() { - boolean saved = true; - - if (changed || hasNewDefaults()) { - saved = save(); - } - - if (saveTask != null) { - //Close Threads - saveTask.cancel(); - autosaveTimer.cancel(); - saveTask = null; - autosaveTimer = null; - } - - return saved; - } - - boolean hasNewDefaults() { - if (file != null && !file.exists()) return true; - - for (String def : defaults.keySet()) { - if (!values.containsKey(def)) { - return true; - } - } - - return false; - } - - public boolean save() { - if (saveTask != null) { - //Close Threads - saveTask.cancel(); - autosaveTimer.cancel(); - saveTask = null; - autosaveTimer = null; - } - - return save(getFile()); - } - - public boolean save(@NotNull String file) { - Validate.notNull(file, "File cannot be null"); - return this.save(new File(file)); - } - - public boolean save(@NotNull File file) { - Validate.notNull(file, "File cannot be null"); - - if (file.getParentFile() != null && !file.getParentFile().exists()) { - file.getParentFile().mkdirs(); - } - - String data = this.saveToString(); - try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file), defaultCharset)) { - writer.write(data); - } catch (IOException ex) { - return false; - } - - return true; - } - - @NotNull - public String saveToString() { - try { - if (autoremove) { - deleteNonDefaultSettings(); - } - - yamlOptions.setIndent(indentation); - yamlOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); - yamlOptions.setSplitLines(false); - yamlRepresenter.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); - - StringWriter str = new StringWriter(); - - if (headerComment != null) { - headerComment.writeComment(str, 0, ConfigFormattingRules.CommentStyle.BLOCKED); - str.write("\n"); // add one space after the header - } - - String dump = yaml.dump(this.getValues(false)); - if (!dump.equals(BLANK_CONFIG)) { - writeComments(dump, str); - } - - if (footerComment != null) { - str.write("\n"); - footerComment.writeComment(str, 0, ConfigFormattingRules.CommentStyle.BLOCKED); - } - - return str.toString(); - } catch (Throwable ex) { - Logger.getLogger(Config.class.getName()).log(Level.SEVERE, "Error saving config", ex); - delaySave(); - } - - return ""; - } - - protected final Pattern yamlNode = Pattern.compile("^( *)([^:{}\\[\\],&*#?|\\-<>=!%@`]+):(.*)$"); - - protected void writeComments(String data, Writer out) throws IOException { - // line-by-line apply line spacing formatting and comments per-node - BufferedReader in = new BufferedReader(new StringReader(data)); - - String line; - boolean insideScalar = false; - boolean firstNode = true; - int index = 0; - - LinkedList currentPath = new LinkedList<>(); - while ((line = in.readLine()) != null) { - // ignore comments and empty lines (there shouldn't be any, but just in case) - if (line.trim().startsWith("#") || line.isEmpty()) { - continue; - } - - // check to see if this is a line that we can process - int lineOffset = getOffset(line); - insideScalar &= lineOffset <= index; - Matcher m; - if (!insideScalar && (m = yamlNode.matcher(line)).find()) { - // we found a config node! ^.^ - // check to see what the full path is - int depth = (m.group(1).length() / indentation); - while (depth < currentPath.size()) { - currentPath.removeLast(); - } - - currentPath.add(m.group(2)); - String path = currentPath.stream().collect(Collectors.joining(String.valueOf(pathChar))); - - // if this is a root-level node, apply extra spacing if we aren't the first node - if (!firstNode && depth == 0 && rootNodeSpacing > 0) { - out.write((new String(new char[rootNodeSpacing])).replace("\0", "\n")); // yes it's silly, but it works :> - } - firstNode = false; // we're no longer on the first node - - // insert the relavant comment - Comment comment = getComment(path); - if (comment != null) { - // add spacing between previous nodes and comments - if (depth != 0) { - out.write((new String(new char[commentSpacing])).replace("\0", "\n")); - } - - // formatting style for this node - ConfigFormattingRules.CommentStyle style = comment.getCommentStyle(); - if (style == null) { - // check to see what type of node this is - if (!m.group(3).trim().isEmpty()) { - // setting node - style = defaultNodeCommentFormat; - } else { - // probably a section? (need to peek ahead to check if this is a list) - in.mark(1000); - String nextLine = in.readLine().trim(); - in.reset(); - if (nextLine.startsWith("-")) { - // not a section :P - style = defaultNodeCommentFormat; - } else { - style = defaultSectionCommentFormat; - } - } - } - - // write it down! - comment.writeComment(out, lineOffset, style); - } - - // ignore scalars - index = lineOffset; - if (m.group(3).trim().equals("|") || m.group(3).trim().equals(">")) { - insideScalar = true; - } - } - - out.write(line); - out.write("\n"); - } - } - - protected static int getOffset(String s) { - char[] chars = s.toCharArray(); - for (int i = 0; i < chars.length; ++i) { - if (chars[i] != ' ') { - return i; - } - } - - return -1; - } - - class SaveTask extends TimerTask { - @Override - public void run() { - saveChanges(); - } - } -} diff --git a/Core/src/main/java/com/songoda/core/configuration/ConfigFileConfigurationAdapter.java b/Core/src/main/java/com/songoda/core/configuration/ConfigFileConfigurationAdapter.java deleted file mode 100644 index d05dedfe..00000000 --- a/Core/src/main/java/com/songoda/core/configuration/ConfigFileConfigurationAdapter.java +++ /dev/null @@ -1,272 +0,0 @@ -package com.songoda.core.configuration; - -import com.songoda.core.compatibility.CompatibleMaterial; -import org.bukkit.configuration.Configuration; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.InvalidConfigurationException; -import org.bukkit.configuration.file.FileConfiguration; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; -import java.util.Map; -import java.util.Set; - -public class ConfigFileConfigurationAdapter extends FileConfiguration { - final Config config; - - public ConfigFileConfigurationAdapter(Config config) { - super(config); - - this.config = config; - } - - public Config getCoreConfig() { - return config; - } - - @Override - public String saveToString() { - return config.saveToString(); - } - - @Override - public void loadFromString(String string) throws InvalidConfigurationException { - config.loadFromString(string); - } - - @Override - protected String buildHeader() { - return "#" + String.join("\n#", config.getHeader()); - } - - @Override - public ConfigOptionsAdapter options() { - return new ConfigOptionsAdapter(config); - } - - @Override - public Set getKeys(boolean deep) { - return config.getKeys(deep); - } - - @Override - public Map getValues(boolean deep) { - return config.getValues(deep); - } - - @Override - public boolean contains(String path) { - return config.contains(path); - } - - @Override - public boolean isSet(String path) { - return config.isSet(path); - } - - @Override - public String getCurrentPath() { - return config.getCurrentPath(); - } - - @Override - public String getName() { - return config.getName(); - } - - @Override - public Configuration getRoot() { - return config; - } - - @Override - public ConfigurationSection getParent() { - return null; - } - - @Override - public void addDefault(String path, Object value) { - config.addDefault(path, value); - } - - @Override - public ConfigurationSection getDefaultSection() { - return config.getDefaultSection(); - } - - @Override - public void set(String path, Object value) { - config.set(path, value); - } - - @Override - public Object get(String path) { - return config.get(path); - } - - @Override - public Object get(String path, Object def) { - return config.get(path, def); - } - - @Override - public ConfigurationSection createSection(String path) { - return config.createSection(path); - } - - @Override - public ConfigurationSection createSection(String path, Map map) { - return config.createSection(path, map); - } - - // Other non-FileConfiguration methods - - @NotNull - public ConfigSection createDefaultSection(@NotNull String path) { - return config.createDefaultSection(path); - } - - @NotNull - public ConfigSection createDefaultSection(@NotNull String path, String... comment) { - return config.createDefaultSection(path, comment); - } - - @NotNull - public ConfigSection createDefaultSection(@NotNull String path, ConfigFormattingRules.CommentStyle commentStyle, String... comment) { - return config.createDefaultSection(path, commentStyle, comment); - } - - @NotNull - public ConfigSection setComment(@NotNull String path, @Nullable ConfigFormattingRules.CommentStyle commentStyle, String... lines) { - return config.setComment(path, commentStyle, lines); - } - - @NotNull - public ConfigSection setComment(@NotNull String path, @Nullable ConfigFormattingRules.CommentStyle commentStyle, @Nullable List lines) { - return config.setComment(path, commentStyle, lines); - } - - @NotNull - public ConfigSection setDefaultComment(@NotNull String path, String... lines) { - return config.setDefaultComment(path, lines); - } - - @NotNull - public ConfigSection setDefaultComment(@NotNull String path, @Nullable List lines) { - return config.setDefaultComment(path, lines); - } - - @NotNull - public ConfigSection setDefaultComment(@NotNull String path, ConfigFormattingRules.CommentStyle commentStyle, String... lines) { - return config.setDefaultComment(path, commentStyle, lines); - } - - @NotNull - public ConfigSection setDefaultComment(@NotNull String path, ConfigFormattingRules.CommentStyle commentStyle, @Nullable List lines) { - return config.setDefaultComment(path, commentStyle, lines); - } - - @Nullable - public Comment getComment(@NotNull String path) { - return config.getComment(path); - } - - @Nullable - public String getCommentString(@NotNull String path) { - return config.getCommentString(path); - } - - @NotNull - public List getSections(String path) { - return config.getSections(path); - } - - @NotNull - public ConfigSection set(@NotNull String path, @Nullable Object value, String... comment) { - return config.set(path, value, comment); - } - - @NotNull - public ConfigSection set(@NotNull String path, @Nullable Object value, List comment) { - return config.set(path, value, comment); - } - - @NotNull - public ConfigSection set(@NotNull String path, @Nullable Object value, @Nullable ConfigFormattingRules.CommentStyle commentStyle, String... comment) { - return config.set(path, value, commentStyle, comment); - } - - @NotNull - public ConfigSection set(@NotNull String path, @Nullable Object value, @Nullable ConfigFormattingRules.CommentStyle commentStyle, List comment) { - return config.set(path, value, commentStyle, comment); - } - - @NotNull - public ConfigSection setDefault(@NotNull String path, @Nullable Object value) { - return config.setDefault(path, value); - } - - @NotNull - public ConfigSection setDefault(@NotNull String path, @Nullable Object value, String... comment) { - return config.setDefault(path, value, comment); - } - - @NotNull - public ConfigSection setDefault(@NotNull String path, @Nullable Object value, List comment) { - return config.setDefault(path, value, comment); - } - - @NotNull - public ConfigSection setDefault(@NotNull String path, @Nullable Object value, ConfigFormattingRules.CommentStyle commentStyle, String... comment) { - return config.setDefault(path, value, commentStyle, comment); - } - - @NotNull - public ConfigSection setDefault(@NotNull String path, @Nullable Object value, ConfigFormattingRules.CommentStyle commentStyle, List comment) { - return config.setDefault(path, value, commentStyle, comment); - } - - @NotNull - public ConfigSection createSection(@NotNull String path, String... comment) { - return config.createSection(path, comment); - } - - @NotNull - public ConfigSection createSection(@NotNull String path, @Nullable List comment) { - return config.createSection(path, comment); - } - - @NotNull - public ConfigSection createSection(@NotNull String path, @Nullable ConfigFormattingRules.CommentStyle commentStyle, String... comment) { - return config.createSection(path, commentStyle, comment); - } - - @NotNull - public ConfigSection createSection(@NotNull String path, @Nullable ConfigFormattingRules.CommentStyle commentStyle, @Nullable List comment) { - return config.createSection(path, commentStyle, comment); - } - - public char getChar(@NotNull String path) { - return config.getChar(path); - } - - public char getChar(@NotNull String path, char def) { - return config.getChar(path, def); - } - - @Nullable - public CompatibleMaterial getMaterial(@NotNull String path) { - return config.getMaterial(path); - } - - @Nullable - public CompatibleMaterial getMaterial(@NotNull String path, @Nullable CompatibleMaterial def) { - return config.getMaterial(path, def); - } - - @NotNull - public ConfigSection getOrCreateConfigurationSection(@NotNull String path) { - return config.getOrCreateConfigurationSection(path); - } -} diff --git a/Core/src/main/java/com/songoda/core/configuration/ConfigFormattingRules.java b/Core/src/main/java/com/songoda/core/configuration/ConfigFormattingRules.java deleted file mode 100644 index 8e279925..00000000 --- a/Core/src/main/java/com/songoda/core/configuration/ConfigFormattingRules.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.songoda.core.configuration; - -import java.util.List; - -public class ConfigFormattingRules { - int spacesBetweenMainCategories; - int spacesBetweenValues; - CommentStyle rootCommentStyle = CommentStyle.BLOCKSPACED; - CommentStyle mainCategoryCommentStyle = CommentStyle.SPACED; - - public enum CommentStyle { - /** - * # Comment - */ - SIMPLE(false, false, " ", ""), - /** - * #
- * # Comment
- * #
- */ - SPACED(false, true, " ", ""), - /** - * ###########
- * # Comment #
- * ###########
- */ - BLOCKED(true, false, " ", " "), - /** - * #############
- * #|¯¯¯¯¯¯¯¯¯|#
- * #| Comment |#
- * #|_________|#
- * #############
- */ - BLOCKSPACED(true, true, "|\u00AF", '\u00AF', "\u00AF|", "| ", " |", "|_", '_', "_|"); - - final boolean drawBorder, drawSpace; - final String commentPrefix, spacePrefixTop, spacePrefixBottom; - final String commentSuffix, spaceSuffixTop, spaceSuffixBottom; - final char spaceCharTop, spaceCharBottom; - - CommentStyle(boolean drawBorder, boolean drawSpace, - String spacePrefixTop, char spaceCharTop, String spaceSuffixTop, - String commentPrefix, String commentSuffix, - String spacePrefixBottom, char spaceCharBottom, String spaceSuffixBottom) { - this.drawBorder = drawBorder; - this.drawSpace = drawSpace; - this.commentPrefix = commentPrefix; - this.spacePrefixTop = spacePrefixTop; - this.spacePrefixBottom = spacePrefixBottom; - this.commentSuffix = commentSuffix; - this.spaceSuffixTop = spaceSuffixTop; - this.spaceSuffixBottom = spaceSuffixBottom; - this.spaceCharTop = spaceCharTop; - this.spaceCharBottom = spaceCharBottom; - } - - CommentStyle(boolean drawBorder, boolean drawSpace, String commentPrefix, String commentSuffix) { - this.drawBorder = drawBorder; - this.drawSpace = drawSpace; - this.commentPrefix = commentPrefix; - this.commentSuffix = commentSuffix; - this.spacePrefixTop = this.spacePrefixBottom = ""; - this.spaceCharTop = this.spaceCharBottom = ' '; - this.spaceSuffixTop = this.spaceSuffixBottom = ""; - } - } - - public static CommentStyle parseStyle(List lines) { - if (lines == null || lines.size() <= 2) { - return CommentStyle.SIMPLE; - } - - if (lines.get(0).trim().equals("#") && lines.get(lines.size() - 1).trim().equals("#")) { - return CommentStyle.SPACED; - } - - boolean hasBorders = lines.get(0).trim().matches("^##+$") && lines.get(lines.size() - 1).trim().matches("^##+$"); - if (!hasBorders) { - // default return - return CommentStyle.SIMPLE; - } - - // now need to figure out if this is blocked or not - if (lines.size() > 4 && lines.get(1).trim().matches(("^#" - + CommentStyle.BLOCKSPACED.spacePrefixTop + CommentStyle.BLOCKSPACED.spaceCharTop + "+" - + CommentStyle.BLOCKSPACED.spaceSuffixTop + "#$").replace("|", "\\|")) - && lines.get(1).trim().matches(("^#" - + CommentStyle.BLOCKSPACED.spacePrefixTop + CommentStyle.BLOCKSPACED.spaceCharTop + "+" - + CommentStyle.BLOCKSPACED.spaceSuffixTop + "#$").replace("|", "\\|"))) { - return CommentStyle.BLOCKSPACED; - } - - return CommentStyle.BLOCKED; - } -} diff --git a/Core/src/main/java/com/songoda/core/configuration/ConfigOptionsAdapter.java b/Core/src/main/java/com/songoda/core/configuration/ConfigOptionsAdapter.java deleted file mode 100644 index f8df4b79..00000000 --- a/Core/src/main/java/com/songoda/core/configuration/ConfigOptionsAdapter.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.songoda.core.configuration; - -import org.bukkit.configuration.file.FileConfigurationOptions; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -public class ConfigOptionsAdapter extends FileConfigurationOptions { - final ConfigSection config; - - public ConfigOptionsAdapter(ConfigSection config) { - super(config); - this.config = config; - } - - public Config getConfig() { - return (Config) config.root; - } - - @NotNull - @Override - public ConfigFileConfigurationAdapter configuration() { - return new ConfigFileConfigurationAdapter((Config) config.root); - } - - @NotNull - @Override - public ConfigOptionsAdapter copyDefaults(boolean value) { - // we always copy new values - return this; - } - - @NotNull - @Override - public ConfigOptionsAdapter pathSeparator(char value) { - (config.root).setPathSeparator(value); - return this; - } - - @NotNull - @Override - public ConfigOptionsAdapter header(@Nullable String value) { - if (value == null) { - ((Config) config.root).setHeader((List) null); - } else { - ((Config) config.root).setHeader(value.split("\n")); - } - - return this; - } - - @NotNull - @Override - public ConfigOptionsAdapter copyHeader(boolean value) { - if (!value) { - ((Config) config.root).setHeader((List) null); - } - - return this; - } - - public int indent() { - return config.root.getIndent(); - } - - @NotNull - public ConfigOptionsAdapter indent(int value) { - config.root.setIndent(value); - return this; - } -} diff --git a/Core/src/main/java/com/songoda/core/configuration/ConfigSection.java b/Core/src/main/java/com/songoda/core/configuration/ConfigSection.java deleted file mode 100644 index 25d37fb0..00000000 --- a/Core/src/main/java/com/songoda/core/configuration/ConfigSection.java +++ /dev/null @@ -1,761 +0,0 @@ -package com.songoda.core.configuration; - -import com.songoda.core.compatibility.CompatibleMaterial; -import org.bukkit.configuration.Configuration; -import org.bukkit.configuration.MemoryConfiguration; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** - * Configuration for a specific node - */ -public class ConfigSection extends MemoryConfiguration { - final String fullPath, nodeKey; - final ConfigSection root; - final ConfigSection parent; - protected int indentation = 2; // between 2 and 9 (inclusive) - protected char pathChar = '.'; - final HashMap configComments; - final HashMap defaultComments; - final LinkedHashMap defaults; - final LinkedHashMap values; - /** - * Internal root state: if any configuration value has changed from file state - */ - boolean changed = false; - final boolean isDefault; - final Object lock = new Object(); - - ConfigSection() { - this.root = this; - this.parent = null; - isDefault = false; - nodeKey = fullPath = ""; - - configComments = new HashMap<>(); - defaultComments = new HashMap<>(); - defaults = new LinkedHashMap<>(); - values = new LinkedHashMap<>(); - } - - ConfigSection(ConfigSection root, ConfigSection parent, String nodeKey, boolean isDefault) { - this.root = root; - this.parent = parent; - this.nodeKey = nodeKey; - this.fullPath = nodeKey != null ? parent.fullPath + nodeKey + root.pathChar : parent.fullPath; - this.isDefault = isDefault; - configComments = defaultComments = null; - defaults = null; - values = null; - } - - public int getIndent() { - return root.indentation; - } - - public void setIndent(int indentation) { - root.indentation = indentation; - } - - protected void onChange() { - if (parent != null) { - root.onChange(); - } - } - - /** - * Sets the character used to separate configuration nodes.
- * IMPORTANT: Do not change this after loading or adding ConfigurationSections! - * - * @param pathChar character to use - */ - public void setPathSeparator(char pathChar) { - if (!root.values.isEmpty() || !root.defaults.isEmpty()) { - throw new RuntimeException("Path change after config initialization"); - } - - root.pathChar = pathChar; - } - - public char getPathSeparator() { - return root.pathChar; - } - - /** - * @return The full key for this section node - */ - public String getKey() { - return !fullPath.endsWith(String.valueOf(root.pathChar)) ? fullPath : fullPath.substring(0, fullPath.length() - 1); - } - - /** - * @return The specific key that was used from the last node to get to this node - */ - public String getNodeKey() { - return nodeKey; - } - - /** - * Create the path required for this node to exist.
- * DO NOT USE THIS IN A SYNCHRONIZED LOCK - * - * @param path full path of the node required. Eg, for foo.bar.node, this will create sections for foo and foo.bar - * @param useDefault set to true if this is a default value - */ - protected void createNodePath(@NotNull String path, boolean useDefault) { - if (path.indexOf(root.pathChar) != -1) { - // if any intermediate nodes don't exist, create them - String[] pathParts = path.split(Pattern.quote(String.valueOf(root.pathChar))); - StringBuilder nodePath = new StringBuilder(fullPath); - LinkedHashMap writeTo = useDefault ? root.defaults : root.values; - ConfigSection travelNode = this; - - synchronized (root.lock) { - for (int i = 0; i < pathParts.length - 1; ++i) { - final String node = (i != 0 ? nodePath.append(root.pathChar) : nodePath).append(pathParts[i]).toString(); - - if (!(writeTo.get(node) instanceof ConfigSection)) { - writeTo.put(node, travelNode = new ConfigSection(root, travelNode, pathParts[i], useDefault)); - } else { - travelNode = (ConfigSection) writeTo.get(node); - } - } - } - } - } - - @NotNull - public ConfigSection createDefaultSection(@NotNull String path) { - createNodePath(path, true); - ConfigSection section = new ConfigSection(root, this, path, true); - - synchronized (root.lock) { - root.defaults.put(fullPath + path, section); - } - - return section; - } - - @NotNull - public ConfigSection createDefaultSection(@NotNull String path, String... comment) { - createNodePath(path, true); - ConfigSection section = new ConfigSection(root, this, path, true); - - synchronized (root.lock) { - root.defaults.put(fullPath + path, section); - root.defaultComments.put(fullPath + path, new Comment(comment)); - } - - return section; - } - - @NotNull - public ConfigSection createDefaultSection(@NotNull String path, ConfigFormattingRules.CommentStyle commentStyle, String... comment) { - createNodePath(path, true); - ConfigSection section = new ConfigSection(root, this, path, true); - - synchronized (root.lock) { - root.defaults.put(fullPath + path, section); - root.defaultComments.put(fullPath + path, new Comment(commentStyle, comment)); - } - - return section; - } - - @NotNull - public ConfigSection setComment(@NotNull String path, @Nullable ConfigFormattingRules.CommentStyle commentStyle, String... lines) { - return setComment(path, lines != null ? new Comment(commentStyle, lines) : null); - } - - @NotNull - public ConfigSection setComment(@NotNull String path, @Nullable ConfigFormattingRules.CommentStyle commentStyle, @Nullable List lines) { - return setComment(path, lines != null ? new Comment(commentStyle, lines) : null); - } - - @NotNull - public ConfigSection setComment(@NotNull String path, @Nullable Comment comment) { - synchronized (root.lock) { - if (isDefault) { - root.defaultComments.put(fullPath + path, comment); - } else { - root.configComments.put(fullPath + path, comment); - } - } - - return this; - } - - @NotNull - public ConfigSection setDefaultComment(@NotNull String path, String... lines) { - return setDefaultComment(path, lines.length == 0 ? null : Arrays.asList(lines)); - } - - @NotNull - public ConfigSection setDefaultComment(@NotNull String path, @Nullable List lines) { - synchronized (root.lock) { - root.defaultComments.put(fullPath + path, new Comment(lines)); - } - - return this; - } - - @NotNull - public ConfigSection setDefaultComment(@NotNull String path, ConfigFormattingRules.CommentStyle commentStyle, String... lines) { - return setDefaultComment(path, commentStyle, lines.length == 0 ? null : Arrays.asList(lines)); - } - - @NotNull - public ConfigSection setDefaultComment(@NotNull String path, ConfigFormattingRules.CommentStyle commentStyle, @Nullable List lines) { - synchronized (root.lock) { - root.defaultComments.put(fullPath + path, new Comment(commentStyle, lines)); - } - - return this; - } - - @NotNull - public ConfigSection setDefaultComment(@NotNull String path, @Nullable Comment comment) { - synchronized (root.lock) { - root.defaultComments.put(fullPath + path, comment); - } - - return this; - } - - @Nullable - public Comment getComment(@NotNull String path) { - Comment result = root.configComments.get(fullPath + path); - - if (result == null) { - result = root.defaultComments.get(fullPath + path); - } - - return result; - } - - @Nullable - public String getCommentString(@NotNull String path) { - Comment result = root.configComments.get(fullPath + path); - - if (result == null) { - result = root.defaultComments.get(fullPath + path); - } - - return result != null ? result.toString() : null; - } - - @Override - public void addDefault(@NotNull String path, @Nullable Object value) { - createNodePath(path, true); - - synchronized (root.lock) { - root.defaults.put(fullPath + path, value); - } - } - - @Override - public void addDefaults(@NotNull Map defaults) { - //defaults.entrySet().stream().forEach(m -> root.defaults.put(fullPath + m.getKey(), m.getValue())); - defaults.entrySet().forEach(m -> addDefault(m.getKey(), m.getValue())); - } - - @Override - public void setDefaults(Configuration c) { - if (fullPath.isEmpty()) { - root.defaults.clear(); - } else { - root.defaults.keySet().stream() - .filter(k -> k.startsWith(fullPath)) - .forEach(root.defaults::remove); - } - - addDefaults(c); - } - - @Override - public ConfigSection getDefaults() { - return new ConfigSection(root, this, null, true); - } - - @Override - public ConfigSection getDefaultSection() { - return new ConfigSection(root, this, null, true); - } - - @Override - public ConfigOptionsAdapter options() { - return new ConfigOptionsAdapter(root); - } - - @NotNull - @Override - public Set getKeys(boolean deep) { - LinkedHashSet result = new LinkedHashSet<>(); - int pathIndex = fullPath.lastIndexOf(root.pathChar); - - if (deep) { - result.addAll(root.defaults.keySet().stream() - .filter(k -> k.startsWith(fullPath)) - .map(k -> !k.endsWith(String.valueOf(root.pathChar)) ? k.substring(pathIndex + 1) : k.substring(pathIndex + 1, k.length() - 1)) - .collect(Collectors.toCollection(LinkedHashSet::new))); - result.addAll(root.values.keySet().stream() - .filter(k -> k.startsWith(fullPath)) - .map(k -> !k.endsWith(String.valueOf(root.pathChar)) ? k.substring(pathIndex + 1) : k.substring(pathIndex + 1, k.length() - 1)) - .collect(Collectors.toCollection(LinkedHashSet::new))); - } else { - result.addAll(root.defaults.keySet().stream() - .filter(k -> k.startsWith(fullPath) && k.lastIndexOf(root.pathChar) == pathIndex) - .map(k -> !k.endsWith(String.valueOf(root.pathChar)) ? k.substring(pathIndex + 1) : k.substring(pathIndex + 1, k.length() - 1)) - .collect(Collectors.toCollection(LinkedHashSet::new))); - result.addAll(root.values.keySet().stream() - .filter(k -> k.startsWith(fullPath) && k.lastIndexOf(root.pathChar) == pathIndex) - .map(k -> !k.endsWith(String.valueOf(root.pathChar)) ? k.substring(pathIndex + 1) : k.substring(pathIndex + 1, k.length() - 1)) - .collect(Collectors.toCollection(LinkedHashSet::new))); - } - - return result; - } - - @NotNull - @Override - public Map getValues(boolean deep) { - LinkedHashMap result = new LinkedHashMap<>(); - int pathIndex = fullPath.lastIndexOf(root.pathChar); - - if (deep) { - result.putAll((Map) root.defaults.entrySet().stream() - .filter(k -> k.getKey().startsWith(fullPath)) - .collect(Collectors.toMap( - e -> !e.getKey().endsWith(String.valueOf(root.pathChar)) ? e.getKey().substring(pathIndex + 1) : e.getKey().substring(pathIndex + 1, e.getKey().length() - 1), - Map.Entry::getValue, - (v1, v2) -> { - throw new IllegalStateException(); - }, // never going to be merging keys - LinkedHashMap::new))); - - result.putAll((Map) root.values.entrySet().stream() - .filter(k -> k.getKey().startsWith(fullPath)) - .collect(Collectors.toMap( - e -> !e.getKey().endsWith(String.valueOf(root.pathChar)) ? e.getKey().substring(pathIndex + 1) : e.getKey().substring(pathIndex + 1, e.getKey().length() - 1), - Map.Entry::getValue, - (v1, v2) -> { - throw new IllegalStateException(); - }, // never going to be merging keys - LinkedHashMap::new))); - } else { - result.putAll((Map) root.defaults.entrySet().stream() - .filter(k -> k.getKey().startsWith(fullPath) && k.getKey().lastIndexOf(root.pathChar) == pathIndex) - .collect(Collectors.toMap( - e -> !e.getKey().endsWith(String.valueOf(root.pathChar)) ? e.getKey().substring(pathIndex + 1) : e.getKey().substring(pathIndex + 1, e.getKey().length() - 1), - Map.Entry::getValue, - (v1, v2) -> { - throw new IllegalStateException(); - }, // never going to be merging keys - LinkedHashMap::new))); - - result.putAll((Map) root.values.entrySet().stream() - .filter(k -> k.getKey().startsWith(fullPath) && k.getKey().lastIndexOf(root.pathChar) == pathIndex) - .collect(Collectors.toMap( - e -> !e.getKey().endsWith(String.valueOf(root.pathChar)) ? e.getKey().substring(pathIndex + 1) : e.getKey().substring(pathIndex + 1, e.getKey().length() - 1), - Map.Entry::getValue, - (v1, v2) -> { - throw new IllegalStateException(); - }, // never going to be merging keys - LinkedHashMap::new))); - } - - return result; - } - - @NotNull - public List getSections(String path) { - ConfigSection rootSection = getConfigurationSection(path); - - if (rootSection == null) { - return Collections.emptyList(); - } - - ArrayList result = new ArrayList<>(); - rootSection.getKeys(false).stream() - .map(rootSection::get) - .filter(ConfigSection.class::isInstance) - .forEachOrdered(object -> result.add((ConfigSection) object)); - - return result; - } - - @Override - public boolean contains(@NotNull String path) { - return root.defaults.containsKey(fullPath + path) || root.values.containsKey(fullPath + path); - } - - @Override - public boolean contains(@NotNull String path, boolean ignoreDefault) { - return (!ignoreDefault && root.defaults.containsKey(fullPath + path)) || root.values.containsKey(fullPath + path); - } - - @Override - public boolean isSet(@NotNull String path) { - return root.defaults.get(fullPath + path) != null || root.values.get(fullPath + path) != null; - } - - @Override - public String getCurrentPath() { - return fullPath.isEmpty() ? "" : fullPath.substring(0, fullPath.length() - 1); - } - - @Override - public String getName() { - if (fullPath.isEmpty()) { - return ""; - } - - String[] parts = fullPath.split(Pattern.quote(String.valueOf(root.pathChar))); - return parts[parts.length - 1]; - } - - @Override - public ConfigSection getRoot() { - return root; - } - - @Override - public ConfigSection getParent() { - return parent; - } - - @Nullable - @Override - public Object get(@NotNull String path) { - Object result = root.values.get(fullPath + path); - - if (result == null) { - result = root.defaults.get(fullPath + path); - } - - return result; - } - - @Nullable - @Override - public Object get(@NotNull String path, @Nullable Object def) { - Object result = root.values.get(fullPath + path); - - return result != null ? result : def; - } - - @Override - public void set(@NotNull String path, @Nullable Object value) { - if (isDefault) { - addDefault(path, value); - return; - } - - createNodePath(path, false); - Object last; - synchronized (root.lock) { - if (value != null) { - root.changed |= (last = root.values.put(fullPath + path, value)) != value; - } else { - root.changed |= (last = root.values.remove(fullPath + path)) != null; - } - } - - if (last != value && last instanceof ConfigSection) { - // clean up orphaned nodes - final String trim = fullPath + path + root.pathChar; - synchronized (root.lock) { - root.values.keySet().stream() - .filter(k -> k.startsWith(trim)) - .collect(Collectors.toSet()) - .forEach(root.values::remove); - } - } - - onChange(); - } - - @NotNull - public ConfigSection set(@NotNull String path, @Nullable Object value, String... comment) { - set(path, value); - return setComment(path, null, comment); - } - - @NotNull - public ConfigSection set(@NotNull String path, @Nullable Object value, List comment) { - set(path, value); - return setComment(path, null, comment); - } - - @NotNull - public ConfigSection set(@NotNull String path, @Nullable Object value, @Nullable ConfigFormattingRules.CommentStyle commentStyle, String... comment) { - set(path, value); - return setComment(path, commentStyle, comment); - } - - @NotNull - public ConfigSection set(@NotNull String path, @Nullable Object value, @Nullable ConfigFormattingRules.CommentStyle commentStyle, List comment) { - set(path, value); - return setComment(path, commentStyle, comment); - } - - @NotNull - public ConfigSection setDefault(@NotNull String path, @Nullable Object value) { - addDefault(path, value); - return this; - } - - @NotNull - public ConfigSection setDefault(@NotNull String path, @Nullable Object value, String... comment) { - addDefault(path, value); - return setDefaultComment(path, comment); - } - - @NotNull - public ConfigSection setDefault(@NotNull String path, @Nullable Object value, List comment) { - addDefault(path, value); - return setDefaultComment(path, comment); - } - - @NotNull - public ConfigSection setDefault(@NotNull String path, @Nullable Object value, ConfigFormattingRules.CommentStyle commentStyle, String... comment) { - addDefault(path, value); - return setDefaultComment(path, commentStyle, comment); - } - - @NotNull - public ConfigSection setDefault(@NotNull String path, @Nullable Object value, ConfigFormattingRules.CommentStyle commentStyle, List comment) { - addDefault(path, value); - return setDefaultComment(path, commentStyle, comment); - } - - @NotNull - @Override - public ConfigSection createSection(@NotNull String path) { - createNodePath(path, false); - ConfigSection section = new ConfigSection(root, this, path, false); - - synchronized (root.lock) { - root.values.put(fullPath + path, section); - } - - root.changed = true; - onChange(); - - return section; - } - - @NotNull - public ConfigSection createSection(@NotNull String path, String... comment) { - return createSection(path, null, comment.length == 0 ? null : Arrays.asList(comment)); - } - - @NotNull - public ConfigSection createSection(@NotNull String path, @Nullable List comment) { - return createSection(path, null, comment); - } - - @NotNull - public ConfigSection createSection(@NotNull String path, @Nullable ConfigFormattingRules.CommentStyle commentStyle, String... comment) { - return createSection(path, commentStyle, comment.length == 0 ? null : Arrays.asList(comment)); - } - - @NotNull - public ConfigSection createSection(@NotNull String path, @Nullable ConfigFormattingRules.CommentStyle commentStyle, @Nullable List comment) { - createNodePath(path, false); - ConfigSection section = new ConfigSection(root, this, path, false); - - synchronized (root.lock) { - root.values.put(fullPath + path, section); - } - - setComment(path, commentStyle, comment); - root.changed = true; - onChange(); - - return section; - } - - @NotNull - @Override - public ConfigSection createSection(@NotNull String path, Map map) { - createNodePath(path, false); - ConfigSection section = new ConfigSection(root, this, path, false); - - synchronized (root.lock) { - root.values.put(fullPath + path, section); - } - - for (Map.Entry entry : map.entrySet()) { - if (entry.getValue() instanceof Map) { - section.createSection(entry.getKey().toString(), (Map) entry.getValue()); - continue; - } - - section.set(entry.getKey().toString(), entry.getValue()); - } - - root.changed = true; - onChange(); - - return section; - } - - @Nullable - @Override - public String getString(@NotNull String path) { - Object result = get(path); - - return result != null ? result.toString() : null; - } - - @Nullable - @Override - public String getString(@NotNull String path, @Nullable String def) { - Object result = get(path); - - return result != null ? result.toString() : def; - } - - public char getChar(@NotNull String path) { - Object result = get(path); - - return result != null && !result.toString().isEmpty() ? result.toString().charAt(0) : '\0'; - } - - public char getChar(@NotNull String path, char def) { - Object result = get(path); - - return result != null && !result.toString().isEmpty() ? result.toString().charAt(0) : def; - } - - @Override - public int getInt(@NotNull String path) { - Object result = get(path); - - return result instanceof Number ? ((Number) result).intValue() : 0; - } - - @Override - public int getInt(@NotNull String path, int def) { - Object result = get(path); - - return result instanceof Number ? ((Number) result).intValue() : def; - } - - @Override - public boolean getBoolean(@NotNull String path) { - Object result = get(path); - - return result instanceof Boolean ? (Boolean) result : false; - } - - @Override - public boolean getBoolean(@NotNull String path, boolean def) { - Object result = get(path); - - return result instanceof Boolean ? (Boolean) result : def; - } - - @Override - public double getDouble(@NotNull String path) { - Object result = get(path); - - return result instanceof Number ? ((Number) result).doubleValue() : 0; - } - - @Override - public double getDouble(@NotNull String path, double def) { - Object result = get(path); - - return result instanceof Number ? ((Number) result).doubleValue() : def; - } - - @Override - public long getLong(@NotNull String path) { - Object result = get(path); - - return result instanceof Number ? ((Number) result).longValue() : 0; - } - - @Override - public long getLong(@NotNull String path, long def) { - Object result = get(path); - - return result instanceof Number ? ((Number) result).longValue() : def; - } - - @Nullable - @Override - public List getList(@NotNull String path) { - Object result = get(path); - - return result instanceof List ? (List) result : null; - } - - @Nullable - @Override - public List getList(@NotNull String path, @Nullable List def) { - Object result = get(path); - - return result instanceof List ? (List) result : def; - } - - @Nullable - public CompatibleMaterial getMaterial(@NotNull String path) { - String val = getString(path); - - return val != null ? CompatibleMaterial.getMaterial(val) : null; - } - - @Nullable - public CompatibleMaterial getMaterial(@NotNull String path, @Nullable CompatibleMaterial def) { - String val = getString(path); - - CompatibleMaterial mat = val != null ? CompatibleMaterial.getMaterial(val) : null; - - return mat != null ? mat : def; - } - - @Nullable - @Override - public T getObject(@NotNull String path, @NotNull Class clazz) { - Object result = get(path); - - return clazz.isInstance(result) ? clazz.cast(result) : null; - } - - @Nullable - @Override - public T getObject(@NotNull String path, @NotNull Class clazz, @Nullable T def) { - Object result = get(path); - - return clazz.isInstance(result) ? clazz.cast(result) : def; - } - - @Override - public ConfigSection getConfigurationSection(@NotNull String path) { - Object result = get(path); - - return result instanceof ConfigSection ? (ConfigSection) result : null; - } - - @NotNull - public ConfigSection getOrCreateConfigurationSection(@NotNull String path) { - Object result = get(path); - - return result instanceof ConfigSection ? (ConfigSection) result : createSection(path); - } -} diff --git a/Core/src/main/java/com/songoda/core/configuration/ConfigSetting.java b/Core/src/main/java/com/songoda/core/configuration/ConfigSetting.java deleted file mode 100644 index 62d4bdcc..00000000 --- a/Core/src/main/java/com/songoda/core/configuration/ConfigSetting.java +++ /dev/null @@ -1,135 +0,0 @@ -package com.songoda.core.configuration; - -import com.songoda.core.SongodaCore; -import com.songoda.core.compatibility.CompatibleMaterial; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; -import java.util.logging.Level; - -public class ConfigSetting { - final Config config; - final String key; - - public ConfigSetting(@NotNull Config config, @NotNull String key) { - this.config = config; - this.key = key; - } - - public ConfigSetting(@NotNull Config config, @NotNull String key, @NotNull Object defaultValue, String... comment) { - this.config = config; - this.key = key; - - config.setDefault(key, defaultValue, comment); - } - - public ConfigSetting(@NotNull Config config, @NotNull String key, @NotNull Object defaultValue, ConfigFormattingRules.CommentStyle commentStyle, String... comment) { - this.config = config; - this.key = key; - - config.setDefault(key, defaultValue, commentStyle, comment); - } - - @NotNull - public String getKey() { - return key; - } - - public List getIntegerList() { - return config.getIntegerList(key); - } - - public List getStringList() { - return config.getStringList(key); - } - - public boolean getBoolean() { - return config.getBoolean(key); - } - - public boolean getBoolean(boolean def) { - return config.getBoolean(key, def); - } - - public int getInt() { - return config.getInt(key); - } - - public int getInt(int def) { - return config.getInt(key, def); - } - - public long getLong() { - return config.getLong(key); - } - - public long getLong(long def) { - return config.getLong(key, def); - } - - public double getDouble() { - return config.getDouble(key); - } - - public double getDouble(double def) { - return config.getDouble(key, def); - } - - public String getString() { - return config.getString(key); - } - - public String getString(String def) { - return config.getString(key, def); - } - - public Object getObject() { - return config.get(key); - } - - public Object getObject(Object def) { - return config.get(key, def); - } - - public T getObject(@NotNull Class clazz) { - return config.getObject(key, clazz); - } - - public T getObject(@NotNull Class clazz, @Nullable T def) { - return config.getObject(key, clazz, def); - } - - public char getChar() { - return config.getChar(key); - } - - public char getChar(char def) { - return config.getChar(key, def); - } - - @NotNull - public CompatibleMaterial getMaterial() { - String val = config.getString(key); - CompatibleMaterial mat = CompatibleMaterial.getMaterial(config.getString(key)); - - if (mat == null) { - SongodaCore.getLogger().log(Level.WARNING, String.format("Config value \"%s\" has an invalid material name: \"%s\"", key, val)); - } - - return mat != null ? mat : CompatibleMaterial.STONE; - } - - @NotNull - public CompatibleMaterial getMaterial(@NotNull CompatibleMaterial def) { - //return config.getMaterial(key, def); - String val = config.getString(key); - CompatibleMaterial mat = val != null ? CompatibleMaterial.getMaterial(val) : null; - - if (mat == null) { - SongodaCore.getLogger().log(Level.WARNING, String.format("Config value \"%s\" has an invalid material name: \"%s\"", key, val)); - } - - return mat != null ? mat : def; - } -} diff --git a/Core/src/main/java/com/songoda/core/configuration/DataStoreObject.java b/Core/src/main/java/com/songoda/core/configuration/DataStoreObject.java deleted file mode 100644 index 243b1180..00000000 --- a/Core/src/main/java/com/songoda/core/configuration/DataStoreObject.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.songoda.core.configuration; - -import org.bukkit.configuration.ConfigurationSection; - -public interface DataStoreObject { - /** - * @return a unique hashable instance of T to store this value under - */ - T getKey(); - - /** - * @return a unique identifier for saving this value with - */ - String getConfigKey(); - - /** - * Save this data to a ConfigurationSection - */ - void saveToSection(ConfigurationSection sec); - - /** - * @return true if this data has changed from the state saved to file - */ - boolean hasChanged(); - - /** - * Mark this data as needing a save or not - */ - void setChanged(boolean isChanged); -} diff --git a/Core/src/main/java/com/songoda/core/configuration/HeaderCommentable.java b/Core/src/main/java/com/songoda/core/configuration/HeaderCommentable.java new file mode 100644 index 00000000..d93730cd --- /dev/null +++ b/Core/src/main/java/com/songoda/core/configuration/HeaderCommentable.java @@ -0,0 +1,18 @@ +package com.songoda.core.configuration; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +public interface HeaderCommentable { + void setHeaderComment(@Nullable Supplier comment); + + default void setHeaderComment(@Nullable String comment) { + setHeaderComment(() -> comment); + } + + @Nullable Supplier getHeaderComment(); + + @NotNull String generateHeaderCommentLines(); +} diff --git a/Core/src/main/java/com/songoda/core/configuration/IConfiguration.java b/Core/src/main/java/com/songoda/core/configuration/IConfiguration.java new file mode 100644 index 00000000..c964806c --- /dev/null +++ b/Core/src/main/java/com/songoda/core/configuration/IConfiguration.java @@ -0,0 +1,98 @@ +package com.songoda.core.configuration; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; + +public interface IConfiguration { + /** + * This method returns whether a given key is set memory, ignoring its possibly null value. + * + * {@link #set(String, Object)} + * {@link #unset(String)} + */ + boolean has(String key); + + /** + * This method returns the value for a given key. + * A value of null can mean that the key does not exist or that the value is null. + * + * @see #has(String) + */ + @Nullable + Object get(String key); + + /** + * This method is mostly identical to {@link #get(String)} + * but returns the given default value if the key doesn't exist or the value is null. + */ + @Nullable + Object getOrDefault(String key, @Nullable Object defaultValue); + + /** + * This method sets a given key to a given value in memory. + * + * @return The previous value associated with key, or null if there was no mapping for key + * + * @see #save(Writer) + */ + Object set(@NotNull String key, @Nullable Object value); + + /** + * This method removes the given key from memory together with its value. + * + * @return The previous value associated with key, or null if there was no mapping for key + */ + Object unset(String key); + + /** + * This method clears all the configuration values from memory that have been loaded or set. + * + * @see #load(Reader) + */ + void reset(); + + /** + * This method does the same as {@link #load(Reader)} but takes in a File. + * By default, this implementation wraps the given file in a {@link FileReader} and calls {@link #load(Reader)}. + * + * @throws FileNotFoundException Thrown by {@link FileReader#FileReader(File)} + * @see #load(Reader) + */ + default void load(File file) throws IOException { + load(new FileReader(file)); + } + + /** + * This method parses and loads the configuration and stores them as key-value pairs in memory. + * Keys that are not loaded with this call but still exist in memory, are removed. + * Additional data may be read depending on the implementation (e.g. comments). + * + * @see #reset() + */ + void load(Reader reader) throws IOException; + + /** + * This method does the same as {@link #save(Writer)} but takes in a File. + * By default, this implementation wraps the given file in a {@link FileWriter} and calls {@link #save(Writer)}. + * + * @throws FileNotFoundException Thrown by {@link FileWriter#FileWriter(File)} + * @see #load(Reader) + */ + default void save(File file) throws IOException { + save(new FileWriter(file)); + } + + /** + * This method serializes the key-value pairs in memory and writes them to the given writer. + * Additional data may be written depending on the implementation (e.g. comments). + */ + void save(Writer writer) throws IOException; +} diff --git a/Core/src/main/java/com/songoda/core/configuration/NodeCommentable.java b/Core/src/main/java/com/songoda/core/configuration/NodeCommentable.java new file mode 100644 index 00000000..ab171e4a --- /dev/null +++ b/Core/src/main/java/com/songoda/core/configuration/NodeCommentable.java @@ -0,0 +1,16 @@ +package com.songoda.core.configuration; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +public interface NodeCommentable { + void setNodeComment(@NotNull String key, @Nullable Supplier comment); + + default void setNodeComment(@NotNull String key, @Nullable String comment) { + setNodeComment(key, () -> comment); + } + + @Nullable Supplier getNodeComment(@Nullable String key); +} diff --git a/Core/src/main/java/com/songoda/core/configuration/SimpleDataStore.java b/Core/src/main/java/com/songoda/core/configuration/SimpleDataStore.java deleted file mode 100644 index 9c656cf2..00000000 --- a/Core/src/main/java/com/songoda/core/configuration/SimpleDataStore.java +++ /dev/null @@ -1,284 +0,0 @@ -package com.songoda.core.configuration; - -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.InvalidConfigurationException; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.io.IOException; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Timer; -import java.util.TimerTask; -import java.util.function.Function; -import java.util.logging.Level; - -/** - * Used to easily store a set of one data value - * - * @param DataObject class that is used to store the data - */ -public class SimpleDataStore { - protected final Plugin plugin; - protected final String filename, dirName; - private final Function getFromSection; - protected final HashMap data = new HashMap<>(); - private File file; - private final Object lock = new Object(); - SaveTask saveTask; - Timer autosaveTimer; - /** - * time in seconds to start a save after a change is made - */ - int autosaveInterval = 60; - - public SimpleDataStore(@NotNull Plugin plugin, @NotNull String filename, @NotNull Function loadFunction) { - this.plugin = plugin; - this.filename = filename; - dirName = null; - this.getFromSection = loadFunction; - } - - public SimpleDataStore(@NotNull Plugin plugin, @Nullable String directory, @NotNull String filename, @NotNull Function loadFunction) { - this.plugin = plugin; - this.filename = filename; - this.dirName = directory; - this.getFromSection = loadFunction; - } - - @NotNull - public File getFile() { - if (file == null) { - if (dirName != null) { - this.file = new File(plugin.getDataFolder() + dirName, filename != null ? filename : "data.yml"); - } else { - this.file = new File(plugin.getDataFolder(), filename != null ? filename : "data.yml"); - } - } - - return file; - } - - /** - * @return a directly-modifiable instance of the data mapping for this - * storage - */ - public Map getData() { - return data; - } - - /** - * Returns the value to which the specified key is mapped, or {@code null} - * if this map contains no mapping for the key. - * - * @param key key whose mapping is to be retrieved from this storage - * - * @return the value associated with key, or - * null if there was no mapping for key. - */ - @Nullable - public T get(Object key) { - return data.get(key); - } - - /** - * Removes the mapping for the specified key from this storage if present. - * - * @param key key whose mapping is to be removed from this storage - * - * @return the previous value associated with key, or - * null if there was no mapping for key. - */ - @Nullable - public T remove(@NotNull Object key) { - T temp; - - synchronized (lock) { - temp = data.remove(key); - } - - save(); - - return temp; - } - - /** - * Removes the mapping for the specified key from this storage if present. - * - * @param value value whose mapping is to be removed from this storage - * - * @return the previous value associated with key, or - * null if there was no mapping for key. - */ - @Nullable - public T remove(@NotNull T value) { - if (value == null) { - return null; - } - - T temp; - - synchronized (lock) { - temp = data.remove(value.getKey()); - } - - save(); - - return temp; - } - - /** - * Adds the specified value in this storage. If the map previously contained - * a mapping for the key, the old value is replaced. - * - * @param value value to be added - * - * @return the previous value associated with value.getKey(), or - * null if there was no mapping for value.getKey(). - */ - @Nullable - public T add(@NotNull T value) { - if (value == null) { - return null; - } - - T temp; - - synchronized (lock) { - temp = data.put(value.getKey(), value); - } - - save(); - - return temp; - } - - /** - * Adds the specified value in this storage. If the map previously contained - * a mapping for the key, the old value is replaced. - * - * @param value values to be added - */ - public void addAll(@NotNull T[] value) { - if (value == null) { - return; - } - - synchronized (lock) { - for (T t : value) { - if (t != null) { - data.put(t.getKey(), t); - } - } - } - - save(); - } - - /** - * Adds the specified value in this storage. If the map previously contained - * a mapping for the key, the old value is replaced. - * - * @param value values to be added - */ - @Nullable - public void addAll(@NotNull Collection value) { - if (value == null) { - return; - } - - synchronized (lock) { - for (T v : value) { - if (v != null) { - data.put(v.getKey(), v); - } - } - } - - save(); - } - - /** - * Load data from the associated file - */ - public void load() { - if (!getFile().exists()) { - return; - } - - try { - YamlConfiguration f = new YamlConfiguration(); - f.options().pathSeparator('\0'); - f.load(file); - - synchronized (lock) { - data.clear(); - - f.getValues(false).values().stream() - .filter(ConfigurationSection.class::isInstance) - .map(v -> getFromSection.apply((ConfigurationSection) v)) - .forEach(v -> data.put(v.getKey(), v)); - } - } catch (IOException | InvalidConfigurationException ex) { - plugin.getLogger().log(Level.SEVERE, "Failed to load data from " + file.getName(), ex); - } - } - - /** - * Optionally save this storage's data to file if there have been changes - * made - */ - public void saveChanges() { - if (saveTask != null || data.values().stream().anyMatch(DataStoreObject::hasChanged)) { - flushSave(); - } - } - - /** - * Save this file data. This saves later asynchronously. - */ - public void save() { - // save async even if no plugin or if plugin disabled - if (saveTask == null) { - autosaveTimer = new Timer((plugin != null ? plugin.getName() + "-DataStoreSave-" : "DataStoreSave-") + getFile().getName()); - autosaveTimer.schedule(saveTask = new SaveTask(), autosaveInterval * 1000L); - } - } - - /** - * Force a new save of this storage's data - */ - public void flushSave() { - if (saveTask != null) { - //Close Threads - saveTask.cancel(); - autosaveTimer.cancel(); - saveTask = null; - autosaveTimer = null; - } - - YamlConfiguration f = new YamlConfiguration(); - - synchronized (lock) { - data.values().forEach(e -> e.saveToSection(f.createSection(e.getConfigKey()))); - } - - try { - f.save(getFile()); - data.values().forEach(e -> e.setChanged(false)); - } catch (IOException ex) { - plugin.getLogger().log(Level.SEVERE, "Failed to save data to " + file.getName(), ex); - } - } - - class SaveTask extends TimerTask { - @Override - public void run() { - flushSave(); - } - } -} diff --git a/Core/src/main/java/com/songoda/core/configuration/yaml/YamlCommentRepresenter.java b/Core/src/main/java/com/songoda/core/configuration/yaml/YamlCommentRepresenter.java new file mode 100644 index 00000000..0c09f4e8 --- /dev/null +++ b/Core/src/main/java/com/songoda/core/configuration/yaml/YamlCommentRepresenter.java @@ -0,0 +1,71 @@ +package com.songoda.core.configuration.yaml; + +import org.yaml.snakeyaml.comments.CommentLine; +import org.yaml.snakeyaml.comments.CommentType; +import org.yaml.snakeyaml.events.CommentEvent; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.ScalarNode; +import org.yaml.snakeyaml.representer.Representer; + +import java.util.Collections; +import java.util.Map; +import java.util.function.Supplier; + +public class YamlCommentRepresenter extends Representer { + private final Map> nodeComments; + + public YamlCommentRepresenter(Map> nodeComments) { + this.nodeComments = nodeComments; + } + + @Override + public Node represent(Object data) { + Node rootNode = super.represent(data); + + if (!(rootNode instanceof MappingNode)) { + return rootNode; + } + + for (NodeTuple nodeTuple : ((MappingNode) rootNode).getValue()) { + if (!(nodeTuple.getKeyNode() instanceof ScalarNode)) { + continue; + } + + applyComment((ScalarNode) nodeTuple.getKeyNode(), ((ScalarNode) nodeTuple.getKeyNode()).getValue()); + + if (nodeTuple.getValueNode() instanceof MappingNode) { + String key = ((ScalarNode) nodeTuple.getKeyNode()).getValue(); + + resolveSubNodes(((MappingNode) nodeTuple.getValueNode()), key); + } + } + + return rootNode; + } + + protected void resolveSubNodes(MappingNode mappingNode, String key) { + for (NodeTuple nodeTuple : mappingNode.getValue()) { + if (!(nodeTuple.getKeyNode() instanceof ScalarNode)) { + continue; + } + + String newKey = key + "." + ((ScalarNode) nodeTuple.getKeyNode()).getValue(); + + applyComment((ScalarNode) nodeTuple.getKeyNode(), newKey); + + if (nodeTuple.getValueNode() instanceof MappingNode) { + resolveSubNodes(((MappingNode) nodeTuple.getValueNode()), newKey); + } + } + } + + protected void applyComment(ScalarNode scalarNode, String key) { + Supplier innerValue = this.nodeComments.get(key); + + if (innerValue != null) { + scalarNode.setBlockComments(Collections.singletonList(new CommentLine(new CommentEvent(CommentType.BLOCK, " " + innerValue.get(), null, null)))); + } + } +} diff --git a/Core/src/main/java/com/songoda/core/configuration/yaml/YamlConfiguration.java b/Core/src/main/java/com/songoda/core/configuration/yaml/YamlConfiguration.java new file mode 100644 index 00000000..f559d93a --- /dev/null +++ b/Core/src/main/java/com/songoda/core/configuration/yaml/YamlConfiguration.java @@ -0,0 +1,336 @@ +package com.songoda.core.configuration.yaml; + +import com.songoda.core.configuration.HeaderCommentable; +import com.songoda.core.configuration.IConfiguration; +import com.songoda.core.configuration.NodeCommentable; +import org.apache.commons.lang.ArrayUtils; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.representer.Representer; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; + +public class YamlConfiguration implements IConfiguration, HeaderCommentable, NodeCommentable { + protected final @NotNull Yaml yaml; + protected final @NotNull DumperOptions yamlDumperOptions; + protected final @NotNull YamlCommentRepresenter yamlCommentRepresenter; + + protected final @NotNull Map values; + protected final @NotNull Map> nodeComments; + protected @Nullable Supplier headerComment; + + public YamlConfiguration() { + this(new HashMap<>(), new HashMap<>()); + } + + protected YamlConfiguration(@NotNull Map values, @NotNull Map> nodeComments) { + this.values = Objects.requireNonNull(values); + this.nodeComments = Objects.requireNonNull(nodeComments); + + this.yamlDumperOptions = createDefaultYamlDumperOptions(); + this.yamlCommentRepresenter = new YamlCommentRepresenter(this.nodeComments); + this.yaml = createDefaultYaml(this.yamlDumperOptions, this.yamlCommentRepresenter); + } + + @Override + @Contract(pure = true, value = "null -> false") + public boolean has(String key) { + if (key == null) { + return false; + } + + String[] fullKeyPath = key.split("\\."); + + Map innerMap = getInnerMap(this.values, Arrays.copyOf(fullKeyPath, fullKeyPath.length - 1), false); + + if (innerMap != null) { + return innerMap.containsKey(fullKeyPath[fullKeyPath.length - 1]); + } + + return false; + } + + @Override + @Contract(pure = true, value = "null -> null") + public @Nullable Object get(String key) { + if (key == null) { + return null; + } + + try { + return getInnerValueForKey(this.values, key); + } catch (IllegalArgumentException ignore) { + } + + return null; + } + + @Override + @Contract(pure = true, value = "null,_ -> param2") + public @Nullable Object getOrDefault(String key, @Nullable Object defaultValue) { + Object value = get(key); + + return value == null ? defaultValue : value; + } + + public @NotNull Set getKeys(String key) { + if (key == null) { + return Collections.emptySet(); + } + + Map innerMap = null; + + try { + innerMap = getInnerMap(this.values, key.split("\\."), false); + } catch (IllegalArgumentException ignore) { + } + + if (innerMap != null) { + return Collections.unmodifiableSet(innerMap.keySet()); + } + + return Collections.emptySet(); + + } + + @Override + public Object set(@NotNull String key, @Nullable Object value) { + if (value != null) { + if (value instanceof Float) { + value = ((Float) value).doubleValue(); + } else if (value instanceof Character) { + value = ((Character) value).toString(); + } else if (value.getClass().isArray()) { + if (value instanceof int[]) { + value = Arrays.asList(ArrayUtils.toObject((int[]) value)); + } else if (value instanceof long[]) { + value = Arrays.asList(ArrayUtils.toObject((long[]) value)); + } else if (value instanceof short[]) { + List newValue = new ArrayList<>(((short[]) value).length); + for (Short s : (short[]) value) { + newValue.add(s.intValue()); + } + value = newValue; + } else if (value instanceof byte[]) { + List newValue = new ArrayList<>(((byte[]) value).length); + for (Byte b : (byte[]) value) { + newValue.add(b.intValue()); + } + value = newValue; + } else if (value instanceof double[]) { + value = Arrays.asList(ArrayUtils.toObject((double[]) value)); + } else if (value instanceof float[]) { + List newValue = new ArrayList<>(((float[]) value).length); + for (float f : (float[]) value) { + newValue.add(new Float(f).doubleValue()); + } + value = newValue; + } else if (value instanceof boolean[]) { + value = Arrays.asList(ArrayUtils.toObject((boolean[]) value)); + } else if (value instanceof char[]) { + List newValue = new ArrayList<>(((char[]) value).length); + for (char c : (char[]) value) { + newValue.add(String.valueOf(c)); + } + value = newValue; + } else { + value = Arrays.asList((Object[]) value); + } + } + } + + return setInnerValueForKey(this.values, key, value); + } + + @Override + public Object unset(String key) { + String[] fullKeyPath = key.split("\\."); + + Map innerMap = getInnerMap(this.values, Arrays.copyOf(fullKeyPath, fullKeyPath.length - 1), false); + + if (innerMap != null) { + return innerMap.remove(fullKeyPath[fullKeyPath.length - 1]); + } + + return null; + } + + @Override + public void reset() { + this.values.clear(); + } + + @Override + public void load(Reader reader) { + Object yamlData = this.yaml.load(reader); + + if (!(yamlData instanceof Map)) { + throw new IllegalStateException("The YAML file does not have the expected tree structure"); + } + + synchronized (this.values) { + this.values.clear(); + + for (Map.Entry yamlEntry : ((Map) yamlData).entrySet()) { + this.values.put(yamlEntry.getKey().toString(), yamlEntry.getValue()); + } + } + } + + @Override + public void save(Writer writer) throws IOException { + String headerCommentLines = generateHeaderCommentLines(); + writer.write(headerCommentLines); + + if (this.values.size() > 0) { + if (headerCommentLines.length() > 0) { + writer.write(this.yamlDumperOptions.getLineBreak().getString()); + } + + this.yaml.dump(this.values, writer); + } + } + + @Override + public void setHeaderComment(@Nullable Supplier comment) { + this.headerComment = comment; + } + + @Override + public @Nullable Supplier getHeaderComment() { + return this.headerComment; + } + + @Override + public @NotNull String generateHeaderCommentLines() { + StringBuilder sb = new StringBuilder(); + + String headerCommentString = this.headerComment == null ? null : this.headerComment.get(); + if (headerCommentString != null) { + for (String commentLine : headerCommentString.split("\r?\n")) { + sb.append("# ") + .append(commentLine) + .append(this.yamlDumperOptions.getLineBreak().getString()); + } + } + + return sb.toString(); + } + + @Override + public void setNodeComment(@NotNull String key, @Nullable Supplier comment) { + this.nodeComments.put(key, comment); + } + + @Override + public @Nullable Supplier getNodeComment(@Nullable String key) { + return this.nodeComments.get(key); + } + + public String toYamlString() throws IOException { + StringWriter writer = new StringWriter(); + save(writer); + + return writer.toString(); + } + + @Override + public String toString() { + return "YamlConfiguration{" + + "values=" + values + + ", headerComment=" + headerComment + + '}'; + } + + protected static DumperOptions createDefaultYamlDumperOptions() { + DumperOptions dumperOptions = new DumperOptions(); + dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + dumperOptions.setIndentWithIndicator(true); + dumperOptions.setIndicatorIndent(2); + + return dumperOptions; + } + + protected static Yaml createDefaultYaml(DumperOptions dumperOptions, Representer representer) { + LoaderOptions yamlOptions = new LoaderOptions(); + yamlOptions.setAllowDuplicateKeys(false); + + return new Yaml(new Constructor(yamlOptions), representer, dumperOptions, yamlOptions); + } + + protected static Object setInnerValueForKey(@NotNull Map map, @NotNull String key, @Nullable Object value) { + String[] fullKeyPath = key.split("\\."); + + Map innerMap = getInnerMap(map, Arrays.copyOf(fullKeyPath, fullKeyPath.length - 1), true); + + return ((Map) innerMap).put(fullKeyPath[fullKeyPath.length - 1], value); + } + + protected static Object getInnerValueForKey(@NotNull Map map, @NotNull String key) { + String[] fullKeyPath = key.split("\\."); + + Map innerMap = getInnerMap(map, Arrays.copyOf(fullKeyPath, fullKeyPath.length - 1), false); + + if (innerMap != null) { + return innerMap.get(fullKeyPath[fullKeyPath.length - 1]); + } + + return null; + } + + @Contract("_,_,true -> !null") + protected static Map getInnerMap(@NotNull Map map, @NotNull String[] keys, boolean createMissingMaps) { + if (keys.length == 0) { + return map; + } + + int currentKeyIndex = 0; + Map currentMap = map; + + while (true) { + Object currentValue = currentMap.get(keys[currentKeyIndex]); + + if (currentValue == null) { + if (!createMissingMaps) { + return null; + } + + currentValue = new HashMap<>(); + ((Map) currentMap).put(keys[currentKeyIndex], currentValue); + } + + if (!(currentValue instanceof Map)) { + if (!createMissingMaps) { + throw new IllegalArgumentException("Expected a Map when resolving key '" + String.join(".", keys) + "' at '" + String.join(".", Arrays.copyOf(keys, currentKeyIndex + 1)) + "'"); + } + + currentValue = new HashMap<>(); + ((Map) currentMap).put(keys[currentKeyIndex], currentValue); + } + + if (currentKeyIndex == keys.length - 1) { + return (Map) currentValue; + } + + currentMap = (Map) currentValue; + ++currentKeyIndex; + } + } +} diff --git a/Core/src/test/java/com/songoda/core/configuration/yaml/YamlConfigurationTest.java b/Core/src/test/java/com/songoda/core/configuration/yaml/YamlConfigurationTest.java new file mode 100644 index 00000000..9b7ea7dd --- /dev/null +++ b/Core/src/test/java/com/songoda/core/configuration/yaml/YamlConfigurationTest.java @@ -0,0 +1,591 @@ +package com.songoda.core.configuration.yaml; + +import org.apache.commons.lang.StringUtils; +import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.constructor.DuplicateKeyException; +import org.yaml.snakeyaml.error.YAMLException; + +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class YamlConfigurationTest { + static final String inputYaml = "foo: bar\n" + + "primitives:\n" + + " int: " + Integer.MIN_VALUE + "\n" + + " long: " + Long.MIN_VALUE + "\n" + + " float: " + Float.MIN_VALUE + "\n" + + " double: " + Double.MIN_VALUE + "\n" + + " char: ä\n" + + " string: string\n" + + " string-long: " + StringUtils.repeat("abc", 512) + "\n" + + " string-multi-line: |\n" + + " a\n" + + " b\n" + + " c\n" + + " boolean: true\n" + + " list: [2, 1, 3]\n" + + " map:\n" + + " key: value\n" + + " set:\n" + + " - 1\n" + + " - 2\n" + + " - 3\n"; + static final String expectedOutYaml = "primitives:\n" + + " int: " + Integer.MIN_VALUE + "\n" + + " long: " + Long.MIN_VALUE + "\n" + + " float: " + Float.MIN_VALUE + "\n" + + " double: " + Double.MIN_VALUE + "\n" + + " char: ä\n" + + " string: string\n" + + " string-long: " + StringUtils.repeat("abc", 512) + "\n" + + " string-multi-line: |\n" + + " a\n" + + " b\n" + + " c\n" + + " boolean: true\n" + + " list:\n" + + " - 2\n" + + " - 1\n" + + " - 3\n" + + " map:\n" + + " key: value\n" + + " set:\n" + + " - 1\n" + + " - 2\n" + + " - 3\n" + + "foo: bar\n"; + + @Test + void testYamlParser() { + final YamlConfiguration cfg = new YamlConfiguration(); + cfg.load(new StringReader(inputYaml)); + + assertEquals(Integer.MIN_VALUE, cfg.get("primitives.int")); + assertEquals(Long.MIN_VALUE, cfg.get("primitives.long")); + assertEquals(Float.MIN_VALUE, ((Number) cfg.get("primitives.float")).floatValue()); + assertEquals(Double.MIN_VALUE, cfg.get("primitives.double")); + + assertEquals("ä", cfg.get("primitives.char")); + + assertEquals("string", cfg.get("primitives.string")); + + assertInstanceOf(Boolean.class, cfg.get("primitives.boolean")); + assertTrue((Boolean) cfg.get("primitives.boolean")); + + List primitivesList = (List) cfg.get("primitives.list"); + assertNotNull(primitivesList); + assertInstanceOf(List.class, cfg.get("primitives.list")); + assertEquals(3, primitivesList.size()); + assertEquals(2, primitivesList.get(0)); + assertEquals(1, primitivesList.get(1)); + assertEquals(3, primitivesList.get(2)); + + assertEquals("value", cfg.get("primitives.map.key")); + + assertInstanceOf(List.class, cfg.get("primitives.set")); + assertEquals(3, ((List) cfg.get("primitives.set")).size()); + } + + @Test + void testYamlParserWithDuplicateKeys() { + assertThrowsExactly(DuplicateKeyException.class, + () -> new YamlConfiguration().load(new StringReader("test: value1\ntest: value2"))); + } + + @Test + void testYamlParserWithInvalidReader() throws IOException { + Reader reader = new StringReader(""); + reader.close(); + + assertThrowsExactly(YAMLException.class, () -> new YamlConfiguration().load(reader)); + } + + @Test + void testYamlWriter() throws IOException { + final YamlConfiguration cfg = new YamlConfiguration(); + final StringWriter stringWriter = new StringWriter(inputYaml.length()); + + cfg.load(new StringReader(inputYaml)); + cfg.save(stringWriter); + + assertEquals(expectedOutYaml, stringWriter.toString()); + assertEquals(expectedOutYaml, cfg.toYamlString()); + } + + @Test + void testYamlWriterWithNoData() throws IOException { + final YamlConfiguration cfg = new YamlConfiguration(); + final StringWriter stringWriter = new StringWriter(inputYaml.length()); + + cfg.save(stringWriter); + + assertEquals("", stringWriter.toString()); + assertEquals("", cfg.toYamlString()); + } + + @Test + void testYamlWriterWithNoDataAndComments() throws IOException { + final YamlConfiguration cfg = new YamlConfiguration(); + final StringWriter stringWriter = new StringWriter(inputYaml.length()); + + cfg.setHeaderComment("baz"); + cfg.setNodeComment("foo", "bar"); + + cfg.save(stringWriter); + + assertEquals("# baz\n", stringWriter.toString()); + assertEquals("# baz\n", cfg.toYamlString()); + } + + @Test + void testSetter() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("foo.bar.innerBar", "bar")); // 'foo.bar' gets overwritten + + Object prevValue = cfg.set("foo.bar", "baz"); + assertInstanceOf(Map.class, prevValue); + assertEquals(1, ((Map) prevValue).size()); + assertEquals("bar", ((Map) prevValue).get("innerBar")); + + assertNull(cfg.set("number", 27)); + assertNull(cfg.set("bar.foo.faa1", "value1")); + assertNull(cfg.set("bar.foo.faa2", "value2")); + + assertFalse(cfg.has("a.b.c")); + assertFalse(cfg.has("a")); + + Map expectedValues = new HashMap() {{ + put("number", 27); + + put("foo", new HashMap() {{ + put("bar", "baz"); + }}); + + put("bar", new HashMap() {{ + put("foo", new HashMap() {{ + put("faa1", "value1"); + put("faa2", "value2"); + }}); + }}); + }}; + + assertEquals(expectedValues, cfg.values); + } + + @Test + void testSetterAndGetterWithPrimitiveValues() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("foobar", "test")); + assertNull(cfg.set("foo.bar", "test2")); + assertEquals("test", cfg.set("foobar", "overwritten-test")); + + assertEquals("overwritten-test", cfg.get("foobar")); + + assertEquals("test2", cfg.get("foo.bar")); + + assertNull(cfg.set("primitives.int", Integer.MIN_VALUE)); + assertNull(cfg.set("primitives.long", Long.MIN_VALUE)); + assertNull(cfg.set("primitives.float", Float.MIN_VALUE)); + assertNull(cfg.set("primitives.double", Double.MIN_VALUE)); + assertNull(cfg.set("primitives.char", 'ä')); + assertNull(cfg.set("primitives.string", "string")); + assertNull(cfg.set("primitives.boolean", true)); + + assertEquals(Integer.MIN_VALUE, cfg.get("primitives.int")); + assertEquals(Long.MIN_VALUE, cfg.get("primitives.long")); + + assertInstanceOf(Double.class, cfg.get("primitives.float")); + assertEquals(Float.MIN_VALUE, ((Number) cfg.get("primitives.float")).floatValue()); + + assertEquals(Double.MIN_VALUE, cfg.get("primitives.double")); + + assertInstanceOf(String.class, cfg.get("primitives.char")); + assertEquals("ä", cfg.get("primitives.char")); + + assertEquals("string", cfg.get("primitives.string")); + assertInstanceOf(Boolean.class, cfg.get("primitives.boolean")); + assertTrue((Boolean) cfg.get("primitives.boolean")); + + assertNull(cfg.set("primitives.map.key", "value")); + assertEquals("value", cfg.get("primitives.map.key")); + } + + @Test + void testGetNonExistingNestedKey() { + final YamlConfiguration cfg = new YamlConfiguration(); + cfg.load(new StringReader(inputYaml)); + + assertNull(cfg.get("primitives.map2.key")); + } + + @Test + void testGetOrDefault() { + final YamlConfiguration cfg = new YamlConfiguration(); + cfg.load(new StringReader(inputYaml)); + + assertEquals("bar", cfg.set("foo", "bar")); + assertNull(cfg.set("bar.baz", "foz")); + + assertEquals("bar", cfg.getOrDefault("foo", "baz")); + assertEquals("foz", cfg.getOrDefault("bar.baz", "baz")); + + assertEquals("default", cfg.getOrDefault("foo.bar", "default")); + assertEquals("default", cfg.getOrDefault("bar.baz.foo", "default")); + } + + @Test + void testGetterWithNullKey() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.get(null)); + } + + @Test + void testGetKeys() { + final YamlConfiguration cfg = new YamlConfiguration(); + cfg.load(new StringReader(inputYaml)); + + assertTrue(cfg.getKeys("").isEmpty()); + assertTrue(cfg.getKeys(null).isEmpty()); + + assertTrue(cfg.getKeys("primitives.map.key.non-existing-subkey").isEmpty()); + assertTrue(cfg.getKeys("foo").isEmpty()); + + assertArrayEquals(new String[] {"key"}, cfg.getKeys("primitives.map").toArray()); + assertArrayEquals(new String[] {"int", "long", "float", "double", "char", "string", "string-long", "string-multi-line", "boolean", "list", "map", "set"}, cfg.getKeys("primitives").toArray()); + } + + @Test + void testSetterWithListValues() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.list", Arrays.asList(2, 1, 3))); + + assertInstanceOf(List.class, cfg.get("primitives.list")); + List primitivesList = (List) cfg.get("primitives.list"); + assertNotNull(primitivesList); + assertEquals(3, primitivesList.size()); + assertEquals(2, primitivesList.get(0)); + assertEquals(1, primitivesList.get(1)); + assertEquals(3, primitivesList.get(2)); + } + + @Test + void testSetterWithBooleanArrayValue() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.array", new boolean[] {Boolean.FALSE, Boolean.TRUE})); + + assertInstanceOf(List.class, cfg.get("primitives.array")); + List primitivesList = (List) cfg.get("primitives.array"); + assert primitivesList != null; + assertEquals(2, primitivesList.size()); + assertEquals(Boolean.FALSE, primitivesList.get(0)); + assertEquals(Boolean.TRUE, primitivesList.get(1)); + } + + @Test + void testSetterWithByteArrayValue() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.array", new byte[] {2, Byte.MIN_VALUE, Byte.MAX_VALUE})); + + assertInstanceOf(List.class, cfg.get("primitives.array")); + List primitivesList = (List) cfg.get("primitives.array"); + assert primitivesList != null; + assertEquals(3, primitivesList.size()); + assertEquals(2, primitivesList.get(0)); + assertEquals((int) Byte.MIN_VALUE, primitivesList.get(1)); + assertEquals((int) Byte.MAX_VALUE, primitivesList.get(2)); + } + + @Test + void testSetterWithCharArrayValue() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.array", new char[] {'x', Character.MIN_VALUE, Character.MAX_VALUE})); + + assertInstanceOf(List.class, cfg.get("primitives.array")); + List primitivesList = (List) cfg.get("primitives.array"); + assert primitivesList != null; + assertEquals(3, primitivesList.size()); + assertEquals("x", primitivesList.get(0)); + assertEquals(String.valueOf(Character.MIN_VALUE), primitivesList.get(1)); + assertEquals(String.valueOf(Character.MAX_VALUE), primitivesList.get(2)); + } + + @Test + void testSetterWithShortArrayValue() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.array", new short[] {2, Short.MIN_VALUE, Short.MAX_VALUE})); + + assertInstanceOf(List.class, cfg.get("primitives.array")); + List primitivesList = (List) cfg.get("primitives.array"); + assert primitivesList != null; + assertEquals(3, primitivesList.size()); + assertEquals(2, primitivesList.get(0)); + assertEquals((int) Short.MIN_VALUE, primitivesList.get(1)); + assertEquals((int) Short.MAX_VALUE, primitivesList.get(2)); + } + + @Test + void testSetterWithIntArrayValue() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.array", new int[] {2, Integer.MIN_VALUE, Integer.MAX_VALUE})); + + assertInstanceOf(List.class, cfg.get("primitives.array")); + List primitivesList = (List) cfg.get("primitives.array"); + assert primitivesList != null; + assertEquals(3, primitivesList.size()); + assertEquals(2, primitivesList.get(0)); + assertEquals(Integer.MIN_VALUE, primitivesList.get(1)); + assertEquals(Integer.MAX_VALUE, primitivesList.get(2)); + } + + @Test + void testSetterWithLongArrayValue() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.array", new long[] {2, Long.MIN_VALUE, Long.MAX_VALUE})); + + assertInstanceOf(List.class, cfg.get("primitives.array")); + List primitivesList = (List) cfg.get("primitives.array"); + assert primitivesList != null; + assertEquals(3, primitivesList.size()); + assertEquals((long) 2, primitivesList.get(0)); + assertEquals(Long.MIN_VALUE, primitivesList.get(1)); + assertEquals(Long.MAX_VALUE, primitivesList.get(2)); + } + + @Test + void testSetterWithFloatArrayValue() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.array", new float[] {2, Float.MIN_VALUE, Float.MAX_VALUE})); + + assertInstanceOf(List.class, cfg.get("primitives.array")); + List primitivesList = (List) cfg.get("primitives.array"); + assert primitivesList != null; + assertEquals(3, primitivesList.size()); + assertEquals((double) 2, primitivesList.get(0)); + assertEquals((double) Float.MIN_VALUE, primitivesList.get(1)); + assertEquals((double) Float.MAX_VALUE, primitivesList.get(2)); + } + + @Test + void testSetterWithDoubleArrayValue() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.array", new double[] {2, Double.MIN_VALUE, Double.MAX_VALUE})); + + assertInstanceOf(List.class, cfg.get("primitives.array")); + List primitivesList = (List) cfg.get("primitives.array"); + assert primitivesList != null; + assertEquals(3, primitivesList.size()); + assertEquals((double) 2, primitivesList.get(0)); + assertEquals(Double.MIN_VALUE, primitivesList.get(1)); + assertEquals(Double.MAX_VALUE, primitivesList.get(2)); + } + + @Test + void testSetterWithStringArrayValue() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.array", new String[] {"zyx", "b", "a"})); + + assertInstanceOf(List.class, cfg.get("primitives.array")); + List primitivesList = (List) cfg.get("primitives.array"); + assert primitivesList != null; + assertEquals(3, primitivesList.size()); + assertEquals("zyx", primitivesList.get(0)); + assertEquals("b", primitivesList.get(1)); + assertEquals("a", primitivesList.get(2)); + } + + @Test + void testHas() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertFalse(cfg.has(null)); + + assertNull(cfg.set("foo", "bar")); + + assertTrue(cfg.has("foo")); + assertFalse(cfg.has("bar")); + + assertNull(cfg.set("foo.bar", "baz")); + assertTrue(cfg.has("foo.bar")); + + assertFalse(cfg.has("foo.baz")); + assertNull(YamlConfiguration.getInnerMap(cfg.values, new String[] {"foo", "baz"}, false)); + } + + @Test + void testReset() { + final YamlConfiguration cfg = new YamlConfiguration(); + cfg.load(new StringReader(inputYaml)); + + cfg.setNodeComment("foo", "bar"); + cfg.setHeaderComment("baz"); + + assertFalse(cfg.values.isEmpty()); + cfg.reset(); + assertTrue(cfg.values.isEmpty()); + + assertNotNull(cfg.getHeaderComment()); + assertNotNull(cfg.getNodeComment("foo")); + } + + @Test + void testUnset() { + final YamlConfiguration cfg = new YamlConfiguration(); + cfg.load(new StringReader(inputYaml)); + + Object unsetResult; + + assertTrue(cfg.has("foo")); + unsetResult = cfg.unset("foo"); + assertEquals("bar", unsetResult); + assertFalse(cfg.has("foo")); + + assertTrue(cfg.has("primitives")); + assertTrue(cfg.has("primitives.int")); + assertTrue(cfg.has("primitives.double")); + unsetResult = cfg.unset("primitives.int"); + assertEquals(Integer.MIN_VALUE, unsetResult); + assertFalse(cfg.has("primitives.int")); + assertTrue(cfg.has("primitives.double")); + + unsetResult = cfg.unset("primitives"); + assertInstanceOf(Map.class, unsetResult); + assertFalse(cfg.has("primitives")); + assertFalse(cfg.has("primitives.double")); + assertFalse(cfg.has("primitives.string")); + + unsetResult = cfg.unset("unknown.nested.key"); + assertNull(unsetResult); + unsetResult = cfg.unset("unknown-key"); + assertNull(unsetResult); + } + + @Test + void testToString() { + final YamlConfiguration cfg = new YamlConfiguration(); + + String firstToString = cfg.toString(); + + assertTrue(firstToString.contains(YamlConfiguration.class.getSimpleName())); + assertTrue(firstToString.contains(cfg.values.toString())); + + cfg.load(new StringReader(inputYaml)); + + String secondToString = cfg.toString(); + + assertNotEquals(firstToString, secondToString); + assertTrue(secondToString.contains(YamlConfiguration.class.getSimpleName())); + assertTrue(secondToString.contains(cfg.values.toString())); + } + + @Test + void testSaveAndLoadToFile() throws IOException { + final YamlConfiguration cfg = new YamlConfiguration(); + cfg.load(new StringReader(inputYaml)); + + File tmpFile = Files.createTempFile(this.getClass().getName(), "yml").toFile(); + tmpFile.deleteOnExit(); + + cfg.save(tmpFile); + + assertArrayEquals(expectedOutYaml.getBytes(StandardCharsets.UTF_8), Files.readAllBytes(tmpFile.toPath())); + + final YamlConfiguration loadedCfg = new YamlConfiguration(); + loadedCfg.set("should-be-overwritten", "foo"); + loadedCfg.load(tmpFile); + + assertEquals(cfg.values, loadedCfg.values); + } + + @Test + void testLoadWithInvalidYaml() { + final YamlConfiguration cfg = new YamlConfiguration(); + + IllegalStateException exception = assertThrowsExactly(IllegalStateException.class, + () -> cfg.load(new StringReader("Hello world"))); + + assertEquals("The YAML file does not have the expected tree structure", exception.getMessage()); + } + + @Test + void testHeaderComments() throws IOException { + String expectedHeaderComment = "This is a header comment"; + + YamlConfiguration cfg = new YamlConfiguration(); + cfg.setHeaderComment(expectedHeaderComment); + cfg.set("foo", "bar"); + + assertNotNull(cfg.getHeaderComment()); + assertEquals(expectedHeaderComment, cfg.getHeaderComment().get()); + + assertEquals("# " + expectedHeaderComment + "\n\nfoo: bar\n", cfg.toYamlString()); + } + + @Test + void testNodeComments() throws IOException { + String expectedYaml = "# Foo-Comment\n" + + "foo: bar\n" + + "# Level1-Comment\n" + + "level1:\n" + + " level2:\n" + + " # Level3-Comment\n" + + " level3: value\n"; + + YamlConfiguration cfg = new YamlConfiguration(); + cfg.set("foo", "bar"); + cfg.set("level1.level2.level3", "value"); + + cfg.setNodeComment("foo", "Foo-Comment"); + cfg.setNodeComment("level1", "Level1-Comment"); + cfg.setNodeComment("level1.level2.level3", "Level3-Comment"); + + Supplier currentNodeComment = cfg.getNodeComment("foo"); + assertNotNull(currentNodeComment); + assertEquals("Foo-Comment", currentNodeComment.get()); + + currentNodeComment = cfg.getNodeComment("level1"); + assertNotNull(currentNodeComment); + assertEquals("Level1-Comment", currentNodeComment.get()); + + currentNodeComment = cfg.getNodeComment("level1.level2"); + assertNull(currentNodeComment); + + currentNodeComment = cfg.getNodeComment("level1.level2.level3"); + assertNotNull(currentNodeComment); + assertEquals("Level3-Comment", currentNodeComment.get()); + + assertEquals(expectedYaml, cfg.toYamlString()); + } +}