From 1b21f7a939bf74aea024a540bc17a43fe17ac3b1 Mon Sep 17 00:00:00 2001 From: EpicKnarvik97 Date: Fri, 5 Sep 2025 19:09:29 +0200 Subject: [PATCH] Adds code for config migration with comment retaining --- .../config/StargateYamlConfiguration.java | 296 ++++++++++++++++++ .../knarcraft/knarlib/util/ConfigHelper.java | 162 ++++++++++ 2 files changed, 458 insertions(+) create mode 100644 src/main/java/net/knarcraft/knarlib/config/StargateYamlConfiguration.java create mode 100644 src/main/java/net/knarcraft/knarlib/util/ConfigHelper.java diff --git a/src/main/java/net/knarcraft/knarlib/config/StargateYamlConfiguration.java b/src/main/java/net/knarcraft/knarlib/config/StargateYamlConfiguration.java new file mode 100644 index 0000000..32d66c5 --- /dev/null +++ b/src/main/java/net/knarcraft/knarlib/config/StargateYamlConfiguration.java @@ -0,0 +1,296 @@ +package net.knarcraft.knarlib.config; + +import net.knarcraft.knarlib.util.ConfigHelper; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; + +/** + * A YAML configuration which retains all comments + * + *

This 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, or use "getKeysWithoutComments".

+ * + *

Also note: This class was originally made for the Stargate rewrite, but can be used for any configuration file. + * In order to make sure the comments are retained, always use this class instead of plugin.getConfig() whenever code + * might choose to save the configuration.

+ * + * @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 + @Deprecated + @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)); + } + + /** + * Loads the configuration of the given plugin + * + * @param plugin

The plugin to load the configuration from

+ * @return

The loaded configuration, or null if unsuccessful

+ */ + @SuppressWarnings("unused") + @Nullable + public static StargateYamlConfiguration loadConfiguration(@NotNull Plugin plugin) { + StargateYamlConfiguration configuration = new StargateYamlConfiguration(); + configuration.options().copyDefaults(true); + try { + configuration.load(new File(plugin.getDataFolder(), ConfigHelper.getConfigFile())); + return configuration; + } catch (IOException | InvalidConfigurationException exception) { + plugin.getLogger().log(Level.SEVERE, "Unable to load the configuration! Message: " + exception.getMessage()); + return null; + } + } + + /** + * Saves the given configuration + * + * @param plugin

The plugin to save configuration for

+ * @param configuration

The configuration to save

+ * @return

True if the configuration was successfully saved

+ */ + @SuppressWarnings("unused") + public static boolean saveConfiguration(@NotNull Plugin plugin, @NotNull StargateYamlConfiguration configuration) { + try { + configuration.save(new File(plugin.getDataFolder(), ConfigHelper.getConfigFile())); + return true; + } catch (IOException exception) { + plugin.getLogger().log(Level.SEVERE, "Unable to save the configuration! Message: " + exception.getMessage()); + return false; + } + } + + /** + * Gets a configuration section's keys, without any comment entries + * + * @param configurationSection

The configuration section to get keys for

+ * @param deep

Whether to get keys for child elements as well

+ * @return

The configuration section's keys, with comment entries removed

+ */ + public static Set getKeysWithoutComments(@NotNull ConfigurationSection configurationSection, boolean deep) { + Set keys = new HashSet<>(configurationSection.getKeys(deep)); + keys.removeIf(key -> key.matches(START_OF_COMMENT + "[0-9]+")); + return keys; + } + + /** + * Reads a file with comments, and recreates them into yaml mappings + * + *

A 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.

+ * + * @param configString

The config string to convert

+ */ + @NotNull + private String convertCommentsToYAMLMappings(@NotNull String configString) { + StringBuilder yamlBuilder = new StringBuilder(); + List currentComment = new ArrayList<>(); + int commentId = 0; + int previousIndentation = 0; + + for (String line : configString.split("\n")) { + String trimmed = line.trim(); + if (trimmed.startsWith("#")) { + // Store the indentation of the block + if (currentComment.isEmpty()) { + previousIndentation = getIndentation(line); + } + //Temporarily store the comment line + addComment(currentComment, trimmed); + } else { + addYamlString(yamlBuilder, currentComment, line, previousIndentation, commentId); + commentId++; + previousIndentation = 0; + } + } + return yamlBuilder.toString(); + } + + /** + * Adds a YAML string to the given string builder + * + * @param yamlBuilder

The string builder used for building YAML

+ * @param currentComment

The comment to add as a YAML string

+ * @param line

The current line

+ * @param previousIndentation

The indentation of the current block comment

+ * @param commentId

The id of the comment

+ */ + private void addYamlString(@NotNull StringBuilder yamlBuilder, @NotNull List currentComment, + @NotNull String line, int previousIndentation, int commentId) { + String trimmed = line.trim(); + //Write the full formatted comment to the StringBuilder + if (!currentComment.isEmpty()) { + int indentation = trimmed.isEmpty() ? previousIndentation : getIndentation(line); + generateCommentYAML(yamlBuilder, currentComment, commentId, indentation); + currentComment.clear(); + } + //Add the non-comment line assuming it isn't empty + if (!trimmed.isEmpty()) { + yamlBuilder.append(line).append("\n"); + } + } + + /** + * Adds the given comment to the given list + * + * @param commentParts

The list to add to

+ * @param comment

The comment to add

+ */ + private void addComment(@NotNull List commentParts, @NotNull String comment) { + if (comment.startsWith("# ")) { + commentParts.add(comment.replaceFirst("# ", START_OF_COMMENT_LINE)); + } else { + commentParts.add(comment.replaceFirst("#", START_OF_COMMENT_LINE)); + } + } + + /** + * Generates a YAML-compatible string for one comment block + * + * @param yamlBuilder

The string builder to add the generated YAML to

+ * @param commentLines

The lines of the comment to convert into YAML

+ * @param commentId

The unique id of the comment

+ * @param indentation

The indentation to add to every line

+ */ + private void generateCommentYAML(@NotNull StringBuilder yamlBuilder, @NotNull List commentLines, + int commentId, int indentation) { + String subIndentation = this.addIndentation(indentation + 2); + //Add the comment start marker + yamlBuilder.append(this.addIndentation(indentation)).append(START_OF_COMMENT).append(commentId).append(": |\n"); + for (String commentLine : commentLines) { + //Add each comment line with the proper indentation + yamlBuilder.append(subIndentation).append(commentLine).append("\n"); + } + //Add the comment end marker + yamlBuilder.append(subIndentation).append(subIndentation).append(END_OF_COMMENT).append("\n"); + } + + /** + * Converts the internal YAML mapping format to a readable config file + * + *

The 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 yamlString

A string using the YAML format

+ * @return

The 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 builder

The string builder to write to

+ * @param lines

The lines to read from

+ * @param startIndex

The index to start reading from

+ * @param commentIndentation

The indentation of the read comment

+ * @return

The 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 indentationSpaces

The number spaces to use for indentation

+ * @return

A 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 line

The line to get indentation of

+ * @return

The 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/knarlib/util/ConfigHelper.java b/src/main/java/net/knarcraft/knarlib/util/ConfigHelper.java new file mode 100644 index 0000000..33d6934 --- /dev/null +++ b/src/main/java/net/knarcraft/knarlib/util/ConfigHelper.java @@ -0,0 +1,162 @@ +package net.knarcraft.knarlib.util; + +import net.knarcraft.knarlib.config.StargateYamlConfiguration; +import net.knarcraft.knarlib.property.ColorConversion; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.MemorySection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.logging.Level; + +/** + * A helper class for dealing with a plugin's configuration file + */ +@SuppressWarnings("unused") +public final class ConfigHelper { + + private static final String CONFIG_FILE = "config.yml"; + private static final String BACKUP_CONFIG_FILE = "config.yml.old"; + private static final String MIGRATION_FILE = "config-migrations.txt"; + + private ConfigHelper() { + + } + + /** + * Gets the relative string path of the configuration file + * + * @return

The configuration file path

+ */ + public static String getConfigFile() { + return CONFIG_FILE; + } + + /** + * Saves any missing configuration values to the given plugin's configuration + * + * @param plugin

The plugin to add missing default values to

+ */ + public static void saveDefaults(@NotNull Plugin plugin) { + plugin.saveDefaultConfig(); + plugin.getConfig().options().copyDefaults(true); + plugin.reloadConfig(); + plugin.saveConfig(); + } + + /** + * Changes all configuration values from the old name to the new name + * + *

Note: This method expects a file "config-migrations.txt" in the resources directory that contains mappings: + * oldKey=replacementKey in order to migrate from old to new configuration values. + * The old configuration file will be saved to config.yml.old

+ * + * @param plugin

The plugin to migrate the configuration for

+ * @return

True if the migration succeeded without any issues

+ */ + public static boolean migrateConfig(@NotNull Plugin plugin) { + File dataFolder = plugin.getDataFolder(); + //Save the old config just in case something goes wrong + try { + StargateYamlConfiguration currentConfiguration = new StargateYamlConfiguration(); + currentConfiguration.load(new File(dataFolder, CONFIG_FILE)); + currentConfiguration.save(new File(dataFolder, BACKUP_CONFIG_FILE)); + } catch (IOException | InvalidConfigurationException exception) { + plugin.getLogger().log(Level.WARNING, "Unable to save old backup and do migration"); + return false; + } + + //Load old and new configuration + plugin.reloadConfig(); + FileConfiguration oldConfiguration = plugin.getConfig(); + InputStream configStream = FileHelper.getInputStreamForInternalFile("/" + CONFIG_FILE); + if (configStream == null) { + plugin.getLogger().log(Level.SEVERE, "Could not migrate the configuration, as the internal " + + "configuration could not be read!"); + return false; + } + YamlConfiguration newConfiguration = StargateYamlConfiguration.loadConfiguration( + FileHelper.getBufferedReaderFromInputStream(configStream)); + + //Read all available config migrations + Map migrationFields; + try { + InputStream migrationStream = FileHelper.getInputStreamForInternalFile("/" + MIGRATION_FILE); + if (migrationStream == null) { + plugin.getLogger().log(Level.SEVERE, "Could not migrate the configuration, as the internal " + + "migration paths could not be read!"); + return false; + } + migrationFields = FileHelper.readKeyValuePairs(FileHelper.getBufferedReaderFromInputStream(migrationStream), + "=", ColorConversion.NONE); + } catch (IOException exception) { + plugin.getLogger().log(Level.WARNING, "Unable to load config migration file"); + return false; + } + + //Replace old config names with the new ones + for (String key : migrationFields.keySet()) { + if (oldConfiguration.contains(key)) { + migrateProperty(migrationFields, key, oldConfiguration); + } + } + + // Copy all keys to the new config + for (String key : StargateYamlConfiguration.getKeysWithoutComments(oldConfiguration, true)) { + if (oldConfiguration.get(key) instanceof MemorySection) { + continue; + } + newConfiguration.set(key, oldConfiguration.get(key)); + } + + try { + newConfiguration.save(new File(dataFolder, CONFIG_FILE)); + } catch (IOException exception) { + plugin.getLogger().log(Level.WARNING, "Unable to save migrated config"); + return false; + } + + plugin.reloadConfig(); + return true; + } + + /** + * Migrates one configuration property + * + * @param migrationFields

The configuration fields to be migrated

+ * @param key

The key/path of the property to migrate

+ * @param oldConfiguration

The original pre-migration configuration

+ */ + private static void migrateProperty(@NotNull Map migrationFields, @NotNull String key, + @NotNull FileConfiguration oldConfiguration) { + String newPath = migrationFields.get(key); + Object oldValue = oldConfiguration.get(key); + if (!newPath.trim().isEmpty()) { + if (oldConfiguration.isConfigurationSection(key)) { + // Copy each value of a configuration section + ConfigurationSection sourceSection = oldConfiguration.getConfigurationSection(key); + ConfigurationSection destinationSection = oldConfiguration.createSection(newPath); + if (sourceSection == null) { + return; + } + for (String path : StargateYamlConfiguration.getKeysWithoutComments(sourceSection, true)) { + destinationSection.set(path, sourceSection.get(path)); + } + } else { + // Copy the value to the new path + oldConfiguration.set(newPath, oldValue); + } + } + + // Remove the old path's value + oldConfiguration.set(key, null); + } + +}