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()); + } +}