From ae9fc1d2b1711b343fd6dc9ef52888cfe864f5fb Mon Sep 17 00:00:00 2001 From: Christian Koop Date: Sat, 6 May 2023 23:36:51 +0200 Subject: [PATCH] Revert "Revert all the SongodaYamlConfig related commits" This reverts commit c725ea69d63ec90e19d1f2e2fce64acb97380a1f. --- .../java/com/songoda/core/SongodaCore.java | 3 +- .../java/com/songoda/core/SongodaPlugin.java | 118 ++- .../songoda/core/configuration/Comment.java | 114 -- .../songoda/core/configuration/Config.java | 793 -------------- .../core/configuration/ConfigEntry.java | 189 ++++ .../ConfigFileConfigurationAdapter.java | 272 ----- .../configuration/ConfigFormattingRules.java | 96 -- .../configuration/ConfigOptionsAdapter.java | 72 -- .../core/configuration/ConfigSection.java | 761 -------------- .../core/configuration/ConfigSetting.java | 135 --- .../core/configuration/DataStoreObject.java | 30 - .../core/configuration/HeaderCommentable.java | 18 + .../core/configuration/IConfiguration.java | 72 ++ .../core/configuration/NodeCommentable.java | 16 + .../configuration/ReadOnlyConfigEntry.java | 72 ++ .../core/configuration/SimpleDataStore.java | 284 ----- .../configuration/WriteableConfigEntry.java | 19 + .../configuration/editor/ConfigEditorGui.java | 8 +- .../editor/ConfigEditorListEditorGui.java | 3 + .../configuration/editor/PluginConfigGui.java | 22 +- .../songoda/SongodaYamlConfig.java | 245 +++++ .../yaml/YamlCommentRepresenter.java | 73 ++ .../configuration/yaml/YamlConfigEntry.java | 90 ++ .../configuration/yaml/YamlConfiguration.java | 371 +++++++ .../com/songoda/core/core/LocaleModule.java | 128 +-- .../com/songoda/core/gui/CustomizableGui.java | 118 ++- .../java/com/songoda/core/locale/Locale.java | 991 +++++++++--------- .../core/locale/LocaleFileManager.java | 102 ++ .../songoda/core/locale/LocaleManager.java | 115 ++ .../java/com/songoda/core/utils/Pair.java | 19 + .../ReadOnlyConfigEntryTest.java | 49 + .../SongodaYamlConfigRoundtripTest.java | 114 ++ .../songoda/SongodaYamlConfigTest.java | 246 +++++ .../yaml/YamlConfigEntryTest.java | 258 +++++ .../yaml/YamlConfigurationTest.java | 613 +++++++++++ .../core/locale/LocaleFileManagerTest.java | 160 +++ 36 files changed, 3553 insertions(+), 3236 deletions(-) delete mode 100644 Core/src/main/java/com/songoda/core/configuration/Comment.java delete mode 100644 Core/src/main/java/com/songoda/core/configuration/Config.java create mode 100644 Core/src/main/java/com/songoda/core/configuration/ConfigEntry.java delete mode 100644 Core/src/main/java/com/songoda/core/configuration/ConfigFileConfigurationAdapter.java delete mode 100644 Core/src/main/java/com/songoda/core/configuration/ConfigFormattingRules.java delete mode 100644 Core/src/main/java/com/songoda/core/configuration/ConfigOptionsAdapter.java delete mode 100644 Core/src/main/java/com/songoda/core/configuration/ConfigSection.java delete mode 100644 Core/src/main/java/com/songoda/core/configuration/ConfigSetting.java delete mode 100644 Core/src/main/java/com/songoda/core/configuration/DataStoreObject.java create mode 100644 Core/src/main/java/com/songoda/core/configuration/HeaderCommentable.java create mode 100644 Core/src/main/java/com/songoda/core/configuration/IConfiguration.java create mode 100644 Core/src/main/java/com/songoda/core/configuration/NodeCommentable.java create mode 100644 Core/src/main/java/com/songoda/core/configuration/ReadOnlyConfigEntry.java delete mode 100644 Core/src/main/java/com/songoda/core/configuration/SimpleDataStore.java create mode 100644 Core/src/main/java/com/songoda/core/configuration/WriteableConfigEntry.java create mode 100644 Core/src/main/java/com/songoda/core/configuration/songoda/SongodaYamlConfig.java create mode 100644 Core/src/main/java/com/songoda/core/configuration/yaml/YamlCommentRepresenter.java create mode 100644 Core/src/main/java/com/songoda/core/configuration/yaml/YamlConfigEntry.java create mode 100644 Core/src/main/java/com/songoda/core/configuration/yaml/YamlConfiguration.java create mode 100644 Core/src/main/java/com/songoda/core/locale/LocaleFileManager.java create mode 100644 Core/src/main/java/com/songoda/core/locale/LocaleManager.java create mode 100644 Core/src/main/java/com/songoda/core/utils/Pair.java create mode 100644 Core/src/test/java/com/songoda/core/configuration/ReadOnlyConfigEntryTest.java create mode 100644 Core/src/test/java/com/songoda/core/configuration/songoda/SongodaYamlConfigRoundtripTest.java create mode 100644 Core/src/test/java/com/songoda/core/configuration/songoda/SongodaYamlConfigTest.java create mode 100644 Core/src/test/java/com/songoda/core/configuration/yaml/YamlConfigEntryTest.java create mode 100644 Core/src/test/java/com/songoda/core/configuration/yaml/YamlConfigurationTest.java create mode 100644 Core/src/test/java/com/songoda/core/locale/LocaleFileManagerTest.java diff --git a/Core/src/main/java/com/songoda/core/SongodaCore.java b/Core/src/main/java/com/songoda/core/SongodaCore.java index 81025e23..cb4ece4f 100644 --- a/Core/src/main/java/com/songoda/core/SongodaCore.java +++ b/Core/src/main/java/com/songoda/core/SongodaCore.java @@ -3,7 +3,6 @@ package com.songoda.core; import com.songoda.core.commands.CommandManager; import com.songoda.core.compatibility.ClientVersion; import com.songoda.core.compatibility.CompatibleMaterial; -import com.songoda.core.core.LocaleModule; import com.songoda.core.core.PluginInfo; import com.songoda.core.core.PluginInfoModule; import com.songoda.core.core.SongodaCoreCommand; @@ -235,7 +234,7 @@ public class SongodaCore { PluginInfo info = new PluginInfo(plugin, pluginID, icon, libraryVersion); // don't forget to check for language pack updates ;) - info.addModule(new LocaleModule()); +// info.addModule(new LocaleModule()); registeredPlugins.add(info); tasks.add(Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, () -> update(info), 60L)); } diff --git a/Core/src/main/java/com/songoda/core/SongodaPlugin.java b/Core/src/main/java/com/songoda/core/SongodaPlugin.java index 41937b30..d7f3acde 100644 --- a/Core/src/main/java/com/songoda/core/SongodaPlugin.java +++ b/Core/src/main/java/com/songoda/core/SongodaPlugin.java @@ -1,8 +1,7 @@ package com.songoda.core; -import com.songoda.core.configuration.Config; +import com.songoda.core.configuration.songoda.SongodaYamlConfig; import com.songoda.core.database.DataManagerAbstract; -import com.songoda.core.locale.Locale; import com.songoda.core.utils.Metrics; import com.songoda.core.utils.SongodaAuth; import de.tr7zw.changeme.nbtapi.utils.MinecraftVersion; @@ -11,6 +10,7 @@ import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.concurrent.TimeUnit; @@ -21,8 +21,7 @@ import java.util.logging.Level; * Must not have two instances of Metrics enabled! */ public abstract class SongodaPlugin extends JavaPlugin { - protected Locale locale; - protected Config config = new Config(this); +// protected Locale locale; protected long dataLoadDelay = 20L; private boolean emergencyStop = false; @@ -42,37 +41,15 @@ public abstract class SongodaPlugin extends JavaPlugin { public abstract void onDataLoad(); /** - * Called after reloadConfig() is called + * All the configuration files belonging to the plugin.
+ * This might for example be used for the ingame config editor.
+ *
+ * Do not include *storage* files here or anything similar that does not intend external modification and access.
+ *
+ * Do not include language files if you are using the Core's localization system. */ public abstract void onConfigReload(); - /** - * Any other plugin configuration files used by the plugin. - * - * @return a list of Configs that are used in addition to the main config. - */ - public abstract List getExtraConfig(); - - @Override - public FileConfiguration getConfig() { - return config.getFileConfig(); - } - - public Config getCoreConfig() { - return config; - } - - @Override - public void reloadConfig() { - config.load(); - onConfigReload(); - } - - @Override - public void saveConfig() { - config.save(); - } - @Override public final void onLoad() { try { @@ -126,7 +103,7 @@ public abstract class SongodaPlugin extends JavaPlugin { ChatColor.GREEN, "Enabling", ChatColor.GRAY)); try { - this.locale = Locale.loadDefaultLocale(this, "en_US"); +// this.locale = Locale.loadDefaultLocale(this, "en_US"); // plugin setup onPluginEnable(); @@ -180,32 +157,32 @@ public abstract class SongodaPlugin extends JavaPlugin { console.sendMessage(" "); // blank line to separate chatter } - public Locale getLocale() { - return this.locale; - } +// public Locale getLocale() { +// return this.locale; +// } - /** - * Set the plugin's locale to a specific language - * - * @param localeName locale to use, eg "en_US" - * @param reload optionally reload the loaded locale if the locale didn't - * change - * - * @return true if the locale exists and was loaded successfully - */ - public boolean setLocale(String localeName, boolean reload) { - if (this.locale != null && this.locale.getName().equals(localeName)) { - return !reload || this.locale.reloadMessages(); - } - - Locale l = Locale.loadLocale(this, localeName); - if (l != null) { - this.locale = l; - return true; - } - - return false; - } +// /** +// * Set the plugin's locale to a specific language +// * +// * @param localeName locale to use, eg "en_US" +// * @param reload optionally reload the loaded locale if the locale didn't +// * change +// * +// * @return true if the locale exists and was loaded successfully +// */ +// public boolean setLocale(String localeName, boolean reload) { +// if (this.locale != null && this.locale.getName().equals(localeName)) { +// return !reload || this.locale.reloadMessages(); +// } +// +// Locale l = Locale.loadLocale(this, localeName); +// if (l != null) { +// this.locale = l; +// return true; +// } +// +// return false; +// } protected void shutdownDataManager(DataManagerAbstract dataManager) { // 3 minutes is overkill, but we just want to make sure @@ -266,4 +243,29 @@ public abstract class SongodaPlugin extends JavaPlugin { emergencyStop(); } + + /** + * Use {@link SongodaYamlConfig} instead. + */ + @Deprecated + @Override + public @NotNull FileConfiguration getConfig() { + return super.getConfig(); + } + + /** + * Use {@link SongodaYamlConfig} instead. + */ + @Deprecated + @Override + public void reloadConfig() { + } + + /** + * Use {@link SongodaYamlConfig} instead. + */ + @Deprecated + @Override + public void saveConfig() { + } } diff --git a/Core/src/main/java/com/songoda/core/configuration/Comment.java b/Core/src/main/java/com/songoda/core/configuration/Comment.java deleted file mode 100644 index e04f2137..00000000 --- a/Core/src/main/java/com/songoda/core/configuration/Comment.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.songoda.core.configuration; - -import com.songoda.core.configuration.ConfigFormattingRules.CommentStyle; - -import java.io.IOException; -import java.io.Writer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; -import java.util.stream.Collectors; - -/** - * A comment for a configuration key - */ -public class Comment { - final List lines = new ArrayList<>(); - CommentStyle commentStyle = null; - - public Comment() { - } - - public Comment(String... lines) { - this(null, Arrays.asList(lines)); - } - - public Comment(List lines) { - this(null, lines); - } - - public Comment(CommentStyle commentStyle, String... lines) { - this(commentStyle, Arrays.asList(lines)); - } - - public Comment(CommentStyle commentStyle, List lines) { - this.commentStyle = commentStyle; - - if (lines != null) { - lines.forEach(s -> this.lines.addAll(Arrays.asList(s.split("\n")))); - } - } - - public CommentStyle getCommentStyle() { - return commentStyle; - } - - public void setCommentStyle(CommentStyle commentStyle) { - this.commentStyle = commentStyle; - } - - public List getLines() { - return lines; - } - - @Override - public String toString() { - return lines.isEmpty() ? "" : String.join("\n", lines); - } - - public static Comment loadComment(List lines) { - CommentStyle style = ConfigFormattingRules.parseStyle(lines); - - int linePad = (style.drawBorder ? 1 : 0) + (style.drawSpace ? 1 : 0); - int prefix = style.commentPrefix.length(); - int suffix = style.commentSuffix.length(); - - return new Comment(style, lines.subList(linePad, lines.size() - linePad).stream().map(s -> s.substring(prefix, s.length() - suffix).trim()).collect(Collectors.toList())); - } - - public void writeComment(Writer output, int offset, CommentStyle defaultStyle) throws IOException { - CommentStyle style = commentStyle != null ? commentStyle : defaultStyle; - int minSpacing = 0, borderSpacing = 0; - - // first draw the top of the comment - if (style.drawBorder) { - // grab the longest line in the list of lines - minSpacing = lines.stream().max(Comparator.comparingInt(String::length)).orElse("").length(); - borderSpacing = minSpacing + style.commentPrefix.length() + style.commentSuffix.length(); - - // draw the first line - output.write((new String(new char[offset])).replace('\0', ' ') + (new String(new char[borderSpacing + 2])).replace('\0', '#') + "\n"); - - if (style.drawSpace) { - output.write((new String(new char[offset])).replace('\0', ' ') - + "#" + style.spacePrefixTop - + (new String(new char[borderSpacing - style.spacePrefixTop.length() - style.spaceSuffixTop.length()])).replace('\0', style.spaceCharTop) - + style.spaceSuffixTop + "#\n"); - } - } else if (style.drawSpace) { - output.write((new String(new char[offset])).replace('\0', ' ') + "#\n"); - } - - // then the actual comment lines - for (String line : lines) { - // todo? should we auto-wrap comment lines that are longer than 80 characters? - output.write((new String(new char[offset])).replace('\0', ' ') + "#" + style.commentPrefix - + (minSpacing == 0 ? line : line + (new String(new char[minSpacing - line.length()])).replace('\0', ' ')) + style.commentSuffix + (style.drawBorder ? "#\n" : "\n")); - } - - // now draw the bottom of the comment border - if (style.drawBorder) { - if (style.drawSpace) { - output.write((new String(new char[offset])).replace('\0', ' ') - + "#" + style.spacePrefixBottom - + (new String(new char[borderSpacing - style.spacePrefixBottom.length() - style.spaceSuffixBottom.length()])).replace('\0', style.spaceCharBottom) - + style.spaceSuffixBottom + "#\n"); - } - - output.write((new String(new char[offset])).replace('\0', ' ') + (new String(new char[borderSpacing + 2])).replace('\0', '#') + "\n"); - } else if (style.drawSpace) { - output.write((new String(new char[offset])).replace('\0', ' ') + "#\n"); - } - } -} diff --git a/Core/src/main/java/com/songoda/core/configuration/Config.java b/Core/src/main/java/com/songoda/core/configuration/Config.java deleted file mode 100644 index 6efabd97..00000000 --- a/Core/src/main/java/com/songoda/core/configuration/Config.java +++ /dev/null @@ -1,793 +0,0 @@ -package com.songoda.core.configuration; - -import com.songoda.core.utils.TextUtils; -import org.apache.commons.lang3.Validate; -import org.bukkit.Bukkit; -import org.bukkit.configuration.InvalidConfigurationException; -import org.bukkit.configuration.file.YamlConstructor; -import org.bukkit.configuration.file.YamlRepresenter; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.yaml.snakeyaml.DumperOptions; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.error.YAMLException; -import org.yaml.snakeyaml.representer.Representer; - -import java.io.BufferedInputStream; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.io.Reader; -import java.io.StringReader; -import java.io.StringWriter; -import java.io.Writer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Timer; -import java.util.TimerTask; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** - * Configuration settings for a plugin - */ -public class Config extends ConfigSection { - /* - Serialization notes: - // implements ConfigurationSerializable: - //public Map serialize(); - - // Class must contain one of: - // public static Object deserialize(@NotNull Map args); - // public static valueOf(Map args); - // public new (Map args) - */ - protected static final String BLANK_CONFIG = "{}\n"; - - protected File file; - protected final ConfigFileConfigurationAdapter config = new ConfigFileConfigurationAdapter(this); - protected Comment headerComment = null; - protected Comment footerComment = null; - final String dirName, fileName; - final Plugin plugin; - final DumperOptions yamlOptions = new DumperOptions(); - final Representer yamlRepresenter = new YamlRepresenter(); - final Yaml yaml = new Yaml(new YamlConstructor(), yamlRepresenter, yamlOptions); - Charset defaultCharset = StandardCharsets.UTF_8; - SaveTask saveTask; - Timer autosaveTimer; - - ////////////// Config settings //////////////// - /** - * save file whenever a change is made - */ - boolean autosave = false; - - /** - * time in seconds to start a save after a change is made - */ - int autosaveInterval = 60; - - /** - * remove nodes not defined in defaults - */ - boolean autoremove = false; - - /** - * load comments when loading the file - */ - boolean loadComments = true; - - /** - * Default comment applied to config nodes - */ - ConfigFormattingRules.CommentStyle defaultNodeCommentFormat = ConfigFormattingRules.CommentStyle.SIMPLE; - - /** - * Default comment applied to section nodes - */ - ConfigFormattingRules.CommentStyle defaultSectionCommentFormat = ConfigFormattingRules.CommentStyle.SPACED; - - /** - * Extra lines to put between root nodes - */ - int rootNodeSpacing = 1; - - /** - * Extra lines to put in front of comments.
- * This is separate from rootNodeSpacing, if applicable. - */ - int commentSpacing = 1; - - public Config() { - this.plugin = null; - this.file = null; - - dirName = null; - fileName = null; - } - - public Config(@NotNull File file) { - this.plugin = null; - this.file = file.getAbsoluteFile(); - - dirName = null; - fileName = null; - } - - public Config(@NotNull Plugin plugin) { - this.plugin = plugin; - - dirName = null; - fileName = null; - } - - public Config(@NotNull Plugin plugin, @NotNull String file) { - this.plugin = plugin; - - dirName = null; - fileName = file; - } - - public Config(@NotNull Plugin plugin, @Nullable String directory, @NotNull String file) { - this.plugin = plugin; - - dirName = directory; - fileName = file; - } - - @NotNull - public ConfigFileConfigurationAdapter getFileConfig() { - return config; - } - - @NotNull - public File getFile() { - if (file == null) { - if (dirName != null) { - this.file = new File(plugin.getDataFolder() + dirName, fileName != null ? fileName : "config.yml"); - } else { - this.file = new File(plugin.getDataFolder(), fileName != null ? fileName : "config.yml"); - } - } - - return file; - } - - public Charset getDefaultCharset() { - return defaultCharset; - } - - /** - * Set the Charset that will be used to save this config - * - * @param defaultCharset Charset to use - * - * @return this class - */ - public Config setDefaultCharset(Charset defaultCharset) { - this.defaultCharset = defaultCharset; - return this; - } - - /** - * Set the default charset to use UTF-16 - * - * @return this class - */ - public Config setUseUTF16() { - this.defaultCharset = StandardCharsets.UTF_16; - return this; - } - - public boolean getLoadComments() { - return loadComments; - } - - /** - * Should comments from the config file be loaded when loading? - * - * @param loadComments set to false if you do not want to preserve node comments - */ - public void setLoadComments(boolean loadComments) { - this.loadComments = loadComments; - } - - public boolean getAutosave() { - return autosave; - } - - /** - * Should the configuration automatically save whenever it's been changed?
- * All saves are done asynchronously, so this should not impact server performance. - * - * @param autosave set to true if autosaving is enabled. - * - * @return this class - */ - @NotNull - public Config setAutosave(boolean autosave) { - this.autosave = autosave; - return this; - } - - public int getAutosaveInterval() { - return autosaveInterval; - } - - /** - * If autosave is enabled, this is the delay between a change and when the save is started.
- * If the configuration is changed within this period, the timer is not reset. - * - * @param autosaveInterval time in seconds - * - * @return this class - */ - @NotNull - public Config setAutosaveInterval(int autosaveInterval) { - this.autosaveInterval = autosaveInterval; - return this; - } - - public boolean getAutoremove() { - return autoremove; - } - - /** - * This setting is used to prevent users to from adding extraneous settings - * to the config and to remove deprecated settings.
- * If this is enabled, the config will delete any nodes that are not defined - * as a default setting. - * - * @param autoremove Remove settings that don't exist as defaults - * - * @return this class - */ - @NotNull - public Config setAutoremove(boolean autoremove) { - this.autoremove = autoremove; - return this; - } - - /** - * Default comment applied to config nodes - */ - @Nullable - public ConfigFormattingRules.CommentStyle getDefaultNodeCommentFormat() { - return defaultNodeCommentFormat; - } - - /** - * Default comment applied to config nodes - * - * @return this config - */ - @NotNull - public Config setDefaultNodeCommentFormat(@Nullable ConfigFormattingRules.CommentStyle defaultNodeCommentFormat) { - this.defaultNodeCommentFormat = defaultNodeCommentFormat; - return this; - } - - /** - * Default comment applied to section nodes - */ - @Nullable - public ConfigFormattingRules.CommentStyle getDefaultSectionCommentFormat() { - return defaultSectionCommentFormat; - } - - /** - * Default comment applied to section nodes - * - * @return this config - */ - @NotNull - public Config setDefaultSectionCommentFormat(@Nullable ConfigFormattingRules.CommentStyle defaultSectionCommentFormat) { - this.defaultSectionCommentFormat = defaultSectionCommentFormat; - return this; - } - - /** - * Extra lines to put between root nodes - */ - public int getRootNodeSpacing() { - return rootNodeSpacing; - } - - /** - * Extra lines to put between root nodes - * - * @return this config - */ - @NotNull - public Config setRootNodeSpacing(int rootNodeSpacing) { - this.rootNodeSpacing = rootNodeSpacing; - return this; - } - - /** - * Extra lines to put in front of comments.
- * This is separate from rootNodeSpacing, if applicable. - */ - public int getCommentSpacing() { - return commentSpacing; - } - - /** - * Extra lines to put in front of comments.
- * This is separate from rootNodeSpacing, if applicable. - * - * @return this config - */ - @NotNull - public Config setCommentSpacing(int commentSpacing) { - this.commentSpacing = commentSpacing; - return this; - } - - @NotNull - public Config setHeader(@NotNull String... description) { - if (description.length == 0) { - headerComment = null; - } else { - headerComment = new Comment(description); - } - - return this; - } - - @NotNull - public Config setHeader(@Nullable ConfigFormattingRules.CommentStyle commentStyle, @NotNull String... description) { - if (description.length == 0) { - headerComment = null; - } else { - headerComment = new Comment(commentStyle, description); - } - - return this; - } - - @NotNull - public Config setHeader(@Nullable List description) { - if (description == null || description.isEmpty()) { - headerComment = null; - } else { - headerComment = new Comment(description); - } - - return this; - } - - @NotNull - public Config setHeader(@Nullable ConfigFormattingRules.CommentStyle commentStyle, @Nullable List description) { - if (description == null || description.isEmpty()) { - headerComment = null; - } else { - headerComment = new Comment(commentStyle, description); - } - - return this; - } - - @NotNull - public List getHeader() { - if (headerComment != null) { - return headerComment.getLines(); - } - - return Collections.emptyList(); - } - - public Config clearConfig(boolean clearDefaults) { - root.values.clear(); - root.configComments.clear(); - - if (clearDefaults) { - root.defaultComments.clear(); - root.defaults.clear(); - } - - return this; - } - - public Config clearDefaults() { - root.defaultComments.clear(); - root.defaults.clear(); - - return this; - } - - public boolean load() { - return load(getFile()); - } - - public boolean load(@NotNull File file) { - Validate.notNull(file, "File cannot be null"); - if (file.exists()) { - try (BufferedInputStream stream = new BufferedInputStream(new FileInputStream(file))) { - Charset charset = TextUtils.detectCharset(stream, StandardCharsets.UTF_8); - - // upgrade charset if file was saved in a more complex format - if (charset == StandardCharsets.UTF_16BE || charset == StandardCharsets.UTF_16LE) { - defaultCharset = StandardCharsets.UTF_16; - } - - this.load(new InputStreamReader(stream, charset)); - - return true; - } catch (IOException | InvalidConfigurationException ex) { - (plugin != null ? plugin.getLogger() : Bukkit.getLogger()).log(Level.SEVERE, "Failed to load config file: " + file.getName(), ex); - } - - return false; - } - - return true; - } - - public void load(@NotNull Reader reader) throws IOException, InvalidConfigurationException { - StringBuilder builder = new StringBuilder(); - - try (BufferedReader input = reader instanceof BufferedReader ? (BufferedReader) reader : new BufferedReader(reader)) { - String line; - boolean firstLine = true; - while ((line = input.readLine()) != null) { - if (firstLine) { - line = line.replaceAll("[\uFEFF\uFFFE\u200B]", ""); // clear BOM markers - firstLine = false; - } - builder.append(line).append('\n'); - } - } - - this.loadFromString(builder.toString()); - } - - public void loadFromString(@NotNull String contents) throws InvalidConfigurationException { - Map input; - - try { - input = this.yaml.load(contents); - } catch (YAMLException e2) { - throw new InvalidConfigurationException(e2); - } catch (ClassCastException e3) { - throw new InvalidConfigurationException("Top level is not a Map."); - } - - if (input != null) { - if (loadComments) { - this.parseComments(contents, input); - } - - this.convertMapsToSections(input, this); - } - } - - protected void convertMapsToSections(@NotNull Map input, @NotNull ConfigSection section) { - // TODO: make this non-recursive - for (Map.Entry entry : input.entrySet()) { - String key = entry.getKey().toString(); - Object value = entry.getValue(); - - if (value instanceof Map) { - this.convertMapsToSections((Map) value, section.createSection(key)); - continue; - } - - section.set(key, value); - } - } - - protected void parseComments(@NotNull String contents, @NotNull Map input) { - // if starts with a comment, load all nonbreaking comments as a header - // then load all comments and assign to the next valid node loaded - // (Only load comments that are on their own line) - - BufferedReader in = new BufferedReader(new StringReader(contents)); - String line; - boolean insideScalar = false; - boolean firstNode = true; - int index = 0; - LinkedList currentPath = new LinkedList<>(); - ArrayList commentBlock = new ArrayList<>(); - - try { - while ((line = in.readLine()) != null) { - if (line.isEmpty()) { - if (firstNode && !commentBlock.isEmpty()) { - // header comment - firstNode = false; - headerComment = Comment.loadComment(commentBlock); - commentBlock.clear(); - } - continue; - } else if (line.trim().startsWith("#")) { - // only load full-line comments - commentBlock.add(line.trim()); - continue; - } - - // check to see if this is a line that we can process - int lineOffset = getOffset(line); - insideScalar &= lineOffset <= index; - Matcher m; - if (!insideScalar && (m = yamlNode.matcher(line)).find()) { - // we found a config node! ^.^ - // check to see what the full path is - int depth = (m.group(1).length() / indentation); - while (depth < currentPath.size()) { - currentPath.removeLast(); - } - currentPath.add(m.group(2)); - - // do we have a comment for this node? - if (!commentBlock.isEmpty()) { - String path = currentPath.stream().collect(Collectors.joining(String.valueOf(pathChar))); - Comment comment = Comment.loadComment(commentBlock); - commentBlock.clear(); - setComment(path, comment); - } - - firstNode = false; // we're no longer on the first node - - // ignore scalars - index = lineOffset; - if (m.group(3).trim().equals("|") || m.group(3).trim().equals(">")) { - insideScalar = true; - } - } - } - - if (!commentBlock.isEmpty()) { - footerComment = Comment.loadComment(commentBlock); - commentBlock.clear(); - } - } catch (IOException ex) { - Logger.getLogger(Config.class.getName()).log(Level.SEVERE, "Error parsing config comment", ex); - } - } - - public void deleteNonDefaultSettings() { - // Delete old config values (thread-safe) - List defaultKeys = Arrays.asList(defaults.keySet().toArray(new String[0])); - - for (String key : values.keySet().toArray(new String[0])) { - if (!defaultKeys.contains(key)) { - values.remove(key); - } - } - } - - @Override - protected void onChange() { - if (autosave) { - delaySave(); - } - } - - public void delaySave() { - // save async even if no plugin or if plugin disabled - if (saveTask == null && (changed || hasNewDefaults())) { - autosaveTimer = new Timer((plugin != null ? plugin.getName() + "-ConfigSave-" : "ConfigSave-") + getFile().getName()); - autosaveTimer.schedule(saveTask = new SaveTask(), autosaveInterval * 1000L); - } - } - - public boolean saveChanges() { - boolean saved = true; - - if (changed || hasNewDefaults()) { - saved = save(); - } - - if (saveTask != null) { - //Close Threads - saveTask.cancel(); - autosaveTimer.cancel(); - saveTask = null; - autosaveTimer = null; - } - - return saved; - } - - boolean hasNewDefaults() { - if (file != null && !file.exists()) return true; - - for (String def : defaults.keySet()) { - if (!values.containsKey(def)) { - return true; - } - } - - return false; - } - - public boolean save() { - if (saveTask != null) { - //Close Threads - saveTask.cancel(); - autosaveTimer.cancel(); - saveTask = null; - autosaveTimer = null; - } - - return save(getFile()); - } - - public boolean save(@NotNull String file) { - Validate.notNull(file, "File cannot be null"); - return this.save(new File(file)); - } - - public boolean save(@NotNull File file) { - Validate.notNull(file, "File cannot be null"); - - if (file.getParentFile() != null && !file.getParentFile().exists()) { - file.getParentFile().mkdirs(); - } - - String data = this.saveToString(); - try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file), defaultCharset)) { - writer.write(data); - } catch (IOException ex) { - return false; - } - - return true; - } - - @NotNull - public String saveToString() { - try { - if (autoremove) { - deleteNonDefaultSettings(); - } - - yamlOptions.setIndent(indentation); - yamlOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); - yamlOptions.setSplitLines(false); - yamlRepresenter.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); - - StringWriter str = new StringWriter(); - - if (headerComment != null) { - headerComment.writeComment(str, 0, ConfigFormattingRules.CommentStyle.BLOCKED); - str.write("\n"); // add one space after the header - } - - String dump = yaml.dump(this.getValues(false)); - if (!dump.equals(BLANK_CONFIG)) { - writeComments(dump, str); - } - - if (footerComment != null) { - str.write("\n"); - footerComment.writeComment(str, 0, ConfigFormattingRules.CommentStyle.BLOCKED); - } - - return str.toString(); - } catch (Throwable ex) { - Logger.getLogger(Config.class.getName()).log(Level.SEVERE, "Error saving config", ex); - delaySave(); - } - - return ""; - } - - protected final Pattern yamlNode = Pattern.compile("^( *)([^:{}\\[\\],&*#?|\\-<>=!%@`]+):(.*)$"); - - protected void writeComments(String data, Writer out) throws IOException { - // line-by-line apply line spacing formatting and comments per-node - BufferedReader in = new BufferedReader(new StringReader(data)); - - String line; - boolean insideScalar = false; - boolean firstNode = true; - int index = 0; - - LinkedList currentPath = new LinkedList<>(); - while ((line = in.readLine()) != null) { - // ignore comments and empty lines (there shouldn't be any, but just in case) - if (line.trim().startsWith("#") || line.isEmpty()) { - continue; - } - - // check to see if this is a line that we can process - int lineOffset = getOffset(line); - insideScalar &= lineOffset <= index; - Matcher m; - if (!insideScalar && (m = yamlNode.matcher(line)).find()) { - // we found a config node! ^.^ - // check to see what the full path is - int depth = (m.group(1).length() / indentation); - while (depth < currentPath.size()) { - currentPath.removeLast(); - } - - currentPath.add(m.group(2)); - String path = currentPath.stream().collect(Collectors.joining(String.valueOf(pathChar))); - - // if this is a root-level node, apply extra spacing if we aren't the first node - if (!firstNode && depth == 0 && rootNodeSpacing > 0) { - out.write((new String(new char[rootNodeSpacing])).replace("\0", "\n")); // yes it's silly, but it works :> - } - firstNode = false; // we're no longer on the first node - - // insert the relavant comment - Comment comment = getComment(path); - if (comment != null) { - // add spacing between previous nodes and comments - if (depth != 0) { - out.write((new String(new char[commentSpacing])).replace("\0", "\n")); - } - - // formatting style for this node - ConfigFormattingRules.CommentStyle style = comment.getCommentStyle(); - if (style == null) { - // check to see what type of node this is - if (!m.group(3).trim().isEmpty()) { - // setting node - style = defaultNodeCommentFormat; - } else { - // probably a section? (need to peek ahead to check if this is a list) - in.mark(1000); - String nextLine = in.readLine().trim(); - in.reset(); - if (nextLine.startsWith("-")) { - // not a section :P - style = defaultNodeCommentFormat; - } else { - style = defaultSectionCommentFormat; - } - } - } - - // write it down! - comment.writeComment(out, lineOffset, style); - } - - // ignore scalars - index = lineOffset; - if (m.group(3).trim().equals("|") || m.group(3).trim().equals(">")) { - insideScalar = true; - } - } - - out.write(line); - out.write("\n"); - } - } - - protected static int getOffset(String s) { - char[] chars = s.toCharArray(); - for (int i = 0; i < chars.length; ++i) { - if (chars[i] != ' ') { - return i; - } - } - - return -1; - } - - class SaveTask extends TimerTask { - @Override - public void run() { - saveChanges(); - } - } -} diff --git a/Core/src/main/java/com/songoda/core/configuration/ConfigEntry.java b/Core/src/main/java/com/songoda/core/configuration/ConfigEntry.java new file mode 100644 index 00000000..d3b18b8f --- /dev/null +++ b/Core/src/main/java/com/songoda/core/configuration/ConfigEntry.java @@ -0,0 +1,189 @@ +package com.songoda.core.configuration; + +import com.songoda.core.compatibility.CompatibleMaterial; +import com.songoda.core.utils.Pair; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; + +public interface ConfigEntry { + @NotNull String getKey(); + + @NotNull IConfiguration getConfig(); + + default boolean has() { + return getConfig().has(getKey()); + } + + void set(@Nullable Object value); + + default @Nullable Object get() { + return getConfig().get(getKey()); + } + + default @Nullable Object getOr(@Nullable Object fallbackValue) { + return getConfig().getOr(getKey(), fallbackValue); + } + + @Nullable Object getDefaultValue(); + + void setDefaultValue(@Nullable Object defaultValue); + + @Contract("_ -> this") + ConfigEntry withDefaultValue(@Nullable Object defaultValue); + + /** + * @see #withComment(Supplier) + */ + @Contract("_ -> this") + default ConfigEntry withComment(String comment) { + return this.withComment(() -> comment); + } + + /** + * @see NodeCommentable#setNodeComment(String, Supplier) + */ + @Contract("_ -> this") + ConfigEntry withComment(Supplier comment); + + /** + * @return <configVersion, Pair<keyInGivenVersion, valueConverter>> + */ + @Nullable Map>> getUpgradeSteps(); + + /** + * @see #withUpgradeStep(int, String, Function) + */ + @Contract("_, _ -> this") + default ConfigEntry withUpgradeStep(int version, @NotNull String keyInGivenVersion) { + return withUpgradeStep(version, keyInGivenVersion, null); + } + + /** + * @param version The version to upgrade from (e.g. 1 for the upgrade from 1 to 2) + * @param keyInGivenVersion The old key in the given version or null if it didn't change + * @param valueConverter A function that converts the old version's value to a new one, or null if it didn't change + */ + @Contract("_, null, null -> fail; _, _, _ -> this") + ConfigEntry withUpgradeStep(int version, @Nullable String keyInGivenVersion, @Nullable Function<@Nullable Object, @Nullable Object> valueConverter); + + default @Nullable String getString() { + return getStringOr(null); + } + + @Contract("!null -> !null") + default @Nullable String getStringOr(String fallbackValue) { + Object value = get(); + + return value == null ? fallbackValue : value.toString(); + } + + /** + * @see #getIntOr(int) + */ + default int getInt() { + return getIntOr(0); + } + + /** + * Returns the values parsed as an integer.
+ * If it is a floating point number, it will be rounded down. + * + * @see Double#valueOf(String) + */ + default int getIntOr(int fallbackValue) { + String value = getString(); + + if (value == null) { + return fallbackValue; + } + + return Double.valueOf(value).intValue(); + } + + /** + * @see #getDoubleOr(double) + */ + default double getDouble() { + return getDoubleOr(0); + } + + /** + * Returns the values parsed as a double. + * + * @see Double#parseDouble(String) + */ + default double getDoubleOr(double fallbackValue) { + String value = getString(); + + if (value == null) { + return fallbackValue; + } + + return Double.parseDouble(value); + } + + /** + * @see #getBooleanOr(boolean) + */ + default boolean getBoolean() { + return getBooleanOr(false); + } + + /** + * Returns the values parsed as a boolean. + * + * @see Boolean#parseBoolean(String) + */ + default boolean getBooleanOr(boolean fallbackValue) { + String value = getString(); + + if (value == null) { + return fallbackValue; + } + + return Boolean.parseBoolean(value); + } + + default @Nullable List getStringList() { + return getStringListOr(null); + } + + @Contract("!null -> !null") + default @Nullable List getStringListOr(List fallbackValue) { + Object value = get(); + + if (value instanceof List) { + //noinspection unchecked + return (List) value; + } + + return fallbackValue; + } + + /** + * @see #getMaterialOr(CompatibleMaterial) + */ + default CompatibleMaterial getMaterial() { + return getMaterialOr(null); + } + + /** + * @see CompatibleMaterial#getMaterial(String) + */ + @Contract("!null -> !null") + default @Nullable CompatibleMaterial getMaterialOr(@Nullable CompatibleMaterial defaultValue) { + String value = getString(); + + if (value == null) { + return defaultValue; + } + + return CompatibleMaterial.getMaterial(value); + } +} diff --git a/Core/src/main/java/com/songoda/core/configuration/ConfigFileConfigurationAdapter.java b/Core/src/main/java/com/songoda/core/configuration/ConfigFileConfigurationAdapter.java deleted file mode 100644 index d05dedfe..00000000 --- a/Core/src/main/java/com/songoda/core/configuration/ConfigFileConfigurationAdapter.java +++ /dev/null @@ -1,272 +0,0 @@ -package com.songoda.core.configuration; - -import com.songoda.core.compatibility.CompatibleMaterial; -import org.bukkit.configuration.Configuration; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.InvalidConfigurationException; -import org.bukkit.configuration.file.FileConfiguration; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; -import java.util.Map; -import java.util.Set; - -public class ConfigFileConfigurationAdapter extends FileConfiguration { - final Config config; - - public ConfigFileConfigurationAdapter(Config config) { - super(config); - - this.config = config; - } - - public Config getCoreConfig() { - return config; - } - - @Override - public String saveToString() { - return config.saveToString(); - } - - @Override - public void loadFromString(String string) throws InvalidConfigurationException { - config.loadFromString(string); - } - - @Override - protected String buildHeader() { - return "#" + String.join("\n#", config.getHeader()); - } - - @Override - public ConfigOptionsAdapter options() { - return new ConfigOptionsAdapter(config); - } - - @Override - public Set getKeys(boolean deep) { - return config.getKeys(deep); - } - - @Override - public Map getValues(boolean deep) { - return config.getValues(deep); - } - - @Override - public boolean contains(String path) { - return config.contains(path); - } - - @Override - public boolean isSet(String path) { - return config.isSet(path); - } - - @Override - public String getCurrentPath() { - return config.getCurrentPath(); - } - - @Override - public String getName() { - return config.getName(); - } - - @Override - public Configuration getRoot() { - return config; - } - - @Override - public ConfigurationSection getParent() { - return null; - } - - @Override - public void addDefault(String path, Object value) { - config.addDefault(path, value); - } - - @Override - public ConfigurationSection getDefaultSection() { - return config.getDefaultSection(); - } - - @Override - public void set(String path, Object value) { - config.set(path, value); - } - - @Override - public Object get(String path) { - return config.get(path); - } - - @Override - public Object get(String path, Object def) { - return config.get(path, def); - } - - @Override - public ConfigurationSection createSection(String path) { - return config.createSection(path); - } - - @Override - public ConfigurationSection createSection(String path, Map map) { - return config.createSection(path, map); - } - - // Other non-FileConfiguration methods - - @NotNull - public ConfigSection createDefaultSection(@NotNull String path) { - return config.createDefaultSection(path); - } - - @NotNull - public ConfigSection createDefaultSection(@NotNull String path, String... comment) { - return config.createDefaultSection(path, comment); - } - - @NotNull - public ConfigSection createDefaultSection(@NotNull String path, ConfigFormattingRules.CommentStyle commentStyle, String... comment) { - return config.createDefaultSection(path, commentStyle, comment); - } - - @NotNull - public ConfigSection setComment(@NotNull String path, @Nullable ConfigFormattingRules.CommentStyle commentStyle, String... lines) { - return config.setComment(path, commentStyle, lines); - } - - @NotNull - public ConfigSection setComment(@NotNull String path, @Nullable ConfigFormattingRules.CommentStyle commentStyle, @Nullable List lines) { - return config.setComment(path, commentStyle, lines); - } - - @NotNull - public ConfigSection setDefaultComment(@NotNull String path, String... lines) { - return config.setDefaultComment(path, lines); - } - - @NotNull - public ConfigSection setDefaultComment(@NotNull String path, @Nullable List lines) { - return config.setDefaultComment(path, lines); - } - - @NotNull - public ConfigSection setDefaultComment(@NotNull String path, ConfigFormattingRules.CommentStyle commentStyle, String... lines) { - return config.setDefaultComment(path, commentStyle, lines); - } - - @NotNull - public ConfigSection setDefaultComment(@NotNull String path, ConfigFormattingRules.CommentStyle commentStyle, @Nullable List lines) { - return config.setDefaultComment(path, commentStyle, lines); - } - - @Nullable - public Comment getComment(@NotNull String path) { - return config.getComment(path); - } - - @Nullable - public String getCommentString(@NotNull String path) { - return config.getCommentString(path); - } - - @NotNull - public List getSections(String path) { - return config.getSections(path); - } - - @NotNull - public ConfigSection set(@NotNull String path, @Nullable Object value, String... comment) { - return config.set(path, value, comment); - } - - @NotNull - public ConfigSection set(@NotNull String path, @Nullable Object value, List comment) { - return config.set(path, value, comment); - } - - @NotNull - public ConfigSection set(@NotNull String path, @Nullable Object value, @Nullable ConfigFormattingRules.CommentStyle commentStyle, String... comment) { - return config.set(path, value, commentStyle, comment); - } - - @NotNull - public ConfigSection set(@NotNull String path, @Nullable Object value, @Nullable ConfigFormattingRules.CommentStyle commentStyle, List comment) { - return config.set(path, value, commentStyle, comment); - } - - @NotNull - public ConfigSection setDefault(@NotNull String path, @Nullable Object value) { - return config.setDefault(path, value); - } - - @NotNull - public ConfigSection setDefault(@NotNull String path, @Nullable Object value, String... comment) { - return config.setDefault(path, value, comment); - } - - @NotNull - public ConfigSection setDefault(@NotNull String path, @Nullable Object value, List comment) { - return config.setDefault(path, value, comment); - } - - @NotNull - public ConfigSection setDefault(@NotNull String path, @Nullable Object value, ConfigFormattingRules.CommentStyle commentStyle, String... comment) { - return config.setDefault(path, value, commentStyle, comment); - } - - @NotNull - public ConfigSection setDefault(@NotNull String path, @Nullable Object value, ConfigFormattingRules.CommentStyle commentStyle, List comment) { - return config.setDefault(path, value, commentStyle, comment); - } - - @NotNull - public ConfigSection createSection(@NotNull String path, String... comment) { - return config.createSection(path, comment); - } - - @NotNull - public ConfigSection createSection(@NotNull String path, @Nullable List comment) { - return config.createSection(path, comment); - } - - @NotNull - public ConfigSection createSection(@NotNull String path, @Nullable ConfigFormattingRules.CommentStyle commentStyle, String... comment) { - return config.createSection(path, commentStyle, comment); - } - - @NotNull - public ConfigSection createSection(@NotNull String path, @Nullable ConfigFormattingRules.CommentStyle commentStyle, @Nullable List comment) { - return config.createSection(path, commentStyle, comment); - } - - public char getChar(@NotNull String path) { - return config.getChar(path); - } - - public char getChar(@NotNull String path, char def) { - return config.getChar(path, def); - } - - @Nullable - public CompatibleMaterial getMaterial(@NotNull String path) { - return config.getMaterial(path); - } - - @Nullable - public CompatibleMaterial getMaterial(@NotNull String path, @Nullable CompatibleMaterial def) { - return config.getMaterial(path, def); - } - - @NotNull - public ConfigSection getOrCreateConfigurationSection(@NotNull String path) { - return config.getOrCreateConfigurationSection(path); - } -} diff --git a/Core/src/main/java/com/songoda/core/configuration/ConfigFormattingRules.java b/Core/src/main/java/com/songoda/core/configuration/ConfigFormattingRules.java deleted file mode 100644 index 8e279925..00000000 --- a/Core/src/main/java/com/songoda/core/configuration/ConfigFormattingRules.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.songoda.core.configuration; - -import java.util.List; - -public class ConfigFormattingRules { - int spacesBetweenMainCategories; - int spacesBetweenValues; - CommentStyle rootCommentStyle = CommentStyle.BLOCKSPACED; - CommentStyle mainCategoryCommentStyle = CommentStyle.SPACED; - - public enum CommentStyle { - /** - * # Comment - */ - SIMPLE(false, false, " ", ""), - /** - * #
- * # Comment
- * #
- */ - SPACED(false, true, " ", ""), - /** - * ###########
- * # Comment #
- * ###########
- */ - BLOCKED(true, false, " ", " "), - /** - * #############
- * #|¯¯¯¯¯¯¯¯¯|#
- * #| Comment |#
- * #|_________|#
- * #############
- */ - BLOCKSPACED(true, true, "|\u00AF", '\u00AF', "\u00AF|", "| ", " |", "|_", '_', "_|"); - - final boolean drawBorder, drawSpace; - final String commentPrefix, spacePrefixTop, spacePrefixBottom; - final String commentSuffix, spaceSuffixTop, spaceSuffixBottom; - final char spaceCharTop, spaceCharBottom; - - CommentStyle(boolean drawBorder, boolean drawSpace, - String spacePrefixTop, char spaceCharTop, String spaceSuffixTop, - String commentPrefix, String commentSuffix, - String spacePrefixBottom, char spaceCharBottom, String spaceSuffixBottom) { - this.drawBorder = drawBorder; - this.drawSpace = drawSpace; - this.commentPrefix = commentPrefix; - this.spacePrefixTop = spacePrefixTop; - this.spacePrefixBottom = spacePrefixBottom; - this.commentSuffix = commentSuffix; - this.spaceSuffixTop = spaceSuffixTop; - this.spaceSuffixBottom = spaceSuffixBottom; - this.spaceCharTop = spaceCharTop; - this.spaceCharBottom = spaceCharBottom; - } - - CommentStyle(boolean drawBorder, boolean drawSpace, String commentPrefix, String commentSuffix) { - this.drawBorder = drawBorder; - this.drawSpace = drawSpace; - this.commentPrefix = commentPrefix; - this.commentSuffix = commentSuffix; - this.spacePrefixTop = this.spacePrefixBottom = ""; - this.spaceCharTop = this.spaceCharBottom = ' '; - this.spaceSuffixTop = this.spaceSuffixBottom = ""; - } - } - - public static CommentStyle parseStyle(List lines) { - if (lines == null || lines.size() <= 2) { - return CommentStyle.SIMPLE; - } - - if (lines.get(0).trim().equals("#") && lines.get(lines.size() - 1).trim().equals("#")) { - return CommentStyle.SPACED; - } - - boolean hasBorders = lines.get(0).trim().matches("^##+$") && lines.get(lines.size() - 1).trim().matches("^##+$"); - if (!hasBorders) { - // default return - return CommentStyle.SIMPLE; - } - - // now need to figure out if this is blocked or not - if (lines.size() > 4 && lines.get(1).trim().matches(("^#" - + CommentStyle.BLOCKSPACED.spacePrefixTop + CommentStyle.BLOCKSPACED.spaceCharTop + "+" - + CommentStyle.BLOCKSPACED.spaceSuffixTop + "#$").replace("|", "\\|")) - && lines.get(1).trim().matches(("^#" - + CommentStyle.BLOCKSPACED.spacePrefixTop + CommentStyle.BLOCKSPACED.spaceCharTop + "+" - + CommentStyle.BLOCKSPACED.spaceSuffixTop + "#$").replace("|", "\\|"))) { - return CommentStyle.BLOCKSPACED; - } - - return CommentStyle.BLOCKED; - } -} diff --git a/Core/src/main/java/com/songoda/core/configuration/ConfigOptionsAdapter.java b/Core/src/main/java/com/songoda/core/configuration/ConfigOptionsAdapter.java deleted file mode 100644 index f8df4b79..00000000 --- a/Core/src/main/java/com/songoda/core/configuration/ConfigOptionsAdapter.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.songoda.core.configuration; - -import org.bukkit.configuration.file.FileConfigurationOptions; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -public class ConfigOptionsAdapter extends FileConfigurationOptions { - final ConfigSection config; - - public ConfigOptionsAdapter(ConfigSection config) { - super(config); - this.config = config; - } - - public Config getConfig() { - return (Config) config.root; - } - - @NotNull - @Override - public ConfigFileConfigurationAdapter configuration() { - return new ConfigFileConfigurationAdapter((Config) config.root); - } - - @NotNull - @Override - public ConfigOptionsAdapter copyDefaults(boolean value) { - // we always copy new values - return this; - } - - @NotNull - @Override - public ConfigOptionsAdapter pathSeparator(char value) { - (config.root).setPathSeparator(value); - return this; - } - - @NotNull - @Override - public ConfigOptionsAdapter header(@Nullable String value) { - if (value == null) { - ((Config) config.root).setHeader((List) null); - } else { - ((Config) config.root).setHeader(value.split("\n")); - } - - return this; - } - - @NotNull - @Override - public ConfigOptionsAdapter copyHeader(boolean value) { - if (!value) { - ((Config) config.root).setHeader((List) null); - } - - return this; - } - - public int indent() { - return config.root.getIndent(); - } - - @NotNull - public ConfigOptionsAdapter indent(int value) { - config.root.setIndent(value); - return this; - } -} diff --git a/Core/src/main/java/com/songoda/core/configuration/ConfigSection.java b/Core/src/main/java/com/songoda/core/configuration/ConfigSection.java deleted file mode 100644 index 25d37fb0..00000000 --- a/Core/src/main/java/com/songoda/core/configuration/ConfigSection.java +++ /dev/null @@ -1,761 +0,0 @@ -package com.songoda.core.configuration; - -import com.songoda.core.compatibility.CompatibleMaterial; -import org.bukkit.configuration.Configuration; -import org.bukkit.configuration.MemoryConfiguration; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** - * Configuration for a specific node - */ -public class ConfigSection extends MemoryConfiguration { - final String fullPath, nodeKey; - final ConfigSection root; - final ConfigSection parent; - protected int indentation = 2; // between 2 and 9 (inclusive) - protected char pathChar = '.'; - final HashMap configComments; - final HashMap defaultComments; - final LinkedHashMap defaults; - final LinkedHashMap values; - /** - * Internal root state: if any configuration value has changed from file state - */ - boolean changed = false; - final boolean isDefault; - final Object lock = new Object(); - - ConfigSection() { - this.root = this; - this.parent = null; - isDefault = false; - nodeKey = fullPath = ""; - - configComments = new HashMap<>(); - defaultComments = new HashMap<>(); - defaults = new LinkedHashMap<>(); - values = new LinkedHashMap<>(); - } - - ConfigSection(ConfigSection root, ConfigSection parent, String nodeKey, boolean isDefault) { - this.root = root; - this.parent = parent; - this.nodeKey = nodeKey; - this.fullPath = nodeKey != null ? parent.fullPath + nodeKey + root.pathChar : parent.fullPath; - this.isDefault = isDefault; - configComments = defaultComments = null; - defaults = null; - values = null; - } - - public int getIndent() { - return root.indentation; - } - - public void setIndent(int indentation) { - root.indentation = indentation; - } - - protected void onChange() { - if (parent != null) { - root.onChange(); - } - } - - /** - * Sets the character used to separate configuration nodes.
- * IMPORTANT: Do not change this after loading or adding ConfigurationSections! - * - * @param pathChar character to use - */ - public void setPathSeparator(char pathChar) { - if (!root.values.isEmpty() || !root.defaults.isEmpty()) { - throw new RuntimeException("Path change after config initialization"); - } - - root.pathChar = pathChar; - } - - public char getPathSeparator() { - return root.pathChar; - } - - /** - * @return The full key for this section node - */ - public String getKey() { - return !fullPath.endsWith(String.valueOf(root.pathChar)) ? fullPath : fullPath.substring(0, fullPath.length() - 1); - } - - /** - * @return The specific key that was used from the last node to get to this node - */ - public String getNodeKey() { - return nodeKey; - } - - /** - * Create the path required for this node to exist.
- * DO NOT USE THIS IN A SYNCHRONIZED LOCK - * - * @param path full path of the node required. Eg, for foo.bar.node, this will create sections for foo and foo.bar - * @param useDefault set to true if this is a default value - */ - protected void createNodePath(@NotNull String path, boolean useDefault) { - if (path.indexOf(root.pathChar) != -1) { - // if any intermediate nodes don't exist, create them - String[] pathParts = path.split(Pattern.quote(String.valueOf(root.pathChar))); - StringBuilder nodePath = new StringBuilder(fullPath); - LinkedHashMap writeTo = useDefault ? root.defaults : root.values; - ConfigSection travelNode = this; - - synchronized (root.lock) { - for (int i = 0; i < pathParts.length - 1; ++i) { - final String node = (i != 0 ? nodePath.append(root.pathChar) : nodePath).append(pathParts[i]).toString(); - - if (!(writeTo.get(node) instanceof ConfigSection)) { - writeTo.put(node, travelNode = new ConfigSection(root, travelNode, pathParts[i], useDefault)); - } else { - travelNode = (ConfigSection) writeTo.get(node); - } - } - } - } - } - - @NotNull - public ConfigSection createDefaultSection(@NotNull String path) { - createNodePath(path, true); - ConfigSection section = new ConfigSection(root, this, path, true); - - synchronized (root.lock) { - root.defaults.put(fullPath + path, section); - } - - return section; - } - - @NotNull - public ConfigSection createDefaultSection(@NotNull String path, String... comment) { - createNodePath(path, true); - ConfigSection section = new ConfigSection(root, this, path, true); - - synchronized (root.lock) { - root.defaults.put(fullPath + path, section); - root.defaultComments.put(fullPath + path, new Comment(comment)); - } - - return section; - } - - @NotNull - public ConfigSection createDefaultSection(@NotNull String path, ConfigFormattingRules.CommentStyle commentStyle, String... comment) { - createNodePath(path, true); - ConfigSection section = new ConfigSection(root, this, path, true); - - synchronized (root.lock) { - root.defaults.put(fullPath + path, section); - root.defaultComments.put(fullPath + path, new Comment(commentStyle, comment)); - } - - return section; - } - - @NotNull - public ConfigSection setComment(@NotNull String path, @Nullable ConfigFormattingRules.CommentStyle commentStyle, String... lines) { - return setComment(path, lines != null ? new Comment(commentStyle, lines) : null); - } - - @NotNull - public ConfigSection setComment(@NotNull String path, @Nullable ConfigFormattingRules.CommentStyle commentStyle, @Nullable List lines) { - return setComment(path, lines != null ? new Comment(commentStyle, lines) : null); - } - - @NotNull - public ConfigSection setComment(@NotNull String path, @Nullable Comment comment) { - synchronized (root.lock) { - if (isDefault) { - root.defaultComments.put(fullPath + path, comment); - } else { - root.configComments.put(fullPath + path, comment); - } - } - - return this; - } - - @NotNull - public ConfigSection setDefaultComment(@NotNull String path, String... lines) { - return setDefaultComment(path, lines.length == 0 ? null : Arrays.asList(lines)); - } - - @NotNull - public ConfigSection setDefaultComment(@NotNull String path, @Nullable List lines) { - synchronized (root.lock) { - root.defaultComments.put(fullPath + path, new Comment(lines)); - } - - return this; - } - - @NotNull - public ConfigSection setDefaultComment(@NotNull String path, ConfigFormattingRules.CommentStyle commentStyle, String... lines) { - return setDefaultComment(path, commentStyle, lines.length == 0 ? null : Arrays.asList(lines)); - } - - @NotNull - public ConfigSection setDefaultComment(@NotNull String path, ConfigFormattingRules.CommentStyle commentStyle, @Nullable List lines) { - synchronized (root.lock) { - root.defaultComments.put(fullPath + path, new Comment(commentStyle, lines)); - } - - return this; - } - - @NotNull - public ConfigSection setDefaultComment(@NotNull String path, @Nullable Comment comment) { - synchronized (root.lock) { - root.defaultComments.put(fullPath + path, comment); - } - - return this; - } - - @Nullable - public Comment getComment(@NotNull String path) { - Comment result = root.configComments.get(fullPath + path); - - if (result == null) { - result = root.defaultComments.get(fullPath + path); - } - - return result; - } - - @Nullable - public String getCommentString(@NotNull String path) { - Comment result = root.configComments.get(fullPath + path); - - if (result == null) { - result = root.defaultComments.get(fullPath + path); - } - - return result != null ? result.toString() : null; - } - - @Override - public void addDefault(@NotNull String path, @Nullable Object value) { - createNodePath(path, true); - - synchronized (root.lock) { - root.defaults.put(fullPath + path, value); - } - } - - @Override - public void addDefaults(@NotNull Map defaults) { - //defaults.entrySet().stream().forEach(m -> root.defaults.put(fullPath + m.getKey(), m.getValue())); - defaults.entrySet().forEach(m -> addDefault(m.getKey(), m.getValue())); - } - - @Override - public void setDefaults(Configuration c) { - if (fullPath.isEmpty()) { - root.defaults.clear(); - } else { - root.defaults.keySet().stream() - .filter(k -> k.startsWith(fullPath)) - .forEach(root.defaults::remove); - } - - addDefaults(c); - } - - @Override - public ConfigSection getDefaults() { - return new ConfigSection(root, this, null, true); - } - - @Override - public ConfigSection getDefaultSection() { - return new ConfigSection(root, this, null, true); - } - - @Override - public ConfigOptionsAdapter options() { - return new ConfigOptionsAdapter(root); - } - - @NotNull - @Override - public Set getKeys(boolean deep) { - LinkedHashSet result = new LinkedHashSet<>(); - int pathIndex = fullPath.lastIndexOf(root.pathChar); - - if (deep) { - result.addAll(root.defaults.keySet().stream() - .filter(k -> k.startsWith(fullPath)) - .map(k -> !k.endsWith(String.valueOf(root.pathChar)) ? k.substring(pathIndex + 1) : k.substring(pathIndex + 1, k.length() - 1)) - .collect(Collectors.toCollection(LinkedHashSet::new))); - result.addAll(root.values.keySet().stream() - .filter(k -> k.startsWith(fullPath)) - .map(k -> !k.endsWith(String.valueOf(root.pathChar)) ? k.substring(pathIndex + 1) : k.substring(pathIndex + 1, k.length() - 1)) - .collect(Collectors.toCollection(LinkedHashSet::new))); - } else { - result.addAll(root.defaults.keySet().stream() - .filter(k -> k.startsWith(fullPath) && k.lastIndexOf(root.pathChar) == pathIndex) - .map(k -> !k.endsWith(String.valueOf(root.pathChar)) ? k.substring(pathIndex + 1) : k.substring(pathIndex + 1, k.length() - 1)) - .collect(Collectors.toCollection(LinkedHashSet::new))); - result.addAll(root.values.keySet().stream() - .filter(k -> k.startsWith(fullPath) && k.lastIndexOf(root.pathChar) == pathIndex) - .map(k -> !k.endsWith(String.valueOf(root.pathChar)) ? k.substring(pathIndex + 1) : k.substring(pathIndex + 1, k.length() - 1)) - .collect(Collectors.toCollection(LinkedHashSet::new))); - } - - return result; - } - - @NotNull - @Override - public Map getValues(boolean deep) { - LinkedHashMap result = new LinkedHashMap<>(); - int pathIndex = fullPath.lastIndexOf(root.pathChar); - - if (deep) { - result.putAll((Map) root.defaults.entrySet().stream() - .filter(k -> k.getKey().startsWith(fullPath)) - .collect(Collectors.toMap( - e -> !e.getKey().endsWith(String.valueOf(root.pathChar)) ? e.getKey().substring(pathIndex + 1) : e.getKey().substring(pathIndex + 1, e.getKey().length() - 1), - Map.Entry::getValue, - (v1, v2) -> { - throw new IllegalStateException(); - }, // never going to be merging keys - LinkedHashMap::new))); - - result.putAll((Map) root.values.entrySet().stream() - .filter(k -> k.getKey().startsWith(fullPath)) - .collect(Collectors.toMap( - e -> !e.getKey().endsWith(String.valueOf(root.pathChar)) ? e.getKey().substring(pathIndex + 1) : e.getKey().substring(pathIndex + 1, e.getKey().length() - 1), - Map.Entry::getValue, - (v1, v2) -> { - throw new IllegalStateException(); - }, // never going to be merging keys - LinkedHashMap::new))); - } else { - result.putAll((Map) root.defaults.entrySet().stream() - .filter(k -> k.getKey().startsWith(fullPath) && k.getKey().lastIndexOf(root.pathChar) == pathIndex) - .collect(Collectors.toMap( - e -> !e.getKey().endsWith(String.valueOf(root.pathChar)) ? e.getKey().substring(pathIndex + 1) : e.getKey().substring(pathIndex + 1, e.getKey().length() - 1), - Map.Entry::getValue, - (v1, v2) -> { - throw new IllegalStateException(); - }, // never going to be merging keys - LinkedHashMap::new))); - - result.putAll((Map) root.values.entrySet().stream() - .filter(k -> k.getKey().startsWith(fullPath) && k.getKey().lastIndexOf(root.pathChar) == pathIndex) - .collect(Collectors.toMap( - e -> !e.getKey().endsWith(String.valueOf(root.pathChar)) ? e.getKey().substring(pathIndex + 1) : e.getKey().substring(pathIndex + 1, e.getKey().length() - 1), - Map.Entry::getValue, - (v1, v2) -> { - throw new IllegalStateException(); - }, // never going to be merging keys - LinkedHashMap::new))); - } - - return result; - } - - @NotNull - public List getSections(String path) { - ConfigSection rootSection = getConfigurationSection(path); - - if (rootSection == null) { - return Collections.emptyList(); - } - - ArrayList result = new ArrayList<>(); - rootSection.getKeys(false).stream() - .map(rootSection::get) - .filter(ConfigSection.class::isInstance) - .forEachOrdered(object -> result.add((ConfigSection) object)); - - return result; - } - - @Override - public boolean contains(@NotNull String path) { - return root.defaults.containsKey(fullPath + path) || root.values.containsKey(fullPath + path); - } - - @Override - public boolean contains(@NotNull String path, boolean ignoreDefault) { - return (!ignoreDefault && root.defaults.containsKey(fullPath + path)) || root.values.containsKey(fullPath + path); - } - - @Override - public boolean isSet(@NotNull String path) { - return root.defaults.get(fullPath + path) != null || root.values.get(fullPath + path) != null; - } - - @Override - public String getCurrentPath() { - return fullPath.isEmpty() ? "" : fullPath.substring(0, fullPath.length() - 1); - } - - @Override - public String getName() { - if (fullPath.isEmpty()) { - return ""; - } - - String[] parts = fullPath.split(Pattern.quote(String.valueOf(root.pathChar))); - return parts[parts.length - 1]; - } - - @Override - public ConfigSection getRoot() { - return root; - } - - @Override - public ConfigSection getParent() { - return parent; - } - - @Nullable - @Override - public Object get(@NotNull String path) { - Object result = root.values.get(fullPath + path); - - if (result == null) { - result = root.defaults.get(fullPath + path); - } - - return result; - } - - @Nullable - @Override - public Object get(@NotNull String path, @Nullable Object def) { - Object result = root.values.get(fullPath + path); - - return result != null ? result : def; - } - - @Override - public void set(@NotNull String path, @Nullable Object value) { - if (isDefault) { - addDefault(path, value); - return; - } - - createNodePath(path, false); - Object last; - synchronized (root.lock) { - if (value != null) { - root.changed |= (last = root.values.put(fullPath + path, value)) != value; - } else { - root.changed |= (last = root.values.remove(fullPath + path)) != null; - } - } - - if (last != value && last instanceof ConfigSection) { - // clean up orphaned nodes - final String trim = fullPath + path + root.pathChar; - synchronized (root.lock) { - root.values.keySet().stream() - .filter(k -> k.startsWith(trim)) - .collect(Collectors.toSet()) - .forEach(root.values::remove); - } - } - - onChange(); - } - - @NotNull - public ConfigSection set(@NotNull String path, @Nullable Object value, String... comment) { - set(path, value); - return setComment(path, null, comment); - } - - @NotNull - public ConfigSection set(@NotNull String path, @Nullable Object value, List comment) { - set(path, value); - return setComment(path, null, comment); - } - - @NotNull - public ConfigSection set(@NotNull String path, @Nullable Object value, @Nullable ConfigFormattingRules.CommentStyle commentStyle, String... comment) { - set(path, value); - return setComment(path, commentStyle, comment); - } - - @NotNull - public ConfigSection set(@NotNull String path, @Nullable Object value, @Nullable ConfigFormattingRules.CommentStyle commentStyle, List comment) { - set(path, value); - return setComment(path, commentStyle, comment); - } - - @NotNull - public ConfigSection setDefault(@NotNull String path, @Nullable Object value) { - addDefault(path, value); - return this; - } - - @NotNull - public ConfigSection setDefault(@NotNull String path, @Nullable Object value, String... comment) { - addDefault(path, value); - return setDefaultComment(path, comment); - } - - @NotNull - public ConfigSection setDefault(@NotNull String path, @Nullable Object value, List comment) { - addDefault(path, value); - return setDefaultComment(path, comment); - } - - @NotNull - public ConfigSection setDefault(@NotNull String path, @Nullable Object value, ConfigFormattingRules.CommentStyle commentStyle, String... comment) { - addDefault(path, value); - return setDefaultComment(path, commentStyle, comment); - } - - @NotNull - public ConfigSection setDefault(@NotNull String path, @Nullable Object value, ConfigFormattingRules.CommentStyle commentStyle, List comment) { - addDefault(path, value); - return setDefaultComment(path, commentStyle, comment); - } - - @NotNull - @Override - public ConfigSection createSection(@NotNull String path) { - createNodePath(path, false); - ConfigSection section = new ConfigSection(root, this, path, false); - - synchronized (root.lock) { - root.values.put(fullPath + path, section); - } - - root.changed = true; - onChange(); - - return section; - } - - @NotNull - public ConfigSection createSection(@NotNull String path, String... comment) { - return createSection(path, null, comment.length == 0 ? null : Arrays.asList(comment)); - } - - @NotNull - public ConfigSection createSection(@NotNull String path, @Nullable List comment) { - return createSection(path, null, comment); - } - - @NotNull - public ConfigSection createSection(@NotNull String path, @Nullable ConfigFormattingRules.CommentStyle commentStyle, String... comment) { - return createSection(path, commentStyle, comment.length == 0 ? null : Arrays.asList(comment)); - } - - @NotNull - public ConfigSection createSection(@NotNull String path, @Nullable ConfigFormattingRules.CommentStyle commentStyle, @Nullable List comment) { - createNodePath(path, false); - ConfigSection section = new ConfigSection(root, this, path, false); - - synchronized (root.lock) { - root.values.put(fullPath + path, section); - } - - setComment(path, commentStyle, comment); - root.changed = true; - onChange(); - - return section; - } - - @NotNull - @Override - public ConfigSection createSection(@NotNull String path, Map map) { - createNodePath(path, false); - ConfigSection section = new ConfigSection(root, this, path, false); - - synchronized (root.lock) { - root.values.put(fullPath + path, section); - } - - for (Map.Entry entry : map.entrySet()) { - if (entry.getValue() instanceof Map) { - section.createSection(entry.getKey().toString(), (Map) entry.getValue()); - continue; - } - - section.set(entry.getKey().toString(), entry.getValue()); - } - - root.changed = true; - onChange(); - - return section; - } - - @Nullable - @Override - public String getString(@NotNull String path) { - Object result = get(path); - - return result != null ? result.toString() : null; - } - - @Nullable - @Override - public String getString(@NotNull String path, @Nullable String def) { - Object result = get(path); - - return result != null ? result.toString() : def; - } - - public char getChar(@NotNull String path) { - Object result = get(path); - - return result != null && !result.toString().isEmpty() ? result.toString().charAt(0) : '\0'; - } - - public char getChar(@NotNull String path, char def) { - Object result = get(path); - - return result != null && !result.toString().isEmpty() ? result.toString().charAt(0) : def; - } - - @Override - public int getInt(@NotNull String path) { - Object result = get(path); - - return result instanceof Number ? ((Number) result).intValue() : 0; - } - - @Override - public int getInt(@NotNull String path, int def) { - Object result = get(path); - - return result instanceof Number ? ((Number) result).intValue() : def; - } - - @Override - public boolean getBoolean(@NotNull String path) { - Object result = get(path); - - return result instanceof Boolean ? (Boolean) result : false; - } - - @Override - public boolean getBoolean(@NotNull String path, boolean def) { - Object result = get(path); - - return result instanceof Boolean ? (Boolean) result : def; - } - - @Override - public double getDouble(@NotNull String path) { - Object result = get(path); - - return result instanceof Number ? ((Number) result).doubleValue() : 0; - } - - @Override - public double getDouble(@NotNull String path, double def) { - Object result = get(path); - - return result instanceof Number ? ((Number) result).doubleValue() : def; - } - - @Override - public long getLong(@NotNull String path) { - Object result = get(path); - - return result instanceof Number ? ((Number) result).longValue() : 0; - } - - @Override - public long getLong(@NotNull String path, long def) { - Object result = get(path); - - return result instanceof Number ? ((Number) result).longValue() : def; - } - - @Nullable - @Override - public List getList(@NotNull String path) { - Object result = get(path); - - return result instanceof List ? (List) result : null; - } - - @Nullable - @Override - public List getList(@NotNull String path, @Nullable List def) { - Object result = get(path); - - return result instanceof List ? (List) result : def; - } - - @Nullable - public CompatibleMaterial getMaterial(@NotNull String path) { - String val = getString(path); - - return val != null ? CompatibleMaterial.getMaterial(val) : null; - } - - @Nullable - public CompatibleMaterial getMaterial(@NotNull String path, @Nullable CompatibleMaterial def) { - String val = getString(path); - - CompatibleMaterial mat = val != null ? CompatibleMaterial.getMaterial(val) : null; - - return mat != null ? mat : def; - } - - @Nullable - @Override - public T getObject(@NotNull String path, @NotNull Class clazz) { - Object result = get(path); - - return clazz.isInstance(result) ? clazz.cast(result) : null; - } - - @Nullable - @Override - public T getObject(@NotNull String path, @NotNull Class clazz, @Nullable T def) { - Object result = get(path); - - return clazz.isInstance(result) ? clazz.cast(result) : def; - } - - @Override - public ConfigSection getConfigurationSection(@NotNull String path) { - Object result = get(path); - - return result instanceof ConfigSection ? (ConfigSection) result : null; - } - - @NotNull - public ConfigSection getOrCreateConfigurationSection(@NotNull String path) { - Object result = get(path); - - return result instanceof ConfigSection ? (ConfigSection) result : createSection(path); - } -} diff --git a/Core/src/main/java/com/songoda/core/configuration/ConfigSetting.java b/Core/src/main/java/com/songoda/core/configuration/ConfigSetting.java deleted file mode 100644 index 62d4bdcc..00000000 --- a/Core/src/main/java/com/songoda/core/configuration/ConfigSetting.java +++ /dev/null @@ -1,135 +0,0 @@ -package com.songoda.core.configuration; - -import com.songoda.core.SongodaCore; -import com.songoda.core.compatibility.CompatibleMaterial; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; -import java.util.logging.Level; - -public class ConfigSetting { - final Config config; - final String key; - - public ConfigSetting(@NotNull Config config, @NotNull String key) { - this.config = config; - this.key = key; - } - - public ConfigSetting(@NotNull Config config, @NotNull String key, @NotNull Object defaultValue, String... comment) { - this.config = config; - this.key = key; - - config.setDefault(key, defaultValue, comment); - } - - public ConfigSetting(@NotNull Config config, @NotNull String key, @NotNull Object defaultValue, ConfigFormattingRules.CommentStyle commentStyle, String... comment) { - this.config = config; - this.key = key; - - config.setDefault(key, defaultValue, commentStyle, comment); - } - - @NotNull - public String getKey() { - return key; - } - - public List getIntegerList() { - return config.getIntegerList(key); - } - - public List getStringList() { - return config.getStringList(key); - } - - public boolean getBoolean() { - return config.getBoolean(key); - } - - public boolean getBoolean(boolean def) { - return config.getBoolean(key, def); - } - - public int getInt() { - return config.getInt(key); - } - - public int getInt(int def) { - return config.getInt(key, def); - } - - public long getLong() { - return config.getLong(key); - } - - public long getLong(long def) { - return config.getLong(key, def); - } - - public double getDouble() { - return config.getDouble(key); - } - - public double getDouble(double def) { - return config.getDouble(key, def); - } - - public String getString() { - return config.getString(key); - } - - public String getString(String def) { - return config.getString(key, def); - } - - public Object getObject() { - return config.get(key); - } - - public Object getObject(Object def) { - return config.get(key, def); - } - - public T getObject(@NotNull Class clazz) { - return config.getObject(key, clazz); - } - - public T getObject(@NotNull Class clazz, @Nullable T def) { - return config.getObject(key, clazz, def); - } - - public char getChar() { - return config.getChar(key); - } - - public char getChar(char def) { - return config.getChar(key, def); - } - - @NotNull - public CompatibleMaterial getMaterial() { - String val = config.getString(key); - CompatibleMaterial mat = CompatibleMaterial.getMaterial(config.getString(key)); - - if (mat == null) { - SongodaCore.getLogger().log(Level.WARNING, String.format("Config value \"%s\" has an invalid material name: \"%s\"", key, val)); - } - - return mat != null ? mat : CompatibleMaterial.STONE; - } - - @NotNull - public CompatibleMaterial getMaterial(@NotNull CompatibleMaterial def) { - //return config.getMaterial(key, def); - String val = config.getString(key); - CompatibleMaterial mat = val != null ? CompatibleMaterial.getMaterial(val) : null; - - if (mat == null) { - SongodaCore.getLogger().log(Level.WARNING, String.format("Config value \"%s\" has an invalid material name: \"%s\"", key, val)); - } - - return mat != null ? mat : def; - } -} diff --git a/Core/src/main/java/com/songoda/core/configuration/DataStoreObject.java b/Core/src/main/java/com/songoda/core/configuration/DataStoreObject.java deleted file mode 100644 index 243b1180..00000000 --- a/Core/src/main/java/com/songoda/core/configuration/DataStoreObject.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.songoda.core.configuration; - -import org.bukkit.configuration.ConfigurationSection; - -public interface DataStoreObject { - /** - * @return a unique hashable instance of T to store this value under - */ - T getKey(); - - /** - * @return a unique identifier for saving this value with - */ - String getConfigKey(); - - /** - * Save this data to a ConfigurationSection - */ - void saveToSection(ConfigurationSection sec); - - /** - * @return true if this data has changed from the state saved to file - */ - boolean hasChanged(); - - /** - * Mark this data as needing a save or not - */ - void setChanged(boolean isChanged); -} diff --git a/Core/src/main/java/com/songoda/core/configuration/HeaderCommentable.java b/Core/src/main/java/com/songoda/core/configuration/HeaderCommentable.java new file mode 100644 index 00000000..d93730cd --- /dev/null +++ b/Core/src/main/java/com/songoda/core/configuration/HeaderCommentable.java @@ -0,0 +1,18 @@ +package com.songoda.core.configuration; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +public interface HeaderCommentable { + void setHeaderComment(@Nullable Supplier comment); + + default void setHeaderComment(@Nullable String comment) { + setHeaderComment(() -> comment); + } + + @Nullable Supplier getHeaderComment(); + + @NotNull String generateHeaderCommentLines(); +} diff --git a/Core/src/main/java/com/songoda/core/configuration/IConfiguration.java b/Core/src/main/java/com/songoda/core/configuration/IConfiguration.java new file mode 100644 index 00000000..f231631e --- /dev/null +++ b/Core/src/main/java/com/songoda/core/configuration/IConfiguration.java @@ -0,0 +1,72 @@ +package com.songoda.core.configuration; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; + +public interface IConfiguration { + /** + * This method returns whether a given key is set memory, ignoring its possibly null value. + * + * {@link #set(String, Object)} + * {@link #unset(String)} + */ + boolean has(String key); + + /** + * This method returns the value for a given key. + * A value of null can mean that the key does not exist or that the value is null. + * + * @see #has(String) + */ + @Nullable + Object get(String key); + + /** + * This method is mostly identical to {@link #get(String)} + * but returns the given default value if the key doesn't exist or the value is null. + */ + @Nullable + Object getOr(String key, @Nullable Object defaultValue); + + /** + * This method sets a given key to a given value in memory. + * + * @return The previous value associated with key, or null if there was no mapping for key + * + * @see #save(Writer) + */ + Object set(@NotNull String key, @Nullable Object value); + + /** + * This method removes the given key from memory together with its value. + * + * @return The previous value associated with key, or null if there was no mapping for key + */ + Object unset(String key); + + /** + * This method clears all the configuration values from memory that have been loaded or set. + * + * @see #load(Reader) + */ + void reset(); + + /** + * This method parses and loads the configuration and stores them as key-value pairs in memory. + * Keys that are not loaded with this call but still exist in memory, are removed. + * Additional data may be read depending on the implementation (e.g. comments). + * + * @see #reset() + */ + void load(Reader reader) throws IOException; + + /** + * This method serializes the key-value pairs in memory and writes them to the given writer. + * Additional data may be written depending on the implementation (e.g. comments). + */ + void save(Writer writer) throws IOException; +} diff --git a/Core/src/main/java/com/songoda/core/configuration/NodeCommentable.java b/Core/src/main/java/com/songoda/core/configuration/NodeCommentable.java new file mode 100644 index 00000000..ab171e4a --- /dev/null +++ b/Core/src/main/java/com/songoda/core/configuration/NodeCommentable.java @@ -0,0 +1,16 @@ +package com.songoda.core.configuration; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +public interface NodeCommentable { + void setNodeComment(@NotNull String key, @Nullable Supplier comment); + + default void setNodeComment(@NotNull String key, @Nullable String comment) { + setNodeComment(key, () -> comment); + } + + @Nullable Supplier getNodeComment(@Nullable String key); +} diff --git a/Core/src/main/java/com/songoda/core/configuration/ReadOnlyConfigEntry.java b/Core/src/main/java/com/songoda/core/configuration/ReadOnlyConfigEntry.java new file mode 100644 index 00000000..2e474ee6 --- /dev/null +++ b/Core/src/main/java/com/songoda/core/configuration/ReadOnlyConfigEntry.java @@ -0,0 +1,72 @@ +package com.songoda.core.configuration; + +import com.songoda.core.utils.Pair; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; + +public class ReadOnlyConfigEntry implements ConfigEntry { + protected final @NotNull IConfiguration config; + protected final @NotNull String key; + + public ReadOnlyConfigEntry(@NotNull IConfiguration config, @NotNull String key) { + this.config = config; + this.key = key; + } + + @Override + public @NotNull String getKey() { + return this.key; + } + + @Override + public @NotNull IConfiguration getConfig() { + return this.config; + } + + @Override + @Contract(" -> null") + public @Nullable Object getDefaultValue() { + return null; + } + + @Override + @Contract("_ -> fail") + public void setDefaultValue(@Nullable Object defaultValue) { + throw new UnsupportedOperationException("Cannot set defaultValue on a read-only config entry"); + } + + @Override + @Contract("_ -> fail") + public ConfigEntry withDefaultValue(@Nullable Object defaultValue) { + throw new UnsupportedOperationException("Cannot set defaultValue on a read-only config entry"); + } + + @Override + @Contract("_ -> fail") + public ConfigEntry withComment(Supplier comment) { + throw new UnsupportedOperationException("Cannot set comment on a read-only config entry"); + } + + @Override + @Contract(" -> null") + public Map>> getUpgradeSteps() { + return null; + } + + @Override + @Contract("_, _, _ -> fail") + public ConfigEntry withUpgradeStep(int version, @Nullable String keyInGivenVersion, @Nullable Function valueConverter) { + throw new UnsupportedOperationException("Cannot set upgrade step on a read-only config entry"); + } + + @Override + @Contract("_ -> fail") + public void set(@Nullable Object value) { + throw new UnsupportedOperationException("Cannot set value on a read-only config entry"); + } +} diff --git a/Core/src/main/java/com/songoda/core/configuration/SimpleDataStore.java b/Core/src/main/java/com/songoda/core/configuration/SimpleDataStore.java deleted file mode 100644 index 9c656cf2..00000000 --- a/Core/src/main/java/com/songoda/core/configuration/SimpleDataStore.java +++ /dev/null @@ -1,284 +0,0 @@ -package com.songoda.core.configuration; - -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.InvalidConfigurationException; -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.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Timer; -import java.util.TimerTask; -import java.util.function.Function; -import java.util.logging.Level; - -/** - * Used to easily store a set of one data value - * - * @param DataObject class that is used to store the data - */ -public class SimpleDataStore { - protected final Plugin plugin; - protected final String filename, dirName; - private final Function getFromSection; - protected final HashMap data = new HashMap<>(); - private File file; - private final Object lock = new Object(); - SaveTask saveTask; - Timer autosaveTimer; - /** - * time in seconds to start a save after a change is made - */ - int autosaveInterval = 60; - - public SimpleDataStore(@NotNull Plugin plugin, @NotNull String filename, @NotNull Function loadFunction) { - this.plugin = plugin; - this.filename = filename; - dirName = null; - this.getFromSection = loadFunction; - } - - public SimpleDataStore(@NotNull Plugin plugin, @Nullable String directory, @NotNull String filename, @NotNull Function loadFunction) { - this.plugin = plugin; - this.filename = filename; - this.dirName = directory; - this.getFromSection = loadFunction; - } - - @NotNull - public File getFile() { - if (file == null) { - if (dirName != null) { - this.file = new File(plugin.getDataFolder() + dirName, filename != null ? filename : "data.yml"); - } else { - this.file = new File(plugin.getDataFolder(), filename != null ? filename : "data.yml"); - } - } - - return file; - } - - /** - * @return a directly-modifiable instance of the data mapping for this - * storage - */ - public Map getData() { - return data; - } - - /** - * Returns the value to which the specified key is mapped, or {@code null} - * if this map contains no mapping for the key. - * - * @param key key whose mapping is to be retrieved from this storage - * - * @return the value associated with key, or - * null if there was no mapping for key. - */ - @Nullable - public T get(Object key) { - return data.get(key); - } - - /** - * Removes the mapping for the specified key from this storage if present. - * - * @param key key whose mapping is to be removed from this storage - * - * @return the previous value associated with key, or - * null if there was no mapping for key. - */ - @Nullable - public T remove(@NotNull Object key) { - T temp; - - synchronized (lock) { - temp = data.remove(key); - } - - save(); - - return temp; - } - - /** - * Removes the mapping for the specified key from this storage if present. - * - * @param value value whose mapping is to be removed from this storage - * - * @return the previous value associated with key, or - * null if there was no mapping for key. - */ - @Nullable - public T remove(@NotNull T value) { - if (value == null) { - return null; - } - - T temp; - - synchronized (lock) { - temp = data.remove(value.getKey()); - } - - save(); - - return temp; - } - - /** - * Adds the specified value in this storage. If the map previously contained - * a mapping for the key, the old value is replaced. - * - * @param value value to be added - * - * @return the previous value associated with value.getKey(), or - * null if there was no mapping for value.getKey(). - */ - @Nullable - public T add(@NotNull T value) { - if (value == null) { - return null; - } - - T temp; - - synchronized (lock) { - temp = data.put(value.getKey(), value); - } - - save(); - - return temp; - } - - /** - * Adds the specified value in this storage. If the map previously contained - * a mapping for the key, the old value is replaced. - * - * @param value values to be added - */ - public void addAll(@NotNull T[] value) { - if (value == null) { - return; - } - - synchronized (lock) { - for (T t : value) { - if (t != null) { - data.put(t.getKey(), t); - } - } - } - - save(); - } - - /** - * Adds the specified value in this storage. If the map previously contained - * a mapping for the key, the old value is replaced. - * - * @param value values to be added - */ - @Nullable - public void addAll(@NotNull Collection value) { - if (value == null) { - return; - } - - synchronized (lock) { - for (T v : value) { - if (v != null) { - data.put(v.getKey(), v); - } - } - } - - save(); - } - - /** - * Load data from the associated file - */ - public void load() { - if (!getFile().exists()) { - return; - } - - try { - YamlConfiguration f = new YamlConfiguration(); - f.options().pathSeparator('\0'); - f.load(file); - - synchronized (lock) { - data.clear(); - - f.getValues(false).values().stream() - .filter(ConfigurationSection.class::isInstance) - .map(v -> getFromSection.apply((ConfigurationSection) v)) - .forEach(v -> data.put(v.getKey(), v)); - } - } catch (IOException | InvalidConfigurationException ex) { - plugin.getLogger().log(Level.SEVERE, "Failed to load data from " + file.getName(), ex); - } - } - - /** - * Optionally save this storage's data to file if there have been changes - * made - */ - public void saveChanges() { - if (saveTask != null || data.values().stream().anyMatch(DataStoreObject::hasChanged)) { - flushSave(); - } - } - - /** - * Save this file data. This saves later asynchronously. - */ - public void save() { - // save async even if no plugin or if plugin disabled - if (saveTask == null) { - autosaveTimer = new Timer((plugin != null ? plugin.getName() + "-DataStoreSave-" : "DataStoreSave-") + getFile().getName()); - autosaveTimer.schedule(saveTask = new SaveTask(), autosaveInterval * 1000L); - } - } - - /** - * Force a new save of this storage's data - */ - public void flushSave() { - if (saveTask != null) { - //Close Threads - saveTask.cancel(); - autosaveTimer.cancel(); - saveTask = null; - autosaveTimer = null; - } - - YamlConfiguration f = new YamlConfiguration(); - - synchronized (lock) { - data.values().forEach(e -> e.saveToSection(f.createSection(e.getConfigKey()))); - } - - try { - f.save(getFile()); - data.values().forEach(e -> e.setChanged(false)); - } catch (IOException ex) { - plugin.getLogger().log(Level.SEVERE, "Failed to save data to " + file.getName(), ex); - } - } - - class SaveTask extends TimerTask { - @Override - public void run() { - flushSave(); - } - } -} diff --git a/Core/src/main/java/com/songoda/core/configuration/WriteableConfigEntry.java b/Core/src/main/java/com/songoda/core/configuration/WriteableConfigEntry.java new file mode 100644 index 00000000..f2610b21 --- /dev/null +++ b/Core/src/main/java/com/songoda/core/configuration/WriteableConfigEntry.java @@ -0,0 +1,19 @@ +package com.songoda.core.configuration; + +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +public interface WriteableConfigEntry extends ConfigEntry { + @Override + default void set(@Nullable Object value) { + getConfig().set(getKey(), value); + } + + @Override + default ConfigEntry withComment(Supplier comment) { + ((NodeCommentable) getConfig()).setNodeComment(getKey(), comment); + + return this; + } +} diff --git a/Core/src/main/java/com/songoda/core/configuration/editor/ConfigEditorGui.java b/Core/src/main/java/com/songoda/core/configuration/editor/ConfigEditorGui.java index 9ab71e8c..df63133a 100644 --- a/Core/src/main/java/com/songoda/core/configuration/editor/ConfigEditorGui.java +++ b/Core/src/main/java/com/songoda/core/configuration/editor/ConfigEditorGui.java @@ -1,7 +1,6 @@ package com.songoda.core.configuration.editor; import com.songoda.core.compatibility.CompatibleMaterial; -import com.songoda.core.configuration.Config; import com.songoda.core.gui.Gui; import com.songoda.core.gui.GuiUtils; import com.songoda.core.gui.SimplePagedGui; @@ -27,7 +26,10 @@ import java.util.logging.Level; /** * Edit a configuration file for a specific plugin + * + * @deprecated Needs a recode in another package */ +@Deprecated public class ConfigEditorGui extends SimplePagedGui { final JavaPlugin plugin; final String file; @@ -273,9 +275,9 @@ public class ConfigEditorGui extends SimplePagedGui { plugin.getLogger().log(Level.SEVERE, "Failed to save config changes to " + file, ex); return; } - } else if (config instanceof Config) { + }/* else if (config instanceof Config) { ((Config) config).save(); - } else { + }*/ else { player.sendMessage(ChatColor.RED + "Unknown configuration type '" + config.getClass().getName() + "' - Please report this error!"); plugin.getLogger().log(Level.WARNING, "Unknown configuration type '" + config.getClass().getName() + "' - Please report this error!"); return; diff --git a/Core/src/main/java/com/songoda/core/configuration/editor/ConfigEditorListEditorGui.java b/Core/src/main/java/com/songoda/core/configuration/editor/ConfigEditorListEditorGui.java index 7433931c..fd4514d8 100644 --- a/Core/src/main/java/com/songoda/core/configuration/editor/ConfigEditorListEditorGui.java +++ b/Core/src/main/java/com/songoda/core/configuration/editor/ConfigEditorListEditorGui.java @@ -12,7 +12,10 @@ import java.util.List; /** * Edit a string list + * + * @deprecated Needs a recode in another package */ +@Deprecated public class ConfigEditorListEditorGui extends SimplePagedGui { final ConfigEditorGui current; diff --git a/Core/src/main/java/com/songoda/core/configuration/editor/PluginConfigGui.java b/Core/src/main/java/com/songoda/core/configuration/editor/PluginConfigGui.java index 5036bc10..bbdd53f8 100644 --- a/Core/src/main/java/com/songoda/core/configuration/editor/PluginConfigGui.java +++ b/Core/src/main/java/com/songoda/core/configuration/editor/PluginConfigGui.java @@ -2,7 +2,6 @@ package com.songoda.core.configuration.editor; import com.songoda.core.SongodaPlugin; import com.songoda.core.compatibility.CompatibleMaterial; -import com.songoda.core.configuration.Config; import com.songoda.core.gui.Gui; import com.songoda.core.gui.GuiUtils; import com.songoda.core.gui.SimplePagedGui; @@ -19,7 +18,10 @@ import java.util.Map; /** * Edit all configuration files for a specific plugin + * + * @deprecated Needs a recode in another package */ +@Deprecated public class PluginConfigGui extends SimplePagedGui { final JavaPlugin plugin; LinkedHashMap configs = new LinkedHashMap<>(); @@ -33,14 +35,18 @@ public class PluginConfigGui extends SimplePagedGui { this.plugin = plugin; + // FIXME: Add SongodaCore config + // FIXME: Add plugin configs + plugin.getLogger().warning("Loading configs for " + plugin.getName() + " is not supported at the moment."); + // collect list of plugins - configs.put(plugin.getCoreConfig().getFile().getName(), plugin.getCoreConfig()); - List more = plugin.getExtraConfig(); - if (more != null && !more.isEmpty()) { - for (Config cfg : more) { - configs.put(cfg.getFile().getName(), cfg); - } - } +// configs.put(plugin.getCoreConfig().getFile().getName(), plugin.getCoreConfig()); +// List more = plugin.getConfigs(); +// if (more != null && !more.isEmpty()) { +// for (Config cfg : more) { +// configs.put(cfg.getFile().getName(), cfg); +// } +// } init(); } diff --git a/Core/src/main/java/com/songoda/core/configuration/songoda/SongodaYamlConfig.java b/Core/src/main/java/com/songoda/core/configuration/songoda/SongodaYamlConfig.java new file mode 100644 index 00000000..0cd29eb5 --- /dev/null +++ b/Core/src/main/java/com/songoda/core/configuration/songoda/SongodaYamlConfig.java @@ -0,0 +1,245 @@ +package com.songoda.core.configuration.songoda; + +import com.songoda.core.configuration.ConfigEntry; +import com.songoda.core.configuration.ReadOnlyConfigEntry; +import com.songoda.core.configuration.yaml.YamlConfigEntry; +import com.songoda.core.configuration.yaml.YamlConfiguration; +import com.songoda.core.utils.Pair; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; + +// TODO: replace all config related exceptions with custom exceptions +// TODO: Allow registering load-Listeners +// TODO: Provide method to only save if changed +public class SongodaYamlConfig extends YamlConfiguration { + protected static final String CANNOT_CREATE_BACKUP_COPY_EXCEPTION_PREFIX = "Unable to create backup copy of config file: "; + + public final @NotNull File file; + protected final @NotNull Logger logger; + + private int targetVersion; + private ConfigEntry versionEntry; + + protected final Map configEntries = new LinkedHashMap<>(0); + + public SongodaYamlConfig(@NotNull JavaPlugin plugin, @NotNull File file) { + this(file, plugin.getLogger()); + } + + public SongodaYamlConfig(@NotNull JavaPlugin plugin, @NotNull String fileName) { + this(new File(plugin.getDataFolder(), fileName), plugin.getLogger()); + } + + public SongodaYamlConfig(@NotNull File file) { + this(file, null); + } + + public SongodaYamlConfig(@NotNull File file, @Nullable Logger logger) { + super(); + + this.file = Objects.requireNonNull(file); + this.logger = logger != null ? logger : Logger.getLogger(getClass().getName()); + } + + /** + * Calls {@link #load()} and then {@link #save()}.
+ *
+ * As this is intended to keep the {@link org.bukkit.plugin.java.JavaPlugin#onEnable()} method clean, + * it catches all exceptions and logs them instead.
+ *
+ * If this method returns false, the plugins should be disabled. + * + * @return true if the load and save were successful, false if an exception was thrown. + * + * @see #save() + * @see #load() + */ + public boolean init() { + try { + this.load(); + this.save(); + + return true; + } catch (IOException ex) { + this.logger.log(Level.SEVERE, "Failed to load config file: " + this.file.getPath(), ex); + } + + return false; + } + + public ReadOnlyConfigEntry getReadEntry(@NotNull String key) { + return new ReadOnlyConfigEntry(this, key); + } + + public ConfigEntry createEntry(@NotNull String key) { + return createEntry(key, null); + } + + public ConfigEntry createEntry(@NotNull String key, @Nullable Object defaultValue) { + ConfigEntry entry = new YamlConfigEntry(this, key, defaultValue); + + if (this.configEntries.putIfAbsent(key, entry) != null) { + throw new IllegalArgumentException("Entry already exists for key: " + key); + } + + if (entry.get() == null) { + entry.set(defaultValue); + } + + return entry; + } + + public SongodaYamlConfig withVersion(int version) { + return withVersion("version", version, () -> "Don't touch this – it's used to track the version of the config."); + } + + public SongodaYamlConfig withVersion(@NotNull String key, int version, @Nullable Supplier comment) { + if (version < 0) { + throw new IllegalArgumentException("Version must be positive"); + } + + if (this.versionEntry != null) { + this.versionEntry.set(null); + } + + this.targetVersion = version; + + this.versionEntry = new YamlConfigEntry(this, key, 0); + this.versionEntry.withComment(comment); + this.versionEntry.set(this.targetVersion); + + return this; + } + + public void load() throws IOException { + try (Reader reader = Files.newBufferedReader(this.file.toPath(), StandardCharsets.UTF_8)) { + load(reader); + } catch (FileNotFoundException ignore) { + } catch (IOException ex) { + throw new IOException("Unable to load '" + this.file.getPath() + "'", ex); + } + } + + public void save() throws IOException { + Files.createDirectories(this.file.toPath().getParent()); + + + try (Writer writer = Files.newBufferedWriter(this.file.toPath(), StandardCharsets.UTF_8)) { + super.save(writer); + } catch (IOException ex) { + throw new IOException("Unable to save '" + this.file.getPath() + "'", ex); + } + } + + @Override + public void load(Reader reader) throws IOException { + super.load(reader); + + upgradeOldConfigVersion(); + + for (ConfigEntry entry : this.configEntries.values()) { + if (entry.get() == null && entry.getDefaultValue() != null) { + entry.set(entry.getDefaultValue()); + } + } + } + + /** + * @return false, if no config version is set or no upgrade is needed + */ + protected boolean upgradeOldConfigVersion() throws IOException { + if (this.versionEntry == null) { + return false; + } + + if (this.versionEntry.getInt() > this.targetVersion) { + throw new IllegalStateException("Cannot upgrade a config version that is higher than the target version"); + } + if (this.versionEntry.getInt() == this.targetVersion) { + return false; + } + + createBackupCopyFile(); + + while (this.versionEntry.getInt() < this.targetVersion) { + upgradeOldConfigVersionByOne(); + } + + cleanValuesMap(this.values); + + return true; + } + + protected void upgradeOldConfigVersionByOne() { + int currentVersion = this.versionEntry.getInt(); + int targetVersion = currentVersion + 1; + + if (targetVersion > this.targetVersion) { + throw new IllegalStateException("Cannot upgrade a config version that is higher than the target version"); + } + + for (ConfigEntry entry : this.configEntries.values()) { + if (entry.getUpgradeSteps() == null) { + continue; + } + + Pair<@Nullable String, @Nullable Function> upgradeStep = entry.getUpgradeSteps().get(currentVersion); + if (upgradeStep == null) { + continue; + } + + String oldEntryKey = upgradeStep.getFirst(); + if (oldEntryKey == null) { + oldEntryKey = entry.getKey(); + } + + Object newValue = get(oldEntryKey); + if (upgradeStep.getSecond() != null) { + newValue = upgradeStep.getSecond().apply(newValue); + } + + set(oldEntryKey, null); + entry.set(newValue); + } + + this.versionEntry.set(targetVersion); + } + + protected void createBackupCopyFile() throws IOException { + if (!this.file.exists()) { + return; + } + + try { + Path targetPath = this.file.toPath().resolveSibling(this.file.getPath() + ".backup-" + System.currentTimeMillis()); + + Files.copy( + this.file.toPath(), + targetPath, + StandardCopyOption.REPLACE_EXISTING + ); + + this.logger.info("Created backup copy of config file '" + this.file.getPath() + "' to '" + targetPath + "'"); + } catch (IOException ex) { + throw new IOException(CANNOT_CREATE_BACKUP_COPY_EXCEPTION_PREFIX + this.file.getPath(), ex); + } + } +} diff --git a/Core/src/main/java/com/songoda/core/configuration/yaml/YamlCommentRepresenter.java b/Core/src/main/java/com/songoda/core/configuration/yaml/YamlCommentRepresenter.java new file mode 100644 index 00000000..8bf4e4c9 --- /dev/null +++ b/Core/src/main/java/com/songoda/core/configuration/yaml/YamlCommentRepresenter.java @@ -0,0 +1,73 @@ +package com.songoda.core.configuration.yaml; + +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.comments.CommentLine; +import org.yaml.snakeyaml.comments.CommentType; +import org.yaml.snakeyaml.events.CommentEvent; +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.representer.Representer; + +import java.util.Collections; +import java.util.Map; +import java.util.function.Supplier; + +public class YamlCommentRepresenter extends Representer { + private final Map> nodeComments; + + public YamlCommentRepresenter(DumperOptions dumperOptions, Map> nodeComments) { + super(dumperOptions); + this.nodeComments = nodeComments; + } + + @Override + public Node represent(Object data) { + Node rootNode = super.represent(data); + + if (!(rootNode instanceof MappingNode)) { + return rootNode; + } + + for (NodeTuple nodeTuple : ((MappingNode) rootNode).getValue()) { + if (!(nodeTuple.getKeyNode() instanceof ScalarNode)) { + continue; + } + + applyComment((ScalarNode) nodeTuple.getKeyNode(), ((ScalarNode) nodeTuple.getKeyNode()).getValue()); + + if (nodeTuple.getValueNode() instanceof MappingNode) { + String key = ((ScalarNode) nodeTuple.getKeyNode()).getValue(); + + resolveSubNodes(((MappingNode) nodeTuple.getValueNode()), key); + } + } + + return rootNode; + } + + protected void resolveSubNodes(MappingNode mappingNode, String key) { + for (NodeTuple nodeTuple : mappingNode.getValue()) { + if (!(nodeTuple.getKeyNode() instanceof ScalarNode)) { + continue; + } + + String newKey = key + "." + ((ScalarNode) nodeTuple.getKeyNode()).getValue(); + + applyComment((ScalarNode) nodeTuple.getKeyNode(), newKey); + + if (nodeTuple.getValueNode() instanceof MappingNode) { + resolveSubNodes(((MappingNode) nodeTuple.getValueNode()), newKey); + } + } + } + + protected void applyComment(ScalarNode scalarNode, String key) { + Supplier innerValue = this.nodeComments.get(key); + + if (innerValue != null) { + scalarNode.setBlockComments(Collections.singletonList(new CommentLine(new CommentEvent(CommentType.BLOCK, " " + innerValue.get(), null, null)))); + } + } +} diff --git a/Core/src/main/java/com/songoda/core/configuration/yaml/YamlConfigEntry.java b/Core/src/main/java/com/songoda/core/configuration/yaml/YamlConfigEntry.java new file mode 100644 index 00000000..93064e15 --- /dev/null +++ b/Core/src/main/java/com/songoda/core/configuration/yaml/YamlConfigEntry.java @@ -0,0 +1,90 @@ +package com.songoda.core.configuration.yaml; + +import com.songoda.core.configuration.ConfigEntry; +import com.songoda.core.configuration.IConfiguration; +import com.songoda.core.configuration.WriteableConfigEntry; +import com.songoda.core.utils.Pair; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +public class YamlConfigEntry implements WriteableConfigEntry { + protected final @NotNull YamlConfiguration config; + protected final @NotNull String key; + protected @Nullable Object defaultValue; + + protected @Nullable Map>> upgradeSteps; + + public YamlConfigEntry(@NotNull YamlConfiguration config, @NotNull String key, @Nullable Object defaultValue) { + this.config = config; + this.key = key; + this.defaultValue = defaultValue; + } + + @Override + public @NotNull String getKey() { + return this.key; + } + + @Override + public @NotNull IConfiguration getConfig() { + return this.config; + } + + @Override + public @Nullable Object getDefaultValue() { + return this.defaultValue; + } + + @Override + public @Nullable Map>> getUpgradeSteps() { + return this.upgradeSteps; + } + + @Override + public void setDefaultValue(@Nullable Object defaultValue) { + this.defaultValue = defaultValue; + } + + @Override + public ConfigEntry withDefaultValue(@Nullable Object defaultValue) { + this.setDefaultValue(defaultValue); + return this; + } + + @Override + public ConfigEntry withUpgradeStep(int version, @Nullable String keyInGivenVersion, @Nullable Function<@Nullable Object, @Nullable Object> valueConverter) { + if (keyInGivenVersion == null && valueConverter == null) { + throw new IllegalArgumentException("You must provide either a key or a value converter"); + } + + if (this.upgradeSteps == null) { + this.upgradeSteps = new HashMap<>(1); + } + + this.upgradeSteps.put(version, new Pair<>(keyInGivenVersion, valueConverter)); + + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + YamlConfigEntry that = (YamlConfigEntry) o; + return this.config.equals(that.config) && + this.key.equals(that.key) && + Objects.equals(this.defaultValue, that.defaultValue) && + Objects.equals(this.upgradeSteps, that.upgradeSteps); + } + + @Override + public int hashCode() { + return Objects.hash(this.config, this.key, this.defaultValue, this.upgradeSteps); + } +} diff --git a/Core/src/main/java/com/songoda/core/configuration/yaml/YamlConfiguration.java b/Core/src/main/java/com/songoda/core/configuration/yaml/YamlConfiguration.java new file mode 100644 index 00000000..286eebe5 --- /dev/null +++ b/Core/src/main/java/com/songoda/core/configuration/yaml/YamlConfiguration.java @@ -0,0 +1,371 @@ +package com.songoda.core.configuration.yaml; + +import com.songoda.core.configuration.HeaderCommentable; +import com.songoda.core.configuration.IConfiguration; +import com.songoda.core.configuration.NodeCommentable; +import org.apache.commons.lang.ArrayUtils; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.representer.Representer; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; + +// TODO: Allow registering own custom value converter (e.g. Bukkit-Location to Map and back) +// + move the huge block from #set into such a converter and register it by default +public class YamlConfiguration implements IConfiguration, HeaderCommentable, NodeCommentable { + protected final @NotNull Yaml yaml; + protected final @NotNull DumperOptions yamlDumperOptions; + protected final @NotNull YamlCommentRepresenter yamlCommentRepresenter; + + protected final @NotNull Map values; + protected final @NotNull Map> nodeComments; + protected @Nullable Supplier headerComment; + + public YamlConfiguration() { + this(new LinkedHashMap<>(), new LinkedHashMap<>()); + } + + protected YamlConfiguration(@NotNull Map values, @NotNull Map> nodeComments) { + this.values = Objects.requireNonNull(values); + this.nodeComments = Objects.requireNonNull(nodeComments); + + this.yamlDumperOptions = createDefaultYamlDumperOptions(); + this.yamlCommentRepresenter = new YamlCommentRepresenter(this.yamlDumperOptions, this.nodeComments); + this.yaml = createDefaultYaml(this.yamlDumperOptions, this.yamlCommentRepresenter); + } + + @Override + @Contract(pure = true, value = "null -> false") + public boolean has(String key) { + if (key == null) { + return false; + } + + String[] fullKeyPath = key.split("\\."); + + Map innerMap = getInnerMap(this.values, Arrays.copyOf(fullKeyPath, fullKeyPath.length - 1), false); + + if (innerMap != null) { + return innerMap.containsKey(fullKeyPath[fullKeyPath.length - 1]); + } + + return false; + } + + @Override + @Contract(pure = true, value = "null -> null") + public @Nullable Object get(String key) { + if (key == null) { + return null; + } + + try { + return getInnerValueForKey(this.values, key); + } catch (IllegalArgumentException ignore) { + } + + return null; + } + + @Override + @Contract(pure = true, value = "null,_ -> param2") + public @Nullable Object getOr(String key, @Nullable Object fallbackValue) { + Object value = get(key); + + return value == null ? fallbackValue : value; + } + + public @NotNull Set getKeys(String key) { + if (key == null) { + return Collections.emptySet(); + } + + if (key.equals("")) { + return Collections.unmodifiableSet(this.values.keySet()); + } + + Map innerMap = null; + + try { + innerMap = getInnerMap(this.values, key.split("\\."), false); + } catch (IllegalArgumentException ignore) { + } + + if (innerMap != null) { + return Collections.unmodifiableSet(innerMap.keySet()); + } + + return Collections.emptySet(); + + } + + @Override + public Object set(@NotNull String key, @Nullable Object value) { + if (value != null) { + if (value instanceof Float) { + value = ((Float) value).doubleValue(); + } else if (value instanceof Character) { + value = ((Character) value).toString(); + } else if (value.getClass().isEnum()) { + value = ((Enum) value).name(); + } else if (value.getClass().isArray()) { + if (value instanceof int[]) { + value = Arrays.asList(ArrayUtils.toObject((int[]) value)); + } else if (value instanceof long[]) { + value = Arrays.asList(ArrayUtils.toObject((long[]) value)); + } else if (value instanceof short[]) { + List newValue = new ArrayList<>(((short[]) value).length); + for (Short s : (short[]) value) { + newValue.add(s.intValue()); + } + value = newValue; + } else if (value instanceof byte[]) { + List newValue = new ArrayList<>(((byte[]) value).length); + for (Byte b : (byte[]) value) { + newValue.add(b.intValue()); + } + value = newValue; + } else if (value instanceof double[]) { + value = Arrays.asList(ArrayUtils.toObject((double[]) value)); + } else if (value instanceof float[]) { + List newValue = new ArrayList<>(((float[]) value).length); + for (float f : (float[]) value) { + newValue.add(new Float(f).doubleValue()); + } + value = newValue; + } else if (value instanceof boolean[]) { + value = Arrays.asList(ArrayUtils.toObject((boolean[]) value)); + } else if (value instanceof char[]) { + List newValue = new ArrayList<>(((char[]) value).length); + for (char c : (char[]) value) { + newValue.add(String.valueOf(c)); + } + value = newValue; + } else { + value = Arrays.asList((Object[]) value); + } + } + } + + return setInnerValueForKey(this.values, key, value); + } + + @Override + public Object unset(String key) { + String[] fullKeyPath = key.split("\\."); + + Map innerMap = getInnerMap(this.values, Arrays.copyOf(fullKeyPath, fullKeyPath.length - 1), false); + + if (innerMap != null) { + return innerMap.remove(fullKeyPath[fullKeyPath.length - 1]); + } + + return null; + } + + @Override + public void reset() { + this.values.clear(); + } + + @Override + public void load(Reader reader) throws IOException { + Object yamlData = this.yaml.load(reader); + if (yamlData == null) { + yamlData = Collections.emptyMap(); + } + + if (!(yamlData instanceof Map)) { + throw new IllegalStateException("The YAML file does not have the expected tree structure: " + yamlData.getClass().getName()); + } + + synchronized (this.values) { + this.values.clear(); + + for (Map.Entry yamlEntry : ((Map) yamlData).entrySet()) { + this.values.put(yamlEntry.getKey().toString(), yamlEntry.getValue()); + } + } + } + + @Override + public void save(Writer writer) throws IOException { + String headerCommentLines = generateHeaderCommentLines(); + writer.write(headerCommentLines); + + cleanValuesMap(this.values); + + if (this.values.size() > 0) { + if (headerCommentLines.length() > 0) { + writer.write(this.yamlDumperOptions.getLineBreak().getString()); + } + + this.yaml.dump(this.values, writer); + } + } + + @Override + public void setHeaderComment(@Nullable Supplier comment) { + this.headerComment = comment; + } + + @Override + public @Nullable Supplier getHeaderComment() { + return this.headerComment; + } + + @Override + public @NotNull String generateHeaderCommentLines() { + StringBuilder sb = new StringBuilder(); + + String headerCommentString = this.headerComment == null ? null : this.headerComment.get(); + if (headerCommentString != null) { + for (String commentLine : headerCommentString.split("\r?\n")) { + sb.append("# ") + .append(commentLine) + .append(this.yamlDumperOptions.getLineBreak().getString()); + } + } + + return sb.toString(); + } + + @Override + public void setNodeComment(@NotNull String key, @Nullable Supplier comment) { + this.nodeComments.put(key, comment); + } + + @Override + public @Nullable Supplier getNodeComment(@Nullable String key) { + return this.nodeComments.get(key); + } + + public String toYamlString() throws IOException { + StringWriter writer = new StringWriter(); + save(writer); + + return writer.toString(); + } + + @Override + public String toString() { + return "YamlConfiguration{" + + "values=" + this.values + + ", headerComment=" + this.headerComment + + '}'; + } + + protected static DumperOptions createDefaultYamlDumperOptions() { + DumperOptions dumperOptions = new DumperOptions(); + dumperOptions.setProcessComments(true); + + dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + dumperOptions.setIndentWithIndicator(true); + dumperOptions.setIndicatorIndent(2); + + return dumperOptions; + } + + protected static Yaml createDefaultYaml(DumperOptions dumperOptions, Representer representer) { + LoaderOptions yamlOptions = new LoaderOptions(); + yamlOptions.setAllowDuplicateKeys(false); + + return new Yaml(new Constructor(yamlOptions), representer, dumperOptions, yamlOptions); + } + + protected static Object setInnerValueForKey(@NotNull Map map, @NotNull String key, @Nullable Object value) { + String[] fullKeyPath = key.split("\\."); + + Map innerMap = getInnerMap(map, Arrays.copyOf(fullKeyPath, fullKeyPath.length - 1), true); + + return ((Map) innerMap).put(fullKeyPath[fullKeyPath.length - 1], value); + } + + protected static Object getInnerValueForKey(@NotNull Map map, @NotNull String key) { + String[] fullKeyPath = key.split("\\."); + + Map innerMap = getInnerMap(map, Arrays.copyOf(fullKeyPath, fullKeyPath.length - 1), false); + + if (innerMap != null) { + return innerMap.get(fullKeyPath[fullKeyPath.length - 1]); + } + + return null; + } + + @Contract("_,_,true -> !null") + protected static Map getInnerMap(@NotNull Map map, @NotNull String[] keys, boolean createMissingMaps) { + if (keys.length == 0) { + return map; + } + + int currentKeyIndex = 0; + Map currentMap = map; + + while (true) { + Object currentValue = currentMap.get(keys[currentKeyIndex]); + + if (currentValue == null) { + if (!createMissingMaps) { + return null; + } + + currentValue = new HashMap<>(); + ((Map) currentMap).put(keys[currentKeyIndex], currentValue); + } + + if (!(currentValue instanceof Map)) { + if (!createMissingMaps) { + throw new IllegalArgumentException("Expected a Map when resolving key '" + String.join(".", keys) + "' at '" + String.join(".", Arrays.copyOf(keys, currentKeyIndex + 1)) + "'"); + } + + currentValue = new HashMap<>(); + ((Map) currentMap).put(keys[currentKeyIndex], currentValue); + } + + if (currentKeyIndex == keys.length - 1) { + return (Map) currentValue; + } + + currentMap = (Map) currentValue; + ++currentKeyIndex; + } + } + + /** + * This takes a map and removes all keys that have a value of null.
+ * Additionally, if the value is a {@link Map}, it will be recursively cleaned too.
+ * {@link Map}s that are or get empty, will be removed (recursively).
+ */ + protected void cleanValuesMap(Map map) { + for (Object key : map.keySet().toArray()) { + Object value = map.get(key); + + if (value instanceof Map) { + cleanValuesMap((Map) value); + } + + if (value == null || (value instanceof Map && ((Map) value).isEmpty())) { + map.remove(key); + } + } + } +} diff --git a/Core/src/main/java/com/songoda/core/core/LocaleModule.java b/Core/src/main/java/com/songoda/core/core/LocaleModule.java index fce8f336..3b121673 100644 --- a/Core/src/main/java/com/songoda/core/core/LocaleModule.java +++ b/Core/src/main/java/com/songoda/core/core/LocaleModule.java @@ -1,64 +1,64 @@ -package com.songoda.core.core; - -import com.songoda.core.locale.Locale; -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class LocaleModule implements PluginInfoModule { - @Override - public void run(PluginInfo plugin) { - if (plugin.getJavaPlugin() == null || plugin.getSongodaId() <= 0) { - return; - } - - try { - JSONObject json = plugin.getJson(); - JSONArray files = (JSONArray) json.get("neededFiles"); - - for (Object o : files) { - JSONObject file = (JSONObject) o; - - if (file.get("type").equals("locale")) { - downloadLocale(plugin, (String) file.get("link"), (String) file.get("name")); - } - } - } catch (IOException ex) { - Logger.getLogger(LocaleModule.class.getName()).log(Level.INFO, "Failed to check for locale files: " + ex.getMessage()); - } - } - - void downloadLocale(PluginInfo plugin, String link, String fileName) throws IOException { - URL url = new URL(link); - - HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); - urlConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.95 Safari/537.11"); - urlConnection.setRequestProperty("Accept", "*/*"); - urlConnection.setInstanceFollowRedirects(true); - urlConnection.setConnectTimeout(5000); - - // do we need to follow a redirect? - int status = urlConnection.getResponseCode(); - if (status == HttpURLConnection.HTTP_MOVED_TEMP || status == HttpURLConnection.HTTP_MOVED_PERM || status == HttpURLConnection.HTTP_SEE_OTHER) { - // get redirect url from "location" header field - String newUrl = urlConnection.getHeaderField("Location"); - // get the cookie if needed - String cookies = urlConnection.getHeaderField("Set-Cookie"); - // open the new connnection again - urlConnection = (HttpURLConnection) new URL(newUrl).openConnection(); - urlConnection.setRequestProperty("Cookie", cookies); - urlConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.95 Safari/537.11"); - urlConnection.setRequestProperty("Accept", "*/*"); - urlConnection.setConnectTimeout(5000); - } - - Locale.saveLocale(plugin.getJavaPlugin(), urlConnection.getInputStream(), fileName); - - urlConnection.disconnect(); - } -} +//package com.songoda.core.core; +// +//import com.songoda.core.locale.Locale; +//import org.json.simple.JSONArray; +//import org.json.simple.JSONObject; +// +//import java.io.IOException; +//import java.net.HttpURLConnection; +//import java.net.URL; +//import java.util.logging.Level; +//import java.util.logging.Logger; +// +//public class LocaleModule implements PluginInfoModule { +// @Override +// public void run(PluginInfo plugin) { +// if (plugin.getJavaPlugin() == null || plugin.getSongodaId() <= 0) { +// return; +// } +// +// try { +// JSONObject json = plugin.getJson(); +// JSONArray files = (JSONArray) json.get("neededFiles"); +// +// for (Object o : files) { +// JSONObject file = (JSONObject) o; +// +// if (file.get("type").equals("locale")) { +// downloadLocale(plugin, (String) file.get("link"), (String) file.get("name")); +// } +// } +// } catch (IOException ex) { +// Logger.getLogger(LocaleModule.class.getName()).log(Level.INFO, "Failed to check for locale files: " + ex.getMessage()); +// } +// } +// +// void downloadLocale(PluginInfo plugin, String link, String fileName) throws IOException { +// URL url = new URL(link); +// +// HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); +// urlConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.95 Safari/537.11"); +// urlConnection.setRequestProperty("Accept", "*/*"); +// urlConnection.setInstanceFollowRedirects(true); +// urlConnection.setConnectTimeout(5000); +// +// // do we need to follow a redirect? +// int status = urlConnection.getResponseCode(); +// if (status == HttpURLConnection.HTTP_MOVED_TEMP || status == HttpURLConnection.HTTP_MOVED_PERM || status == HttpURLConnection.HTTP_SEE_OTHER) { +// // get redirect url from "location" header field +// String newUrl = urlConnection.getHeaderField("Location"); +// // get the cookie if needed +// String cookies = urlConnection.getHeaderField("Set-Cookie"); +// // open the new connnection again +// urlConnection = (HttpURLConnection) new URL(newUrl).openConnection(); +// urlConnection.setRequestProperty("Cookie", cookies); +// urlConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.95 Safari/537.11"); +// urlConnection.setRequestProperty("Accept", "*/*"); +// urlConnection.setConnectTimeout(5000); +// } +// +// Locale.saveLocale(plugin.getJavaPlugin(), urlConnection.getInputStream(), fileName); +// +// urlConnection.disconnect(); +// } +//} diff --git a/Core/src/main/java/com/songoda/core/gui/CustomizableGui.java b/Core/src/main/java/com/songoda/core/gui/CustomizableGui.java index f41c4d5b..c1d5e63a 100644 --- a/Core/src/main/java/com/songoda/core/gui/CustomizableGui.java +++ b/Core/src/main/java/com/songoda/core/gui/CustomizableGui.java @@ -2,8 +2,8 @@ package com.songoda.core.gui; import com.songoda.core.compatibility.CompatibleMaterial; import com.songoda.core.compatibility.ServerVersion; -import com.songoda.core.configuration.Config; -import com.songoda.core.configuration.ConfigSection; +import com.songoda.core.configuration.ConfigEntry; +import com.songoda.core.configuration.songoda.SongodaYamlConfig; import com.songoda.core.gui.methods.Clickable; import com.songoda.core.utils.TextUtils; import org.bukkit.Bukkit; @@ -16,6 +16,7 @@ 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.Arrays; import java.util.Collections; @@ -45,70 +46,89 @@ public class CustomizableGui extends Gui { localeFolder.mkdir(); } - Config config = new Config(plugin, "gui/" + guiKey + ".yml"); - config.load(); - - if (!config.isConfigurationSection("overrides")) { - config.setDefault("overrides.example.item", CompatibleMaterial.STONE.name(), - "This is the icon material you would like to replace", - "the current material with.") - .setDefault("overrides.example.position", 5, - "This is the current position of the icon you would like to move.", - "The number represents the cell the icon currently resides in.") - .setDefaultComment("overrides.example", - "This is just an example and does not override to any items", - "in this GUI.") - .setDefaultComment("overrides", - "For information on how to apply overrides please visit", - "https://wiki.craftaro.com/index.php/Gui"); - - config.saveChanges(); + SongodaYamlConfig config = new SongodaYamlConfig(new File(plugin.getDataFolder(), "gui/" + guiKey + ".yml")); + try { + config.load(); + } catch (IOException ex) { + // FIXME + throw new RuntimeException(ex); } - if (!config.isConfigurationSection("disabled")) { - config.setDefault("disabled", Arrays.asList("example3", "example4", "example5"), - "All keys on this list will be disabled. You can add any items key here", - "if you no longer want that item in the GUI."); + config.setNodeComment("overrides", "For information on how to apply overrides please visit\n" + + "https://wiki.craftaro.com/index.php/Gui"); + config.setNodeComment("overrides.example", "This is just an example and does not override to any items in this GUI."); - config.saveChanges(); + config.createEntry("overrides.example.item", CompatibleMaterial.STONE) + .withComment("This is the icon material you would like to replace\n" + + "the current material with."); + config.createEntry("overrides.example.position", 5) + .withComment("This is the current position of the icon you would like to move.\n" + + "The number represents the cell the icon currently resides in."); + + ConfigEntry disabledGuis = config.createEntry("disabled", Arrays.asList("example3", "example4", "example5")) + .withComment("All keys on this list will be disabled. You can add any items key here\n" + + "if you no longer want that item in the GUI."); + + try { + config.save(); + } catch (IOException ex) { + // FIXME + throw new RuntimeException(ex); } CustomContent customContent = loadedGuis.computeIfAbsent(guiKey, g -> new CustomContent(guiKey)); loadedGuis.put(guiKey, customContent); this.customContent = customContent; - int rows = config.getInt("overrides.__ROWS__", -1); + int rows = config.getReadEntry("overrides.__ROWS__").getIntOr(-1); if (rows != -1) { customContent.setRows(rows); } - for (ConfigSection section : config.getSections("overrides")) { - if (section.contains("row") || - section.contains("col") || - section.contains("mirrorrow") || - section.contains("mirrorcol")) { - if (section.contains("mirrorrow") || section.contains("mirrorcol")) { - customContent.addButton(section.getNodeKey(), section.getInt("row", -1), - section.getInt("col", -1), - section.getBoolean("mirrorrow", false), - section.getBoolean("mirrorcol", false), - section.isSet("item") ? CompatibleMaterial.getMaterial(section.getString("item")) : null); - } else { - customContent.addButton(section.getNodeKey(), section.getInt("row", -1), - section.getInt("col", -1), - section.getString("title", null), - section.isSet("lore") ? section.getStringList("lore") : null, - section.isSet("item") ? CompatibleMaterial.getMaterial(section.getString("item")) : null); - } + for (String overrideKey : config.getKeys("overrides")) { + String keyPrefix = "overrides." + overrideKey; + + ConfigEntry title = config.getReadEntry(keyPrefix + ".title"); + + ConfigEntry position = config.getReadEntry(keyPrefix + ".position"); + + ConfigEntry row = config.getReadEntry(keyPrefix + ".row"); + ConfigEntry col = config.getReadEntry(keyPrefix + ".col"); + + ConfigEntry mirrorRow = config.getReadEntry(keyPrefix + ".mirrorrow"); + ConfigEntry mirrorCol = config.getReadEntry(keyPrefix + ".mirrorcol"); + + ConfigEntry item = config.getReadEntry(keyPrefix + ".item"); + ConfigEntry lore = config.getReadEntry(keyPrefix + ".lore"); + + boolean configHasRowOrColSet = row.has() || col.has(); + boolean configHasMirrorRowOrColSet = mirrorRow.has() || mirrorCol.has(); + + if (configHasMirrorRowOrColSet) { + customContent.addButton(overrideKey, + row.getIntOr(-1), + col.getIntOr(-1), + mirrorRow.getBoolean(), + mirrorCol.getBoolean(), + item.getMaterial()); + } else if (configHasRowOrColSet) { + customContent.addButton(overrideKey, + row.getIntOr(-1), + col.getIntOr(-1), + title.getString(), + lore.getStringList(), + item.getMaterial()); } else { - customContent.addButton(section.getNodeKey(), section.getString("position", "-1"), - section.getString("title", null), - section.isSet("lore") ? section.getStringList("lore") : null, - section.isSet("item") ? CompatibleMaterial.getMaterial(section.getString("item")) : null); + customContent.addButton(overrideKey, + position.getStringOr("-1"), + title.getString(), + lore.getStringList(), + item.getMaterial()); + } } - for (String disabled : config.getStringList("disabled")) { + for (String disabled : disabledGuis.getStringListOr(Collections.emptyList())) { customContent.disableButton(disabled); } } else { diff --git a/Core/src/main/java/com/songoda/core/locale/Locale.java b/Core/src/main/java/com/songoda/core/locale/Locale.java index 2a07dcd3..d818cb13 100644 --- a/Core/src/main/java/com/songoda/core/locale/Locale.java +++ b/Core/src/main/java/com/songoda/core/locale/Locale.java @@ -1,495 +1,496 @@ -package com.songoda.core.locale; - -import com.songoda.core.configuration.Config; -import com.songoda.core.configuration.ConfigSection; -import com.songoda.core.utils.TextUtils; -import org.bukkit.configuration.InvalidConfigurationException; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.java.JavaPlugin; - -import java.io.BufferedInputStream; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** - * Assists in the utilization of localization files. - */ -public class Locale { - private static final Pattern OLD_NODE_PATTERN = Pattern.compile("^([^ ]+)\\s*=\\s*\"?(.*?)\"?$"); - private static final String FILE_EXTENSION = ".lang"; - - private final Map nodes = new HashMap<>(); - private final Plugin plugin; - private final File file; - private final String name; - - /** - * Instantiate the Locale class for future use - * - * @param plugin Owning Plugin - * @param file Location of the locale file - * @param name The locale name for the language - */ - public Locale(Plugin plugin, File file, String name) { - this.plugin = plugin; - this.file = file; - this.name = name; - } - - /** - * Load a default-included lang file from the plugin's jar file - * - * @param plugin plugin to load from - * @param name name of the default locale, eg "en_US" - * - * @return returns the loaded Locale, or null if there was an error - */ - public static Locale loadDefaultLocale(JavaPlugin plugin, String name) { - saveDefaultLocale(plugin, name, name); - - return loadLocale(plugin, name); - } - - /** - * Load a locale from this plugin's locale directory - * - * @param plugin plugin to load from - * @param name name of the locale, eg "en_US" - * - * @return returns the loaded Locale, or null if there was an error - */ - public static Locale loadLocale(JavaPlugin plugin, String name) { - File localeFolder = new File(plugin.getDataFolder(), "locales/"); - if (!localeFolder.exists()) { - return null; - } - - File localeFile = new File(localeFolder, name + FILE_EXTENSION); - if (!localeFolder.exists()) { - return null; - } - - // found the lang file, now load it in! - Locale l = new Locale(plugin, localeFile, name); - - if (!l.reloadMessages()) { - return null; - } - - plugin.getLogger().info("Loaded locale \"" + name + "\""); - - return l; - } - - /** - * Load all locales from this plugin's locale directory - * - * @param plugin plugin to load from - * - * @return returns the loaded Locales - */ - public static List loadAllLocales(JavaPlugin plugin) { - File localeFolder = new File(plugin.getDataFolder(), "locales/"); - List all = new ArrayList<>(); - - for (File localeFile : localeFolder.listFiles()) { - String fileName = localeFile.getName(); - if (!fileName.endsWith(FILE_EXTENSION)) { - continue; - } - - fileName = fileName.substring(0, fileName.lastIndexOf('.')); - if (fileName.split("_").length != 2) { - continue; - } - - Locale l = new Locale(plugin, localeFile, fileName); - - if (l.reloadMessages()) { - plugin.getLogger().info("Loaded locale \"" + fileName + "\""); - all.add(l); - } - } - - return all; - } - - /** - * Get a list of all locale files in this plugin's locale directory - * - * @param plugin Plugin to check for - */ - public static List getLocales(Plugin plugin) { - File localeFolder = new File(plugin.getDataFolder(), "locales/"); - List all = new ArrayList<>(); - - for (File localeFile : localeFolder.listFiles()) { - String fileName = localeFile.getName(); - if (!fileName.endsWith(FILE_EXTENSION)) { - continue; - } - - fileName = fileName.substring(0, fileName.lastIndexOf('.')); - - if (fileName.split("_").length != 2) { - continue; - } - - all.add(fileName); - } - - return all; - } - - /** - * Save a locale file from the Plugin's Resources to the locale folder - * - * @param plugin plugin owning the locale file - * @param locale the specific locale file to save - * @param fileName where to save the file - * - * @return true if the operation was successful, false otherwise - */ - public static boolean saveDefaultLocale(JavaPlugin plugin, String locale, String fileName) { - return saveLocale(plugin, plugin.getResource(locale + FILE_EXTENSION), fileName, true); - } - - /** - * Save a locale file from an InputStream to the locale folder - * - * @param plugin plugin owning the locale file - * @param in file to save - * @param fileName the name of the file to save - * - * @return true if the operation was successful, false otherwise - */ - public static boolean saveLocale(Plugin plugin, InputStream in, String fileName) { - return saveLocale(plugin, in, fileName, false); - } - - private static boolean saveLocale(Plugin plugin, InputStream in, String fileName, boolean builtin) { - if (in == null) { - return false; - } - - File localeFolder = new File(plugin.getDataFolder(), "locales/"); - if (!localeFolder.exists()) localeFolder.mkdirs(); - - if (!fileName.endsWith(FILE_EXTENSION)) { - fileName = fileName + FILE_EXTENSION; - } - - File destinationFile = new File(localeFolder, fileName); - if (destinationFile.exists()) { - return updateFiles(plugin, in, destinationFile, builtin); - } - - try (OutputStream outputStream = new FileOutputStream(destinationFile)) { - copy(in, outputStream); - - fileName = fileName.substring(0, fileName.lastIndexOf('.')); - - return fileName.split("_").length == 2; - } catch (IOException ignore) { - } - - return false; - } - - // Write new changes to existing files, if any at all - private static boolean updateFiles(Plugin plugin, InputStream defaultFile, File existingFile, boolean builtin) { - try (BufferedInputStream defaultIn = new BufferedInputStream(defaultFile); - BufferedInputStream existingIn = new BufferedInputStream(new FileInputStream(existingFile))) { - - Charset defaultCharset = TextUtils.detectCharset(defaultIn, StandardCharsets.UTF_8); - Charset existingCharset = TextUtils.detectCharset(existingIn, StandardCharsets.UTF_8); - - try (BufferedReader defaultReaderOriginal = new BufferedReader(new InputStreamReader(defaultIn, defaultCharset)); - BufferedReader existingReaderOriginal = new BufferedReader(new InputStreamReader(existingIn, existingCharset)); - BufferedReader defaultReader = translatePropertyToYAML(defaultReaderOriginal, defaultCharset); - BufferedReader existingReader = translatePropertyToYAML(existingReaderOriginal, existingCharset)) { - - Config existingLang = new Config(existingFile); - existingLang.load(existingReader); - translateMsgRoot(existingLang, existingFile, existingCharset); - - Config defaultLang = new Config(); - String defaultData = defaultReader.lines().map(s -> s.replaceAll("[\uFEFF\uFFFE\u200B]", "")).collect(Collectors.joining("\n")); - defaultLang.loadFromString(defaultData); - translateMsgRoot(defaultLang, defaultData, defaultCharset); - - List added = new ArrayList<>(); - - for (String defaultValueKey : defaultLang.getKeys(true)) { - Object val = defaultLang.get(defaultValueKey); - if (val instanceof ConfigSection) { - continue; - } - - if (!existingLang.contains(defaultValueKey)) { - added.add(defaultValueKey); - existingLang.set(defaultValueKey, val); - } - } - - if (!added.isEmpty()) { - if (!builtin) { - existingLang.setHeader("New messages added for " + plugin.getName() + " v" + plugin.getDescription().getVersion() + ".", - "", - "These translations were found untranslated, join", - "our translation Discord https://discord.gg/f7fpZEf", - "to request an official update!", - "", - String.join("\n", added) - ); - } else { - existingLang.setHeader("New messages added for " + plugin.getName() + " v" + plugin.getDescription().getVersion() + ".", - "", - String.join("\n", added) - ); - } - - existingLang.setRootNodeSpacing(0); - existingLang.save(); - } - - existingLang.setRootNodeSpacing(0); - existingLang.save(); - - return !added.isEmpty(); - } catch (InvalidConfigurationException ex) { - plugin.getLogger().log(Level.SEVERE, "Error checking config " + existingFile.getName(), ex); - } - } catch (IOException ignore) { - } - - return false; - } - - /** - * Clear the previous message cache and load new messages directly from file - * - * @return reload messages from file - */ - public boolean reloadMessages() { - if (!this.file.exists()) { - plugin.getLogger().warning("Could not find file for locale \"" + this.name + "\""); - return false; - } - - this.nodes.clear(); // Clear previous data (if any) - - // guess what encoding this file is in - Charset charset = TextUtils.detectCharset(file, null); - if (charset == null) { - plugin.getLogger().warning("Could not determine charset for locale \"" + this.name + "\""); - charset = StandardCharsets.UTF_8; - } - - // load in the file! - try (FileInputStream stream = new FileInputStream(file); - BufferedReader source = new BufferedReader(new InputStreamReader(stream, charset)); - BufferedReader reader = translatePropertyToYAML(source, charset)) { - Config lang = new Config(file); - lang.load(reader); - translateMsgRoot(lang, file, charset); - - // load lists as strings with newlines - lang.getValues(true).forEach((k, v) -> nodes.put(k, - v instanceof List - ? (((List) v).stream().map(Object::toString).collect(Collectors.joining("\n"))) - : v.toString())); - - return true; - } catch (IOException ex) { - ex.printStackTrace(); - } catch (InvalidConfigurationException ex) { - Logger.getLogger(Locale.class.getName()).log(Level.SEVERE, "Configuration error in language file \"" + file.getName() + "\"", ex); - } - - return false; - } - - protected static BufferedReader translatePropertyToYAML(BufferedReader source, Charset charset) throws IOException { - StringBuilder output = new StringBuilder(); - - String line, line1; - for (int lineNumber = 0; (line = source.readLine()) != null; ++lineNumber) { - if (lineNumber == 0) { - // remove BOM markers, if any - line1 = line; - line = line.replaceAll("[\uFEFF\uFFFE\u200B]", ""); - if (line1.length() != line.length()) { - output.append(line1, 0, line1.length() - line.length()); - } - } - - Matcher matcher; - if ((line = line.replace('\r', ' ') - .replaceAll("\\p{C}", "?") - .replace(";", "")).trim().isEmpty() - || line.trim().startsWith("#") /* Comment */ - // need to trim the search group because tab characters somehow ended up at the end of lines in a lot of these files - || !(matcher = OLD_NODE_PATTERN.matcher(line.trim())).find()) { - if (line.startsWith("//")) { - // someone used an improper comment in some files *grumble grumble* - output.append("#").append(line).append("\n"); - } else { - output.append(line).append("\n"); - } - } else { - output.append(matcher.group(1)).append(": \"").append(matcher.group(2)).append("\"\n"); - } - } - - // I hate Java sometimes because of crap like this: - return new BufferedReader(new InputStreamReader(new BufferedInputStream(new ByteArrayInputStream(output.toString().getBytes(charset))), charset)); - } - - protected static void translateMsgRoot(Config lang, File file, Charset charset) throws IOException { - List msgs = lang.getValues(true).entrySet().stream() - .filter(e -> e.getValue() instanceof ConfigSection) - .map(Map.Entry::getKey) - .collect(Collectors.toList()); - - if (!msgs.isEmpty()) { - try (FileInputStream stream = new FileInputStream(file); - BufferedReader source = new BufferedReader(new InputStreamReader(stream, charset))) { - String line; - for (int lineNumber = 0; (line = source.readLine()) != null; ++lineNumber) { - if (lineNumber == 0) { - // remove BOM markers, if any - line = line.replaceAll("[\uFEFF\uFFFE\u200B]", ""); - } - - Matcher matcher; - if (!(line = line.trim()).isEmpty() && !line.startsWith("#") - && (matcher = OLD_NODE_PATTERN.matcher(line)).find() - && msgs.contains(matcher.group(1))) { - lang.set(matcher.group(1) + ".message", matcher.group(2)); - } - } - } - } - } - - protected static void translateMsgRoot(Config lang, String file, Charset charset) throws IOException { - List msgs = lang.getValues(true).entrySet().stream() - .filter(e -> e.getValue() instanceof ConfigSection) - .map(Map.Entry::getKey) - .collect(Collectors.toList()); - - if (!msgs.isEmpty()) { - String[] source = file.split("\n"); - - String line; - for (int lineNumber = 0; lineNumber < source.length; ++lineNumber) { - line = source[lineNumber]; - if (lineNumber == 0) { - // remove BOM markers, if any - line = line.replaceAll("[\uFEFF\uFFFE\u200B]", ""); - } - - Matcher matcher; - if (!(line = line.trim()).isEmpty() && !line.startsWith("#") - && (matcher = OLD_NODE_PATTERN.matcher(line)).find() - && msgs.contains(matcher.group(1))) { - lang.set(matcher.group(1) + ".message", matcher.group(2)); - } - } - } - } - - /** - * Supply the Message object with the plugins prefix. - * - * @param message message to be applied - * - * @return applied message - */ - private Message supplyPrefix(Message message) { - return message.setPrefix(this.nodes.getOrDefault("general.nametag.prefix", "[" + plugin.getName() + "]")); - } - - /** - * Create a new unsaved Message - * - * @param message the message to create - * - * @return the created message - */ - public Message newMessage(String message) { - return supplyPrefix(new Message(message)); - } - - /** - * Get a message set for a specific node. - * - * @param node the node to get - * - * @return the message for the specified node - */ - public Message getMessage(String node) { - if (this.nodes.containsKey(node + ".message")) { - node += ".message"; - } - - return this.getMessageOrDefault(node, node); - } - - /** - * Get a message set for a specific node - * - * @param node the node to get - * @param defaultValue the default value given that a value for the node was not found - * - * @return the message for the specified node. Default if none found - */ - public Message getMessageOrDefault(String node, String defaultValue) { - if (this.nodes.containsKey(node + ".message")) { - node += ".message"; - } - - return supplyPrefix(new Message(this.nodes.getOrDefault(node, defaultValue))); - } - - /** - * Return the locale name (i.e. "en_US") - * - * @return the locale name - */ - public String getName() { - return name; - } - - private static void copy(InputStream input, OutputStream output) { - int n; - byte[] buffer = new byte[1024 * 4]; - - try { - while ((n = input.read(buffer)) != -1) { - output.write(buffer, 0, n); - } - } catch (IOException ex) { - ex.printStackTrace(); - } - } -} +// +//package com.songoda.core.locale; +// +//import com.songoda.core.configuration.Config; +//import com.songoda.core.configuration.ConfigSection; +//import com.songoda.core.utils.TextUtils; +//import org.bukkit.configuration.InvalidConfigurationException; +//import org.bukkit.plugin.Plugin; +//import org.bukkit.plugin.java.JavaPlugin; +// +//import java.io.BufferedInputStream; +//import java.io.BufferedReader; +//import java.io.ByteArrayInputStream; +//import java.io.File; +//import java.io.FileInputStream; +//import java.io.FileOutputStream; +//import java.io.IOException; +//import java.io.InputStream; +//import java.io.InputStreamReader; +//import java.io.OutputStream; +//import java.nio.charset.Charset; +//import java.nio.charset.StandardCharsets; +//import java.util.ArrayList; +//import java.util.HashMap; +//import java.util.List; +//import java.util.Map; +//import java.util.logging.Level; +//import java.util.logging.Logger; +//import java.util.regex.Matcher; +//import java.util.regex.Pattern; +//import java.util.stream.Collectors; +// +///** +// * Assists in the utilization of localization files. +// */ +//public class Locale { +// private static final Pattern OLD_NODE_PATTERN = Pattern.compile("^([^ ]+)\\s*=\\s*\"?(.*?)\"?$"); +// private static final String FILE_EXTENSION = ".lang"; +// +// private final Map nodes = new HashMap<>(); +// private final Plugin plugin; +// private final File file; +// private final String name; +// +// /** +// * Instantiate the Locale class for future use +// * +// * @param plugin Owning Plugin +// * @param file Location of the locale file +// * @param name The locale name for the language +// */ +// public Locale(Plugin plugin, File file, String name) { +// this.plugin = plugin; +// this.file = file; +// this.name = name; +// } +// +// /** +// * Load a default-included lang file from the plugin's jar file +// * +// * @param plugin plugin to load from +// * @param name name of the default locale, eg "en_US" +// * +// * @return returns the loaded Locale, or null if there was an error +// */ +// public static Locale loadDefaultLocale(JavaPlugin plugin, String name) { +// saveDefaultLocale(plugin, name, name); +// +// return loadLocale(plugin, name); +// } +// +// /** +// * Load a locale from this plugin's locale directory +// * +// * @param plugin plugin to load from +// * @param name name of the locale, eg "en_US" +// * +// * @return returns the loaded Locale, or null if there was an error +// */ +// public static Locale loadLocale(JavaPlugin plugin, String name) { +// File localeFolder = new File(plugin.getDataFolder(), "locales/"); +// if (!localeFolder.exists()) { +// return null; +// } +// +// File localeFile = new File(localeFolder, name + FILE_EXTENSION); +// if (!localeFolder.exists()) { +// return null; +// } +// +// // found the lang file, now load it in! +// Locale l = new Locale(plugin, localeFile, name); +// +// if (!l.reloadMessages()) { +// return null; +// } +// +// plugin.getLogger().info("Loaded locale \"" + name + "\""); +// +// return l; +// } +// +// /** +// * Load all locales from this plugin's locale directory +// * +// * @param plugin plugin to load from +// * +// * @return returns the loaded Locales +// */ +// public static List loadAllLocales(JavaPlugin plugin) { +// File localeFolder = new File(plugin.getDataFolder(), "locales/"); +// List all = new ArrayList<>(); +// +// for (File localeFile : localeFolder.listFiles()) { +// String fileName = localeFile.getName(); +// if (!fileName.endsWith(FILE_EXTENSION)) { +// continue; +// } +// +// fileName = fileName.substring(0, fileName.lastIndexOf('.')); +// if (fileName.split("_").length != 2) { +// continue; +// } +// +// Locale l = new Locale(plugin, localeFile, fileName); +// +// if (l.reloadMessages()) { +// plugin.getLogger().info("Loaded locale \"" + fileName + "\""); +// all.add(l); +// } +// } +// +// return all; +// } +// +// /** +// * Get a list of all locale files in this plugin's locale directory +// * +// * @param plugin Plugin to check for +// */ +// public static List getLocales(Plugin plugin) { +// File localeFolder = new File(plugin.getDataFolder(), "locales/"); +// List all = new ArrayList<>(); +// +// for (File localeFile : localeFolder.listFiles()) { +// String fileName = localeFile.getName(); +// if (!fileName.endsWith(FILE_EXTENSION)) { +// continue; +// } +// +// fileName = fileName.substring(0, fileName.lastIndexOf('.')); +// +// if (fileName.split("_").length != 2) { +// continue; +// } +// +// all.add(fileName); +// } +// +// return all; +// } +// +// /** +// * Save a locale file from the Plugin's Resources to the locale folder +// * +// * @param plugin plugin owning the locale file +// * @param locale the specific locale file to save +// * @param fileName where to save the file +// * +// * @return true if the operation was successful, false otherwise +// */ +// public static boolean saveDefaultLocale(JavaPlugin plugin, String locale, String fileName) { +// return saveLocale(plugin, plugin.getResource(locale + FILE_EXTENSION), fileName, true); +// } +// +// /** +// * Save a locale file from an InputStream to the locale folder +// * +// * @param plugin plugin owning the locale file +// * @param in file to save +// * @param fileName the name of the file to save +// * +// * @return true if the operation was successful, false otherwise +// */ +// public static boolean saveLocale(Plugin plugin, InputStream in, String fileName) { +// return saveLocale(plugin, in, fileName, false); +// } +// +// private static boolean saveLocale(Plugin plugin, InputStream in, String fileName, boolean builtin) { +// if (in == null) { +// return false; +// } +// +// File localeFolder = new File(plugin.getDataFolder(), "locales/"); +// if (!localeFolder.exists()) localeFolder.mkdirs(); +// +// if (!fileName.endsWith(FILE_EXTENSION)) { +// fileName = fileName + FILE_EXTENSION; +// } +// +// File destinationFile = new File(localeFolder, fileName); +// if (destinationFile.exists()) { +// return updateFiles(plugin, in, destinationFile, builtin); +// } +// +// try (OutputStream outputStream = new FileOutputStream(destinationFile)) { +// copy(in, outputStream); +// +// fileName = fileName.substring(0, fileName.lastIndexOf('.')); +// +// return fileName.split("_").length == 2; +// } catch (IOException ignore) { +// } +// +// return false; +// } +// +// // Write new changes to existing files, if any at all +// private static boolean updateFiles(Plugin plugin, InputStream defaultFile, File existingFile, boolean builtin) { +// try (BufferedInputStream defaultIn = new BufferedInputStream(defaultFile); +// BufferedInputStream existingIn = new BufferedInputStream(new FileInputStream(existingFile))) { +// +// Charset defaultCharset = TextUtils.detectCharset(defaultIn, StandardCharsets.UTF_8); +// Charset existingCharset = TextUtils.detectCharset(existingIn, StandardCharsets.UTF_8); +// +// try (BufferedReader defaultReaderOriginal = new BufferedReader(new InputStreamReader(defaultIn, defaultCharset)); +// BufferedReader existingReaderOriginal = new BufferedReader(new InputStreamReader(existingIn, existingCharset)); +// BufferedReader defaultReader = translatePropertyToYAML(defaultReaderOriginal, defaultCharset); +// BufferedReader existingReader = translatePropertyToYAML(existingReaderOriginal, existingCharset)) { +// +// Config existingLang = new Config(existingFile); +// existingLang.load(existingReader); +// translateMsgRoot(existingLang, existingFile, existingCharset); +// +// Config defaultLang = new Config(); +// String defaultData = defaultReader.lines().map(s -> s.replaceAll("[\uFEFF\uFFFE\u200B]", "")).collect(Collectors.joining("\n")); +// defaultLang.loadFromString(defaultData); +// translateMsgRoot(defaultLang, defaultData, defaultCharset); +// +// List added = new ArrayList<>(); +// +// for (String defaultValueKey : defaultLang.getKeys(true)) { +// Object val = defaultLang.get(defaultValueKey); +// if (val instanceof ConfigSection) { +// continue; +// } +// +// if (!existingLang.contains(defaultValueKey)) { +// added.add(defaultValueKey); +// existingLang.set(defaultValueKey, val); +// } +// } +// +// if (!added.isEmpty()) { +// if (!builtin) { +// existingLang.setHeader("New messages added for " + plugin.getName() + " v" + plugin.getDescription().getVersion() + ".", +// "", +// "These translations were found untranslated, join", +// "our translation Discord https://discord.gg/f7fpZEf", +// "to request an official update!", +// "", +// String.join("\n", added) +// ); +// } else { +// existingLang.setHeader("New messages added for " + plugin.getName() + " v" + plugin.getDescription().getVersion() + ".", +// "", +// String.join("\n", added) +// ); +// } +// +// existingLang.setRootNodeSpacing(0); +// existingLang.save(); +// } +// +// existingLang.setRootNodeSpacing(0); +// existingLang.save(); +// +// return !added.isEmpty(); +// } catch (InvalidConfigurationException ex) { +// plugin.getLogger().log(Level.SEVERE, "Error checking config " + existingFile.getName(), ex); +// } +// } catch (IOException ignore) { +// } +// +// return false; +// } +// +// /** +// * Clear the previous message cache and load new messages directly from file +// * +// * @return reload messages from file +// */ +// public boolean reloadMessages() { +// if (!this.file.exists()) { +// plugin.getLogger().warning("Could not find file for locale \"" + this.name + "\""); +// return false; +// } +// +// this.nodes.clear(); // Clear previous data (if any) +// +// // guess what encoding this file is in +// Charset charset = TextUtils.detectCharset(file, null); +// if (charset == null) { +// plugin.getLogger().warning("Could not determine charset for locale \"" + this.name + "\""); +// charset = StandardCharsets.UTF_8; +// } +// +// // load in the file! +// try (FileInputStream stream = new FileInputStream(file); +// BufferedReader source = new BufferedReader(new InputStreamReader(stream, charset)); +// BufferedReader reader = translatePropertyToYAML(source, charset)) { +// Config lang = new Config(file); +// lang.load(reader); +// translateMsgRoot(lang, file, charset); +// +// // load lists as strings with newlines +// lang.getValues(true).forEach((k, v) -> nodes.put(k, +// v instanceof List +// ? (((List) v).stream().map(Object::toString).collect(Collectors.joining("\n"))) +// : v.toString())); +// +// return true; +// } catch (IOException ex) { +// ex.printStackTrace(); +// } catch (InvalidConfigurationException ex) { +// Logger.getLogger(Locale.class.getName()).log(Level.SEVERE, "Configuration error in language file \"" + file.getName() + "\"", ex); +// } +// +// return false; +// } +// +// protected static BufferedReader translatePropertyToYAML(BufferedReader source, Charset charset) throws IOException { +// StringBuilder output = new StringBuilder(); +// +// String line, line1; +// for (int lineNumber = 0; (line = source.readLine()) != null; ++lineNumber) { +// if (lineNumber == 0) { +// // remove BOM markers, if any +// line1 = line; +// line = line.replaceAll("[\uFEFF\uFFFE\u200B]", ""); +// if (line1.length() != line.length()) { +// output.append(line1, 0, line1.length() - line.length()); +// } +// } +// +// Matcher matcher; +// if ((line = line.replace('\r', ' ') +// .replaceAll("\\p{C}", "?") +// .replace(";", "")).trim().isEmpty() +// || line.trim().startsWith("#") /* Comment */ +// // need to trim the search group because tab characters somehow ended up at the end of lines in a lot of these files +// || !(matcher = OLD_NODE_PATTERN.matcher(line.trim())).find()) { +// if (line.startsWith("//")) { +// // someone used an improper comment in some files *grumble grumble* +// output.append("#").append(line).append("\n"); +// } else { +// output.append(line).append("\n"); +// } +// } else { +// output.append(matcher.group(1)).append(": \"").append(matcher.group(2)).append("\"\n"); +// } +// } +// +// // I hate Java sometimes because of crap like this: +// return new BufferedReader(new InputStreamReader(new BufferedInputStream(new ByteArrayInputStream(output.toString().getBytes(charset))), charset)); +// } +// +// protected static void translateMsgRoot(Config lang, File file, Charset charset) throws IOException { +// List msgs = lang.getValues(true).entrySet().stream() +// .filter(e -> e.getValue() instanceof ConfigSection) +// .map(Map.Entry::getKey) +// .collect(Collectors.toList()); +// +// if (!msgs.isEmpty()) { +// try (FileInputStream stream = new FileInputStream(file); +// BufferedReader source = new BufferedReader(new InputStreamReader(stream, charset))) { +// String line; +// for (int lineNumber = 0; (line = source.readLine()) != null; ++lineNumber) { +// if (lineNumber == 0) { +// // remove BOM markers, if any +// line = line.replaceAll("[\uFEFF\uFFFE\u200B]", ""); +// } +// +// Matcher matcher; +// if (!(line = line.trim()).isEmpty() && !line.startsWith("#") +// && (matcher = OLD_NODE_PATTERN.matcher(line)).find() +// && msgs.contains(matcher.group(1))) { +// lang.set(matcher.group(1) + ".message", matcher.group(2)); +// } +// } +// } +// } +// } +// +// protected static void translateMsgRoot(Config lang, String file, Charset charset) throws IOException { +// List msgs = lang.getValues(true).entrySet().stream() +// .filter(e -> e.getValue() instanceof ConfigSection) +// .map(Map.Entry::getKey) +// .collect(Collectors.toList()); +// +// if (!msgs.isEmpty()) { +// String[] source = file.split("\n"); +// +// String line; +// for (int lineNumber = 0; lineNumber < source.length; ++lineNumber) { +// line = source[lineNumber]; +// if (lineNumber == 0) { +// // remove BOM markers, if any +// line = line.replaceAll("[\uFEFF\uFFFE\u200B]", ""); +// } +// +// Matcher matcher; +// if (!(line = line.trim()).isEmpty() && !line.startsWith("#") +// && (matcher = OLD_NODE_PATTERN.matcher(line)).find() +// && msgs.contains(matcher.group(1))) { +// lang.set(matcher.group(1) + ".message", matcher.group(2)); +// } +// } +// } +// } +// +// /** +// * Supply the Message object with the plugins prefix. +// * +// * @param message message to be applied +// * +// * @return applied message +// */ +// private Message supplyPrefix(Message message) { +// return message.setPrefix(this.nodes.getOrDefault("general.nametag.prefix", "[" + plugin.getName() + "]")); +// } +// +// /** +// * Create a new unsaved Message +// * +// * @param message the message to create +// * +// * @return the created message +// */ +// public Message newMessage(String message) { +// return supplyPrefix(new Message(message)); +// } +// +// /** +// * Get a message set for a specific node. +// * +// * @param node the node to get +// * +// * @return the message for the specified node +// */ +// public Message getMessage(String node) { +// if (this.nodes.containsKey(node + ".message")) { +// node += ".message"; +// } +// +// return this.getMessageOrDefault(node, node); +// } +// +// /** +// * Get a message set for a specific node +// * +// * @param node the node to get +// * @param defaultValue the default value given that a value for the node was not found +// * +// * @return the message for the specified node. Default if none found +// */ +// public Message getMessageOrDefault(String node, String defaultValue) { +// if (this.nodes.containsKey(node + ".message")) { +// node += ".message"; +// } +// +// return supplyPrefix(new Message(this.nodes.getOrDefault(node, defaultValue))); +// } +// +// /** +// * Return the locale name (i.e. "en_US") +// * +// * @return the locale name +// */ +// public String getName() { +// return name; +// } +// +// private static void copy(InputStream input, OutputStream output) { +// int n; +// byte[] buffer = new byte[1024 * 4]; +// +// try { +// while ((n = input.read(buffer)) != -1) { +// output.write(buffer, 0, n); +// } +// } catch (IOException ex) { +// ex.printStackTrace(); +// } +// } +//} diff --git a/Core/src/main/java/com/songoda/core/locale/LocaleFileManager.java b/Core/src/main/java/com/songoda/core/locale/LocaleFileManager.java new file mode 100644 index 00000000..a9c7cd1a --- /dev/null +++ b/Core/src/main/java/com/songoda/core/locale/LocaleFileManager.java @@ -0,0 +1,102 @@ +package com.songoda.core.locale; + +import com.songoda.core.http.HttpClient; +import com.songoda.core.http.HttpResponse; +import com.songoda.core.http.UnexpectedHttpStatusException; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +public class LocaleFileManager { + private final HttpClient httpClient; + private final String projectName; + + public LocaleFileManager(HttpClient httpClient, String projectName) { + this.httpClient = httpClient; + this.projectName = projectName; + } + + public List downloadMissingTranslations(File targetDirectory) throws IOException { + List availableLanguages = this.fetchAvailableLanguageFiles(); + if (availableLanguages == null) { + return Collections.emptyList(); + } + + Files.createDirectories(targetDirectory.toPath()); + + List downloadedLocales = new LinkedList<>(); + for (String languageFileName : availableLanguages) { + File languageFile = new File(targetDirectory, languageFileName); + if (languageFile.exists()) { + continue; + } + + String languageFileContents = fetchProjectFile(languageFileName); + if (languageFileContents == null) { + throw new IOException("Failed to download language file " + languageFileName); // TODO: Better exception + } + + try (Writer writer = Files.newBufferedWriter(languageFile.toPath(), StandardCharsets.UTF_8)) { + writer.write(languageFileContents); + } + + downloadedLocales.add(languageFileName); + } + + return downloadedLocales; + } + + public @Nullable List fetchAvailableLanguageFiles() throws IOException { + String projectLanguageIndex = fetchProjectFile("_index.txt"); + + if (projectLanguageIndex == null) { + return null; + } + + List result = new LinkedList<>(); + + for (String line : projectLanguageIndex.split("\r?\n")) { + line = line.trim(); + + if (!line.startsWith("#") && !line.isEmpty()) { + result.add(line); + } + } + + return result; + } + + public String fetchProjectFile(String fileName) throws IOException { + String url = formatUrl("https://songoda.github.io/Translations/projects/%s/%s", this.projectName, fileName); + HttpResponse httpResponse = this.httpClient.get(url); + + if (httpResponse.getResponseCode() == 404) { + return null; + } + + if (httpResponse.getResponseCode() != 200) { + throw new UnexpectedHttpStatusException(httpResponse.getResponseCode(), url); + } + + return httpResponse.getBodyAsString(); + } + + private static String formatUrl(String url, Object... params) throws UnsupportedEncodingException { + Object[] encodedParams = new Object[params.length]; + for (int i = 0; i < params.length; i++) { + encodedParams[i] = URLEncoder.encode(params[i].toString(), "UTF-8"); + } + + return String.format(url, encodedParams); + } +} diff --git a/Core/src/main/java/com/songoda/core/locale/LocaleManager.java b/Core/src/main/java/com/songoda/core/locale/LocaleManager.java new file mode 100644 index 00000000..82b874a6 --- /dev/null +++ b/Core/src/main/java/com/songoda/core/locale/LocaleManager.java @@ -0,0 +1,115 @@ +package com.songoda.core.locale; + +import com.songoda.core.configuration.songoda.SongodaYamlConfig; +import com.songoda.core.configuration.yaml.YamlConfiguration; +import com.songoda.core.http.SimpleHttpClient; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +public class LocaleManager { + protected final Plugin plugin; + protected final File localesDirectory; + + protected final List loadedLocales = new LinkedList<>(); + protected final @Nullable YamlConfiguration fallbackLocale; + + public LocaleManager(Plugin plugin) throws IOException { + this.plugin = plugin; + this.localesDirectory = new File(this.plugin.getDataFolder(), "locales"); + + this.fallbackLocale = loadFallbackLocale(); + } + + public List downloadMissingLocales() { + LocaleFileManager localeFileManager = new LocaleFileManager(new SimpleHttpClient(), this.plugin.getName()); + + try { + return localeFileManager.downloadMissingTranslations(this.localesDirectory); + } catch (IOException ex) { + this.plugin.getLogger().warning("Failed to download missing locales: " + ex.getMessage()); + } + + return Collections.emptyList(); + } + + public void load(String locale) throws IOException { + File fileToLoad = determineAvailableLocaleVariation(locale); + if (fileToLoad == null) { + throw new FileNotFoundException("Locale file " + locale + " not found"); + } + + for (SongodaYamlConfig loadedLocale : this.loadedLocales) { + if (loadedLocale.file.equals(fileToLoad)) { + return; + } + } + + SongodaYamlConfig localeConfig = new SongodaYamlConfig(fileToLoad); + localeConfig.load(); + this.loadedLocales.add(localeConfig); + } + + public void loadExclusively(String locale) { + loadExclusively(Collections.singletonList(locale)); + } + + public void loadExclusively(List locales) { + unloadAll(); + } + + public void unloadAll() { + this.loadedLocales.clear(); + } + + protected @Nullable File determineAvailableLocaleVariation(String locale) { + File localeFile = new File(this.localesDirectory, locale + ".lang"); + if (localeFile.exists()) { + return localeFile; + } + + File[] availableLocales = this.localesDirectory.listFiles(); + if (availableLocales == null) { + return null; + } + + for (File availableLocale : availableLocales) { + if (availableLocale.getName().startsWith(locale)) { + return availableLocale; + } + } + + return null; + } + + protected @Nullable YamlConfiguration loadFallbackLocale() throws IOException { + URL fallbackLocaleUrl = this.plugin.getClass().getResource("/en_US.lang"); + if (fallbackLocaleUrl == null) { + return null; + } + + YamlConfiguration locale = new YamlConfiguration(); + try (Reader reader = new InputStreamReader(fallbackLocaleUrl.openStream(), StandardCharsets.UTF_8)) { + locale.load(reader); + } + + return locale; + } + + protected SongodaYamlConfig parseLocaleFile(File file) throws IOException { + SongodaYamlConfig locale = new SongodaYamlConfig(file, this.plugin.getLogger()); + locale.load(); + + return locale; + } +} diff --git a/Core/src/main/java/com/songoda/core/utils/Pair.java b/Core/src/main/java/com/songoda/core/utils/Pair.java new file mode 100644 index 00000000..93a255b9 --- /dev/null +++ b/Core/src/main/java/com/songoda/core/utils/Pair.java @@ -0,0 +1,19 @@ +package com.songoda.core.utils; + +public class Pair { + private final T first; + private final U second; + + public Pair(T first, U second) { + this.first = first; + this.second = second; + } + + public T getFirst() { + return this.first; + } + + public U getSecond() { + return this.second; + } +} diff --git a/Core/src/test/java/com/songoda/core/configuration/ReadOnlyConfigEntryTest.java b/Core/src/test/java/com/songoda/core/configuration/ReadOnlyConfigEntryTest.java new file mode 100644 index 00000000..a01bd2c4 --- /dev/null +++ b/Core/src/test/java/com/songoda/core/configuration/ReadOnlyConfigEntryTest.java @@ -0,0 +1,49 @@ +package com.songoda.core.configuration; + +import com.songoda.core.configuration.yaml.YamlConfiguration; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ReadOnlyConfigEntryTest { + @Test + void testGetKey() { + ConfigEntry entry = new ReadOnlyConfigEntry(new YamlConfiguration(), "key-1"); + + assertEquals("key-1", entry.getKey()); + } + + @Test + void testGetConfig() { + IConfiguration config = new YamlConfiguration(); + ConfigEntry entry = new ReadOnlyConfigEntry(config, "key"); + + assertSame(config, entry.getConfig()); + } + + @Test + void testNullGetters() { + ConfigEntry entry = new ReadOnlyConfigEntry(new YamlConfiguration(), "key-null"); + + assertNull(entry.getDefaultValue()); + assertNull(entry.getUpgradeSteps()); + } + + @Test + void testWritingMethodsDoingNothing() { + YamlConfiguration config = new YamlConfiguration(); + ConfigEntry entry = new ReadOnlyConfigEntry(config, "key"); + + assertThrows(UnsupportedOperationException.class, () -> entry.setDefaultValue("value")); + assertThrows(UnsupportedOperationException.class, () -> entry.withDefaultValue("value")); + assertThrows(UnsupportedOperationException.class, () -> entry.withComment("A comment.")); + assertThrows(UnsupportedOperationException.class, () -> entry.withUpgradeStep(0, "old-key")); + assertThrows(UnsupportedOperationException.class, () -> entry.set("value")); + + assertNull(entry.getDefaultValue()); + assertNull(entry.getUpgradeSteps()); + assertNull(config.getNodeComment("key")); + assertNull(entry.getUpgradeSteps()); + assertNull(entry.get()); + } +} diff --git a/Core/src/test/java/com/songoda/core/configuration/songoda/SongodaYamlConfigRoundtripTest.java b/Core/src/test/java/com/songoda/core/configuration/songoda/SongodaYamlConfigRoundtripTest.java new file mode 100644 index 00000000..aa41c5c7 --- /dev/null +++ b/Core/src/test/java/com/songoda/core/configuration/songoda/SongodaYamlConfigRoundtripTest.java @@ -0,0 +1,114 @@ +package com.songoda.core.configuration.songoda; + +import com.songoda.core.configuration.ConfigEntry; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +class SongodaYamlConfigRoundtripTest { + private Path testDirectoryPath; + + @BeforeEach + void setUp() throws IOException { + this.testDirectoryPath = Files.createTempDirectory("SongodaCore-YamlConfigRoundtripTest"); + this.testDirectoryPath.toFile().deleteOnExit(); + } + + @AfterEach + void tearDown() throws IOException { + try (Stream paths = Files.list(this.testDirectoryPath)) { + for (Path path : paths.toArray(Path[]::new)) { + Files.deleteIfExists(path); + } + } + Files.deleteIfExists(this.testDirectoryPath); + } + + @Test + void roundtripTest() throws IOException { + Path testFilePath = this.testDirectoryPath.resolve("config.yml"); + + Files.write(testFilePath, ("# Don't touch this – it's used to track the version of the config.\n" + + "version: 1\n" + + "messages:\n" + + " # This message is shown when the 'foo' command succeeds.\n" + + " fooSuccess: Remastered success value\n" + + "# This is the range of the 'foo' command\n").getBytes()); + + SongodaYamlConfig cfg = new SongodaYamlConfig(testFilePath.toFile()) + .withVersion(3); + + ConfigEntry cmdFooSuccess = cfg.createEntry("command.foo.success") + .withDefaultValue("Default success value") + .withComment("This message is shown when the 'foo' command succeeds.") + .withUpgradeStep(1, "messages.fooSuccess"); + ConfigEntry range = cfg.createEntry("range") + .withComment("This is the range of the 'foo' command") + .withUpgradeStep(1, null, o -> { + if (o == null) { + return 10; + } + + return o; + }) + .withUpgradeStep(2, null, o -> o + " blocks"); + ConfigEntry incrementer = cfg.createEntry("incrementer", 0) + .withComment("This is the incrementer of the 'foo' command") + .withUpgradeStep(1, null, o -> { + if (o == null) { + return null; + } + + return (int) o + 1; + }) + .withUpgradeStep(3, null, (o) -> "text"); + ConfigEntry entryWithoutUpgradeStep = cfg.createEntry("entryWithoutUpgradeStep") + .withDefaultValue("Default value") + .withComment("This is the entry without an upgrade step"); + + ConfigEntry entryWithCyrillic = cfg.createEntry("entryWithCyrillic") + .withDefaultValue("Кириллица") + .withComment("This is the entry with cyrillic characters"); + + assertTrue(cfg.init()); + + assertNull(cfg.get("messages.fooSuccess")); + assertEquals("Remastered success value", cfg.get("command.foo.success")); + assertEquals("Remastered success value", cmdFooSuccess.get()); + assertTrue(cmdFooSuccess.has()); + + assertTrue(range.has()); + assertEquals(cfg.get("range"), range.get()); + + assertTrue(incrementer.has()); + assertEquals(cfg.get("incrementer"), incrementer.get()); + + assertTrue(entryWithoutUpgradeStep.has()); + assertEquals(cfg.get("entryWithoutUpgradeStep"), entryWithoutUpgradeStep.get()); + + assertTrue(entryWithCyrillic.has()); + assertEquals(cfg.get("entryWithCyrillic"), entryWithCyrillic.get()); + + assertEquals("# Don't touch this – it's used to track the version of the config.\n" + + "version: 3\n" + + "command:\n" + + " foo:\n" + + " # This message is shown when the 'foo' command succeeds.\n" + + " success: Remastered success value\n" + + "# This is the range of the 'foo' command\n" + + "range: 10 blocks\n" + + "# This is the incrementer of the 'foo' command\n" + + "incrementer: 0\n" + + "# This is the entry without an upgrade step\n" + + "entryWithoutUpgradeStep: Default value\n" + + "# This is the entry with cyrillic characters\n" + + "entryWithCyrillic: Кириллица\n", new String(Files.readAllBytes(testFilePath))); + } +} diff --git a/Core/src/test/java/com/songoda/core/configuration/songoda/SongodaYamlConfigTest.java b/Core/src/test/java/com/songoda/core/configuration/songoda/SongodaYamlConfigTest.java new file mode 100644 index 00000000..ed5f3c75 --- /dev/null +++ b/Core/src/test/java/com/songoda/core/configuration/songoda/SongodaYamlConfigTest.java @@ -0,0 +1,246 @@ +package com.songoda.core.configuration.songoda; + +import com.songoda.core.configuration.ConfigEntry; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +class SongodaYamlConfigTest { + Path tmpDir; + Path cfg; + + @BeforeEach + void setUp() throws IOException { + this.tmpDir = Files.createTempDirectory("SongodaYamlConfigTest"); + + this.cfg = Files.createTempFile(this.tmpDir, "config", ".yml"); + this.tmpDir.toFile().deleteOnExit(); + } + + @AfterEach + void tearDown() throws IOException { + try (Stream stream = Files.walk(this.tmpDir)) { + stream + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + } + + @Test + void testLoad() throws IOException { + Files.write(this.cfg, "test-key: foo\n".getBytes()); + + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()); + cfg.set("test-key", "bar"); + cfg.load(); + + assertEquals("foo", cfg.get("test-key")); + } + + @Test + void testSave() throws IOException { + Files.write(this.cfg, "test-key: foo\n".getBytes()); + + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()); + cfg.set("test-key", "bar"); + cfg.save(); + + assertEquals("test-key: bar\n", new String(Files.readAllBytes(this.cfg))); + } + + @Test + void testSaveToNonExistingSubDirectory() throws IOException { + File configFile = new File(this.tmpDir.toFile(), "testSaveToNonExistingSubDirectory/config.yml"); + + SongodaYamlConfig cfg = new SongodaYamlConfig(configFile); + cfg.set("test-key", "bar"); + cfg.save(); + + assertEquals("test-key: bar\n", new String(Files.readAllBytes(configFile.toPath()))); + } + + @Test + void testWithVersion() throws IOException { + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()); + cfg.withVersion("version-key", 1, null); + + assertEquals(1, cfg.get("version-key")); + + cfg.save(); + assertEquals("version-key: 1\n", new String(Files.readAllBytes(this.cfg))); + + cfg.withVersion(2); + + assertEquals(2, cfg.get("version")); + assertNull(cfg.get("version-key")); + + cfg.save(); + assertEquals( + "# Don't touch this – it's used to track the version of the config.\n" + + "version: 2\n", + new String(Files.readAllBytes(this.cfg)) + ); + } + + @Test + void testWithZeroVersion() throws IOException { + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()); + cfg.withVersion("version-key", 0, null); + + assertEquals(0, cfg.get("version-key")); + + cfg.save(); + assertEquals("version-key: 0\n", new String(Files.readAllBytes(this.cfg))); + } + + @Test + void testWithNegativeVersion() { + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()); + assertThrows(IllegalArgumentException.class, () -> cfg.withVersion("version-key", -1, null)); + } + + @Test + void testLoadWithTooNewVersion() { + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()) + .withVersion(1); + + assertThrows(IllegalStateException.class, () -> cfg.load(new StringReader("version: 10\n"))); + } + + @Test + void testWithUpToDateVersion() throws IOException { + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()) + .withVersion(2); + + assertFalse(cfg.upgradeOldConfigVersion()); + } + + @Test + void testWithNewerVersion() { + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()) + .withVersion(5); + + assertThrows(IllegalStateException.class, cfg::upgradeOldConfigVersionByOne); + } + + @Test + void testWithKeyWithoutConfigEntry() throws IOException { + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()); + + cfg.set("test-key", "foo"); + cfg.load(); + + assertNull(cfg.get("test-key")); + + cfg.set("test-key", "foo"); + assertEquals("foo", cfg.get("test-key")); + + cfg.save(); + cfg.load(); + + assertEquals("foo", cfg.get("test-key")); + assertEquals(1, cfg.getKeys("").size()); + } + + @Test + void testCreateEntryAppliesDefaultValueForNullValue() { + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()); + ConfigEntry entry = cfg.createEntry("key", "value"); + + cfg.init(); + + assertEquals("value", entry.get()); + } + + @Test + void testCreateDuplicateEntry() { + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()); + ConfigEntry entry = cfg.createEntry("key", null); + + assertThrows(IllegalArgumentException.class, () -> cfg.createEntry("key", "other-value")); + + assertNull(entry.get()); + } + + @Test + void testVersionUpgradePersistsCommentsOnKeyChange() throws IOException { + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()) + .withVersion(2); + + cfg.createEntry("newKey", "value") + .withComment("This is a comment") + .withUpgradeStep(1, "oldKey"); + + cfg.load(new StringReader("version: 1\noldKey: old-value\n")); + + assertNull(cfg.get("oldKey")); + assertNull(cfg.getNodeComment("oldKey")); + + assertEquals("old-value", cfg.get("newKey")); + assertEquals("This is a comment", Objects.requireNonNull(cfg.getNodeComment("newKey")).get()); + + StringWriter writer = new StringWriter(); + cfg.save(writer); + + assertEquals("# Don't touch this – it's used to track the version of the config.\n" + + "version: 2\n" + + "# This is a comment\n" + + "newKey: old-value\n", + writer.toString()); + } + + @Test + void testReadOnlyEntry() { + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()); + ConfigEntry entry = cfg.createEntry("key", "default-value"); + ConfigEntry readOnlyConfigEntry = cfg.getReadEntry("key"); + + assertThrows(UnsupportedOperationException.class, () -> readOnlyConfigEntry.set("new-value")); + assertEquals("default-value", entry.get()); + + assertThrows(UnsupportedOperationException.class, () -> readOnlyConfigEntry.setDefaultValue("new-default-value")); + assertEquals("default-value", entry.get()); + + assertThrows(UnsupportedOperationException.class, () -> readOnlyConfigEntry.withComment("test-comment")); + assertThrows(UnsupportedOperationException.class, () -> readOnlyConfigEntry.withComment(() -> "test-comment")); + + assertThrows(UnsupportedOperationException.class, () -> readOnlyConfigEntry.withUpgradeStep(10, "new-key")); + assertThrows(UnsupportedOperationException.class, () -> readOnlyConfigEntry.withUpgradeStep(10, "new-key", (o) -> "new-value")); + + assertEquals("default-value", entry.get()); + + entry.set("new-value"); + assertEquals("new-value", readOnlyConfigEntry.get()); + } + + @Test + void testInit_Failure() { + assertTrue(this.cfg.toFile().setWritable(false)); + + Logger mockLogger = Mockito.mock(Logger.class); + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile(), mockLogger); + + cfg.createEntry("key", "default-value"); + + assertFalse(cfg.init()); + Mockito.verify(mockLogger).log(Mockito.eq(Level.SEVERE), Mockito.anyString(), Mockito.any(IOException.class)); + + assertTrue(this.cfg.toFile().setWritable(true)); + } +} diff --git a/Core/src/test/java/com/songoda/core/configuration/yaml/YamlConfigEntryTest.java b/Core/src/test/java/com/songoda/core/configuration/yaml/YamlConfigEntryTest.java new file mode 100644 index 00000000..f0cc5ead --- /dev/null +++ b/Core/src/test/java/com/songoda/core/configuration/yaml/YamlConfigEntryTest.java @@ -0,0 +1,258 @@ +package com.songoda.core.configuration.yaml; + +import com.songoda.core.compatibility.CompatibleMaterial; +import com.songoda.core.configuration.ConfigEntry; +import com.songoda.core.configuration.songoda.SongodaYamlConfig; +import com.songoda.ultimatestacker.core.configuration.Config; +import org.bukkit.Material; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class YamlConfigEntryTest { + @Test + void testHas() { + YamlConfiguration config = new YamlConfiguration(); + config.set("key-2", "value-2"); + + ConfigEntry entry1 = new YamlConfigEntry(config, "key-1", null); + ConfigEntry entry2 = new YamlConfigEntry(config, "key-2", null); + + assertFalse(entry1.has()); + assertTrue(entry2.has()); + } + + @Test + void testGetKey() { + ConfigEntry entry = new YamlConfigEntry(new YamlConfiguration(), "key-1", null); + assertEquals("key-1", entry.getKey()); + } + + @Test + void testGetConfig() { + YamlConfiguration config = new YamlConfiguration(); + ConfigEntry entry = new YamlConfigEntry(config, "key-1", null); + assertSame(config, entry.getConfig()); + } + + @Test + void testGetDefaultValue() { + SongodaYamlConfig cfg = new SongodaYamlConfig(new File("ConfigEntryTest.yml")); + ConfigEntry entry = cfg.createEntry("key", "value"); + + assertEquals("value", entry.getDefaultValue()); + + entry.setDefaultValue("new-value"); + assertEquals("new-value", entry.getDefaultValue()); + + entry.withDefaultValue("new-value-2"); + assertEquals("new-value-2", entry.getDefaultValue()); + } + + @Test + void testGetOr() { + SongodaYamlConfig cfg = new SongodaYamlConfig(new File("ConfigEntryTest.yml")); + ConfigEntry entry = cfg.createEntry("key", "value"); + + assertEquals("value", entry.getOr("invalid")); + + entry.set(null); + assertEquals("invalid", entry.getOr("invalid")); + } + + @Test + void testGetString() { + SongodaYamlConfig cfg = new SongodaYamlConfig(new File("ConfigEntryTest.yml")); + ConfigEntry entry = cfg.createEntry("key", null); + + entry.set("value"); + assertEquals("value", entry.getString()); + + entry.set("new-value"); + assertEquals("new-value", entry.getString()); + + entry.set(null); + assertNull(entry.getString()); + assertNull(entry.getStringOr(null)); + assertEquals("12", entry.getStringOr("12")); + + entry.set(10.5); + assertEquals("10.5", entry.getString()); + + entry.set(true); + assertEquals("true", entry.getString()); + + entry.set(CompatibleMaterial.STONE); + assertEquals("STONE", entry.getString()); + } + + @Test + void testGetInt() { + SongodaYamlConfig cfg = new SongodaYamlConfig(new File("ConfigEntryTest.yml")); + ConfigEntry entry = cfg.createEntry("key", null); + + entry.set(1.0); + assertEquals(1, entry.getInt()); + + entry.set("1.5"); + assertEquals(1, entry.getInt()); + + entry.set("10"); + assertEquals(10.0, entry.getInt()); + + entry.set("10,0"); + assertThrows(NumberFormatException.class, entry::getInt); + + entry.set(null); + assertEquals(0, entry.getInt()); + assertEquals(11, entry.getIntOr(11)); + } + + @Test + void testGetDouble() { + SongodaYamlConfig cfg = new SongodaYamlConfig(new File("ConfigEntryTest.yml")); + ConfigEntry entry = cfg.createEntry("key", null); + + entry.set(1.0); + assertEquals(1.0, entry.getDouble()); + + entry.set("1.5"); + assertEquals(1.5, entry.getDouble()); + + entry.set("10"); + assertEquals(10.0, entry.getDouble()); + + entry.set("10,0"); + assertThrows(NumberFormatException.class, entry::getDouble); + + entry.set(null); + assertEquals(0.0, entry.getDouble()); + assertEquals(11.5, entry.getDoubleOr(11.5)); + } + + @Test + void testGetBoolean() { + SongodaYamlConfig cfg = new SongodaYamlConfig(new File("ConfigEntryTest.yml")); + ConfigEntry entry = cfg.createEntry("key", null); + + entry.set(false); + assertFalse(entry.getBoolean()); + + entry.set("false"); + assertFalse(entry.getBoolean()); + + entry.set("invalid"); + assertFalse(entry.getBoolean()); + + entry.set(1); + assertFalse(entry.getBoolean()); + + entry.set(true); + assertTrue(entry.getBoolean()); + + entry.set("true"); + assertTrue(entry.getBoolean()); + + entry.set(null); + assertFalse(entry.getBoolean()); + assertTrue(entry.getBooleanOr(true)); + } + + @Test + void testGetStringList() { + SongodaYamlConfig cfg = new SongodaYamlConfig(new File("ConfigEntryTest.yml")); + ConfigEntry entry = cfg.createEntry("key", null); + + final List fallbackValue = Collections.unmodifiableList(new LinkedList<>()); + + entry.set(null); + assertNull(entry.getStringList()); + assertSame(fallbackValue, entry.getStringListOr(fallbackValue)); + + entry.set(Collections.singletonList("value")); + assertEquals(Collections.singletonList("value"), entry.getStringList()); + + entry.set(new String[] {"value2"}); + assertEquals(Collections.singletonList("value2"), entry.getStringList()); + + entry.set("string-value"); + assertNull(entry.getStringList()); + } + + @Test + void testGetMaterial() { + SongodaYamlConfig cfg = new SongodaYamlConfig(new File("ConfigEntryTest.yml")); + ConfigEntry entry = cfg.createEntry("key", null); + + entry.set("LOG"); + assertEquals(CompatibleMaterial.BIRCH_LOG, entry.getMaterial()); + + entry.set("OAK_LOG"); + assertEquals(CompatibleMaterial.OAK_LOG, entry.getMaterial()); + + entry.set("10"); + assertNull(entry.getMaterial()); + + entry.set(null); + assertNull(entry.getMaterial()); + assertEquals(CompatibleMaterial.ACACIA_BOAT, entry.getMaterialOr(CompatibleMaterial.ACACIA_BOAT)); + + entry.set(CompatibleMaterial.GRASS); + assertEquals(CompatibleMaterial.GRASS, entry.getMaterial()); + + entry.set(Material.GRASS); + assertEquals(CompatibleMaterial.GRASS, entry.getMaterial()); + } + + @Test + void testInvalidWithUpgradeNull() { + SongodaYamlConfig cfg = new SongodaYamlConfig(new File("ConfigEntryTest.yml")); + ConfigEntry entry = cfg.createEntry("key", "value"); + + assertThrows(IllegalArgumentException.class, () -> entry.withUpgradeStep(1, null, null)); + } + + @Test + void testEqualsAndHashCode() { + SongodaYamlConfig cfg = new SongodaYamlConfig(new File("ConfigEntryTest.yml")); + ConfigEntry entry = cfg.createEntry("key", "value"); + + assertEquals(entry, entry); + assertEquals(entry.hashCode(), entry.hashCode()); + + + ConfigEntry other = new YamlConfigEntry(cfg, "key", "value"); + assertEquals(entry, other); + assertEquals(entry.hashCode(), other.hashCode()); + + other = new YamlConfigEntry(cfg, "key", "value2"); + assertNotEquals(entry, other); + assertNotEquals(entry.hashCode(), other.hashCode()); + + other = new YamlConfigEntry(cfg, "key2", "value"); + assertNotEquals(entry, other); + assertNotEquals(entry.hashCode(), other.hashCode()); + + other = new YamlConfigEntry(cfg, "key", "value2"); + assertNotEquals(entry, other); + assertNotEquals(entry.hashCode(), other.hashCode()); + + other = new YamlConfigEntry(cfg, "key2", "value2"); + assertNotEquals(entry, other); + assertNotEquals(entry.hashCode(), other.hashCode()); + + assertNotEquals(entry, null); + assertNotEquals(entry, "key"); + } +} diff --git a/Core/src/test/java/com/songoda/core/configuration/yaml/YamlConfigurationTest.java b/Core/src/test/java/com/songoda/core/configuration/yaml/YamlConfigurationTest.java new file mode 100644 index 00000000..de14d931 --- /dev/null +++ b/Core/src/test/java/com/songoda/core/configuration/yaml/YamlConfigurationTest.java @@ -0,0 +1,613 @@ +package com.songoda.core.configuration.yaml; + +import org.apache.commons.lang.StringUtils; +import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.constructor.DuplicateKeyException; +import org.yaml.snakeyaml.error.YAMLException; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import static org.junit.jupiter.api.Assertions.*; + +class YamlConfigurationTest { + static final String inputYaml = "foo: bar\n" + + "primitives:\n" + + " int: " + Integer.MIN_VALUE + "\n" + + " long: " + Long.MIN_VALUE + "\n" + + " float: " + Float.MIN_VALUE + "\n" + + " double: " + Double.MIN_VALUE + "\n" + + " char: ä\n" + + " string: string\n" + + " string-long: " + StringUtils.repeat("abc", 512) + "\n" + + " string-multi-line: |\n" + + " a\n" + + " b\n" + + " c\n" + + " boolean: true\n" + + " list: [2, 1, 3]\n" + + " map:\n" + + " key: value\n" + + " set:\n" + + " - 1\n" + + " - 2\n" + + " - 3\n"; + static final String expectedOutYaml = "foo: bar\n" + + "primitives:\n" + + " int: " + Integer.MIN_VALUE + "\n" + + " long: " + Long.MIN_VALUE + "\n" + + " float: " + Float.MIN_VALUE + "\n" + + " double: " + Double.MIN_VALUE + "\n" + + " char: ä\n" + + " string: string\n" + + " string-long: " + StringUtils.repeat("abc", 512) + "\n" + + " string-multi-line: |\n" + + " a\n" + + " b\n" + + " c\n" + + " boolean: true\n" + + " list:\n" + + " - 2\n" + + " - 1\n" + + " - 3\n" + + " map:\n" + + " key: value\n" + + " set:\n" + + " - 1\n" + + " - 2\n" + + " - 3\n"; + + @Test + void testYamlParser() throws IOException { + final YamlConfiguration cfg = new YamlConfiguration(); + cfg.load(new StringReader(inputYaml)); + + assertEquals(Integer.MIN_VALUE, cfg.get("primitives.int")); + assertEquals(Long.MIN_VALUE, cfg.get("primitives.long")); + assertEquals(Float.MIN_VALUE, ((Number) cfg.get("primitives.float")).floatValue()); + assertEquals(Double.MIN_VALUE, cfg.get("primitives.double")); + + assertEquals("ä", cfg.get("primitives.char")); + + assertEquals("string", cfg.get("primitives.string")); + + assertInstanceOf(Boolean.class, cfg.get("primitives.boolean")); + assertTrue((Boolean) cfg.get("primitives.boolean")); + + List primitivesList = (List) cfg.get("primitives.list"); + assertNotNull(primitivesList); + assertInstanceOf(List.class, cfg.get("primitives.list")); + assertEquals(3, primitivesList.size()); + assertEquals(2, primitivesList.get(0)); + assertEquals(1, primitivesList.get(1)); + assertEquals(3, primitivesList.get(2)); + + assertEquals("value", cfg.get("primitives.map.key")); + + assertInstanceOf(List.class, cfg.get("primitives.set")); + assertEquals(3, ((List) cfg.get("primitives.set")).size()); + } + + @Test + void testYamlParserWithEmptyFile() throws IOException { + final YamlConfiguration cfg = new YamlConfiguration(); + cfg.load(new StringReader("")); + assertTrue(cfg.getKeys("").isEmpty()); + + cfg.load(new StringReader("\n")); + assertTrue(cfg.getKeys("").isEmpty()); + } + + @Test + void testYamlParserWithDuplicateKeys() { + assertThrowsExactly(DuplicateKeyException.class, + () -> new YamlConfiguration().load(new StringReader("test: value1\ntest: value2"))); + } + + @Test + void testYamlParserWithInvalidReader() throws IOException { + Reader reader = new StringReader(""); + reader.close(); + + assertThrowsExactly(YAMLException.class, () -> new YamlConfiguration().load(reader)); + } + + @Test + void testYamlWriter() throws IOException { + final YamlConfiguration cfg = new YamlConfiguration(); + final StringWriter stringWriter = new StringWriter(inputYaml.length()); + + cfg.load(new StringReader(inputYaml)); + cfg.save(stringWriter); + + assertEquals(expectedOutYaml, stringWriter.toString()); + assertEquals(expectedOutYaml, cfg.toYamlString()); + } + + @Test + void testYamlWriterWithNullValue() throws IOException { + final YamlConfiguration cfg = new YamlConfiguration(); + final StringWriter stringWriter = new StringWriter(1); + + cfg.set("null-value", null); + cfg.set("nested.null-value", null); + cfg.save(stringWriter); + + assertEquals("", stringWriter.toString()); + assertEquals("", cfg.toYamlString()); + } + + @Test + void testYamlWriterWithNoData() throws IOException { + final YamlConfiguration cfg = new YamlConfiguration(); + final StringWriter stringWriter = new StringWriter(inputYaml.length()); + + cfg.save(stringWriter); + + assertEquals("", stringWriter.toString()); + assertEquals("", cfg.toYamlString()); + } + + @Test + void testYamlWriterWithNoDataAndComments() throws IOException { + final YamlConfiguration cfg = new YamlConfiguration(); + final StringWriter stringWriter = new StringWriter(inputYaml.length()); + + cfg.setHeaderComment("baz"); + cfg.setNodeComment("foo", "bar"); + + cfg.save(stringWriter); + + assertEquals("# baz\n", stringWriter.toString()); + assertEquals("# baz\n", cfg.toYamlString()); + } + + @Test + void testSetter() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("foo.bar.innerBar", "bar")); // 'foo.bar' gets overwritten + + Object prevValue = cfg.set("foo.bar", "baz"); + assertInstanceOf(Map.class, prevValue); + assertEquals(1, ((Map) prevValue).size()); + assertEquals("bar", ((Map) prevValue).get("innerBar")); + + assertNull(cfg.set("number", 27)); + assertNull(cfg.set("bar.foo.faa1", "value1")); + assertNull(cfg.set("bar.foo.faa2", "value2")); + + assertFalse(cfg.has("a.b.c")); + assertFalse(cfg.has("a")); + + Map expectedValues = new HashMap() {{ + put("number", 27); + + put("foo", new HashMap() {{ + put("bar", "baz"); + }}); + + put("bar", new HashMap() {{ + put("foo", new HashMap() {{ + put("faa1", "value1"); + put("faa2", "value2"); + }}); + }}); + }}; + + assertEquals(expectedValues, cfg.values); + } + + @Test + void testSetterAndGetterWithPrimitiveValues() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("foobar", "test")); + assertNull(cfg.set("foo.bar", "test2")); + assertEquals("test", cfg.set("foobar", "overwritten-test")); + + assertEquals("overwritten-test", cfg.get("foobar")); + + assertEquals("test2", cfg.get("foo.bar")); + + assertNull(cfg.set("primitives.int", Integer.MIN_VALUE)); + assertNull(cfg.set("primitives.long", Long.MIN_VALUE)); + assertNull(cfg.set("primitives.float", Float.MIN_VALUE)); + assertNull(cfg.set("primitives.double", Double.MIN_VALUE)); + assertNull(cfg.set("primitives.char", 'ä')); + assertNull(cfg.set("primitives.string", "string")); + assertNull(cfg.set("primitives.boolean", true)); + + assertEquals(Integer.MIN_VALUE, cfg.get("primitives.int")); + assertEquals(Long.MIN_VALUE, cfg.get("primitives.long")); + + assertInstanceOf(Double.class, cfg.get("primitives.float")); + assertEquals(Float.MIN_VALUE, ((Number) cfg.get("primitives.float")).floatValue()); + + assertEquals(Double.MIN_VALUE, cfg.get("primitives.double")); + + assertInstanceOf(String.class, cfg.get("primitives.char")); + assertEquals("ä", cfg.get("primitives.char")); + + assertEquals("string", cfg.get("primitives.string")); + assertInstanceOf(Boolean.class, cfg.get("primitives.boolean")); + assertTrue((Boolean) cfg.get("primitives.boolean")); + + assertNull(cfg.set("primitives.map.key", "value")); + assertEquals("value", cfg.get("primitives.map.key")); + } + + @Test + void testSetterWithNullValue() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("foo.bar.null", "not-null-string")); + assertEquals("not-null-string", cfg.set("foo.bar.null", null)); + + assertNull(cfg.get("foo.bar.null")); + } + + @Test + void testGetNonExistingNestedKey() throws IOException { + final YamlConfiguration cfg = new YamlConfiguration(); + cfg.load(new StringReader(inputYaml)); + + assertNull(cfg.get("primitives.map2.key")); + } + + @Test + void testGetOrDefault() throws IOException { + final YamlConfiguration cfg = new YamlConfiguration(); + cfg.load(new StringReader(inputYaml)); + + assertEquals("bar", cfg.set("foo", "bar")); + assertNull(cfg.set("bar.baz", "foz")); + + assertEquals("bar", cfg.getOr("foo", "baz")); + assertEquals("foz", cfg.getOr("bar.baz", "baz")); + + assertEquals("default", cfg.getOr("foo.bar", "default")); + assertEquals("default", cfg.getOr("bar.baz.foo", "default")); + } + + @Test + void testGetterWithNullKey() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.get(null)); + } + + @Test + void testGetKeys() throws IOException { + final YamlConfiguration cfg = new YamlConfiguration(); + cfg.load(new StringReader(inputYaml)); + + assertEquals(2, cfg.getKeys("").size()); + assertTrue(cfg.getKeys(null).isEmpty()); + + assertTrue(cfg.getKeys("primitives.map.key.non-existing-subkey").isEmpty()); + assertTrue(cfg.getKeys("foo").isEmpty()); + + assertArrayEquals(new String[] {"key"}, cfg.getKeys("primitives.map").toArray()); + assertArrayEquals(new String[] {"int", "long", "float", "double", "char", "string", "string-long", "string-multi-line", "boolean", "list", "map", "set"}, cfg.getKeys("primitives").toArray()); + } + + @Test + void testSetterWithListValues() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.list", Arrays.asList(2, 1, 3))); + + assertInstanceOf(List.class, cfg.get("primitives.list")); + List primitivesList = (List) cfg.get("primitives.list"); + assertNotNull(primitivesList); + assertEquals(3, primitivesList.size()); + assertEquals(2, primitivesList.get(0)); + assertEquals(1, primitivesList.get(1)); + assertEquals(3, primitivesList.get(2)); + } + + @Test + void testSetterWithEnumValue() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.enum", TestEnum.ENUM_VALUE)); + + assertInstanceOf(String.class, cfg.get("primitives.enum")); + assertEquals(TestEnum.ENUM_VALUE, TestEnum.valueOf((String) cfg.get("primitives.enum"))); + } + + @Test + void testSetterWithBooleanArrayValue() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.array", new boolean[] {Boolean.FALSE, Boolean.TRUE})); + + assertInstanceOf(List.class, cfg.get("primitives.array")); + List primitivesList = (List) cfg.get("primitives.array"); + assert primitivesList != null; + assertEquals(2, primitivesList.size()); + assertEquals(Boolean.FALSE, primitivesList.get(0)); + assertEquals(Boolean.TRUE, primitivesList.get(1)); + } + + @Test + void testSetterWithByteArrayValue() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.array", new byte[] {2, Byte.MIN_VALUE, Byte.MAX_VALUE})); + + assertInstanceOf(List.class, cfg.get("primitives.array")); + List primitivesList = (List) cfg.get("primitives.array"); + assert primitivesList != null; + assertEquals(3, primitivesList.size()); + assertEquals(2, primitivesList.get(0)); + assertEquals((int) Byte.MIN_VALUE, primitivesList.get(1)); + assertEquals((int) Byte.MAX_VALUE, primitivesList.get(2)); + } + + @Test + void testSetterWithCharArrayValue() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.array", new char[] {'x', Character.MIN_VALUE, Character.MAX_VALUE})); + + assertInstanceOf(List.class, cfg.get("primitives.array")); + List primitivesList = (List) cfg.get("primitives.array"); + assert primitivesList != null; + assertEquals(3, primitivesList.size()); + assertEquals("x", primitivesList.get(0)); + assertEquals(String.valueOf(Character.MIN_VALUE), primitivesList.get(1)); + assertEquals(String.valueOf(Character.MAX_VALUE), primitivesList.get(2)); + } + + @Test + void testSetterWithShortArrayValue() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.array", new short[] {2, Short.MIN_VALUE, Short.MAX_VALUE})); + + assertInstanceOf(List.class, cfg.get("primitives.array")); + List primitivesList = (List) cfg.get("primitives.array"); + assert primitivesList != null; + assertEquals(3, primitivesList.size()); + assertEquals(2, primitivesList.get(0)); + assertEquals((int) Short.MIN_VALUE, primitivesList.get(1)); + assertEquals((int) Short.MAX_VALUE, primitivesList.get(2)); + } + + @Test + void testSetterWithIntArrayValue() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.array", new int[] {2, Integer.MIN_VALUE, Integer.MAX_VALUE})); + + assertInstanceOf(List.class, cfg.get("primitives.array")); + List primitivesList = (List) cfg.get("primitives.array"); + assert primitivesList != null; + assertEquals(3, primitivesList.size()); + assertEquals(2, primitivesList.get(0)); + assertEquals(Integer.MIN_VALUE, primitivesList.get(1)); + assertEquals(Integer.MAX_VALUE, primitivesList.get(2)); + } + + @Test + void testSetterWithLongArrayValue() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.array", new long[] {2, Long.MIN_VALUE, Long.MAX_VALUE})); + + assertInstanceOf(List.class, cfg.get("primitives.array")); + List primitivesList = (List) cfg.get("primitives.array"); + assert primitivesList != null; + assertEquals(3, primitivesList.size()); + assertEquals((long) 2, primitivesList.get(0)); + assertEquals(Long.MIN_VALUE, primitivesList.get(1)); + assertEquals(Long.MAX_VALUE, primitivesList.get(2)); + } + + @Test + void testSetterWithFloatArrayValue() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.array", new float[] {2, Float.MIN_VALUE, Float.MAX_VALUE})); + + assertInstanceOf(List.class, cfg.get("primitives.array")); + List primitivesList = (List) cfg.get("primitives.array"); + assert primitivesList != null; + assertEquals(3, primitivesList.size()); + assertEquals((double) 2, primitivesList.get(0)); + assertEquals((double) Float.MIN_VALUE, primitivesList.get(1)); + assertEquals((double) Float.MAX_VALUE, primitivesList.get(2)); + } + + @Test + void testSetterWithDoubleArrayValue() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.array", new double[] {2, Double.MIN_VALUE, Double.MAX_VALUE})); + + assertInstanceOf(List.class, cfg.get("primitives.array")); + List primitivesList = (List) cfg.get("primitives.array"); + assert primitivesList != null; + assertEquals(3, primitivesList.size()); + assertEquals((double) 2, primitivesList.get(0)); + assertEquals(Double.MIN_VALUE, primitivesList.get(1)); + assertEquals(Double.MAX_VALUE, primitivesList.get(2)); + } + + @Test + void testSetterWithStringArrayValue() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertNull(cfg.set("primitives.array", new String[] {"zyx", "b", "a"})); + + assertInstanceOf(List.class, cfg.get("primitives.array")); + List primitivesList = (List) cfg.get("primitives.array"); + assert primitivesList != null; + assertEquals(3, primitivesList.size()); + assertEquals("zyx", primitivesList.get(0)); + assertEquals("b", primitivesList.get(1)); + assertEquals("a", primitivesList.get(2)); + } + + @Test + void testHas() { + final YamlConfiguration cfg = new YamlConfiguration(); + + assertFalse(cfg.has(null)); + + assertNull(cfg.set("foo", "bar")); + + assertTrue(cfg.has("foo")); + assertFalse(cfg.has("bar")); + + assertNull(cfg.set("foo.bar", "baz")); + assertTrue(cfg.has("foo.bar")); + + assertFalse(cfg.has("foo.baz")); + assertNull(YamlConfiguration.getInnerMap(cfg.values, new String[] {"foo", "baz"}, false)); + } + + @Test + void testReset() throws IOException { + final YamlConfiguration cfg = new YamlConfiguration(); + cfg.load(new StringReader(inputYaml)); + + cfg.setNodeComment("foo", "bar"); + cfg.setHeaderComment("baz"); + + assertFalse(cfg.values.isEmpty()); + cfg.reset(); + assertTrue(cfg.values.isEmpty()); + + assertNotNull(cfg.getHeaderComment()); + assertNotNull(cfg.getNodeComment("foo")); + } + + @Test + void testUnset() throws IOException { + final YamlConfiguration cfg = new YamlConfiguration(); + cfg.load(new StringReader(inputYaml)); + + Object unsetResult; + + assertTrue(cfg.has("foo")); + unsetResult = cfg.unset("foo"); + assertEquals("bar", unsetResult); + assertFalse(cfg.has("foo")); + + assertTrue(cfg.has("primitives")); + assertTrue(cfg.has("primitives.int")); + assertTrue(cfg.has("primitives.double")); + unsetResult = cfg.unset("primitives.int"); + assertEquals(Integer.MIN_VALUE, unsetResult); + assertFalse(cfg.has("primitives.int")); + assertTrue(cfg.has("primitives.double")); + + unsetResult = cfg.unset("primitives"); + assertInstanceOf(Map.class, unsetResult); + assertFalse(cfg.has("primitives")); + assertFalse(cfg.has("primitives.double")); + assertFalse(cfg.has("primitives.string")); + + unsetResult = cfg.unset("unknown.nested.key"); + assertNull(unsetResult); + unsetResult = cfg.unset("unknown-key"); + assertNull(unsetResult); + } + + @Test + void testToString() throws IOException { + final YamlConfiguration cfg = new YamlConfiguration(); + + String firstToString = cfg.toString(); + + assertTrue(firstToString.contains(YamlConfiguration.class.getSimpleName())); + assertTrue(firstToString.contains(cfg.values.toString())); + + cfg.load(new StringReader(inputYaml)); + + String secondToString = cfg.toString(); + + assertNotEquals(firstToString, secondToString); + assertTrue(secondToString.contains(YamlConfiguration.class.getSimpleName())); + assertTrue(secondToString.contains(cfg.values.toString())); + } + + @Test + void testLoadWithInvalidYaml() { + final YamlConfiguration cfg = new YamlConfiguration(); + + IllegalStateException exception = assertThrowsExactly(IllegalStateException.class, + () -> cfg.load(new StringReader("Hello world"))); + + assertEquals("The YAML file does not have the expected tree structure: java.lang.String", exception.getMessage()); + } + + @Test + void testHeaderComments() throws IOException { + String expectedHeaderComment = "This is a header comment"; + + YamlConfiguration cfg = new YamlConfiguration(); + cfg.setHeaderComment(expectedHeaderComment); + cfg.set("foo", "bar"); + + assertNotNull(cfg.getHeaderComment()); + assertEquals(expectedHeaderComment, cfg.getHeaderComment().get()); + + assertEquals("# " + expectedHeaderComment + "\n\nfoo: bar\n", cfg.toYamlString()); + } + + @Test + void testNodeComments() throws IOException { + String expectedYaml = "# Foo-Comment\n" + + "foo: bar\n" + + "# Level1-Comment\n" + + "level1:\n" + + " level2:\n" + + " # Level3-Comment\n" + + " level3: value\n"; + + YamlConfiguration cfg = new YamlConfiguration(); + cfg.set("foo", "bar"); + cfg.set("level1.level2.level3", "value"); + + cfg.setNodeComment("foo", "Foo-Comment"); + cfg.setNodeComment("level1", "Level1-Comment"); + cfg.setNodeComment("level1.level2.level3", "Level3-Comment"); + + Supplier currentNodeComment = cfg.getNodeComment("foo"); + assertNotNull(currentNodeComment); + assertEquals("Foo-Comment", currentNodeComment.get()); + + currentNodeComment = cfg.getNodeComment("level1"); + assertNotNull(currentNodeComment); + assertEquals("Level1-Comment", currentNodeComment.get()); + + currentNodeComment = cfg.getNodeComment("level1.level2"); + assertNull(currentNodeComment); + + currentNodeComment = cfg.getNodeComment("level1.level2.level3"); + assertNotNull(currentNodeComment); + assertEquals("Level3-Comment", currentNodeComment.get()); + + assertEquals(expectedYaml, cfg.toYamlString()); + } + + private enum TestEnum { + ENUM_VALUE; + + @Override + public String toString() { + return "#toString(): " + super.toString(); + } + } +} diff --git a/Core/src/test/java/com/songoda/core/locale/LocaleFileManagerTest.java b/Core/src/test/java/com/songoda/core/locale/LocaleFileManagerTest.java new file mode 100644 index 00000000..8f5b29fb --- /dev/null +++ b/Core/src/test/java/com/songoda/core/locale/LocaleFileManagerTest.java @@ -0,0 +1,160 @@ +package com.songoda.core.locale; + +import com.songoda.core.http.MockHttpClient; +import com.songoda.core.http.MockHttpResponse; +import org.bukkit.plugin.Plugin; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +class LocaleFileManagerTest { + private final byte[] validIndexFile = ("# This is a comment\n\nen_US.lang\nen.yml\nde.txt\n").getBytes(StandardCharsets.UTF_8); + + private Path testDirectoryPath; + + @BeforeEach + void setUp() throws IOException { + this.testDirectoryPath = Files.createTempDirectory("SongodaCore-LocaleFileManagerTest"); + this.testDirectoryPath.toFile().deleteOnExit(); + } + + @AfterEach + void tearDown() throws IOException { + try (Stream paths = Files.list(this.testDirectoryPath)) { + for (Path path : paths.toArray(Path[]::new)) { + Files.deleteIfExists(path); + } + } + Files.deleteIfExists(this.testDirectoryPath); + } + + @Test + void downloadMissingTranslations_EmptyTargetDir() throws IOException { + Plugin plugin = Mockito.mock(Plugin.class); + Mockito.when(plugin.getDataFolder()).thenReturn(this.testDirectoryPath.toFile()); + + MockHttpClient httpClient = new MockHttpClient(new MockHttpResponse(200, this.validIndexFile)); + LocaleFileManager localeFileManager = new LocaleFileManager(httpClient, "test"); + + List downloadedFiles = localeFileManager.downloadMissingTranslations(plugin.getDataFolder()); + + Assertions.assertSame(3, downloadedFiles.size()); + Assertions.assertEquals("en.yml", downloadedFiles.get(1)); + Assertions.assertEquals("en_US.lang", downloadedFiles.get(0)); + Assertions.assertEquals("de.txt", downloadedFiles.get(2)); + + String[] localeFiles = plugin.getDataFolder().list(); + Assertions.assertNotNull(localeFiles); + Arrays.sort(localeFiles); + Assertions.assertArrayEquals(new String[] {"de.txt", "en.yml", "en_US.lang"}, localeFiles); + + Assertions.assertSame(4, httpClient.callsOnGet.size()); + Assertions.assertTrue(httpClient.callsOnGet.get(0).contains("/test/_index.txt")); + Assertions.assertTrue(httpClient.callsOnGet.get(1).contains("/test/en_US.lang")); + Assertions.assertTrue(httpClient.callsOnGet.get(2).contains("/test/en.yml")); + Assertions.assertTrue(httpClient.callsOnGet.get(3).contains("/test/de.txt")); + } + + @Test + void downloadMissingTranslations() throws IOException { + Plugin plugin = Mockito.mock(Plugin.class); + Mockito.when(plugin.getDataFolder()).thenReturn(this.testDirectoryPath.toFile()); + + Files.createDirectories(plugin.getDataFolder().toPath()); + Files.createFile(new File(plugin.getDataFolder(), "en_US.lang").toPath()); + Files.createFile(new File(plugin.getDataFolder(), "fr.lang").toPath()); + + MockHttpClient httpClient = new MockHttpClient(new MockHttpResponse(200, this.validIndexFile)); + LocaleFileManager localeFileManager = new LocaleFileManager(httpClient, "test"); + + List downloadedFiles = localeFileManager.downloadMissingTranslations(plugin.getDataFolder()); + + Assertions.assertSame(2, downloadedFiles.size()); + Assertions.assertEquals("en.yml", downloadedFiles.get(0)); + Assertions.assertEquals("de.txt", downloadedFiles.get(1)); + + String[] localeFiles = plugin.getDataFolder().list(); + + Assertions.assertNotNull(localeFiles); + Arrays.sort(localeFiles); + Assertions.assertArrayEquals(new String[] {"de.txt", "en.yml", "en_US.lang", "fr.lang"}, localeFiles); + + Assertions.assertSame(3, httpClient.callsOnGet.size()); + Assertions.assertTrue(httpClient.callsOnGet.get(0).contains("/test/_index.txt")); + Assertions.assertTrue(httpClient.callsOnGet.get(1).contains("/test/en.yml")); + Assertions.assertTrue(httpClient.callsOnGet.get(2).contains("/test/de.txt")); + } + + @Test + void fetchAvailableLanguageFiles() throws IOException { + MockHttpClient httpClient = new MockHttpClient(new MockHttpResponse(200, this.validIndexFile)); + LocaleFileManager localeFileManager = new LocaleFileManager(httpClient, "test"); + + List availableLanguages = localeFileManager.fetchAvailableLanguageFiles(); + + Assertions.assertSame(1, httpClient.callsOnGet.size()); + Assertions.assertTrue(httpClient.callsOnGet.get(0).contains("/test/")); + + Assertions.assertNotNull(availableLanguages); + Assertions.assertSame(3, availableLanguages.size()); + Assertions.assertTrue(availableLanguages.contains("en_US.lang")); + Assertions.assertTrue(availableLanguages.contains("en.yml")); + Assertions.assertTrue(availableLanguages.contains("de.txt")); + } + + @Test + void fetchAvailableLanguageFiles_SpecialCharsInProjectName() throws IOException { + MockHttpClient httpClient = new MockHttpClient(new MockHttpResponse(200, this.validIndexFile)); + LocaleFileManager localeFileManager = new LocaleFileManager(httpClient, "test project (special)"); + + List availableLanguages = localeFileManager.fetchAvailableLanguageFiles(); + + Assertions.assertSame(1, httpClient.callsOnGet.size()); + Assertions.assertTrue(httpClient.callsOnGet.get(0).contains("/test+project+%28special%29/")); + + Assertions.assertNotNull(availableLanguages); + Assertions.assertSame(3, availableLanguages.size()); + Assertions.assertTrue(availableLanguages.contains("en_US.lang")); + Assertions.assertTrue(availableLanguages.contains("en.yml")); + Assertions.assertTrue(availableLanguages.contains("de.txt")); + } + + @Test + void fetchAvailableLanguageFiles_EmptyIndex() throws IOException { + MockHttpClient httpClient = new MockHttpClient(new MockHttpResponse(200, new byte[0])); + LocaleFileManager localeFileManager = new LocaleFileManager(httpClient, "empty-project"); + + List availableLanguages = localeFileManager.fetchAvailableLanguageFiles(); + + Assertions.assertNotNull(availableLanguages); + Assertions.assertTrue(availableLanguages.isEmpty()); + } + + @Test + void fetchAvailableLanguageFiles_UnknownProject() throws IOException { + MockHttpClient httpClient = new MockHttpClient(new MockHttpResponse(404, new byte[0])); + LocaleFileManager localeFileManager = new LocaleFileManager(httpClient, "unknown"); + + Assertions.assertNull(localeFileManager.fetchAvailableLanguageFiles()); + } + + @Test + void fetchAvailableLanguageFiles_HttpStatus500() { + MockHttpClient httpClient = new MockHttpClient(new MockHttpResponse(500, new byte[0])); + LocaleFileManager localeFileManager = new LocaleFileManager(httpClient, "test"); + + Assertions.assertThrows(IOException.class, localeFileManager::fetchAvailableLanguageFiles); + } +}