From 20b4f884d234b56645d06b8398aff765d348d3a7 Mon Sep 17 00:00:00 2001 From: Logan Magnan Date: Tue, 24 Sep 2024 16:07:24 -0400 Subject: [PATCH] added config updater --- .../pluginbase/utils/ConfigUpdater.java | 368 ++++++++++++++++++ .../pluginbase/utils/KeyBuilder.java | 106 +++++ .../pluginbase/utils/KeyUtils.java | 22 ++ .../pluginbase/utils/config/FileConfig.java | 9 + .../pluginbase/utils/config/file/Config.java | 9 + .../utils/config/file/ConfigFile.java | 8 + 6 files changed, 522 insertions(+) create mode 100644 src/main/java/com/loganmagnan/pluginbase/utils/ConfigUpdater.java create mode 100644 src/main/java/com/loganmagnan/pluginbase/utils/KeyBuilder.java create mode 100644 src/main/java/com/loganmagnan/pluginbase/utils/KeyUtils.java diff --git a/src/main/java/com/loganmagnan/pluginbase/utils/ConfigUpdater.java b/src/main/java/com/loganmagnan/pluginbase/utils/ConfigUpdater.java new file mode 100644 index 0000000..6efb4fd --- /dev/null +++ b/src/main/java/com/loganmagnan/pluginbase/utils/ConfigUpdater.java @@ -0,0 +1,368 @@ +package com.loganmagnan.pluginbase.utils; + +import com.google.common.base.Preconditions; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.configuration.file.YamlConstructor; +import org.bukkit.configuration.file.YamlRepresenter; +import org.bukkit.plugin.Plugin; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +public class ConfigUpdater { + + //Used for separating keys in the keyBuilder inside parseComments method + private static final char SEPARATOR = '.'; + + public static void update(Plugin plugin, String resourceName, File toUpdate, String... ignoredSections) throws IOException { + update(plugin, resourceName, toUpdate, Arrays.asList(ignoredSections)); + } + + public static void update(Plugin plugin, String resourceName, File toUpdate, List ignoredSections) throws IOException { + Preconditions.checkArgument(toUpdate.exists(), "The toUpdate file doesn't exist!"); + + FileConfiguration defaultConfig = YamlConfiguration.loadConfiguration(new InputStreamReader(plugin.getResource(resourceName), StandardCharsets.UTF_8)); + FileConfiguration currentConfig = YamlConfiguration.loadConfiguration(toUpdate); + Map comments = parseComments(plugin, resourceName, defaultConfig); + Map ignoredSectionsValues = parseIgnoredSections(toUpdate, comments, ignoredSections == null ? Collections.emptyList() : ignoredSections); + // will write updated config file "contents" to a string + StringWriter writer = new StringWriter(); + write(defaultConfig, currentConfig, new BufferedWriter(writer), comments, ignoredSectionsValues); + String value = writer.toString(); // config contents + + Path toUpdatePath = toUpdate.toPath(); + if (!value.equals(new String(Files.readAllBytes(toUpdatePath), StandardCharsets.UTF_8))) { // if updated contents are not the same as current file contents, update + Files.write(toUpdatePath, value.getBytes(StandardCharsets.UTF_8)); + } + } + + private static void write(FileConfiguration defaultConfig, FileConfiguration currentConfig, BufferedWriter writer, Map comments, Map ignoredSectionsValues) throws IOException { + //Used for converting objects to yaml, then cleared + FileConfiguration parserConfig = new YamlConfiguration(); + + for (String fullKey : defaultConfig.getKeys(true)) { + String indents = KeyUtils.getIndents(fullKey, SEPARATOR); + + + if (!ignoredSectionsValues.isEmpty()) { + if (writeIgnoredSectionValueIfExists(ignoredSectionsValues, writer, fullKey)) + continue; + } + writeCommentIfExists(comments, writer, fullKey, indents); + Object currentValue = currentConfig.get(fullKey); + + if (currentValue == null) + currentValue = defaultConfig.get(fullKey); + + String[] splitFullKey = fullKey.split("[" + SEPARATOR + "]"); + String trailingKey = splitFullKey[splitFullKey.length - 1]; + + if (currentValue instanceof ConfigurationSection) { + writeConfigurationSection(writer, indents, trailingKey, (ConfigurationSection) currentValue); + continue; + } + writeYamlValue(parserConfig, writer, indents, trailingKey, currentValue); + } + + String danglingComments = comments.get(null); + + if (danglingComments != null) + writer.write(danglingComments); + + writer.close(); + } + + //Returns a map of key comment pairs. If a key doesn't have any comments it won't be included in the map. + private static Map parseComments(Plugin plugin, String resourceName, FileConfiguration defaultConfig) throws IOException { + //keys are in order + List keys = new ArrayList<>(defaultConfig.getKeys(true)); + BufferedReader reader = new BufferedReader(new InputStreamReader(plugin.getResource(resourceName))); + Map comments = new LinkedHashMap<>(); + StringBuilder commentBuilder = new StringBuilder(); + KeyBuilder keyBuilder = new KeyBuilder(defaultConfig, SEPARATOR); + String currentValidKey = null; + + String line; + while ((line = reader.readLine()) != null) { + String trimmedLine = line.trim(); + //Only getting comments for keys. A list/array element comment(s) not supported + if (trimmedLine.startsWith("-")) continue; + + if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) {//Is blank line or is comment + commentBuilder.append(trimmedLine).append("\n"); + } else {//is a valid yaml key + //This part verifies if it is the first non-nested key in the YAML file and then stores the result as the next non-nested value. + if (!line.startsWith(" ")) { + keyBuilder.clear();//add clear method instead of create new instance. + currentValidKey = trimmedLine; + } + + keyBuilder.parseLine(trimmedLine, true); + String key = keyBuilder.toString(); + + //If there is a comment associated with the key it is added to comments map and the commentBuilder is reset + if (commentBuilder.length() > 0) { + comments.put(key, commentBuilder.toString()); + commentBuilder.setLength(0); + } + + int nextKeyIndex = keys.indexOf(keyBuilder.toString()) + 1; + if (nextKeyIndex < keys.size()) { + + String nextKey = keys.get(nextKeyIndex); + while (!keyBuilder.isEmpty() && !nextKey.startsWith(keyBuilder.toString())) { + keyBuilder.removeLastKey(); + } + //If all keys are cleared in a loop, then the first key from the nested keys in the YAML file is assigned to this keyBuilder instance. + //If the file contains multiple non-nested keys, the next first non-nested key will be used. + if (keyBuilder.isEmpty()) { + keyBuilder.parseLine(currentValidKey, false); + } + } + } + } + reader.close(); + + if (commentBuilder.length() > 0) + comments.put(null, commentBuilder.toString()); + + return comments; + } + + private static Map parseIgnoredSections(File toUpdate, Map comments, List ignoredSections) throws IOException { + Map ignoredSectionValues = new LinkedHashMap<>(ignoredSections.size()); + + DumperOptions options = new DumperOptions(); + options.setLineBreak(DumperOptions.LineBreak.UNIX); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + Yaml yaml = new Yaml(new YamlConstructor(), new YamlRepresenter(), options); + + Map root = (Map) yaml.load(new FileReader(toUpdate)); + ignoredSections.forEach(section -> { + String[] split = section.split("[" + SEPARATOR + "]"); + String key = split[split.length - 1]; + Map map = getSection(section, root); + + StringBuilder keyBuilder = new StringBuilder(); + for (int i = 0; i < split.length; i++) { + if (i != split.length - 1) { + if (keyBuilder.length() > 0) + keyBuilder.append(SEPARATOR); + + keyBuilder.append(split[i]); + } + } + + ignoredSectionValues.put(section, buildIgnored(key, map, comments, keyBuilder, new StringBuilder(), yaml)); + }); + + return ignoredSectionValues; + } + + private static Map getSection(String fullKey, Map root) { + String[] keys = fullKey.split("[" + SEPARATOR + "]", 2); + String key = keys[0]; + Object value = root.get(getKeyAsObject(key, root)); + + if (keys.length == 1) { + if (value instanceof Map) + return root; + /* if (value == null) { + Map map= new HashMap<>(); + map.put(key,"{}"); + System.out.println("key " + key); + return map; + }*/ + throw new IllegalArgumentException("Ignored sections must be a ConfigurationSection not a value!"); + } + + if (!(value instanceof Map)) + throw new IllegalArgumentException("Invalid ignored ConfigurationSection specified!"); + + return getSection(keys[1], (Map) value); + } + + private static String buildIgnored(String fullKey, Map ymlMap, Map comments, StringBuilder keyBuilder, StringBuilder ignoredBuilder, Yaml yaml) { + //0 will be the next key, 1 will be the remaining keys + String[] keys = fullKey.split("[" + SEPARATOR + "]", 2); + String key = keys[0]; + Object originalKey = getKeyAsObject(key, ymlMap); + + if (keyBuilder.length() > 0) + keyBuilder.append("."); + + keyBuilder.append(key); + + if (!ymlMap.containsKey(originalKey)) { + if (keys.length == 1) + throw new IllegalArgumentException("Invalid ignored section: " + keyBuilder); + + throw new IllegalArgumentException("Invalid ignored section: " + keyBuilder + "." + keys[1]); + } + + String comment = comments.get(keyBuilder.toString()); + String indents = KeyUtils.getIndents(keyBuilder.toString(), SEPARATOR); + + if (comment != null) + ignoredBuilder.append(addIndentation(comment, indents)).append("\n"); + + ignoredBuilder.append(addIndentation(key, indents)).append(":"); + Object obj = ymlMap.get(originalKey); + + if (obj instanceof Map) { + Map map = (Map) obj; + + if (map.isEmpty()) { + ignoredBuilder.append(" {}\n"); + } else { + ignoredBuilder.append("\n"); + } + + StringBuilder preLoopKey = new StringBuilder(keyBuilder); + + for (Object o : map.keySet()) { + buildIgnored(o.toString(), map, comments, keyBuilder, ignoredBuilder, yaml); + keyBuilder = new StringBuilder(preLoopKey); + } + } else { + writeIgnoredValue(yaml, obj, ignoredBuilder, indents); + } + + return ignoredBuilder.toString(); + } + + private static void writeIgnoredValue(Yaml yaml, Object toWrite, StringBuilder ignoredBuilder, String indents) { + String yml = yaml.dump(toWrite); + if (toWrite instanceof Collection) { + ignoredBuilder.append("\n").append(addIndentation(yml, indents)).append("\n"); + } else { + ignoredBuilder.append(" ").append(yml); + } + } + + private static String addIndentation(String s, String indents) { + StringBuilder builder = new StringBuilder(); + String[] split = s.split("\n"); + + for (String value : split) { + if (builder.length() > 0) + builder.append("\n"); + + builder.append(indents).append(value); + } + + return builder.toString(); + } + + private static void writeCommentIfExists(Map comments, BufferedWriter writer, String fullKey, String indents) throws IOException { + String comment = comments.get(fullKey); + + //Comments always end with new line (\n) + if (comment != null) + //Replaces all '\n' with '\n' + indents except for the last one + writer.write(indents + comment.substring(0, comment.length() - 1).replace("\n", "\n" + indents) + "\n"); + } + + //Will try to get the correct key by using the sectionContext + private static Object getKeyAsObject(String key, Map sectionContext) { + if (sectionContext.containsKey(key)) + return key; + + try { + Float keyFloat = Float.parseFloat(key); + + if (sectionContext.containsKey(keyFloat)) + return keyFloat; + } catch (NumberFormatException ignored) {} + + try { + Double keyDouble = Double.parseDouble(key); + + if (sectionContext.containsKey(keyDouble)) + return keyDouble; + } catch (NumberFormatException ignored) {} + + try { + Integer keyInteger = Integer.parseInt(key); + + if (sectionContext.containsKey(keyInteger)) + return keyInteger; + } catch (NumberFormatException ignored) {} + + try { + Long longKey = Long.parseLong(key); + + if (sectionContext.containsKey(longKey)) + return longKey; + } catch (NumberFormatException ignored) {} + + return null; + } + + /** + * Writes the current value with the provided trailing key to the provided writer. + * + * @param parserConfig The parser configuration to use for writing the YAML value. + * @param bufferedWriter The writer to write the value to. + * @param indents The string representation of the indentation. + * @param trailingKey The trailing key for the YAML value. + * @param currentValue The current value to write as YAML. + * @throws IOException If an I/O error occurs while writing the YAML value. + */ + private static void writeYamlValue(final FileConfiguration parserConfig, final BufferedWriter bufferedWriter, final String indents, final String trailingKey, final Object currentValue) throws IOException { + parserConfig.set(trailingKey, currentValue); + String yaml = parserConfig.saveToString(); + yaml = yaml.substring(0, yaml.length() - 1).replace("\n", "\n" + indents); + final String toWrite = indents + yaml + "\n"; + parserConfig.set(trailingKey, null); + bufferedWriter.write(toWrite); + } + + /** + * Writes the value associated with the ignored section to the provided writer, + * if it exists in the ignoredSectionsValues map. + * + * @param ignoredSectionsValues The map containing the ignored section-value mappings. + * @param bufferedWriter The writer to write the value to. + * @param fullKey The full key to search for in the ignoredSectionsValues map. + * @throws IOException If an I/O error occurs while writing the value. + */ + private static boolean writeIgnoredSectionValueIfExists(final Map ignoredSectionsValues, final BufferedWriter bufferedWriter, final String fullKey) throws IOException { + String ignored = ignoredSectionsValues.get(fullKey); + if (ignored != null) { + bufferedWriter.write(ignored); + return true; + } + for (final Map.Entry entry : ignoredSectionsValues.entrySet()) { + if (KeyUtils.isSubKeyOf(entry.getKey(), fullKey, SEPARATOR)) { + return true; + } + } + return false; + } + + /** + * Writes a configuration section with the provided trailing key and the current value to the provided writer. + * + * @param bufferedWriter The writer to write the configuration section to. + * @param indents The string representation of the indentation level. + * @param trailingKey The trailing key for the configuration section. + * @param configurationSection The current value of the configuration section. + * @throws IOException If an I/O error occurs while writing the configuration section. + */ + private static void writeConfigurationSection(final BufferedWriter bufferedWriter, final String indents, final String trailingKey, final ConfigurationSection configurationSection) throws IOException { + bufferedWriter.write(indents + trailingKey + ":"); + if (!(configurationSection).getKeys(false).isEmpty()) { + bufferedWriter.write("\n"); + } else { + bufferedWriter.write(" {}\n"); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/loganmagnan/pluginbase/utils/KeyBuilder.java b/src/main/java/com/loganmagnan/pluginbase/utils/KeyBuilder.java new file mode 100644 index 0000000..f8482a8 --- /dev/null +++ b/src/main/java/com/loganmagnan/pluginbase/utils/KeyBuilder.java @@ -0,0 +1,106 @@ +package com.loganmagnan.pluginbase.utils; + +import org.bukkit.configuration.file.FileConfiguration; + +public class KeyBuilder implements Cloneable { + + private final FileConfiguration config; + private final char separator; + private final StringBuilder builder; + + public KeyBuilder(FileConfiguration config, char separator) { + this.config = config; + this.separator = separator; + this.builder = new StringBuilder(); + } + + private KeyBuilder(KeyBuilder keyBuilder) { + this.config = keyBuilder.config; + this.separator = keyBuilder.separator; + this.builder = new StringBuilder(keyBuilder.toString()); + } + + public void parseLine(String line, boolean checkIfExists) { + line = line.trim(); + + String[] currentSplitLine = line.split(":"); + + if (currentSplitLine.length > 2) + currentSplitLine = line.split(": "); + + String key = currentSplitLine[0].replace("'", "").replace("\"", ""); + + if (checkIfExists) { + //Checks keyBuilder path against config to see if the path is valid. + //If the path doesn't exist in the config it keeps removing last key in keyBuilder. + while (builder.length() > 0 && !config.contains(builder.toString() + separator + key)) { + removeLastKey(); + } + } + + //Add the separator if there is already a key inside keyBuilder + //If currentSplitLine[0] is 'key2' and keyBuilder contains 'key1' the result will be 'key1.' if '.' is the separator + if (builder.length() > 0) + builder.append(separator); + + //Appends the current key to keyBuilder + //If keyBuilder is 'key1.' and currentSplitLine[0] is 'key2' the resulting keyBuilder will be 'key1.key2' if separator is '.' + builder.append(key); + } + + public String getLastKey() { + if (builder.length() == 0) + return ""; + + return builder.toString().split("[" + separator + "]")[0]; + } + + public boolean isEmpty() { + return builder.length() == 0; + } + public void clear() { + builder.setLength(0); + } + //Checks to see if the full key path represented by this instance is a sub-key of the key parameter + public boolean isSubKeyOf(String parentKey) { + return KeyUtils.isSubKeyOf(parentKey, builder.toString(), separator); + } + + //Checks to see if subKey is a sub-key of the key path this instance represents + public boolean isSubKey(String subKey) { + return KeyUtils.isSubKeyOf(builder.toString(), subKey, separator); + } + + public boolean isConfigSection() { + String key = builder.toString(); + return config.isConfigurationSection(key); + } + + public boolean isConfigSectionWithKeys() { + String key = builder.toString(); + return config.isConfigurationSection(key) && !config.getConfigurationSection(key).getKeys(false).isEmpty(); + } + + //Input: 'key1.key2' Result: 'key1' + public void removeLastKey() { + if (builder.length() == 0) + return; + + String keyString = builder.toString(); + //Must be enclosed in brackets in case a regex special character is the separator + String[] split = keyString.split("[" + separator + "]"); + //Makes sure begin index isn't < 0 (error). Occurs when there is only one key in the path + int minIndex = Math.max(0, builder.length() - split[split.length - 1].length() - 1); + builder.replace(minIndex, builder.length(), ""); + } + + @Override + public String toString() { + return builder.toString(); + } + + @Override + protected KeyBuilder clone() { + return new KeyBuilder(this); + } +} diff --git a/src/main/java/com/loganmagnan/pluginbase/utils/KeyUtils.java b/src/main/java/com/loganmagnan/pluginbase/utils/KeyUtils.java new file mode 100644 index 0000000..17f617e --- /dev/null +++ b/src/main/java/com/loganmagnan/pluginbase/utils/KeyUtils.java @@ -0,0 +1,22 @@ +package com.loganmagnan.pluginbase.utils; + +public class KeyUtils { + + public static boolean isSubKeyOf(final String parentKey, final String subKey, final char separator) { + if (parentKey.isEmpty()) + return false; + + return subKey.startsWith(parentKey) + && subKey.substring(parentKey.length()).startsWith(String.valueOf(separator)); + } + + public static String getIndents(final String key, final char separator) { + final String[] splitKey = key.split("[" + separator + "]"); + final StringBuilder builder = new StringBuilder(); + + for (int i = 1; i < splitKey.length; i++) { + builder.append(" "); + } + return builder.toString(); + } +} diff --git a/src/main/java/com/loganmagnan/pluginbase/utils/config/FileConfig.java b/src/main/java/com/loganmagnan/pluginbase/utils/config/FileConfig.java index e05b878..be0f9ff 100644 --- a/src/main/java/com/loganmagnan/pluginbase/utils/config/FileConfig.java +++ b/src/main/java/com/loganmagnan/pluginbase/utils/config/FileConfig.java @@ -1,5 +1,7 @@ package com.loganmagnan.pluginbase.utils.config; +import com.loganmagnan.pluginbase.PluginBase; +import com.loganmagnan.pluginbase.utils.ConfigUpdater; import org.bukkit.Bukkit; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; @@ -36,6 +38,13 @@ public class FileConfig { plugin.saveResource(fileName, false); } } + + try { + ConfigUpdater.update(PluginBase.getInstance(), fileName, this.file); + } catch (Exception exception) { + exception.printStackTrace(); + } + this.config = YamlConfiguration.loadConfiguration(this.file); } diff --git a/src/main/java/com/loganmagnan/pluginbase/utils/config/file/Config.java b/src/main/java/com/loganmagnan/pluginbase/utils/config/file/Config.java index 1ae67e7..ed7064f 100644 --- a/src/main/java/com/loganmagnan/pluginbase/utils/config/file/Config.java +++ b/src/main/java/com/loganmagnan/pluginbase/utils/config/file/Config.java @@ -1,5 +1,7 @@ package com.loganmagnan.pluginbase.utils.config.file; +import com.loganmagnan.pluginbase.PluginBase; +import com.loganmagnan.pluginbase.utils.ConfigUpdater; import lombok.Getter; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; @@ -26,6 +28,13 @@ public class Config { e.printStackTrace(); } } + + try { + ConfigUpdater.update(PluginBase.getInstance(), name, this.configFile); + } catch (Exception exception) { + exception.printStackTrace(); + } + this.config = YamlConfiguration.loadConfiguration(this.configFile); } diff --git a/src/main/java/com/loganmagnan/pluginbase/utils/config/file/ConfigFile.java b/src/main/java/com/loganmagnan/pluginbase/utils/config/file/ConfigFile.java index b0a53c7..b695d73 100644 --- a/src/main/java/com/loganmagnan/pluginbase/utils/config/file/ConfigFile.java +++ b/src/main/java/com/loganmagnan/pluginbase/utils/config/file/ConfigFile.java @@ -1,5 +1,7 @@ package com.loganmagnan.pluginbase.utils.config.file; +import com.loganmagnan.pluginbase.PluginBase; +import com.loganmagnan.pluginbase.utils.ConfigUpdater; import lombok.Getter; import org.bukkit.ChatColor; import org.bukkit.configuration.file.YamlConfiguration; @@ -24,6 +26,12 @@ public class ConfigFile { plugin.saveResource(name + ".yml", false); + try { + ConfigUpdater.update(PluginBase.getInstance(), name, this.file); + } catch (Exception exception) { + exception.printStackTrace(); + } + configuration = YamlConfiguration.loadConfiguration(file); }