add comment system to new config

This commit is contained in:
jascotty2 2019-08-30 15:12:27 -05:00
parent 7c3936cdf8
commit 167e8c0faf
3 changed files with 396 additions and 44 deletions

View File

@ -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<String> lines) {
this.commentStyle = commentStyle;
this.lines.addAll(lines);
}
public CommentStyle getCommentStyle() {
return commentStyle;
}

View File

@ -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. <br>
* 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. <br>
* 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. <br>
* This is separate from rootNodeSpacing, if applicable.
*/
public int getCommentSpacing() {
return commentSpacing;
}
/**
* Extra lines to put in front of comments. <br>
* 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<String> 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<String> description) {
if (description == null || description.isEmpty()) {
configComments.remove(null);
} else {
configComments.put(null, new Comment(commentStyle, description));
}
return this;
}
@NotNull
public List<String> 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<String> 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

View File

@ -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<String, Comment> configComments;
final HashMap<String, Class> strictKeys;
final HashMap<String, Comment> defaultComments;
final LinkedHashMap<String, Object> defaults;
final LinkedHashMap<String, Object> 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<String> 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<String> 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<String> 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<String, Object>) root.values.entrySet().stream()
result.putAll((Map<String, Object>) 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<String, Object>) root.defaults.entrySet().stream()
result.putAll((Map<String, Object>) 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<String> 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<String> 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<String> 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<String> 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<String> 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);
}
}