diff --git a/src/main/java/org/bukkit/configuration/ConfigurationSection.java b/src/main/java/org/bukkit/configuration/ConfigurationSection.java
index 715ef162..b6b00af0 100644
--- a/src/main/java/org/bukkit/configuration/ConfigurationSection.java
+++ b/src/main/java/org/bukkit/configuration/ConfigurationSection.java
@@ -996,4 +996,66 @@ public interface ConfigurationSection {
* @throws IllegalArgumentException Thrown if path is null.
*/
public void addDefault(@NotNull String path, @Nullable Object value);
+
+ /**
+ * Gets the requested comment list by path.
+ *
+ * If no comments exist, an empty list will be returned. A null entry
+ * represents an empty line and an empty String represents an empty comment
+ * line.
+ *
+ * @param path Path of the comments to get.
+ * @return A unmodifiable list of the requested comments, every entry
+ * represents one line.
+ */
+ @NotNull
+ public List getComments(@NotNull String path);
+
+ /**
+ * Gets the requested inline comment list by path.
+ *
+ * If no comments exist, an empty list will be returned. A null entry
+ * represents an empty line and an empty String represents an empty comment
+ * line.
+ *
+ * @param path Path of the comments to get.
+ * @return A unmodifiable list of the requested comments, every entry
+ * represents one line.
+ */
+ @NotNull
+ public List getInlineComments(@NotNull String path);
+
+ /**
+ * Sets the comment list at the specified path.
+ *
+ * If value is null, the comments will be removed. A null entry is an empty
+ * line and an empty String entry is an empty comment line. If the path does
+ * not exist, no comments will be set. Any existing comments will be
+ * replaced, regardless of what the new comments are.
+ *
+ * Some implementations may have limitations on what persists. See their
+ * individual javadocs for details.
+ *
+ * @param path Path of the comments to set.
+ * @param comments New comments to set at the path, every entry represents
+ * one line.
+ */
+ public void setComments(@NotNull String path, @Nullable List comments);
+
+ /**
+ * Sets the inline comment list at the specified path.
+ *
+ * If value is null, the comments will be removed. A null entry is an empty
+ * line and an empty String entry is an empty comment line. If the path does
+ * not exist, no comment will be set. Any existing comments will be
+ * replaced, regardless of what the new comments are.
+ *
+ * Some implementations may have limitations on what persists. See their
+ * individual javadocs for details.
+ *
+ * @param path Path of the comments to set.
+ * @param comments New comments to set at the path, every entry represents
+ * one line.
+ */
+ public void setInlineComments(@NotNull String path, @Nullable List comments);
}
diff --git a/src/main/java/org/bukkit/configuration/MemorySection.java b/src/main/java/org/bukkit/configuration/MemorySection.java
index a6ac5d45..28b17198 100644
--- a/src/main/java/org/bukkit/configuration/MemorySection.java
+++ b/src/main/java/org/bukkit/configuration/MemorySection.java
@@ -2,6 +2,7 @@ package org.bukkit.configuration;
import static org.bukkit.util.NumberConversions.*;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -22,7 +23,7 @@ import org.jetbrains.annotations.Nullable;
* A type of {@link ConfigurationSection} that is stored in memory.
*/
public class MemorySection implements ConfigurationSection {
- protected final Map map = new LinkedHashMap();
+ protected final Map map = new LinkedHashMap();
private final Configuration root;
private final ConfigurationSection parent;
private final String path;
@@ -217,7 +218,12 @@ public class MemorySection implements ConfigurationSection {
if (value == null) {
map.remove(key);
} else {
- map.put(key, value);
+ SectionPathData entry = map.get(key);
+ if (entry == null) {
+ map.put(key, new SectionPathData(value));
+ } else {
+ entry.setData(value);
+ }
}
} else {
section.set(key, value);
@@ -263,8 +269,8 @@ public class MemorySection implements ConfigurationSection {
String key = path.substring(i2);
if (section == this) {
- Object result = map.get(key);
- return (result == null) ? def : result;
+ SectionPathData result = map.get(key);
+ return (result == null) ? def : result.getData();
}
return section.get(key, def);
}
@@ -296,7 +302,7 @@ public class MemorySection implements ConfigurationSection {
String key = path.substring(i2);
if (section == this) {
ConfigurationSection result = new MemorySection(this, key);
- map.put(key, result);
+ map.put(key, new SectionPathData(result));
return result;
}
return section.createSection(key);
@@ -860,11 +866,11 @@ public class MemorySection implements ConfigurationSection {
if (section instanceof MemorySection) {
MemorySection sec = (MemorySection) section;
- for (Map.Entry entry : sec.map.entrySet()) {
+ for (Map.Entry entry : sec.map.entrySet()) {
output.add(createPath(section, entry.getKey(), this));
- if ((deep) && (entry.getValue() instanceof ConfigurationSection)) {
- ConfigurationSection subsection = (ConfigurationSection) entry.getValue();
+ if ((deep) && (entry.getValue().getData() instanceof ConfigurationSection)) {
+ ConfigurationSection subsection = (ConfigurationSection) entry.getValue().getData();
mapChildrenKeys(output, subsection, deep);
}
}
@@ -881,17 +887,17 @@ public class MemorySection implements ConfigurationSection {
if (section instanceof MemorySection) {
MemorySection sec = (MemorySection) section;
- for (Map.Entry entry : sec.map.entrySet()) {
+ for (Map.Entry entry : sec.map.entrySet()) {
// Because of the copyDefaults call potentially copying out of order, we must remove and then add in our saved order
// This means that default values we haven't set end up getting placed first
// See SPIGOT-4558 for an example using spigot.yml - watch subsections move around to default order
String childPath = createPath(section, entry.getKey(), this);
output.remove(childPath);
- output.put(childPath, entry.getValue());
+ output.put(childPath, entry.getValue().getData());
- if (entry.getValue() instanceof ConfigurationSection) {
+ if (entry.getValue().getData() instanceof ConfigurationSection) {
if (deep) {
- mapChildrenValues(output, (ConfigurationSection) entry.getValue(), deep);
+ mapChildrenValues(output, (ConfigurationSection) entry.getValue().getData(), deep);
}
}
}
@@ -942,14 +948,11 @@ public class MemorySection implements ConfigurationSection {
char separator = root.options().pathSeparator();
StringBuilder builder = new StringBuilder();
- if (section != null) {
- for (ConfigurationSection parent = section; (parent != null) && (parent != relativeTo); parent = parent.getParent()) {
- if (builder.length() > 0) {
- builder.insert(0, separator);
- }
-
- builder.insert(0, parent.getName());
+ for (ConfigurationSection parent = section; (parent != null) && (parent != relativeTo); parent = parent.getParent()) {
+ if (builder.length() > 0) {
+ builder.insert(0, separator);
}
+ builder.insert(0, parent.getName());
}
if ((key != null) && (key.length() > 0)) {
@@ -963,6 +966,69 @@ public class MemorySection implements ConfigurationSection {
return builder.toString();
}
+ @Override
+ @NotNull
+ public List getComments(@NotNull final String path) {
+ final SectionPathData pathData = getSectionPathData(path);
+ return pathData == null ? Collections.emptyList() : pathData.getComments();
+ }
+
+ @Override
+ @NotNull
+ public List getInlineComments(@NotNull final String path) {
+ final SectionPathData pathData = getSectionPathData(path);
+ return pathData == null ? Collections.emptyList() : pathData.getInlineComments();
+ }
+
+ @Override
+ public void setComments(@NotNull final String path, @Nullable final List comments) {
+ final SectionPathData pathData = getSectionPathData(path);
+ if (pathData != null) {
+ pathData.setComments(comments);
+ }
+ }
+
+ @Override
+ public void setInlineComments(@NotNull final String path, @Nullable final List comments) {
+ final SectionPathData pathData = getSectionPathData(path);
+ if (pathData != null) {
+ pathData.setInlineComments(comments);
+ }
+ }
+
+ @Nullable
+ private SectionPathData getSectionPathData(@NotNull String path) {
+ Validate.notNull(path, "Path cannot be null");
+
+ Configuration root = getRoot();
+ if (root == null) {
+ throw new IllegalStateException("Cannot access section without a root");
+ }
+
+ final char separator = root.options().pathSeparator();
+ // i1 is the leading (higher) index
+ // i2 is the trailing (lower) index
+ int i1 = -1, i2;
+ ConfigurationSection section = this;
+ while ((i1 = path.indexOf(separator, i2 = i1 + 1)) != -1) {
+ section = section.getConfigurationSection(path.substring(i2, i1));
+ if (section == null) {
+ return null;
+ }
+ }
+
+ String key = path.substring(i2);
+ if (section == this) {
+ SectionPathData entry = map.get(key);
+ if (entry != null) {
+ return entry;
+ }
+ } else if (section instanceof MemorySection) {
+ return ((MemorySection) section).getSectionPathData(key);
+ }
+ return null;
+ }
+
@Override
public String toString() {
Configuration root = getRoot();
diff --git a/src/main/java/org/bukkit/configuration/SectionPathData.java b/src/main/java/org/bukkit/configuration/SectionPathData.java
new file mode 100644
index 00000000..54647c81
--- /dev/null
+++ b/src/main/java/org/bukkit/configuration/SectionPathData.java
@@ -0,0 +1,81 @@
+package org.bukkit.configuration;
+
+import java.util.Collections;
+import java.util.List;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+final class SectionPathData {
+
+ private Object data;
+ private List comments;
+ private List inlineComments;
+
+ public SectionPathData(@Nullable Object data) {
+ this.data = data;
+ comments = Collections.emptyList();
+ inlineComments = Collections.emptyList();
+ }
+
+ @Nullable
+ public Object getData() {
+ return data;
+ }
+
+ public void setData(@Nullable final Object data) {
+ this.data = data;
+ }
+
+ /**
+ * If no comments exist, an empty list will be returned. A null entry in the
+ * list represents an empty line and an empty String represents an empty
+ * comment line.
+ *
+ * @return A unmodifiable list of the requested comments, every entry
+ * represents one line.
+ */
+ @NotNull
+ public List getComments() {
+ return comments;
+ }
+
+ /**
+ * Represents the comments on a {@link ConfigurationSection} entry.
+ *
+ * A null entry in the List is an empty line and an empty String entry is an
+ * empty comment line. Any existing comments will be replaced, regardless of
+ * what the new comments are.
+ *
+ * @param comments New comments to set every entry represents one line.
+ */
+ public void setComments(@Nullable final List comments) {
+ this.comments = (comments == null) ? Collections.emptyList() : Collections.unmodifiableList(comments);
+ }
+
+ /**
+ * If no comments exist, an empty list will be returned. A null entry in the
+ * list represents an empty line and an empty String represents an empty
+ * comment line.
+ *
+ * @return A unmodifiable list of the requested comments, every entry
+ * represents one line.
+ */
+ @NotNull
+ public List getInlineComments() {
+ return inlineComments;
+ }
+
+ /**
+ * Represents the comments on a {@link ConfigurationSection} entry.
+ *
+ * A null entry in the List is an empty line and an empty String entry is an
+ * empty comment line. Any existing comments will be replaced, regardless of
+ * what the new comments are.
+ *
+ * @param inlineComments New comments to set every entry represents one
+ * line.
+ */
+ public void setInlineComments(@Nullable final List inlineComments) {
+ this.inlineComments = (inlineComments == null) ? Collections.emptyList() : Collections.unmodifiableList(inlineComments);
+ }
+}
diff --git a/src/main/java/org/bukkit/configuration/file/FileConfiguration.java b/src/main/java/org/bukkit/configuration/file/FileConfiguration.java
index 581889ff..50c58f15 100644
--- a/src/main/java/org/bukkit/configuration/file/FileConfiguration.java
+++ b/src/main/java/org/bukkit/configuration/file/FileConfiguration.java
@@ -202,17 +202,17 @@ public abstract class FileConfiguration extends MemoryConfiguration {
public abstract void loadFromString(@NotNull String contents) throws InvalidConfigurationException;
/**
- * Compiles the header for this {@link FileConfiguration} and returns the
- * result.
- *
- * This will use the header from {@link #options()} -> {@link
- * FileConfigurationOptions#header()}, respecting the rules of {@link
- * FileConfigurationOptions#copyHeader()} if set.
+ * @return empty string
*
- * @return Compiled header
+ * @deprecated This method only exists for backwards compatibility. It will
+ * do nothing and should not be used! Please use
+ * {@link FileConfigurationOptions#getHeader()} instead.
*/
@NotNull
- protected abstract String buildHeader();
+ @Deprecated
+ protected String buildHeader() {
+ return "";
+ }
@NotNull
@Override
diff --git a/src/main/java/org/bukkit/configuration/file/FileConfigurationOptions.java b/src/main/java/org/bukkit/configuration/file/FileConfigurationOptions.java
index eaa0afac..c71f8a7b 100644
--- a/src/main/java/org/bukkit/configuration/file/FileConfigurationOptions.java
+++ b/src/main/java/org/bukkit/configuration/file/FileConfigurationOptions.java
@@ -1,5 +1,8 @@
package org.bukkit.configuration.file;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
import org.bukkit.configuration.MemoryConfiguration;
import org.bukkit.configuration.MemoryConfigurationOptions;
import org.jetbrains.annotations.NotNull;
@@ -10,8 +13,9 @@ import org.jetbrains.annotations.Nullable;
* FileConfiguration}
*/
public class FileConfigurationOptions extends MemoryConfigurationOptions {
- private String header = null;
- private boolean copyHeader = true;
+ private List header = Collections.emptyList();
+ private List footer = Collections.emptyList();
+ private boolean parseComments = true;
protected FileConfigurationOptions(@NotNull MemoryConfiguration configuration) {
super(configuration);
@@ -46,16 +50,32 @@ public class FileConfigurationOptions extends MemoryConfigurationOptions {
* automatically be applied, but you may include one if you wish for extra
* spacing.
*
- * Null is a valid value which will indicate that no header is to be
- * applied. The default value is null.
+ * If no comments exist, an empty list will be returned. A null entry
+ * represents an empty line and an empty String represents an empty comment
+ * line.
*
- * @return Header
+ * @return Unmodifiable header, every entry represents one line.
*/
- @Nullable
- public String header() {
+ @NotNull
+ public List getHeader() {
return header;
}
+ /**
+ * @return The string header.
+ *
+ * @deprecated use getHeader() instead.
+ */
+ @NotNull
+ @Deprecated
+ public String header() {
+ StringBuilder stringHeader = new StringBuilder();
+ for (String line : header) {
+ stringHeader.append(line == null ? "\n" : line + "\n");
+ }
+ return stringHeader.toString();
+ }
+
/**
* Sets the header that will be applied to the top of the saved output.
*
@@ -65,63 +85,119 @@ public class FileConfigurationOptions extends MemoryConfigurationOptions {
* automatically be applied, but you may include one if you wish for extra
* spacing.
*
- * Null is a valid value which will indicate that no header is to be
- * applied.
+ * If no comments exist, an empty list will be returned. A null entry
+ * represents an empty line and an empty String represents an empty comment
+ * line.
*
- * @param value New header
+ * @param value New header, every entry represents one line.
* @return This object, for chaining
*/
@NotNull
- public FileConfigurationOptions header(@Nullable String value) {
- this.header = value;
+ public FileConfigurationOptions setHeader(@Nullable List value) {
+ this.header = (value == null) ? Collections.emptyList() : Collections.unmodifiableList(value);
return this;
}
/**
- * Gets whether or not the header should be copied from a default source.
- *
- * If this is true, if a default {@link FileConfiguration} is passed to
- * {@link
- * FileConfiguration#setDefaults(org.bukkit.configuration.Configuration)}
- * then upon saving it will use the header from that config, instead of
- * the one provided here.
- *
- * If no default is set on the configuration, or the default is not of
- * type FileConfiguration, or that config has no header ({@link #header()}
- * returns null) then the header specified in this configuration will be
- * used.
- *
- * Defaults to true.
+ * @param value The string header.
+ * @return This object, for chaining.
*
- * @return Whether or not to copy the header
+ * @deprecated use setHeader() instead
*/
- public boolean copyHeader() {
- return copyHeader;
+ @NotNull
+ @Deprecated
+ public FileConfigurationOptions header(@Nullable String value) {
+ this.header = (value == null) ? Collections.emptyList() : Collections.unmodifiableList(Arrays.asList(value.split("\\n")));
+ return this;
}
/**
- * Sets whether or not the header should be copied from a default source.
+ * Gets the footer that will be applied to the bottom of the saved output.
*
- * If this is true, if a default {@link FileConfiguration} is passed to
- * {@link
- * FileConfiguration#setDefaults(org.bukkit.configuration.Configuration)}
- * then upon saving it will use the header from that config, instead of
- * the one provided here.
+ * This footer will be commented out and applied directly at the bottom of
+ * the generated output of the {@link FileConfiguration}. It is not required
+ * to include a newline at the beginning of the footer as it will
+ * automatically be applied, but you may include one if you wish for extra
+ * spacing.
*
- * If no default is set on the configuration, or the default is not of
- * type FileConfiguration, or that config has no header ({@link #header()}
- * returns null) then the header specified in this configuration will be
- * used.
- *
- * Defaults to true.
+ * If no comments exist, an empty list will be returned. A null entry
+ * represents an empty line and an empty String represents an empty comment
+ * line.
*
- * @param value Whether or not to copy the header
+ * @return Unmodifiable footer, every entry represents one line.
+ */
+ @NotNull
+ public List getFooter() {
+ return footer;
+ }
+
+ /**
+ * Sets the footer that will be applied to the bottom of the saved output.
+ *
+ * This footer will be commented out and applied directly at the bottom of
+ * the generated output of the {@link FileConfiguration}. It is not required
+ * to include a newline at the beginning of the footer as it will
+ * automatically be applied, but you may include one if you wish for extra
+ * spacing.
+ *
+ * If no comments exist, an empty list will be returned. A null entry
+ * represents an empty line and an empty String represents an empty comment
+ * line.
+ *
+ * @param value New footer, every entry represents one line.
* @return This object, for chaining
*/
@NotNull
- public FileConfigurationOptions copyHeader(boolean value) {
- copyHeader = value;
+ public FileConfigurationOptions setFooter(@Nullable List value) {
+ this.footer = (value == null) ? Collections.emptyList() : Collections.unmodifiableList(value);
+ return this;
+ }
+ /**
+ * Gets whether or not comments should be loaded and saved.
+ *
+ * Defaults to true.
+ *
+ * @return Whether or not comments are parsed.
+ */
+ public boolean parseComments() {
+ return parseComments;
+ }
+
+ /**
+ * Sets whether or not comments should be loaded and saved.
+ *
+ * Defaults to true.
+ *
+ * @param value Whether or not comments are parsed.
+ * @return This object, for chaining
+ */
+ @NotNull
+ public MemoryConfigurationOptions parseComments(boolean value) {
+ parseComments = value;
+ return this;
+ }
+
+ /**
+ * @return Whether or not comments are parsed.
+ *
+ * @deprecated Call {@link #parseComments()} instead.
+ */
+ @Deprecated
+ public boolean copyHeader() {
+ return parseComments;
+ }
+
+ /**
+ * @param value Should comments be parsed.
+ * @return This object, for chaining
+ *
+ * @deprecated Call {@link #parseComments(boolean)} instead.
+ */
+ @NotNull
+ @Deprecated
+ public FileConfigurationOptions copyHeader(boolean value) {
+ parseComments = value;
return this;
}
}
diff --git a/src/main/java/org/bukkit/configuration/file/YamlConfiguration.java b/src/main/java/org/bukkit/configuration/file/YamlConfiguration.java
index 6476f379..80048d42 100644
--- a/src/main/java/org/bukkit/configuration/file/YamlConfiguration.java
+++ b/src/main/java/org/bukkit/configuration/file/YamlConfiguration.java
@@ -1,9 +1,14 @@
package org.bukkit.configuration.file;
+import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import org.apache.commons.lang.Validate;
@@ -11,148 +16,231 @@ import org.bukkit.Bukkit;
import org.bukkit.configuration.Configuration;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
+import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.jetbrains.annotations.NotNull;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.comments.CommentLine;
+import org.yaml.snakeyaml.comments.CommentType;
import org.yaml.snakeyaml.error.YAMLException;
-import org.yaml.snakeyaml.representer.Representer;
+import org.yaml.snakeyaml.nodes.AnchorNode;
+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.nodes.SequenceNode;
+import org.yaml.snakeyaml.nodes.Tag;
+import org.yaml.snakeyaml.reader.UnicodeReader;
/**
* An implementation of {@link Configuration} which saves all files in Yaml.
* Note that this implementation is not synchronized.
*/
public class YamlConfiguration extends FileConfiguration {
- protected static final String COMMENT_PREFIX = "# ";
- protected static final String BLANK_CONFIG = "{}\n";
- private final DumperOptions yamlOptions = new DumperOptions();
- private final LoaderOptions loaderOptions = new LoaderOptions();
- private final Representer yamlRepresenter = new YamlRepresenter();
- private final Yaml yaml = new Yaml(new YamlConstructor(), yamlRepresenter, yamlOptions, loaderOptions);
+ private final DumperOptions yamlDumperOptions;
+ private final LoaderOptions yamlLoaderOptions;
+ private final YamlConstructor constructor;
+ private final YamlRepresenter representer;
+ private final Yaml yaml;
+
+ public YamlConfiguration() {
+ constructor = new YamlConstructor();
+ representer = new YamlRepresenter();
+ representer.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
+
+ yamlDumperOptions = new DumperOptions();
+ yamlDumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
+ yamlLoaderOptions = new LoaderOptions();
+ yamlLoaderOptions.setMaxAliasesForCollections(Integer.MAX_VALUE); // SPIGOT-5881: Not ideal, but was default pre SnakeYAML 1.26
+
+ yaml = new Yaml(constructor, representer, yamlDumperOptions, yamlLoaderOptions);
+ }
@NotNull
@Override
public String saveToString() {
- yamlOptions.setIndent(options().indent());
- yamlOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
- yamlRepresenter.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
+ yamlDumperOptions.setIndent(options().indent());
+ yamlDumperOptions.setProcessComments(options().parseComments());
- String header = buildHeader();
- String dump = yaml.dump(getValues(false));
+ MappingNode node = toNodeTree(this);
- if (dump.equals(BLANK_CONFIG)) {
- dump = "";
+ node.setBlockComments(getCommentLines(saveHeader(options().getHeader()), CommentType.BLOCK));
+ node.setEndComments(getCommentLines(options().getFooter(), CommentType.BLOCK));
+
+ StringWriter writer = new StringWriter();
+ if (node.getEndComments().isEmpty() && node.getEndComments().isEmpty() && node.getValue().isEmpty()) {
+ writer.write("");
+ } else {
+ if (node.getValue().isEmpty()) {
+ node.setFlowStyle(DumperOptions.FlowStyle.FLOW);
+ }
+ yaml.serialize(node, writer);
}
-
- return header + dump;
+ return writer.toString();
}
@Override
public void loadFromString(@NotNull String contents) throws InvalidConfigurationException {
- Validate.notNull(contents, "Contents cannot be null");
+ Validate.notNull(contents, "String cannot be null");
+ yamlLoaderOptions.setProcessComments(options().parseComments());
- Map, ?> input;
- try {
- loaderOptions.setMaxAliasesForCollections(Integer.MAX_VALUE); // SPIGOT-5881: Not ideal, but was default pre SnakeYAML 1.26
- input = (Map, ?>) yaml.load(contents);
- } catch (YAMLException e) {
+ MappingNode node;
+ try (Reader reader = new UnicodeReader(new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8)))) {
+ node = (MappingNode) yaml.compose(reader);
+ } catch (YAMLException | IOException e) {
throw new InvalidConfigurationException(e);
} catch (ClassCastException e) {
throw new InvalidConfigurationException("Top level is not a Map.");
}
- String header = parseHeader(contents);
- if (header.length() > 0) {
- options().header(header);
- }
-
this.map.clear();
- if (input != null) {
- convertMapsToSections(input, this);
+ if (node != null) {
+ adjustNodeComments(node);
+ options().setHeader(loadHeader(getCommentLines(node.getBlockComments())));
+ options().setFooter(getCommentLines(node.getEndComments()));
+ fromNodeTree(node, this);
}
}
- protected void convertMapsToSections(@NotNull Map, ?> input, @NotNull ConfigurationSection section) {
- for (Map.Entry, ?> entry : input.entrySet()) {
- String key = entry.getKey().toString();
- Object value = entry.getValue();
+ /**
+ * This method splits the header on the last empty line, and sets the
+ * comments below this line as comments for the first key on the map object.
+ *
+ * @param node The root node of the yaml object
+ */
+ private void adjustNodeComments(final MappingNode node) {
+ if (node.getBlockComments() == null && !node.getValue().isEmpty()) {
+ Node firstNode = node.getValue().get(0).getKeyNode();
+ List lines = firstNode.getBlockComments();
+ if (lines != null) {
+ int index = -1;
+ for (int i = 0; i < lines.size(); i++) {
+ if (lines.get(i).getCommentType() == CommentType.BLANK_LINE) {
+ index = i;
+ }
+ }
+ if (index != -1) {
+ node.setBlockComments(lines.subList(0, index + 1));
+ firstNode.setBlockComments(lines.subList(index + 1, lines.size()));
+ }
+ }
+ }
+ }
- if (value instanceof Map) {
- convertMapsToSections((Map, ?>) value, section.createSection(key));
+ protected void fromNodeTree(@NotNull MappingNode input, @NotNull ConfigurationSection section) {
+ for (NodeTuple nodeTuple : input.getValue()) {
+ ScalarNode key = (ScalarNode) nodeTuple.getKeyNode();
+ String keyString = key.getValue();
+ Node value = nodeTuple.getValueNode();
+
+ while (value instanceof AnchorNode) {
+ value = ((AnchorNode) value).getRealNode();
+ }
+
+ if (value instanceof MappingNode && !hasSerializedTypeKey((MappingNode) value)) {
+ fromNodeTree((MappingNode) value, section.createSection(keyString));
} else {
- section.set(key, value);
+ section.set(keyString, constructor.construct(value));
+ }
+
+ section.setComments(keyString, getCommentLines(key.getBlockComments()));
+ if (value instanceof MappingNode || value instanceof SequenceNode) {
+ section.setInlineComments(keyString, getCommentLines(key.getInLineComments()));
+ } else {
+ section.setInlineComments(keyString, getCommentLines(value.getInLineComments()));
}
}
}
- @NotNull
- protected String parseHeader(@NotNull String input) {
- String[] lines = input.split("\r?\n", -1);
- StringBuilder result = new StringBuilder();
- boolean readingHeader = true;
- boolean foundHeader = false;
-
- for (int i = 0; (i < lines.length) && (readingHeader); i++) {
- String line = lines[i];
-
- if (line.startsWith(COMMENT_PREFIX)) {
- if (i > 0) {
- result.append("\n");
- }
-
- if (line.length() > COMMENT_PREFIX.length()) {
- result.append(line.substring(COMMENT_PREFIX.length()));
- }
-
- foundHeader = true;
- } else if ((foundHeader) && (line.length() == 0)) {
- result.append("\n");
- } else if (foundHeader) {
- readingHeader = false;
+ private boolean hasSerializedTypeKey(MappingNode node) {
+ for (NodeTuple nodeTuple : node.getValue()) {
+ String key = ((ScalarNode) nodeTuple.getKeyNode()).getValue();
+ if (key.equals(ConfigurationSerialization.SERIALIZED_TYPE_KEY)) {
+ return true;
}
}
-
- return result.toString();
+ return false;
}
- @NotNull
- @Override
- protected String buildHeader() {
- String header = options().header();
+ private MappingNode toNodeTree(@NotNull ConfigurationSection section) {
+ List nodeTuples = new ArrayList<>();
+ for (Map.Entry entry : section.getValues(false).entrySet()) {
+ ScalarNode key = (ScalarNode) representer.represent(entry.getKey());
+ Node value;
+ if (entry.getValue() instanceof ConfigurationSection) {
+ value = toNodeTree((ConfigurationSection) entry.getValue());
+ } else {
+ value = representer.represent(entry.getValue());
+ }
+ key.setBlockComments(getCommentLines(section.getComments(entry.getKey()), CommentType.BLOCK));
+ if (value instanceof MappingNode || value instanceof SequenceNode) {
+ key.setInLineComments(getCommentLines(section.getInlineComments(entry.getKey()), CommentType.IN_LINE));
+ } else {
+ value.setInLineComments(getCommentLines(section.getInlineComments(entry.getKey()), CommentType.IN_LINE));
+ }
- if (options().copyHeader()) {
- Configuration def = getDefaults();
+ nodeTuples.add(new NodeTuple(key, value));
+ }
- if ((def != null) && (def instanceof FileConfiguration)) {
- FileConfiguration filedefaults = (FileConfiguration) def;
- String defaultsHeader = filedefaults.buildHeader();
+ return new MappingNode(Tag.MAP, nodeTuples, DumperOptions.FlowStyle.BLOCK);
+ }
- if ((defaultsHeader != null) && (defaultsHeader.length() > 0)) {
- return defaultsHeader;
+ private List getCommentLines(List comments) {
+ List lines = new ArrayList<>();
+ if (comments != null) {
+ for (CommentLine comment : comments) {
+ if (comment.getCommentType() == CommentType.BLANK_LINE) {
+ lines.add(null);
+ } else {
+ lines.add(comment.getValue());
}
}
}
+ return lines;
+ }
- if (header == null) {
- return "";
- }
-
- StringBuilder builder = new StringBuilder();
- String[] lines = header.split("\r?\n", -1);
- boolean startedHeader = false;
-
- for (int i = lines.length - 1; i >= 0; i--) {
- builder.insert(0, "\n");
-
- if ((startedHeader) || (lines[i].length() != 0)) {
- builder.insert(0, lines[i]);
- builder.insert(0, COMMENT_PREFIX);
- startedHeader = true;
+ private List getCommentLines(List comments, CommentType commentType) {
+ List lines = new ArrayList();
+ for (String comment : comments) {
+ if (comment == null) {
+ lines.add(new CommentLine(null, null, "", CommentType.BLANK_LINE));
+ } else {
+ lines.add(new CommentLine(null, null, comment, commentType));
}
}
+ return lines;
+ }
- return builder.toString();
+ /**
+ * Removes the empty line at the end of the header that separates the header
+ * from further comments.
+ *
+ * @param header The list of heading comments
+ * @return The modified list
+ */
+ private List loadHeader(List header) {
+ ArrayList list = new ArrayList(header);
+ if (list.size() != 0) {
+ list.remove(list.size() - 1);
+ }
+ return list;
+ }
+
+ /**
+ * Adds the empty line at the end of the header that separates the header
+ * from further comments.
+ *
+ * @param header The list of heading comments
+ * @return The modified list
+ */
+ private List saveHeader(List header) {
+ ArrayList list = new ArrayList(header);
+ if (list.size() != 0) {
+ list.add(null);
+ }
+ return list;
}
@NotNull
diff --git a/src/main/java/org/bukkit/configuration/file/YamlConfigurationOptions.java b/src/main/java/org/bukkit/configuration/file/YamlConfigurationOptions.java
index b2bf9785..d6873899 100644
--- a/src/main/java/org/bukkit/configuration/file/YamlConfigurationOptions.java
+++ b/src/main/java/org/bukkit/configuration/file/YamlConfigurationOptions.java
@@ -1,5 +1,6 @@
package org.bukkit.configuration.file;
+import java.util.List;
import org.apache.commons.lang.Validate;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -37,6 +38,14 @@ public class YamlConfigurationOptions extends FileConfigurationOptions {
@NotNull
@Override
+ public YamlConfigurationOptions setHeader(@Nullable List value) {
+ super.setHeader(value);
+ return this;
+ }
+
+ @NotNull
+ @Override
+ @Deprecated
public YamlConfigurationOptions header(@Nullable String value) {
super.header(value);
return this;
@@ -44,6 +53,21 @@ public class YamlConfigurationOptions extends FileConfigurationOptions {
@NotNull
@Override
+ public YamlConfigurationOptions setFooter(@Nullable List value) {
+ super.setFooter(value);
+ return this;
+ }
+
+ @NotNull
+ @Override
+ public YamlConfigurationOptions parseComments(boolean value) {
+ super.parseComments(value);
+ return this;
+ }
+
+ @NotNull
+ @Override
+ @Deprecated
public YamlConfigurationOptions copyHeader(boolean value) {
super.copyHeader(value);
return this;
diff --git a/src/main/java/org/bukkit/configuration/file/YamlConstructor.java b/src/main/java/org/bukkit/configuration/file/YamlConstructor.java
index c8466a29..ca167df5 100644
--- a/src/main/java/org/bukkit/configuration/file/YamlConstructor.java
+++ b/src/main/java/org/bukkit/configuration/file/YamlConstructor.java
@@ -16,6 +16,11 @@ public class YamlConstructor extends SafeConstructor {
this.yamlConstructors.put(Tag.MAP, new ConstructCustomObject());
}
+ @NotNull
+ public Object construct(@NotNull Node node) {
+ return constructObject(node);
+ }
+
private class ConstructCustomObject extends ConstructYamlMap {
@Nullable
diff --git a/src/main/java/org/bukkit/configuration/file/YamlRepresenter.java b/src/main/java/org/bukkit/configuration/file/YamlRepresenter.java
index 9dd3890f..20e96876 100644
--- a/src/main/java/org/bukkit/configuration/file/YamlRepresenter.java
+++ b/src/main/java/org/bukkit/configuration/file/YamlRepresenter.java
@@ -2,7 +2,6 @@ package org.bukkit.configuration.file;
import java.util.LinkedHashMap;
import java.util.Map;
-import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.jetbrains.annotations.NotNull;
@@ -12,22 +11,12 @@ import org.yaml.snakeyaml.representer.Representer;
public class YamlRepresenter extends Representer {
public YamlRepresenter() {
- this.multiRepresenters.put(ConfigurationSection.class, new RepresentConfigurationSection());
this.multiRepresenters.put(ConfigurationSerializable.class, new RepresentConfigurationSerializable());
// SPIGOT-6234: We could just switch YamlConstructor to extend Constructor rather than SafeConstructor, however there is a very small risk of issues with plugins treating config as untrusted input
// So instead we will just allow future plugins to have their enums extend ConfigurationSerializable
this.multiRepresenters.remove(Enum.class);
}
- private class RepresentConfigurationSection extends RepresentMap {
-
- @NotNull
- @Override
- public Node representData(@NotNull Object data) {
- return super.representData(((ConfigurationSection) data).getValues(false));
- }
- }
-
private class RepresentConfigurationSerializable extends RepresentMap {
@NotNull
diff --git a/src/test/java/org/bukkit/configuration/file/FileConfigurationTest.java b/src/test/java/org/bukkit/configuration/file/FileConfigurationTest.java
index 4ef7aa98..87bfa2c1 100644
--- a/src/test/java/org/bukkit/configuration/file/FileConfigurationTest.java
+++ b/src/test/java/org/bukkit/configuration/file/FileConfigurationTest.java
@@ -4,6 +4,8 @@ import static org.junit.Assert.*;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
+import java.util.Arrays;
+import java.util.List;
import java.util.Map;
import org.bukkit.configuration.MemoryConfigurationTest;
import org.junit.Rule;
@@ -19,9 +21,17 @@ public abstract class FileConfigurationTest extends MemoryConfigurationTest {
public abstract String getTestValuesString();
- public abstract String getTestHeaderInput();
+ public abstract List getTestCommentInput();
- public abstract String getTestHeaderResult();
+ public abstract String getTestCommentResult();
+
+ public abstract List getTestHeaderComments();
+
+ public abstract String getTestHeaderCommentsResult();
+
+ public abstract List getTestKeyComments();
+
+ public abstract String getTestHeaderKeyCommentResult();
@Test
public void testSave_File() throws Exception {
@@ -127,69 +137,6 @@ public abstract class FileConfigurationTest extends MemoryConfigurationTest {
assertEquals(saved, config.saveToString());
}
- @Test
- public void testSaveToStringWithHeader() {
- FileConfiguration config = getConfig();
- config.options().header(getTestHeaderInput());
-
- for (Map.Entry entry : getTestValues().entrySet()) {
- config.set(entry.getKey(), entry.getValue());
- }
-
- String result = config.saveToString();
- String expected = getTestHeaderResult() + "\n" + getTestValuesString();
-
- assertEquals(expected, result);
- }
-
- @Test
- public void testParseHeader() throws Exception {
- FileConfiguration config = getConfig();
- Map values = getTestValues();
- String saved = getTestValuesString();
- String header = getTestHeaderResult();
- String expected = getTestHeaderInput();
-
- config.loadFromString(header + "\n" + saved);
-
- assertEquals(expected, config.options().header());
-
- for (Map.Entry entry : values.entrySet()) {
- assertEquals(entry.getValue(), config.get(entry.getKey()));
- }
-
- assertEquals(values.keySet(), config.getKeys(true));
- assertEquals(header + "\n" + saved, config.saveToString());
- }
-
- @Test
- public void testCopyHeader() throws Exception {
- FileConfiguration config = getConfig();
- FileConfiguration defaults = getConfig();
- Map values = getTestValues();
- String saved = getTestValuesString();
- String header = getTestHeaderResult();
- String expected = getTestHeaderInput();
-
- defaults.loadFromString(header);
- config.loadFromString(saved);
- config.setDefaults(defaults);
-
- assertNull(config.options().header());
- assertEquals(expected, defaults.options().header());
-
- for (Map.Entry entry : values.entrySet()) {
- assertEquals(entry.getValue(), config.get(entry.getKey()));
- }
-
- assertEquals(values.keySet(), config.getKeys(true));
- assertEquals(header + "\n" + saved, config.saveToString());
-
- config = getConfig();
- config.loadFromString(getTestHeaderResult() + saved);
- assertEquals(getTestHeaderResult() + saved, config.saveToString());
- }
-
@Test
public void testReloadEmptyConfig() throws Exception {
FileConfiguration config = getConfig();
@@ -271,4 +218,178 @@ public abstract class FileConfigurationTest extends MemoryConfigurationTest {
assertFalse(config.contains("test"));
assertFalse(config.getBoolean("test"));
}
+
+ @Test
+ public void testSaveWithComments() {
+ FileConfiguration config = getConfig();
+ config.options().parseComments(true);
+
+ for (Map.Entry entry : getTestValues().entrySet()) {
+ config.set(entry.getKey(), entry.getValue());
+ }
+ String key = getTestValues().keySet().iterator().next();
+ config.setComments(key, getTestCommentInput());
+
+ String result = config.saveToString();
+ String expected = getTestCommentResult() + "\n" + getTestValuesString();
+
+ assertEquals(expected, result);
+ }
+
+ @Test
+ public void testSaveWithoutComments() {
+ FileConfiguration config = getConfig();
+ config.options().parseComments(false);
+
+ for (Map.Entry entry : getTestValues().entrySet()) {
+ config.set(entry.getKey(), entry.getValue());
+ }
+ String key = getTestValues().keySet().iterator().next();
+ config.setComments(key, getTestCommentInput());
+
+ String result = config.saveToString();
+ String expected = getTestValuesString();
+
+ assertEquals(expected, result);
+ }
+
+ @Test
+ public void testLoadWithComments() throws Exception {
+ FileConfiguration config = getConfig();
+ Map values = getTestValues();
+ String saved = getTestValuesString();
+ String comments = getTestCommentResult();
+
+ config.options().parseComments(true);
+ config.loadFromString(comments + "\n" + saved);
+
+ for (Map.Entry entry : values.entrySet()) {
+ assertEquals(entry.getValue(), config.get(entry.getKey()));
+ }
+
+ assertEquals(values.keySet(), config.getKeys(true));
+ assertEquals(comments + "\n" + saved, config.saveToString());
+ }
+
+ @Test
+ public void testLoadWithoutComments() throws Exception {
+ FileConfiguration config = getConfig();
+ Map values = getTestValues();
+ String saved = getTestValuesString();
+ String comments = getTestCommentResult();
+
+ config.options().parseComments(false);
+ config.loadFromString(comments + "\n" + saved);
+ config.options().parseComments(true);
+
+ for (Map.Entry entry : values.entrySet()) {
+ assertEquals(entry.getValue(), config.get(entry.getKey()));
+ }
+
+ assertEquals(values.keySet(), config.getKeys(true));
+ assertEquals(saved, config.saveToString());
+ }
+
+ @Test
+ public void testSaveWithCommentsHeader() {
+ FileConfiguration config = getConfig();
+ config.options().parseComments(true);
+
+ for (Map.Entry entry : getTestValues().entrySet()) {
+ config.set(entry.getKey(), entry.getValue());
+ }
+ String key = getTestValues().keySet().iterator().next();
+ config.options().setHeader(getTestHeaderComments());
+ config.setComments(key, getTestKeyComments());
+
+ String result = config.saveToString();
+ String expected = getTestHeaderKeyCommentResult() + getTestValuesString();
+
+ assertEquals(expected, result);
+ }
+
+ @Test
+ public void testLoadWithCommentsHeader() throws Exception {
+ FileConfiguration config = getConfig();
+ Map values = getTestValues();
+ String saved = getTestValuesString();
+ String comments = getTestHeaderKeyCommentResult();
+
+ config.options().parseComments(true);
+ config.loadFromString(comments + saved);
+
+ for (Map.Entry entry : values.entrySet()) {
+ assertEquals(entry.getValue(), config.get(entry.getKey()));
+ }
+
+ String key = getTestValues().keySet().iterator().next();
+ assertEquals(getTestHeaderComments(), config.options().getHeader());
+ assertEquals(getTestKeyComments(), config.getComments(key));
+
+ assertEquals(values.keySet(), config.getKeys(true));
+ assertEquals(comments + saved, config.saveToString());
+ }
+
+ @Test
+ public void testSaveWithCommentsFooter() {
+ FileConfiguration config = getConfig();
+ config.options().parseComments(true);
+
+ for (Map.Entry entry : getTestValues().entrySet()) {
+ config.set(entry.getKey(), entry.getValue());
+ }
+ config.options().setFooter(getTestHeaderComments());
+
+ String result = config.saveToString();
+ String expected = getTestValuesString() + getTestHeaderCommentsResult();
+
+ assertEquals(expected, result);
+ }
+
+ @Test
+ public void testLoadWithCommentsFooter() throws Exception {
+ FileConfiguration config = getConfig();
+ Map values = getTestValues();
+ String saved = getTestValuesString();
+ String comments = getTestHeaderCommentsResult();
+
+ config.options().parseComments(true);
+ config.loadFromString(saved + comments);
+
+ for (Map.Entry entry : values.entrySet()) {
+ assertEquals(entry.getValue(), config.get(entry.getKey()));
+ }
+
+ assertEquals(getTestHeaderComments(), config.options().getFooter());
+
+ assertEquals(values.keySet(), config.getKeys(true));
+ assertEquals(saved + comments, config.saveToString());
+ }
+
+ @Test
+ public void testLoadWithCommentsInline() throws Exception {
+ FileConfiguration config = getConfig();
+
+ config.options().parseComments(true);
+ config.loadFromString("key1: value1\nkey2: value2 # Test inline\nkey3: value3");
+
+ assertEquals(Arrays.asList(" Test inline"), config.getInlineComments("key2"));
+ }
+
+ @Test
+ public void testSaveWithCommentsInline() {
+ FileConfiguration config = getConfig();
+
+ config.options().parseComments(true);
+ config.set("key1", "value1");
+ config.set("key2", "value2");
+ config.set("key3", "value3");
+ config.setInlineComments("key2", Arrays.asList(" Test inline"));
+
+ String result = config.saveToString();
+ String expected = "key1: value1\nkey2: value2 # Test inline\nkey3: value3\n";
+
+ assertEquals(expected, result);
+ }
+
}
diff --git a/src/test/java/org/bukkit/configuration/file/YamlConfigurationTest.java b/src/test/java/org/bukkit/configuration/file/YamlConfigurationTest.java
index cd79a348..57e31a28 100644
--- a/src/test/java/org/bukkit/configuration/file/YamlConfigurationTest.java
+++ b/src/test/java/org/bukkit/configuration/file/YamlConfigurationTest.java
@@ -1,6 +1,9 @@
package org.bukkit.configuration.file;
import static org.junit.Assert.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import org.junit.Test;
public class YamlConfigurationTest extends FileConfigurationTest {
@@ -11,13 +14,43 @@ public class YamlConfigurationTest extends FileConfigurationTest {
}
@Override
- public String getTestHeaderInput() {
- return "This is a sample\nheader.\n\nNewline above should be commented.\n\n";
+ public List getTestCommentInput() {
+ List comments = new ArrayList<>();
+ comments.add(" This is a sample");
+ comments.add(" header.");
+ comments.add(" Newline above should be commented.");
+ comments.add("");
+ comments.add("");
+ comments.add(null);
+ comments.add(null);
+ comments.add(" Comment of first Key");
+ comments.add(" and a second line.");
+ return comments;
}
@Override
- public String getTestHeaderResult() {
- return "# This is a sample\n# header.\n# \n# Newline above should be commented.\n\n";
+ public String getTestCommentResult() {
+ return "# This is a sample\n# header.\n# Newline above should be commented.\n#\n#\n\n\n# Comment of first Key\n# and a second line.";
+ }
+
+ @Override
+ public List getTestHeaderComments() {
+ return Arrays.asList(" Header", " Second Line");
+ }
+
+ @Override
+ public String getTestHeaderCommentsResult() {
+ return "# Header\n# Second Line\n";
+ }
+
+ @Override
+ public List getTestKeyComments() {
+ return Arrays.asList(" First key Comment", " Second Line");
+ }
+
+ @Override
+ public String getTestHeaderKeyCommentResult() {
+ return "# Header\n# Second Line\n\n# First key Comment\n# Second Line\n";
}
@Override