Replace Songoda's YAML Configuration wrapper with an own implementation

Because Spigot 1.18 still hasn't fixed a critical bug like PaperMC did, I recoded the current YAML Configuration classes and access SnakeYaml directly instead of using the Spigot wrapper.
This implementation approach also allows for adding node comments using the lib instead of some woodo string manipulation.


#41
// I might move this into my own library in the future, lets see :p
This commit is contained in:
Christian Koop 2022-04-27 21:40:21 +02:00
parent 2a037e2853
commit 6d6fa7210a
No known key found for this signature in database
GPG Key ID: 89A8181384E010A3
15 changed files with 1130 additions and 2557 deletions

View File

@ -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<String> lines = new ArrayList<>();
CommentStyle commentStyle = null;
public Comment() {
}
public Comment(String... lines) {
this(null, Arrays.asList(lines));
}
public Comment(List<String> lines) {
this(null, lines);
}
public Comment(CommentStyle commentStyle, String... lines) {
this(commentStyle, Arrays.asList(lines));
}
public Comment(CommentStyle commentStyle, List<String> 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<String> getLines() {
return lines;
}
@Override
public String toString() {
return lines.isEmpty() ? "" : String.join("\n", lines);
}
public static Comment loadComment(List<String> 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");
}
}
}

View File

@ -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<String, Object> serialize();
// Class must contain one of:
// public static Object deserialize(@NotNull Map<String, ?> args);
// public static valueOf(Map<String, ?> args);
// public new (Map<String, ?> 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. <br>
* 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? <br>
* 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. <br>
* 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. <br>
* 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. <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.
*
* @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<String> 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<String> description) {
if (description == null || description.isEmpty()) {
headerComment = null;
} else {
headerComment = new Comment(commentStyle, description);
}
return this;
}
@NotNull
public List<String> 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<String> currentPath = new LinkedList<>();
ArrayList<String> 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<String> 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<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
public void run() {
saveChanges();
}
}
}

View File

@ -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<String> getKeys(boolean deep) {
return config.getKeys(deep);
}
@Override
public Map<String, Object> 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<String> 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<String> 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<String> 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<ConfigSection> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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);
}
}

View File

@ -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, " ", ""),
/**
* # <br />
* # Comment <br />
* # <br />
*/
SPACED(false, true, " ", ""),
/**
* ########### <br />
* # Comment # <br />
* ########### <br />
*/
BLOCKED(true, false, " ", " "),
/**
* ############# <br />
* #|¯¯¯¯¯¯¯¯¯|# <br />
* #| Comment |# <br />
* #|_________|# <br />
* ############# <br />
*/
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<String> 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;
}
}

View File

@ -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;
}
}

View File

@ -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<String, Comment> configComments;
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();
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. <br>
* 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. <br />
* <b>DO NOT USE THIS IN A SYNCHRONIZED LOCK</b>
*
* @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<String, Object> 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<String> 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<String> 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<String> 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<String, Object> 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<String> getKeys(boolean deep) {
LinkedHashSet<String> 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<String, Object> getValues(boolean deep) {
LinkedHashMap<String, Object> result = new LinkedHashMap<>();
int pathIndex = fullPath.lastIndexOf(root.pathChar);
if (deep) {
result.putAll((Map<String, Object>) 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<String, Object>) 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<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),
Map.Entry::getValue,
(v1, v2) -> {
throw new IllegalStateException();
}, // never going to be merging keys
LinkedHashMap::new)));
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),
Map.Entry::getValue,
(v1, v2) -> {
throw new IllegalStateException();
}, // never going to be merging keys
LinkedHashMap::new)));
}
return result;
}
@NotNull
public List<ConfigSection> getSections(String path) {
ConfigSection rootSection = getConfigurationSection(path);
if (rootSection == null) {
return Collections.emptyList();
}
ArrayList<ConfigSection> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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> T getObject(@NotNull String path, @NotNull Class<T> clazz) {
Object result = get(path);
return clazz.isInstance(result) ? clazz.cast(result) : null;
}
@Nullable
@Override
public <T> T getObject(@NotNull String path, @NotNull Class<T> 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);
}
}

View File

@ -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<Integer> getIntegerList() {
return config.getIntegerList(key);
}
public List<String> 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> T getObject(@NotNull Class<T> clazz) {
return config.getObject(key, clazz);
}
public <T> T getObject(@NotNull Class<T> 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;
}
}

View File

@ -1,30 +0,0 @@
package com.songoda.core.configuration;
import org.bukkit.configuration.ConfigurationSection;
public interface DataStoreObject<T> {
/**
* @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);
}

View File

@ -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<String> comment);
default void setHeaderComment(@Nullable String comment) {
setHeaderComment(() -> comment);
}
@Nullable Supplier<String> getHeaderComment();
@NotNull String generateHeaderCommentLines();
}

View File

@ -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;
}

View File

@ -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<String> comment);
default void setNodeComment(@NotNull String key, @Nullable String comment) {
setNodeComment(key, () -> comment);
}
@Nullable Supplier<String> getNodeComment(@Nullable String key);
}

View File

@ -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 <T> DataObject class that is used to store the data
*/
public class SimpleDataStore<T extends DataStoreObject> {
protected final Plugin plugin;
protected final String filename, dirName;
private final Function<ConfigurationSection, T> getFromSection;
protected final HashMap<Object, T> 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<ConfigurationSection, T> 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<ConfigurationSection, T> 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<Object, T> 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 <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
*/
@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 <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
*/
@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 <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
*/
@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 <tt>value.getKey()</tt>, or
* <tt>null</tt> if there was no mapping for <tt>value.getKey()</tt>.
*/
@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<T> 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();
}
}
}

View File

@ -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<String, Supplier<String>> nodeComments;
public YamlCommentRepresenter(Map<String, Supplier<String>> 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<String> innerValue = this.nodeComments.get(key);
if (innerValue != null) {
scalarNode.setBlockComments(Collections.singletonList(new CommentLine(new CommentEvent(CommentType.BLOCK, " " + innerValue.get(), null, null))));
}
}
}

View File

@ -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<String, Object> values;
protected final @NotNull Map<String, Supplier<String>> nodeComments;
protected @Nullable Supplier<String> headerComment;
public YamlConfiguration() {
this(new HashMap<>(), new HashMap<>());
}
protected YamlConfiguration(@NotNull Map<String, Object> values, @NotNull Map<String, Supplier<String>> 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<String, ?> 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<String> getKeys(String key) {
if (key == null) {
return Collections.emptySet();
}
Map<String, ?> 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<Integer> newValue = new ArrayList<>(((short[]) value).length);
for (Short s : (short[]) value) {
newValue.add(s.intValue());
}
value = newValue;
} else if (value instanceof byte[]) {
List<Integer> 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<Double> 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<String> 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<String, ?> 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<String> comment) {
this.headerComment = comment;
}
@Override
public @Nullable Supplier<String> 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<String> comment) {
this.nodeComments.put(key, comment);
}
@Override
public @Nullable Supplier<String> 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<String, Object> map, @NotNull String key, @Nullable Object value) {
String[] fullKeyPath = key.split("\\.");
Map<String, ?> innerMap = getInnerMap(map, Arrays.copyOf(fullKeyPath, fullKeyPath.length - 1), true);
return ((Map<String, Object>) innerMap).put(fullKeyPath[fullKeyPath.length - 1], value);
}
protected static Object getInnerValueForKey(@NotNull Map<String, Object> map, @NotNull String key) {
String[] fullKeyPath = key.split("\\.");
Map<String, ?> 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<String, ?> getInnerMap(@NotNull Map<String, ?> map, @NotNull String[] keys, boolean createMissingMaps) {
if (keys.length == 0) {
return map;
}
int currentKeyIndex = 0;
Map<String, ?> currentMap = map;
while (true) {
Object currentValue = currentMap.get(keys[currentKeyIndex]);
if (currentValue == null) {
if (!createMissingMaps) {
return null;
}
currentValue = new HashMap<>();
((Map<String, Object>) 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<String, Object>) currentMap).put(keys[currentKeyIndex], currentValue);
}
if (currentKeyIndex == keys.length - 1) {
return (Map<String, ?>) currentValue;
}
currentMap = (Map<String, ?>) currentValue;
++currentKeyIndex;
}
}
}

View File

@ -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<String, Object> expectedValues = new HashMap<String, Object>() {{
put("number", 27);
put("foo", new HashMap<String, Object>() {{
put("bar", "baz");
}});
put("bar", new HashMap<String, Object>() {{
put("foo", new HashMap<String, Object>() {{
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<String> 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());
}
}