diff --git a/src/main/java/com/songoda/core/settingsv2/Comment.java b/src/main/java/com/songoda/core/settingsv2/Comment.java index 11a9c58b..a4689411 100644 --- a/src/main/java/com/songoda/core/settingsv2/Comment.java +++ b/src/main/java/com/songoda/core/settingsv2/Comment.java @@ -29,6 +29,16 @@ public class Comment { this.lines.addAll(lines); } + public Comment(CommentStyle commentStyle, String... lines) { + this.commentStyle = commentStyle; + this.lines.addAll(Arrays.asList(lines)); + } + + public Comment(CommentStyle commentStyle, List lines) { + this.commentStyle = commentStyle; + this.lines.addAll(lines); + } + public CommentStyle getCommentStyle() { return commentStyle; } diff --git a/src/main/java/com/songoda/core/settingsv2/Config.java b/src/main/java/com/songoda/core/settingsv2/Config.java index 9d828b68..9c6c2d83 100644 --- a/src/main/java/com/songoda/core/settingsv2/Config.java +++ b/src/main/java/com/songoda/core/settingsv2/Config.java @@ -12,15 +12,21 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; +import java.io.StringReader; import java.io.StringWriter; +import java.io.Writer; 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; import org.apache.commons.lang.Validate; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.YamlConstructor; @@ -61,6 +67,7 @@ public class Config extends SongodaConfigurationSection { final Yaml yaml = new Yaml(new YamlConstructor(), yamlRepresenter, yamlOptions); SaveTask saveTask; Timer autosaveTimer; + ////////////// Config settings //////////////// /** * save file whenever a change is made */ @@ -73,6 +80,28 @@ public class Config extends SongodaConfigurationSection { * remove nodes not defined in defaults */ boolean autoremove = false; + /** + * load comments when loading the file + * TODO + */ + boolean loadComments = false; + /** + * 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(@NotNull File file) { this.plugin = null; @@ -109,6 +138,7 @@ public class Config extends SongodaConfigurationSection { * @param autosave set to true if autosaving is enabled. * @return this class */ + @NotNull public Config setAutosave(boolean autosave) { this.autosave = autosave; return this; @@ -135,17 +165,76 @@ public class Config extends SongodaConfigurationSection { /** * 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. + * 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 + */ + public ConfigFormattingRules.CommentStyle getDefaultNodeCommentFormat() { + return defaultNodeCommentFormat; + } + + /** + * Default comment applied to config nodes + */ + public void setDefaultNodeCommentFormat(ConfigFormattingRules.CommentStyle defaultNodeCommentFormat) { + this.defaultNodeCommentFormat = defaultNodeCommentFormat; + } + + /** + * Default comment applied to section nodes + */ + public ConfigFormattingRules.CommentStyle getDefaultSectionCommentFormat() { + return defaultSectionCommentFormat; + } + + /** + * Default comment applied to section nodes + */ + public void setDefaultSectionCommentFormat(ConfigFormattingRules.CommentStyle defaultSectionCommentFormat) { + this.defaultSectionCommentFormat = defaultSectionCommentFormat; + } + + /** + * Extra lines to put between root nodes + */ + public int getRootNodeSpacing() { + return rootNodeSpacing; + } + + /** + * Extra lines to put between root nodes + */ + public void setRootNodeSpacing(int rootNodeSpacing) { + this.rootNodeSpacing = rootNodeSpacing; + } + + /** + * 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. + */ + public void setCommentSpacing(int commentSpacing) { + this.commentSpacing = commentSpacing; + } + @NotNull public Config setHeader(@NotNull String... description) { if (description.length == 0) { @@ -156,6 +245,16 @@ public class Config extends SongodaConfigurationSection { return this; } + @NotNull + public Config setHeader(@Nullable ConfigFormattingRules.CommentStyle commentStyle, @NotNull String... description) { + if (description.length == 0) { + configComments.remove(null); + } else { + configComments.put(null, new Comment(commentStyle, description)); + } + return this; + } + @NotNull public Config setHeader(@Nullable List description) { if (description == null || description.isEmpty()) { @@ -166,6 +265,16 @@ public class Config extends SongodaConfigurationSection { return this; } + @NotNull + public Config setHeader(@Nullable ConfigFormattingRules.CommentStyle commentStyle, @Nullable List description) { + if (description == null || description.isEmpty()) { + configComments.remove(null); + } else { + configComments.put(null, new Comment(commentStyle, description)); + } + return this; + } + @NotNull public List getHeader() { if (configComments.containsKey(null)) { @@ -208,7 +317,9 @@ public class Config extends SongodaConfigurationSection { throw new InvalidConfigurationException("Top level is not a Map."); } if (input != null) { - this.parseComments(contents, input); + if(loadComments) { + this.parseComments(contents, input); + } this.convertMapsToSections(input, this); } } @@ -226,9 +337,10 @@ public class Config extends SongodaConfigurationSection { } protected void parseComments(@NotNull String contents, @NotNull Map input) { - // TODO + // TODO? // 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) } public void deleteNonDefaultSettings() { @@ -272,6 +384,13 @@ public class Config extends SongodaConfigurationSection { } public boolean save() { + if(saveTask != null) { + //Close Threads + saveTask.cancel(); + autosaveTimer.cancel(); + saveTask = null; + autosaveTimer = null; + } return save(file); } @@ -307,14 +426,13 @@ public class Config extends SongodaConfigurationSection { Comment header = configComments.get(null); if (header != null) { header.writeComment(str, 0, ConfigFormattingRules.CommentStyle.SPACED); + str.write("\n"); // add one space after the header } String dump = yaml.dump(this.getValues(false)); - if (dump.equals(BLANK_CONFIG)) { - dump = ""; - } else { - // line-by-line apply line spacing formatting and comments per-node + if (!dump.equals(BLANK_CONFIG)) { + writeComments(dump, str); } - return str.toString() + dump; + return str.toString(); } catch (Throwable ex) { Logger.getLogger(Config.class.getName()).log(Level.SEVERE, "Error saving config", ex); delaySave(); @@ -322,6 +440,96 @@ public class Config extends SongodaConfigurationSection { 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 diff --git a/src/main/java/com/songoda/core/settingsv2/SongodaConfigurationSection.java b/src/main/java/com/songoda/core/settingsv2/SongodaConfigurationSection.java index 86aae5a7..3115091d 100644 --- a/src/main/java/com/songoda/core/settingsv2/SongodaConfigurationSection.java +++ b/src/main/java/com/songoda/core/settingsv2/SongodaConfigurationSection.java @@ -2,6 +2,7 @@ package com.songoda.core.settingsv2; import com.songoda.core.settingsv2.adapters.ConfigDefaultsAdapter; import com.songoda.core.settingsv2.adapters.ConfigOptionsAdapter; +import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -29,31 +30,33 @@ public class SongodaConfigurationSection extends MemoryConfiguration { protected int indentation = 2; // between 2 and 9 (inclusive) protected char pathChar = '.'; final HashMap configComments; - final HashMap strictKeys; + 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(); SongodaConfigurationSection() { this.root = this; this.parent = null; + isDefault = false; fullPath = ""; configComments = new HashMap(); - strictKeys = new HashMap(); + defaultComments = new HashMap(); defaults = new LinkedHashMap(); values = new LinkedHashMap(); } - SongodaConfigurationSection(SongodaConfigurationSection root, SongodaConfigurationSection parent, String path) { + SongodaConfigurationSection(SongodaConfigurationSection root, SongodaConfigurationSection parent, String path, boolean isDefault) { this.root = root; this.parent = parent; - fullPath = parent.fullPath + path + root.pathChar; - configComments = null; - strictKeys = null; + this.fullPath = parent.fullPath + path + root.pathChar; + this.isDefault = isDefault; + configComments = defaultComments = null; defaults = null; values = null; } @@ -66,12 +69,8 @@ public class SongodaConfigurationSection extends MemoryConfiguration { root.indentation = indentation; } - public char getPathSeparator() { - return root.pathChar; - } - protected void onChange() { - if(parent != null) { + if (parent != null) { root.onChange(); } } @@ -83,18 +82,75 @@ public class SongodaConfigurationSection extends MemoryConfiguration { * @param pathChar character to use */ public void setPathSeparator(char pathChar) { - if(!root.values.isEmpty() || !root.defaults.isEmpty()) + if (!root.values.isEmpty() || !root.defaults.isEmpty()) throw new RuntimeException("Path change after config initialization"); root.pathChar = pathChar; } + public char getPathSeparator() { + return root.pathChar; + } + + @NotNull + public SongodaConfigurationSection createDefaultSection(@NotNull String path) { + SongodaConfigurationSection section = new SongodaConfigurationSection(root, this, path, true); + synchronized (root.lock) { + root.defaults.put(fullPath + path, section); + } + return section; + } + + @NotNull + public SongodaConfigurationSection createDefaultSection(@NotNull String path, String... comment) { + SongodaConfigurationSection section = new SongodaConfigurationSection(root, this, path, true); + synchronized (root.lock) { + root.defaults.put(fullPath + path, section); + } + return section; + } + + @NotNull + public SongodaConfigurationSection setComment(@NotNull String path, @Nullable ConfigFormattingRules.CommentStyle commentStyle, String... lines) { + return setComment(path, commentStyle, lines.length == 0 ? (List) null : Arrays.asList(lines)); + } + + @NotNull + public SongodaConfigurationSection setComment(@NotNull String path, @Nullable ConfigFormattingRules.CommentStyle commentStyle, @Nullable List lines) { + synchronized (root.lock) { + if (isDefault) { + root.defaultComments.put(fullPath + path, lines != null ? new Comment(commentStyle, lines) : null); + } else { + root.configComments.put(fullPath + path, lines != null ? new Comment(commentStyle, lines) : null); + } + } + return this; + } + + @NotNull + public SongodaConfigurationSection setDefaultComment(@NotNull String path, String... lines) { + return setDefaultComment(path, lines.length == 0 ? (List) null : Arrays.asList(lines)); + } + + @NotNull + public SongodaConfigurationSection setDefaultComment(@NotNull String path, @Nullable List lines) { + synchronized (root.lock) { + root.defaultComments.put(fullPath + path, new Comment(lines)); + } + 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; + } + @Override public void addDefault(@NotNull String path, @Nullable Object value) { root.defaults.put(fullPath + path, value); - if(!root.changed) { - root.changed = root.values.get(fullPath + path) == null; - } - onChange(); } @Override @@ -104,7 +160,7 @@ public class SongodaConfigurationSection extends MemoryConfiguration { @Override public void setDefaults(Configuration c) { - if(fullPath.isEmpty()) { + if (fullPath.isEmpty()) { root.defaults.clear(); } else { root.defaults.keySet().stream() @@ -135,23 +191,23 @@ public class SongodaConfigurationSection extends MemoryConfiguration { LinkedHashSet result = new LinkedHashSet(); int pathIndex = fullPath.lastIndexOf(root.pathChar); if (deep) { - result.addAll(root.values.keySet().stream() + 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.defaults.keySet().stream() + 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.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))); result.addAll(root.defaults.keySet().stream() .filter(k -> k.startsWith(fullPath) && k.lastIndexOf(root.pathChar) == pathIndex + 1) .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; } @@ -177,14 +233,14 @@ public class SongodaConfigurationSection extends MemoryConfiguration { (v1, v2) -> { throw new IllegalStateException(); }, // never going to be merging keys LinkedHashMap::new))); } else { - result.putAll((Map) root.values.entrySet().stream() + 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), e -> e.getValue(), (v1, v2) -> { throw new IllegalStateException(); }, // never going to be merging keys LinkedHashMap::new))); - result.putAll((Map) root.defaults.entrySet().stream() + 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), @@ -217,7 +273,7 @@ public class SongodaConfigurationSection extends MemoryConfiguration { @Override public String getName() { - if(fullPath.isEmpty()) + if (fullPath.isEmpty()) return ""; String[] parts = fullPath.split(Pattern.quote(String.valueOf(root.pathChar))); return parts[parts.length - 1]; @@ -252,20 +308,66 @@ public class SongodaConfigurationSection extends MemoryConfiguration { @Override public void set(@NotNull String path, @Nullable Object value) { - synchronized(root.lock) { - if (value != null) { - root.changed |= root.values.put(fullPath + path, value) != value; - } else { - root.changed |= root.values.remove(fullPath + path) != null; + if (isDefault) { + root.defaults.put(fullPath + path, value); + } else { + synchronized (root.lock) { + if (value != null) { + root.changed |= root.values.put(fullPath + path, value) != value; + } else { + root.changed |= root.values.remove(fullPath + path) != null; + } } + onChange(); } - onChange(); + } + + @NotNull + public SongodaConfigurationSection set(@NotNull String path, @Nullable Object value, String ... comment) { + set(path, value); + return setComment(path, null, comment); + } + + @NotNull + public SongodaConfigurationSection set(@NotNull String path, @Nullable Object value, List comment) { + set(path, value); + return setComment(path, null, comment); + } + + @NotNull + public SongodaConfigurationSection set(@NotNull String path, @Nullable Object value, @Nullable ConfigFormattingRules.CommentStyle commentStyle, String ... comment) { + set(path, value); + return setComment(path, commentStyle, comment); + } + + @NotNull + public SongodaConfigurationSection set(@NotNull String path, @Nullable Object value, @Nullable ConfigFormattingRules.CommentStyle commentStyle, List comment) { + set(path, value); + return setComment(path, commentStyle, comment); + } + + @NotNull + public SongodaConfigurationSection setDefault(@NotNull String path, @Nullable Object value) { + addDefault(path, value); + return this; + } + + @NotNull + public SongodaConfigurationSection setDefault(@NotNull String path, @Nullable Object value, String ... comment) { + addDefault(path, value); + return setDefaultComment(path, comment); + } + + @NotNull + public SongodaConfigurationSection setDefault(@NotNull String path, @Nullable Object value, List comment) { + addDefault(path, value); + return setDefaultComment(path, comment); } @NotNull @Override public SongodaConfigurationSection createSection(@NotNull String path) { - SongodaConfigurationSection section = new SongodaConfigurationSection(root, this, path); + SongodaConfigurationSection section = new SongodaConfigurationSection(root, this, path, false); synchronized(root.lock) { root.values.put(fullPath + path, section); } @@ -274,11 +376,38 @@ public class SongodaConfigurationSection extends MemoryConfiguration { return section; } + @NotNull + public SongodaConfigurationSection createSection(@NotNull String path, String... comment) { + return createSection(path, null, comment.length == 0 ? (List) null : Arrays.asList(comment)); + } + + @NotNull + public SongodaConfigurationSection createSection(@NotNull String path, @Nullable List comment) { + return createSection(path, null, comment); + } + + @NotNull + public SongodaConfigurationSection createSection(@NotNull String path, @Nullable ConfigFormattingRules.CommentStyle commentStyle, String... comment) { + return createSection(path, commentStyle, comment.length == 0 ? (List) null : Arrays.asList(comment)); + } + + @NotNull + public SongodaConfigurationSection createSection(@NotNull String path, @Nullable ConfigFormattingRules.CommentStyle commentStyle, @Nullable List comment) { + SongodaConfigurationSection section = new SongodaConfigurationSection(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 SongodaConfigurationSection createSection(@NotNull String path, Map map) { - SongodaConfigurationSection section = new SongodaConfigurationSection(root, this, path); - synchronized(root.lock) { + SongodaConfigurationSection section = new SongodaConfigurationSection(root, this, path, false); + synchronized (root.lock) { root.values.put(fullPath + path, section); } for (Map.Entry entry : map.entrySet()) { @@ -388,4 +517,9 @@ public class SongodaConfigurationSection extends MemoryConfiguration { Object result = get(path); return result instanceof SongodaConfigurationSection ? (SongodaConfigurationSection) result : null; } + + public SongodaConfigurationSection getOrCreateConfigurationSection(@NotNull String path) { + Object result = get(path); + return result instanceof SongodaConfigurationSection ? (SongodaConfigurationSection) result : createSection(path); + } }