diff --git a/paper-api/src/main/java/org/bukkit/configuration/file/YamlRepresenter.java b/paper-api/src/main/java/org/bukkit/configuration/file/YamlRepresenter.java index 20e9687647..4670f86681 100644 --- a/paper-api/src/main/java/org/bukkit/configuration/file/YamlRepresenter.java +++ b/paper-api/src/main/java/org/bukkit/configuration/file/YamlRepresenter.java @@ -2,6 +2,7 @@ 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; @@ -11,12 +12,23 @@ 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); } + // SPIGOT-6949: Used by configuration sections that are nested within lists or maps. + 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/paper-api/src/test/java/org/bukkit/configuration/file/YamlConfigurationTest.java b/paper-api/src/test/java/org/bukkit/configuration/file/YamlConfigurationTest.java index 3522baa0a2..194949d74a 100644 --- a/paper-api/src/test/java/org/bukkit/configuration/file/YamlConfigurationTest.java +++ b/paper-api/src/test/java/org/bukkit/configuration/file/YamlConfigurationTest.java @@ -3,8 +3,14 @@ package org.bukkit.configuration.file; import static org.junit.Assert.*; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.MemoryConfiguration; import org.junit.Test; public class YamlConfigurationTest extends FileConfigurationTest { @@ -201,4 +207,61 @@ public class YamlConfigurationTest extends FileConfigurationTest { + "'!!null': X\n"; assertEquals(expected, config.saveToString()); } + + // SPIGOT-6949 + @Test + public void testNestedConfigSections() throws InvalidConfigurationException { + YamlConfiguration config = getConfig(); + List configList = new ArrayList<>(); + + MemoryConfiguration nestedSection = new MemoryConfiguration(); + nestedSection.set("something", "value"); + configList.add(nestedSection); + + Map nestedMap = new HashMap<>(); + nestedMap.put("scalar", 10); + nestedMap.put("string", "something"); + + MemoryConfiguration nestedSection2 = new MemoryConfiguration(); + nestedSection2.set("embedded", "value"); + nestedMap.put("section", nestedSection2); + configList.add(nestedMap); + + config.set("list", configList); + + String serialized = config.saveToString(); + YamlConfiguration deserialized = new YamlConfiguration(); + deserialized.loadFromString(serialized); + + // The types of nested maps or configuration sections or configs might not be preserved, but + // their contents should be preserved: + assertEquals(convertSectionsToMaps(config), convertSectionsToMaps(deserialized)); + } + + // Recursively converts all configuration sections to Maps, including within any nested data + // structures such as Maps and Lists. + private Object convertSectionsToMaps(Object object) { + if (object instanceof ConfigurationSection) { + ConfigurationSection section = (ConfigurationSection) object; + Map values = section.getValues(false); + return convertSectionsToMaps(values); + } else if (object instanceof Map) { + Map map = (Map) object; // Might be immutable + Map newMap = new LinkedHashMap<>(); + for (Entry entry : map.entrySet()) { + newMap.put(entry.getKey(), convertSectionsToMaps(entry.getValue())); + } + return newMap; + } else if (object instanceof Iterable) { + // Any other type of Collection is converted to a list: + Iterable iterable = (Iterable) object; // Might be immutable + List newList = new ArrayList<>(); + for (Object element : iterable) { + newList.add(convertSectionsToMaps(element)); + } + return newList; + } else { + return object; + } + } }