diff --git a/src/main/java/net/knarcraft/blacksmith/BlacksmithPlugin.java b/src/main/java/net/knarcraft/blacksmith/BlacksmithPlugin.java index 15fb73c..29dea58 100644 --- a/src/main/java/net/knarcraft/blacksmith/BlacksmithPlugin.java +++ b/src/main/java/net/knarcraft/blacksmith/BlacksmithPlugin.java @@ -13,6 +13,7 @@ import net.knarcraft.blacksmith.command.scrapper.ScrapperConfigCommand; import net.knarcraft.blacksmith.command.scrapper.ScrapperConfigTabCompleter; import net.knarcraft.blacksmith.command.scrapper.ScrapperEditCommand; import net.knarcraft.blacksmith.command.scrapper.ScrapperEditTabCompleter; +import net.knarcraft.blacksmith.config.StargateYamlConfiguration; import net.knarcraft.blacksmith.config.blacksmith.GlobalBlacksmithSettings; import net.knarcraft.blacksmith.config.scrapper.GlobalScrapperSettings; import net.knarcraft.blacksmith.formatting.BlacksmithTranslatableMessage; @@ -21,15 +22,15 @@ import net.knarcraft.blacksmith.listener.PlayerListener; import net.knarcraft.blacksmith.manager.EconomyManager; import net.knarcraft.blacksmith.trait.BlacksmithTrait; import net.knarcraft.blacksmith.trait.ScrapperTrait; +import net.knarcraft.blacksmith.util.ConfigHelper; import net.knarcraft.knarlib.formatting.StringFormatter; import net.knarcraft.knarlib.formatting.TranslatableTimeUnit; import net.knarcraft.knarlib.formatting.Translator; -import net.knarcraft.knarlib.property.ColorConversion; -import net.knarcraft.knarlib.util.FileHelper; import net.knarcraft.knarlib.util.UpdateChecker; import org.bukkit.command.CommandExecutor; import org.bukkit.command.PluginCommand; import org.bukkit.command.TabCompleter; +import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.PluginManager; @@ -40,9 +41,6 @@ import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import java.util.Map; import java.util.logging.Level; /** @@ -50,11 +48,13 @@ import java.util.logging.Level; */ public class BlacksmithPlugin extends JavaPlugin { + private static final String CONFIG_FILE_NAME = "config.yml"; private static BlacksmithPlugin instance; private GlobalBlacksmithSettings blacksmithConfig; private GlobalScrapperSettings scrapperConfig; private static Translator translator; private static StringFormatter stringFormatter; + private FileConfiguration configuration; /** * Constructor required for MockBukkit @@ -107,8 +107,18 @@ public class BlacksmithPlugin extends JavaPlugin { this.reloadConfig(); blacksmithConfig.load(); scrapperConfig.load(); - translator.loadLanguages(this.getDataFolder(), "en", - this.getConfig().getString("language", "en")); + translator.loadLanguages(this.getDataFolder(), "en", this.getConfiguration().getString( + "language", "en")); + } + + /** + * Gets the raw configuration + * + * @return
The raw configuration
+ */ + @NotNull + public FileConfiguration getConfiguration() { + return this.configuration; } /** @@ -139,18 +149,23 @@ public class BlacksmithPlugin extends JavaPlugin { instance = this; //Copy default config to disk - FileConfiguration fileConfiguration = this.getConfig(); this.saveDefaultConfig(); - if (fileConfiguration.getString("defaults.dropItem") != null) { - migrateConfig(fileConfiguration); - this.saveConfig(); + this.getConfig(); + this.configuration = new StargateYamlConfiguration(); + try { + this.configuration.load(new File(getDataFolder(), CONFIG_FILE_NAME)); + } catch (IOException | InvalidConfigurationException exception) { + getLogger().log(Level.SEVERE, exception.getMessage()); } - fileConfiguration.options().copyDefaults(true); - this.reloadConfig(); - this.saveConfig(); + this.configuration.options().copyDefaults(true); // Initialize custom configuration files - initializeConfigurations(fileConfiguration); + this.reloadConfig(); + if (this.configuration.getString("scrapper.defaults.dropItem") == null) { + ConfigHelper.migrateConfig(this.getDataFolder().getPath().replaceAll("\\\\", "/"), this.configuration); + this.reloadConfig(); + } + initializeConfigurations(this.configuration); //Set up Vault integration if (!setUpVault()) { @@ -174,6 +189,28 @@ public class BlacksmithPlugin extends JavaPlugin { () -> this.getDescription().getVersion(), null); } + @Override + public void reloadConfig() { + super.reloadConfig(); + this.configuration = new StargateYamlConfiguration(); + this.configuration.options().copyDefaults(true); + try { + this.configuration.load(new File(getDataFolder(), CONFIG_FILE_NAME)); + } catch (IOException | InvalidConfigurationException exception) { + getLogger().log(Level.SEVERE, "Unable to load the configuration! Message: " + exception.getMessage()); + } + } + + @Override + public void saveConfig() { + super.saveConfig(); + try { + this.configuration.save(new File(getDataFolder(), CONFIG_FILE_NAME)); + } catch (IOException exception) { + getLogger().log(Level.SEVERE, "Unable to save the configuration! Message: " + exception.getMessage()); + } + } + /** * Initializes custom configuration and translation * @@ -256,48 +293,4 @@ public class BlacksmithPlugin extends JavaPlugin { } } - /** - * Changes all configuration values from the old name to the new name - * - * @param fileConfigurationThe config to read from and write to
- */ - private void migrateConfig(@NotNull FileConfiguration fileConfiguration) { - //Save the old config just in case something goes wrong - try { - fileConfiguration.save(getDataFolder() + "/config.yml.old"); - } catch (IOException exception) { - BlacksmithPlugin.getInstance().getLogger().log(Level.SEVERE, Arrays.toString(exception.getStackTrace())); - return; - } - - //Read all available config migrations - MapThis configuration converts all comments to YAML values when loaded, which all start with comment_. When saved, + * those YAML values are converted to normal text comments. This ensures that the comments aren't removed by the + * YamlConfiguration during its parsing.
+ * + *Note: When retrieving a configuration section, that will include comments that start with "comment_". You should + * filter those before parsing input.
+ * + * @author Kristian Knarvik + * @author Thorin + */ +public class StargateYamlConfiguration extends YamlConfiguration { + + private static final String START_OF_COMMENT_LINE = "[HASHTAG]"; + private static final String END_OF_COMMENT = "_endOfComment_"; + private static final String START_OF_COMMENT = "comment_"; + + @Override + @NotNull + @SuppressWarnings("deprecation") + protected String buildHeader() { + return ""; + } + + @Override + @NotNull + public String saveToString() { + // Convert YAML comments to normal comments + return this.convertYAMLMappingsToComments(super.saveToString()); + } + + @Override + public void loadFromString(@NotNull String contents) throws InvalidConfigurationException { + // Convert normal comments to YAML comments to prevent them from disappearing + super.loadFromString(this.convertCommentsToYAMLMappings(contents)); + } + + /** + * Gets a configuration section's keys, without any comment entries + * + * @param configurationSectionThe configuration section to get keys for
+ * @param deepWhether to get keys for child elements as well
+ * @returnThe configuration section's keys, with comment entries removed
+ */ + public static SetA mapping follows this format: comment_{CommentNumber}: "The comment" + * This needs to be done as comments otherwise get removed using + * the {@link FileConfiguration#save(File)} method. The config + * needs to be saved if a config value has changed.
+ */ + @NotNull + private String convertCommentsToYAMLMappings(@NotNull String configString) { + StringBuilder yamlBuilder = new StringBuilder(); + ListThe string builder used for building YAML
+ * @param currentCommentThe comment to add as a YAML string
+ * @param lineThe current line
+ * @param previousIndentationThe indentation of the current block comment
+ * @param commentIdThe id of the comment
+ */ + private void addYamlString(@NotNull StringBuilder yamlBuilder, @NotNull ListThe list to add to
+ * @param commentThe comment to add
+ */ + private void addComment(@NotNull ListThe string builder to add the generated YAML to
+ * @param commentLinesThe lines of the comment to convert into YAML
+ * @param commentIdThe unique id of the comment
+ * @param indentationThe indentation to add to every line
+ */ + private void generateCommentYAML(@NotNull StringBuilder yamlBuilder, @NotNull ListThe internal YAML structure is converted to a string with the same format as a standard configuration file. + * The internal structure has comments in the format: START_OF_COMMENT + id + multi-line YAML string + + * END_OF_COMMENT.
+ * + * @param yamlStringA string using the YAML format
+ * @returnThe corresponding comment string
+ */ + @NotNull + private String convertYAMLMappingsToComments(@NotNull String yamlString) { + StringBuilder finalText = new StringBuilder(); + + String[] lines = yamlString.split("\n"); + for (int currentIndex = 0; currentIndex < lines.length; currentIndex++) { + String line = lines[currentIndex]; + String possibleComment = line.trim(); + + if (possibleComment.startsWith(START_OF_COMMENT)) { + //Add an empty line before every comment block + finalText.append("\n"); + currentIndex = readComment(finalText, lines, currentIndex + 1, getIndentation(line)); + } else { + //Output the configuration key + finalText.append(line).append("\n"); + } + } + + return finalText.toString().trim(); + } + + /** + * Fully reads a comment + * + * @param builderThe string builder to write to
+ * @param linesThe lines to read from
+ * @param startIndexThe index to start reading from
+ * @param commentIndentationThe indentation of the read comment
+ * @returnThe index containing the next non-comment line
+ */ + private int readComment(@NotNull StringBuilder builder, @NotNull String[] lines, int startIndex, + int commentIndentation) { + for (int currentIndex = startIndex; currentIndex < lines.length; currentIndex++) { + String line = lines[currentIndex]; + String possibleComment = line.trim(); + if (!line.contains(END_OF_COMMENT)) { + possibleComment = possibleComment.replace(START_OF_COMMENT_LINE, ""); + builder.append(addIndentation(commentIndentation)).append("# ").append(possibleComment).append("\n"); + } else { + return currentIndex; + } + } + + return startIndex; + } + + /** + * Gets a string containing the given indentation + * + * @param indentationSpacesThe number spaces to use for indentation
+ * @returnA string containing the number of spaces specified
+ */ + @NotNull + private String addIndentation(int indentationSpaces) { + return " ".repeat(Math.max(0, indentationSpaces)); + } + + + /** + * Gets the indentation (number of spaces) of the given line + * + * @param lineThe line to get indentation of
+ * @returnThe number of spaces in the line's indentation
+ */ + private int getIndentation(@NotNull String line) { + int spacesFound = 0; + for (char aCharacter : line.toCharArray()) { + if (aCharacter == ' ') { + spacesFound++; + } else { + break; + } + } + return spacesFound; + } + +} \ No newline at end of file diff --git a/src/main/java/net/knarcraft/blacksmith/config/blacksmith/GlobalBlacksmithSettings.java b/src/main/java/net/knarcraft/blacksmith/config/blacksmith/GlobalBlacksmithSettings.java index c3cb514..0173001 100644 --- a/src/main/java/net/knarcraft/blacksmith/config/blacksmith/GlobalBlacksmithSettings.java +++ b/src/main/java/net/knarcraft/blacksmith/config/blacksmith/GlobalBlacksmithSettings.java @@ -1,20 +1,21 @@ package net.knarcraft.blacksmith.config.blacksmith; -import net.citizensnpcs.api.util.DataKey; -import net.citizensnpcs.api.util.YamlStorage; import net.knarcraft.blacksmith.BlacksmithPlugin; import net.knarcraft.blacksmith.config.SettingValueType; import net.knarcraft.blacksmith.config.Settings; +import net.knarcraft.blacksmith.config.StargateYamlConfiguration; import net.knarcraft.blacksmith.util.ConfigHelper; import net.knarcraft.blacksmith.util.InputParsingHelper; import net.knarcraft.blacksmith.util.ItemHelper; import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.enchantments.Enchantment; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.File; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -31,28 +32,21 @@ public class GlobalBlacksmithSettings implements SettingsA reference to the blacksmith plugin
+ * @param instanceThe blacksmith plugin to load and save configurations for
*/ - public GlobalBlacksmithSettings(BlacksmithPlugin plugin) { - this.defaultConfig = new YamlStorage(new File(plugin.getDataFolder() + File.separator + "config.yml"), - "Blacksmith Configuration\nWarning: The values under defaults are the values set for a " + - "blacksmith upon creation. To change any values for existing NPCs, edit the citizens NPC file."); + public GlobalBlacksmithSettings(BlacksmithPlugin instance) { + this.instance = instance; } /** * Loads all configuration values from the config file */ public void load() { - // Load the config from disk - this.defaultConfig.load(); - DataKey root = this.defaultConfig.getKey(""); - // Just in case, clear existing values this.settings.clear(); this.materialBasePrices.clear(); @@ -60,10 +54,10 @@ public class GlobalBlacksmithSettings implements SettingsThe root node of all global settings
*/ - private void loadSettings(DataKey root) { + private void loadSettings() { + instance.reloadConfig(); + FileConfiguration configuration = instance.getConfiguration(); + for (BlacksmithSetting blacksmithSetting : BlacksmithSetting.values()) { - if (!root.keyExists(blacksmithSetting.getPath())) { + if (!configuration.contains(blacksmithSetting.getPath())) { //If the setting does not exist in the config file, add it - root.setRaw(blacksmithSetting.getPath(), blacksmithSetting.getDefaultValue()); + configuration.set(blacksmithSetting.getPath(), blacksmithSetting.getDefaultValue()); } else { //Set the setting to the value found in the path - settings.put(blacksmithSetting, root.getRaw(blacksmithSetting.getPath())); + settings.put(blacksmithSetting, configuration.get(blacksmithSetting.getPath())); } } loadReforgeAbleItems(); loadEnchantmentBlockList(); //Load all base prices - loadBasePrices(root); + loadBasePrices(configuration); //Load all per-durability-point prices - loadPricesPerDurabilityPoint(root); + loadPricesPerDurabilityPoint(configuration); //Load all enchantment prices - DataKey enchantmentCostNode = root.getRelative(BlacksmithSetting.ENCHANTMENT_COST.getPath()); + loadEnchantmentPrices(configuration); + } + + /** + * Loads all prices for enchantments + * + * @param fileConfigurationThe configuration to read
+ */ + private void loadEnchantmentPrices(@NotNull FileConfiguration fileConfiguration) { + ConfigurationSection enchantmentCostNode = fileConfiguration.getConfigurationSection( + getBase(BlacksmithSetting.ENCHANTMENT_COST.getPath())); + if (enchantmentCostNode == null) { + instance.getLogger().log(Level.WARNING, "Could not load enchantment prices. because the " + + "configuration section doesn't exist"); + return; + } MapThe configuration root node to search from
+ * @param fileConfigurationThe configuration to read
*/ - private void loadPricesPerDurabilityPoint(DataKey root) { - DataKey basePerDurabilityPriceNode = root.getRelative(BlacksmithSetting.PRICE_PER_DURABILITY_POINT.getPath()); + private void loadPricesPerDurabilityPoint(@NotNull FileConfiguration fileConfiguration) { + ConfigurationSection basePerDurabilityPriceNode = fileConfiguration.getConfigurationSection( + getBase(BlacksmithSetting.PRICE_PER_DURABILITY_POINT.getPath())); + if (basePerDurabilityPriceNode == null) { + instance.getLogger().log(Level.WARNING, "Could not load per durability prices. because the " + + "configuration section doesn't exist"); + return; + } MapThe configuration root node to search from
+ * @param fileConfigurationThe configuration to read
*/ - private void loadBasePrices(DataKey root) { - DataKey basePriceNode = root.getRelative(BlacksmithSetting.BASE_PRICE.getPath()); + private void loadBasePrices(@NotNull FileConfiguration fileConfiguration) { + ConfigurationSection basePriceNode = fileConfiguration.getConfigurationSection( + getBase(BlacksmithSetting.BASE_PRICE.getPath())); + if (basePriceNode == null) { + instance.getLogger().log(Level.WARNING, "Could not load base prices, because the configuration " + + "section doesn't exist"); + return; + } MapThe material name to match
* @param priceThe price to set for the matched materials
*/ - private void setMatchedMaterialPrices(MapThe material parsed from the name
* @param priceThe price to set
*/ - privateThe root data key containing sub-keys
+ * @param configurationSectionThe configuration section to search
* @returnAny sub-keys found that aren't the default
*/ - private MapThe normalized name to un-normalize
* @returnThe un-normalized name
*/ - private String unNormalizeName(String normalizedName) { + @NotNull + private String unNormalizeName(@NotNull String normalizedName) { return normalizedName.toLowerCase().replace("_", "-"); } @@ -481,32 +505,66 @@ public class GlobalBlacksmithSettings implements SettingsThe file configuration to read
+ * @param sectionPathThe path to the configuration section to get
+ * @returnThe configuration section
+ */ + @NotNull + private ConfigurationSection getAndCreateSection(@NotNull FileConfiguration fileConfiguration, @NotNull String sectionPath) { + ConfigurationSection configurationSection = fileConfiguration.getConfigurationSection(sectionPath); + if (configurationSection == null) { + fileConfiguration.createSection(sectionPath); + configurationSection = fileConfiguration.getConfigurationSection(sectionPath); + } + if (configurationSection == null) { + throw new RuntimeException("Unable to create configuration section!"); + } + return configurationSection; + } + + /** + * Gets the base path of a configuration path pointing to a specific value + * + * @param configurationPathThe configuration path to get the base of
+ * @returnThe base configuration path
+ */ + @NotNull + private String getBase(@NotNull String configurationPath) { + String[] parts = configurationPath.split("\\."); + String[] base = Arrays.copyOfRange(parts, 0, parts.length - 1); + return String.join(".", base); } } diff --git a/src/main/java/net/knarcraft/blacksmith/config/scrapper/GlobalScrapperSettings.java b/src/main/java/net/knarcraft/blacksmith/config/scrapper/GlobalScrapperSettings.java index 3b25055..80dcce9 100644 --- a/src/main/java/net/knarcraft/blacksmith/config/scrapper/GlobalScrapperSettings.java +++ b/src/main/java/net/knarcraft/blacksmith/config/scrapper/GlobalScrapperSettings.java @@ -1,17 +1,15 @@ package net.knarcraft.blacksmith.config.scrapper; -import net.citizensnpcs.api.util.DataKey; -import net.citizensnpcs.api.util.YamlStorage; import net.knarcraft.blacksmith.BlacksmithPlugin; import net.knarcraft.blacksmith.config.SettingValueType; import net.knarcraft.blacksmith.config.Settings; import net.knarcraft.blacksmith.util.ConfigHelper; import net.knarcraft.blacksmith.util.ItemHelper; import org.bukkit.Material; +import org.bukkit.configuration.file.FileConfiguration; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -29,35 +27,29 @@ public class GlobalScrapperSettings implements SettingsA reference to the blacksmith plugin
+ * @param instanceA reference to the blacksmith plugin
*/ - public GlobalScrapperSettings(BlacksmithPlugin plugin) { - this.defaultConfig = new YamlStorage(new File(plugin.getDataFolder() + File.separator + "config.yml"), - "Scrapper Configuration\nWarning: The values under defaults are the values set for a " + - "scrapper upon creation. To change any values for existing NPCs, edit the citizens NPC file."); + public GlobalScrapperSettings(BlacksmithPlugin instance) { + this.instance = instance; } /** * Loads all configuration values from the config file */ public void load() { - //Load the config from disk - this.defaultConfig.load(); - DataKey root = this.defaultConfig.getKey(""); - //Just in case, clear existing values this.settings.clear(); //Load/Save global settings - loadSettings(root); + loadSettings(); //Save any modified values to disk - this.defaultConfig.save(); + instance.saveConfig(); } /** @@ -137,17 +129,18 @@ public class GlobalScrapperSettings implements SettingsThe root node of all global settings
*/ - private void loadSettings(DataKey root) { + private void loadSettings() { + instance.reloadConfig(); + FileConfiguration configuration = instance.getConfiguration(); + for (ScrapperSetting setting : ScrapperSetting.values()) { - if (!root.keyExists(setting.getPath())) { + if (!configuration.contains(setting.getPath())) { //If the setting does not exist in the config file, add it - root.setRaw(setting.getPath(), setting.getDefaultValue()); + configuration.set(setting.getPath(), setting.getDefaultValue()); } else { //Set the setting to the value found in the path - this.settings.put(setting, root.getRaw(setting.getPath())); + this.settings.put(setting, configuration.get(setting.getPath())); } } loadSalvageAbleItems(); @@ -158,14 +151,15 @@ public class GlobalScrapperSettings implements SettingsThe current config to back up
+ */ + public static void migrateConfig(@NotNull String dataFolderPath, @NotNull FileConfiguration currentConfiguration) { + BlacksmithPlugin instance = BlacksmithPlugin.getInstance(); + + //Save the old config just in case something goes wrong + try { + currentConfiguration.save(new File(dataFolderPath, "config.yml.old")); + } catch (IOException exception) { + instance.getLogger().log(Level.WARNING, "Unable to save old backup and do migration"); + return; + } + + //Load old and new configuration + instance.reloadConfig(); + FileConfiguration oldConfiguration = instance.getConfig(); + InputStream configStream = FileHelper.getInputStreamForInternalFile("/config.yml"); + if (configStream == null) { + instance.getLogger().log(Level.SEVERE, "Could not migrate the configuration, as the internal " + + "configuration could not be read!"); + return; + } + YamlConfiguration newConfiguration = StargateYamlConfiguration.loadConfiguration( + FileHelper.getBufferedReaderFromInputStream(configStream)); + + //Read all available config migrations + MapThe configuration fields to be migrated
+ * @param keyThe key/path of the property to migrate
+ * @param oldConfigurationThe original pre-migration configuration
+ */ + private static void migrateProperty(@NotNull Map