From 62144c8b40038f23ebca140366febd46280c7b0c Mon Sep 17 00:00:00 2001 From: md_5 Date: Sun, 5 Dec 2021 08:34:33 +1100 Subject: [PATCH 01/10] Add Player#openSign API to edit a placed sign --- src/main/java/org/bukkit/entity/Player.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java index 0213fe48..a9b65083 100644 --- a/src/main/java/org/bukkit/entity/Player.java +++ b/src/main/java/org/bukkit/entity/Player.java @@ -16,6 +16,7 @@ import org.bukkit.WeatherType; import org.bukkit.advancement.Advancement; import org.bukkit.advancement.AdvancementProgress; import org.bukkit.block.Block; +import org.bukkit.block.Sign; import org.bukkit.block.data.BlockData; import org.bukkit.conversations.Conversable; import org.bukkit.event.block.BlockBreakEvent; @@ -1341,6 +1342,15 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM */ public void openBook(@NotNull ItemStack book); + /** + * Open a Sign for editing by the Player. + * + * The Sign must be placed in the same world as the player. + * + * @param sign The sign to edit + */ + public void openSign(@NotNull Sign sign); + /** * Shows the demo screen to the player, this screen is normally only seen in * the demo version of the game. From 0912ace4df83bd6b7331a1e716925055fb33f3d5 Mon Sep 17 00:00:00 2001 From: Wolf2323 Date: Sat, 4 Dec 2021 21:38:45 +0100 Subject: [PATCH 02/10] SPIGOT-6830: Fix addDefaults with Configuration overrides child Sections in the defaults --- .../configuration/MemoryConfiguration.java | 6 ++- .../configuration/ConfigurationTest.java | 37 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/bukkit/configuration/MemoryConfiguration.java b/src/main/java/org/bukkit/configuration/MemoryConfiguration.java index 010a97fe..47dbd5c6 100644 --- a/src/main/java/org/bukkit/configuration/MemoryConfiguration.java +++ b/src/main/java/org/bukkit/configuration/MemoryConfiguration.java @@ -54,7 +54,11 @@ public class MemoryConfiguration extends MemorySection implements Configuration public void addDefaults(@NotNull Configuration defaults) { Validate.notNull(defaults, "Defaults may not be null"); - addDefaults(defaults.getValues(true)); + for (String key : defaults.getKeys(true)) { + if (!defaults.isConfigurationSection(key)) { + addDefault(key, defaults.get(key)); + } + } } @Override diff --git a/src/test/java/org/bukkit/configuration/ConfigurationTest.java b/src/test/java/org/bukkit/configuration/ConfigurationTest.java index e554ebba..65f6ba38 100644 --- a/src/test/java/org/bukkit/configuration/ConfigurationTest.java +++ b/src/test/java/org/bukkit/configuration/ConfigurationTest.java @@ -103,6 +103,43 @@ public abstract class ConfigurationTest { } } + /** + * Test of addDefaults method, of class Configuration but with existing + * defaults in a child section. + */ + @Test + public void testAddDefaults_Configuration_WithExisting() { + Configuration config = getConfig(); + Map values = getTestValues(); + values.put("default-section.string", "String Value"); + Configuration defaults = getConfig(); + Configuration defaultsAdditional = getConfig(); + + for (Map.Entry entry : values.entrySet()) { + defaults.set(entry.getKey(), entry.getValue()); + } + config.addDefaults(defaults); + + Map additionalValues = new HashMap<>(); + additionalValues.put("default-section.additionalString", "Additional String"); + additionalValues.put("default-section.additionalInt", 42); + for (Map.Entry entry : additionalValues.entrySet()) { + defaultsAdditional.set(entry.getKey(), entry.getValue()); + } + config.addDefaults(defaultsAdditional); + values.putAll(additionalValues); + + for (Map.Entry entry : values.entrySet()) { + String path = entry.getKey(); + Object object = entry.getValue(); + + assertEquals(object, config.get(path)); + assertTrue(config.contains(path)); + assertFalse(config.isSet(path)); + assertTrue(config.getDefaults().isSet(path)); + } + } + /** * Test of setDefaults method, of class Configuration. */ From e4358b8217126bbcc3a38b0d17097ad5ab87c50a Mon Sep 17 00:00:00 2001 From: Wolf2323 Date: Sat, 4 Dec 2021 22:05:54 +0100 Subject: [PATCH 03/10] #686: Fix contains for default section generating real sections --- .../java/org/bukkit/configuration/MemorySection.java | 4 ++-- .../bukkit/configuration/ConfigurationSectionTest.java | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/bukkit/configuration/MemorySection.java b/src/main/java/org/bukkit/configuration/MemorySection.java index 90cc523f..09f1debe 100644 --- a/src/main/java/org/bukkit/configuration/MemorySection.java +++ b/src/main/java/org/bukkit/configuration/MemorySection.java @@ -251,10 +251,10 @@ public class MemorySection implements ConfigurationSection { 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) { + if (section == null || !section.contains(path.substring(i2, i1), true)) { return def; } + section = section.getConfigurationSection(path.substring(i2, i1)); } String key = path.substring(i2); diff --git a/src/test/java/org/bukkit/configuration/ConfigurationSectionTest.java b/src/test/java/org/bukkit/configuration/ConfigurationSectionTest.java index 1e529928..bfaacbaa 100644 --- a/src/test/java/org/bukkit/configuration/ConfigurationSectionTest.java +++ b/src/test/java/org/bukkit/configuration/ConfigurationSectionTest.java @@ -110,6 +110,16 @@ public abstract class ConfigurationSectionTest { assertTrue(section.contains("doenst-exist-two", false)); } + @Test + public void testContainsDoesNotCreateSection() { + ConfigurationSection section = getConfigurationSection(); + section.addDefault("notExistingSection.Value", "Test String"); + + assertFalse(section.contains("notExistingSection", true)); + assertFalse(section.contains("notExistingSection.Value", true)); + assertFalse(section.contains("notExistingSection", true)); + } + @Test public void testIsSet() { ConfigurationSection section = getConfigurationSection(); From ffd8b28939b4ec84855f8a10c93463ec113def13 Mon Sep 17 00:00:00 2001 From: Wolf2323 Date: Mon, 6 Dec 2021 07:22:16 +1100 Subject: [PATCH 04/10] #687: Fix NPE from previous commits --- src/main/java/org/bukkit/configuration/MemorySection.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/bukkit/configuration/MemorySection.java b/src/main/java/org/bukkit/configuration/MemorySection.java index 09f1debe..a6ac5d45 100644 --- a/src/main/java/org/bukkit/configuration/MemorySection.java +++ b/src/main/java/org/bukkit/configuration/MemorySection.java @@ -251,10 +251,14 @@ public class MemorySection implements ConfigurationSection { int i1 = -1, i2; ConfigurationSection section = this; while ((i1 = path.indexOf(separator, i2 = i1 + 1)) != -1) { - if (section == null || !section.contains(path.substring(i2, i1), true)) { + final String currentPath = path.substring(i2, i1); + if (!section.contains(currentPath, true)) { + return def; + } + section = section.getConfigurationSection(currentPath); + if (section == null) { return def; } - section = section.getConfigurationSection(path.substring(i2, i1)); } String key = path.substring(i2); From 5906bed05d29c64af69983e2521cc3e9060d95ec Mon Sep 17 00:00:00 2001 From: md_5 Date: Sat, 11 Dec 2021 00:00:00 +1100 Subject: [PATCH 05/10] Update to Minecraft 1.18.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 78afc6c1..36909ca5 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.bukkit bukkit - 1.18-R0.1-SNAPSHOT + 1.18.1-R0.1-SNAPSHOT jar Bukkit From b6e3b0f5b0587778b69e15df1a46089dc6bcf441 Mon Sep 17 00:00:00 2001 From: md_5 Date: Sat, 18 Dec 2021 11:36:31 +1100 Subject: [PATCH 06/10] Upgrade to SnakeYAML 1.30 release --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 36909ca5..bc9dea1f 100644 --- a/pom.xml +++ b/pom.xml @@ -54,7 +54,7 @@ org.yaml snakeyaml - 1.28 + 1.30 compile From 5dca4a4b8455ba1ee8d3e4e36894f6dcc4b04555 Mon Sep 17 00:00:00 2001 From: Doc Date: Sat, 18 Dec 2021 11:44:31 +1100 Subject: [PATCH 07/10] SPIGOT-6836: Add more API methods in MerchantRecipe --- .../org/bukkit/inventory/MerchantRecipe.java | 128 +++++++++++++++++- 1 file changed, 127 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/bukkit/inventory/MerchantRecipe.java b/src/main/java/org/bukkit/inventory/MerchantRecipe.java index 1fb4a1c5..620a4df8 100644 --- a/src/main/java/org/bukkit/inventory/MerchantRecipe.java +++ b/src/main/java/org/bukkit/inventory/MerchantRecipe.java @@ -3,7 +3,12 @@ package org.bukkit.inventory; import com.google.common.base.Preconditions; import java.util.ArrayList; import java.util.List; +import org.bukkit.Material; +import org.bukkit.event.entity.VillagerReplenishTradeEvent; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.util.NumberConversions; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * Represents a merchant's trade. @@ -16,6 +21,30 @@ import org.jetbrains.annotations.NotNull; * uses to increase. *
* A trade may or may not reward experience for being completed. + *
+ * During trades, the {@link MerchantRecipe} dynamically adjusts the amount of + * its first ingredient based on the following criteria: + *
    + *
  • {@link #getDemand() Demand}: This value is periodically updated by the + * villager that owns this merchant recipe based on how often the recipe has + * been used since it has been last restocked in relation to its + * {@link #getMaxUses maximum uses}. The amount by which the demand influences + * the amount of the first ingredient is scaled by the recipe's + * {@link #getPriceMultiplier price multiplier}, and can never be below zero. + *
  • {@link #getSpecialPrice() Special price}: This value is dynamically + * updated whenever a player starts and stops trading with a villager that owns + * this merchant recipe. It is based on the player's individual reputation with + * the villager, and the player's currently active status effects (see + * {@link PotionEffectType#HERO_OF_THE_VILLAGE}). The influence of the player's + * reputation on the special price is scaled by the recipe's + * {@link #getPriceMultiplier price multiplier}. + *
+ * The adjusted amount of the first ingredient is calculated by adding up the + * original amount of the first ingredient, the demand scaled by the recipe's + * {@link #getPriceMultiplier price multiplier} and truncated to the next lowest + * integer value greater than or equal to 0, and the special price, and then + * constraining the resulting value between 1 and the item stack's + * {@link ItemStack#getMaxStackSize() maximum stack size}. * * @see org.bukkit.event.entity.VillagerReplenishTradeEvent */ @@ -26,6 +55,8 @@ public class MerchantRecipe implements Recipe { private int uses; private int maxUses; private boolean experienceReward; + private int specialPrice; + private int demand; private int villagerExperience; private float priceMultiplier; @@ -34,16 +65,22 @@ public class MerchantRecipe implements Recipe { } public MerchantRecipe(@NotNull ItemStack result, int uses, int maxUses, boolean experienceReward) { - this(result, uses, maxUses, experienceReward, 0, 0.0F); + this(result, uses, maxUses, experienceReward, 0, 0.0F, 0, 0); } public MerchantRecipe(@NotNull ItemStack result, int uses, int maxUses, boolean experienceReward, int villagerExperience, float priceMultiplier) { + this(result, uses, maxUses, experienceReward, villagerExperience, priceMultiplier, 0, 0); + } + + public MerchantRecipe(@NotNull ItemStack result, int uses, int maxUses, boolean experienceReward, int villagerExperience, float priceMultiplier, int demand, int specialPrice) { this.result = result; this.uses = uses; this.maxUses = maxUses; this.experienceReward = experienceReward; this.villagerExperience = villagerExperience; this.priceMultiplier = priceMultiplier; + this.demand = demand; + this.specialPrice = specialPrice; } @NotNull @@ -78,6 +115,95 @@ public class MerchantRecipe implements Recipe { return copy; } + /** + * Gets the {@link #adjust(ItemStack) adjusted} first ingredient. + * + * @return the adjusted first ingredient, or null if this + * recipe has no ingredients + * @see #adjust(ItemStack) + */ + @Nullable + public ItemStack getAdjustedIngredient1() { + if (this.ingredients.isEmpty()) { + return null; + } + + ItemStack firstIngredient = this.ingredients.get(0).clone(); + adjust(firstIngredient); + return firstIngredient; + } + + /** + * Modifies the amount of the given {@link ItemStack} in the same way as + * MerchantRecipe dynamically adjusts the amount of the first ingredient + * during trading. + *
+ * This is calculated by adding up the original amount of the item, the + * demand scaled by the recipe's + * {@link #getPriceMultiplier price multiplier} and truncated to the next + * lowest integer value greater than or equal to 0, and the special price, + * and then constraining the resulting value between 1 and the + * {@link ItemStack}'s {@link ItemStack#getMaxStackSize() + * maximum stack size}. + * + * @param itemStack the item to adjust + */ + public void adjust(@Nullable ItemStack itemStack) { + if (itemStack == null || itemStack.getType() == Material.AIR || itemStack.getAmount() <= 0) { + return; + } + + int amount = itemStack.getAmount(); + int demandAdjustment = Math.max(0, NumberConversions.floor((float) (amount * getDemand()) * getPriceMultiplier())); + itemStack.setAmount(Math.max(1, Math.min(itemStack.getMaxStackSize(), amount + demandAdjustment + getSpecialPrice()))); + } + + /** + * Get the value of the demand for the item in {@link #getResult()}. + * + * @return the demand for the item + */ + public int getDemand() { + return demand; + } + + /** + * Set the value of the demand for the item in {@link #getResult()}. + *
+ * Note: This value is updated when the item is purchase + * + * @param demand demand value + */ + public void setDemand(int demand) { + this.demand = demand; + } + + /** + * Get the special price for this trade. + *
+ * Note: This value can be updated by + * {@link VillagerReplenishTradeEvent#getBonus()} or by + * {@link PotionEffectType#HERO_OF_THE_VILLAGE} + * + * @return special price value + */ + public int getSpecialPrice() { + return specialPrice; + } + + /** + * Set the special value for this trade. + *
+ * Note: This value can be updated by + * {@link VillagerReplenishTradeEvent#getBonus()} or by + * {@link PotionEffectType#HERO_OF_THE_VILLAGE} + * + * @param specialPrice special price value + */ + public void setSpecialPrice(int specialPrice) { + this.specialPrice = specialPrice; + } + /** * Get the number of times this trade has been used. * From 0c7075426331703c128edb648193c57fdd98aa4e Mon Sep 17 00:00:00 2001 From: Patrick Choe Date: Sat, 18 Dec 2021 11:46:20 +1100 Subject: [PATCH 08/10] SPIGOT-6789: Improve resource pack related API --- src/main/java/org/bukkit/Bukkit.java | 43 ++++++ src/main/java/org/bukkit/Server.java | 35 +++++ src/main/java/org/bukkit/entity/Player.java | 153 +++++++++++++++++++- 3 files changed, 225 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java index 3b150b96..47f1ff8f 100644 --- a/src/main/java/org/bukkit/Bukkit.java +++ b/src/main/java/org/bukkit/Bukkit.java @@ -245,6 +245,49 @@ public final class Bukkit { return server.getAllowNether(); } + /** + * Gets the server resource pack uri, or empty string if not specified. + * + * @return the server resource pack uri, otherwise empty string + */ + @NotNull + public static String getResourcePack() { + return server.getResourcePack(); + } + + /** + * Gets the SHA-1 digest of the server resource pack, or empty string if + * not specified. + * + * @return the SHA-1 digest of the server resource pack, otherwise empty + * string + */ + @NotNull + public static String getResourcePackHash() { + return server.getResourcePackHash(); + } + + /** + * Gets the custom prompt message to be shown when the server resource + * pack is required, or empty string if not specified. + * + * @return the custom prompt message to be shown when the server resource, + * otherwise empty string + */ + @NotNull + public static String getResourcePackPrompt() { + return server.getResourcePackPrompt(); + } + + /** + * Gets whether the server resource pack is enforced. + * + * @return whether the server resource pack is enforced + */ + public static boolean isResourcePackRequired() { + return server.isResourcePackRequired(); + } + /** * Gets whether this server has a whitelist or not. * diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java index 7d2ba1f5..8a6a3867 100644 --- a/src/main/java/org/bukkit/Server.java +++ b/src/main/java/org/bukkit/Server.java @@ -202,6 +202,41 @@ public interface Server extends PluginMessageRecipient { */ public boolean getAllowNether(); + /** + * Gets the server resource pack uri, or empty string if not specified. + * + * @return the server resource pack uri, otherwise empty string + */ + @NotNull + public String getResourcePack(); + + /** + * Gets the SHA-1 digest of the server resource pack, or empty string if + * not specified. + * + * @return the SHA-1 digest of the server resource pack, otherwise empty + * string + */ + @NotNull + public String getResourcePackHash(); + + /** + * Gets the custom prompt message to be shown when the server resource + * pack is required, or empty string if not specified. + * + * @return the custom prompt message to be shown when the server resource, + * otherwise empty string + */ + @NotNull + public String getResourcePackPrompt(); + + /** + * Gets whether the server resource pack is enforced. + * + * @return whether the server resource pack is enforced + */ + public boolean isResourcePackRequired(); + /** * Gets whether this server has a whitelist or not. * diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java index a9b65083..73f9118d 100644 --- a/src/main/java/org/bukkit/entity/Player.java +++ b/src/main/java/org/bukkit/entity/Player.java @@ -909,7 +909,7 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM *
  • There is no concept of resetting resource packs back to default * within Minecraft, so players will have to relog to do so or you * have to send an empty pack. - *
  • The request is send with "null" as the hash. This might result + *
  • The request is send with empty string as the hash. This might result * in newer versions not loading the pack correctly. * * @@ -929,9 +929,13 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM * in the background, and will automatically switch to it once the * download is complete. If the client has downloaded and cached a * resource pack with the same hash in the past it will not download but - * directly apply the cached pack. When this request is sent for the very - * first time from a given server, the client will first display a - * confirmation GUI to the player before proceeding with the download. + * directly apply the cached pack. If the hash is null and the client has + * downloaded and cached the same resource pack in the past, it will + * perform a file size check against the response content to determine if + * the resource pack has changed and needs to be downloaded again. When + * this request is sent for the very first time from a given server, the + * client will first display a confirmation GUI to the player before + * proceeding with the download. *

    * Notes: *

      @@ -942,6 +946,9 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM *
    • There is no concept of resetting resource packs back to default * within Minecraft, so players will have to relog to do so or you * have to send an empty pack. + *
    • The request is sent with empty string as the hash when the hash is + * not provided. This might result in newer versions not loading the + * pack correctly. *
    * * @param url The URL from which the client will download the resource @@ -953,11 +960,145 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM * @throws IllegalArgumentException Thrown if the URL is null. * @throws IllegalArgumentException Thrown if the URL is too long. The * length restriction is an implementation specific arbitrary value. - * @throws IllegalArgumentException Thrown if the hash is null. * @throws IllegalArgumentException Thrown if the hash is not 20 bytes * long. */ - public void setResourcePack(@NotNull String url, @NotNull byte[] hash); + public void setResourcePack(@NotNull String url, @Nullable byte[] hash); + + /** + * Request that the player's client download and switch resource packs. + *

    + * The player's client will download the new resource pack asynchronously + * in the background, and will automatically switch to it once the + * download is complete. If the client has downloaded and cached a + * resource pack with the same hash in the past it will not download but + * directly apply the cached pack. If the hash is null and the client has + * downloaded and cached the same resource pack in the past, it will + * perform a file size check against the response content to determine if + * the resource pack has changed and needs to be downloaded again. When + * this request is sent for the very first time from a given server, the + * client will first display a confirmation GUI to the player before + * proceeding with the download. + *

    + * Notes: + *

      + *
    • Players can disable server resources on their client, in which + * case this method will have no affect on them. Use the + * {@link PlayerResourcePackStatusEvent} to figure out whether or not + * the player loaded the pack! + *
    • There is no concept of resetting resource packs back to default + * within Minecraft, so players will have to relog to do so or you + * have to send an empty pack. + *
    • The request is sent with empty string as the hash when the hash is + * not provided. This might result in newer versions not loading the + * pack correctly. + *
    + * + * @param url The URL from which the client will download the resource + * pack. The string must contain only US-ASCII characters and should + * be encoded as per RFC 1738. + * @param hash The sha1 hash sum of the resource pack file which is used + * to apply a cached version of the pack directly without downloading + * if it is available. Hast to be 20 bytes long! + * @param prompt The optional custom prompt message to be shown to client. + * @throws IllegalArgumentException Thrown if the URL is null. + * @throws IllegalArgumentException Thrown if the URL is too long. The + * length restriction is an implementation specific arbitrary value. + * @throws IllegalArgumentException Thrown if the hash is not 20 bytes + * long. + */ + public void setResourcePack(@NotNull String url, @Nullable byte[] hash, @Nullable String prompt); + + /** + * Request that the player's client download and switch resource packs. + *

    + * The player's client will download the new resource pack asynchronously + * in the background, and will automatically switch to it once the + * download is complete. If the client has downloaded and cached a + * resource pack with the same hash in the past it will not download but + * directly apply the cached pack. If the hash is null and the client has + * downloaded and cached the same resource pack in the past, it will + * perform a file size check against the response content to determine if + * the resource pack has changed and needs to be downloaded again. When + * this request is sent for the very first time from a given server, the + * client will first display a confirmation GUI to the player before + * proceeding with the download. + *

    + * Notes: + *

      + *
    • Players can disable server resources on their client, in which + * case this method will have no affect on them. Use the + * {@link PlayerResourcePackStatusEvent} to figure out whether or not + * the player loaded the pack! + *
    • There is no concept of resetting resource packs back to default + * within Minecraft, so players will have to relog to do so or you + * have to send an empty pack. + *
    • The request is sent with empty string as the hash when the hash is + * not provided. This might result in newer versions not loading the + * pack correctly. + *
    + * + * @param url The URL from which the client will download the resource + * pack. The string must contain only US-ASCII characters and should + * be encoded as per RFC 1738. + * @param hash The sha1 hash sum of the resource pack file which is used + * to apply a cached version of the pack directly without downloading + * if it is available. Hast to be 20 bytes long! + * @param force If true, the client will be disconnected from the server + * when it declines to use the resource pack. + * @throws IllegalArgumentException Thrown if the URL is null. + * @throws IllegalArgumentException Thrown if the URL is too long. The + * length restriction is an implementation specific arbitrary value. + * @throws IllegalArgumentException Thrown if the hash is not 20 bytes + * long. + */ + public void setResourcePack(@NotNull String url, @Nullable byte[] hash, boolean force); + + /** + * Request that the player's client download and switch resource packs. + *

    + * The player's client will download the new resource pack asynchronously + * in the background, and will automatically switch to it once the + * download is complete. If the client has downloaded and cached a + * resource pack with the same hash in the past it will not download but + * directly apply the cached pack. If the hash is null and the client has + * downloaded and cached the same resource pack in the past, it will + * perform a file size check against the response content to determine if + * the resource pack has changed and needs to be downloaded again. When + * this request is sent for the very first time from a given server, the + * client will first display a confirmation GUI to the player before + * proceeding with the download. + *

    + * Notes: + *

      + *
    • Players can disable server resources on their client, in which + * case this method will have no affect on them. Use the + * {@link PlayerResourcePackStatusEvent} to figure out whether or not + * the player loaded the pack! + *
    • There is no concept of resetting resource packs back to default + * within Minecraft, so players will have to relog to do so or you + * have to send an empty pack. + *
    • The request is sent with empty string as the hash when the hash is + * not provided. This might result in newer versions not loading the + * pack correctly. + *
    + * + * @param url The URL from which the client will download the resource + * pack. The string must contain only US-ASCII characters and should + * be encoded as per RFC 1738. + * @param hash The sha1 hash sum of the resource pack file which is used + * to apply a cached version of the pack directly without downloading + * if it is available. Hast to be 20 bytes long! + * @param prompt The optional custom prompt message to be shown to client. + * @param force If true, the client will be disconnected from the server + * when it declines to use the resource pack. + * @throws IllegalArgumentException Thrown if the URL is null. + * @throws IllegalArgumentException Thrown if the URL is too long. The + * length restriction is an implementation specific arbitrary value. + * @throws IllegalArgumentException Thrown if the hash is not 20 bytes + * long. + */ + public void setResourcePack(@NotNull String url, @Nullable byte[] hash, @Nullable String prompt, boolean force); /** * Gets the Scoreboard displayed to this player From 031731e60ece4b3fe0468a0df94b89d339b4169d Mon Sep 17 00:00:00 2001 From: md_5 Date: Sat, 18 Dec 2021 12:09:21 +1100 Subject: [PATCH 09/10] Dependency upgrades --- pom.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index bc9dea1f..b8c80195 100644 --- a/pom.xml +++ b/pom.xml @@ -61,33 +61,33 @@ org.apache.maven maven-resolver-provider - 3.8.1 + 3.8.4 provided org.apache.maven.resolver maven-resolver-connector-basic - 1.7.0 + 1.7.2 provided org.apache.maven.resolver maven-resolver-transport-http - 1.7.0 + 1.7.2 provided org.jetbrains annotations-java5 - 21.0.1 + 23.0.0 provided junit junit - 4.13.1 + 4.13.2 test @@ -136,7 +136,7 @@ org.eclipse.jdt ecj - 3.27.0 + 3.28.0 @@ -187,8 +187,8 @@ https://guava.dev/releases/31.0.1-jre/api/docs/ - https://javadoc.io/doc/org.yaml/snakeyaml/1.28/ - https://javadoc.io/doc/org.jetbrains/annotations-java5/21.0.1/ + https://javadoc.io/doc/org.yaml/snakeyaml/1.30/ + https://javadoc.io/doc/org.jetbrains/annotations-java5/23.0.0/ From 3e2dd2bc120754ea4db193e878050d0eb31a6894 Mon Sep 17 00:00:00 2001 From: Wolf2323 Date: Tue, 21 Dec 2021 08:35:19 +1100 Subject: [PATCH 10/10] SPIGOT-3247: Comment support for YAML files --- .../configuration/ConfigurationSection.java | 62 ++++ .../bukkit/configuration/MemorySection.java | 104 +++++-- .../bukkit/configuration/SectionPathData.java | 81 ++++++ .../configuration/file/FileConfiguration.java | 16 +- .../file/FileConfigurationOptions.java | 164 ++++++++--- .../configuration/file/YamlConfiguration.java | 266 ++++++++++++------ .../file/YamlConfigurationOptions.java | 24 ++ .../configuration/file/YamlConstructor.java | 5 + .../configuration/file/YamlRepresenter.java | 11 - .../file/FileConfigurationTest.java | 251 ++++++++++++----- .../file/YamlConfigurationTest.java | 41 ++- 11 files changed, 785 insertions(+), 240 deletions(-) create mode 100644 src/main/java/org/bukkit/configuration/SectionPathData.java 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