Revert all the SongodaYamlConfig related commits

Revert "Ensures usage of UTF-8 in SongodaYamlConfig"

This reverts commit 339a4d6f6c.

Revert "Improve test coverage + stability of configuration.yaml/songoda classes"

This reverts commit ef6c37b80c.

Revert "Adds ConfigEntry#withDefaultValue for easier chaining"

This reverts commit 88e28689f7.

Revert "Code cleanup (rename e->ex in catch; better type for #withUpgradeStep)"

This reverts commit 7eff3c86ec.

Revert "Rename constant into upper case to match code conventions"

This reverts commit 4d194ed92b.

Revert "Fix typo in JavaDoc"

This reverts commit 0b2a253014.

Revert "Makes SongodaYamlConfig#cannotCreateBackupCopyExceptionPrefix static"

This reverts commit 8e91cc18eb.

Revert "Make unit tests in LocaleFileManagerTest deterministic"

This reverts commit 67a69e34e8.

Revert "Add unit test for SongodaYamlConfig persisting comments on key-upgrades"

This reverts commit d710b2d2d5.

Revert "Improve temporary file deletion in YamlConfig and FileManager tests"

This reverts commit 02330b5ca7.

Revert "Adds hyphen before timestamp in file name, when creating backup YamlCfg"

This reverts commit f8b3942de2.

Revert "Fix YamlConfiguration not dumping comments"

This reverts commit e7da328dc6.

Revert "Provisional first implementation of the new localization system"

This reverts commit b168ad0738.

Revert "Fix error handling of SongodaYamlConfig#load(Reader)"

This reverts commit 163e4d9eaf.

Partially reverts "Adds some deprecation notices to configuration.editor classes"

This partially reverts commit eea951ecc6.

Revert "Redo ConfigEntry abstraction"

This reverts commit 20b44327e0.

Revert "Migrate CustomizableGui from old Config to SongodaYamlConfig"

This reverts commit d5ddde3e08.

Revert "Adds SongodaYamlConfig#getAsEntry(String) for convenience"

This reverts commit 20b7a353b8.

Revert "Add contract to `ConfigEntry#getString(String)` for non-null-argument"

This reverts commit 78b6039d39.

Revert "Adds getter to ConfigEntry for List<String>"

This reverts commit 3a09c19dbb.

Revert "Remove usage of Locale classes"

This reverts commit da3c89450e.

Revert "Mark overwritten and empty config methods in SongodaPlugin as deprecated"

This reverts commit 73685b62dd.

Revert "Adjust log levels in SongodaYamlConfig"

This reverts commit 7ef00bb8f9.

Revert "Fix SongodaYamlConfig not creating parent directory when saving"

This reverts commit b0f006aed0.

Revert "Fix SongodaYamlConfigTest leaving created backup files in tmp dir"

This reverts commit c9a48387de.

Revert "Remove Config related methods in SongodaPlugin"

This reverts commit fce5c5c6a1.

Revert "Introduce new SongodaYamlConfig and ConfigEntry classes"

This reverts commit eb10b3f70a.

Revert "Fix YamlConfiguration dumping null values and empty tree nodes"

This reverts commit 02ab8d4bb2.

Revert "Fix `YamlConfiruration#getKeys("")` not returning root node keys"

This reverts commit 885cc9a87e.

Revert "Fix exception on loading empty file in YamlConfiguration"

This reverts commit 2683bc12c0.

Revert "Make YamlConfiguration insertion-sorted"

This reverts commit 2262652577.

Revert "Rename `IConfiguration#getOrDefault` to `#getOr`"

This reverts commit f6e207cdda.

Revert "Adds Enum support to YamlConfiguration class (#41)"

This reverts commit 41bd5c633a.

Revert "Removes the default implementations for #save(File) and #load(File)"

This reverts commit 8f15df3601.

Revert "Replace Songoda's YAML Configuration wrapper with an own implementation"

This reverts commit 6d6fa7210a.
This commit is contained in:
Christian Koop 2022-04-27 21:40:21 +02:00
parent 748f10b77b
commit c725ea69d6
No known key found for this signature in database
GPG Key ID: 89A8181384E010A3
36 changed files with 3223 additions and 3550 deletions

View File

@ -3,6 +3,7 @@ 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;
@ -233,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));
}

View File

@ -1,16 +1,16 @@
package com.songoda.core;
import com.songoda.core.configuration.songoda.SongodaYamlConfig;
import com.songoda.core.configuration.Config;
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;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
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,7 +21,8 @@ import java.util.logging.Level;
* Must not have two instances of Metrics enabled!
*/
public abstract class SongodaPlugin extends JavaPlugin {
// protected Locale locale;
protected Locale locale;
protected Config config = new Config(this);
protected long dataLoadDelay = 20L;
private boolean emergencyStop = false;
@ -41,15 +42,37 @@ public abstract class SongodaPlugin extends JavaPlugin {
public abstract void onDataLoad();
/**
* All the configuration files belonging to the plugin.<br>
* This might for example be used for the ingame config editor.<br>
* <br>
* Do not include *storage* files here or anything similar that does not intend external modification and access.<br>
* <br>
* Do not include language files if you are using the Core's localization system.
* Called after reloadConfig() is called
*/
public abstract @NotNull List<SongodaYamlConfig> getConfigs();
/**
* 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<Config> 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 {
@ -103,7 +126,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();
@ -157,32 +180,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
@ -243,29 +266,4 @@ 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() {
}
}

View File

@ -0,0 +1,114 @@
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<String> lines = new ArrayList<>();
CommentStyle commentStyle = null;
public Comment() {
}
public Comment(String... lines) {
this(null, Arrays.asList(lines));
}
public Comment(List<String> lines) {
this(null, lines);
}
public Comment(CommentStyle commentStyle, String... lines) {
this(commentStyle, Arrays.asList(lines));
}
public Comment(CommentStyle commentStyle, List<String> 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<String> getLines() {
return lines;
}
@Override
public String toString() {
return lines.isEmpty() ? "" : String.join("\n", lines);
}
public static Comment loadComment(List<String> 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");
}
}
}

View File

@ -0,0 +1,793 @@
package com.songoda.core.configuration;
import com.songoda.core.utils.TextUtils;
import org.apache.commons.lang.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<String, Object> serialize();
// Class must contain one of:
// public static Object deserialize(@NotNull Map<String, ?> args);
// public static valueOf(Map<String, ?> args);
// public new (Map<String, ?> 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. <br>
* 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? <br>
* 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. <br>
* 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. <br>
* 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. <br>
* This is separate from rootNodeSpacing, if applicable.
*/
public int getCommentSpacing() {
return commentSpacing;
}
/**
* Extra lines to put in front of comments. <br>
* 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<String> 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<String> description) {
if (description == null || description.isEmpty()) {
headerComment = null;
} else {
headerComment = new Comment(commentStyle, description);
}
return this;
}
@NotNull
public List<String> 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<String> currentPath = new LinkedList<>();
ArrayList<String> 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<String> 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<String> 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();
}
}
}

View File

@ -1,189 +0,0 @@
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<String> comment);
/**
* @return <code>&lt;configVersion, Pair&lt;keyInGivenVersion, valueConverter&gt;&gt;</code>
*/
@Nullable Map<Integer, Pair<@Nullable String, @Nullable Function<@Nullable Object, @Nullable Object>>> 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.<br>
* 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<String> getStringList() {
return getStringListOr(null);
}
@Contract("!null -> !null")
default @Nullable List<String> getStringListOr(List<String> fallbackValue) {
Object value = get();
if (value instanceof List) {
//noinspection unchecked
return (List<String>) 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);
}
}

View File

@ -0,0 +1,272 @@
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<String> getKeys(boolean deep) {
return config.getKeys(deep);
}
@Override
public Map<String, Object> 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<String> 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<String> 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<String> 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<ConfigSection> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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);
}
}

View File

@ -0,0 +1,96 @@
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, " ", ""),
/**
* # <br />
* # Comment <br />
* # <br />
*/
SPACED(false, true, " ", ""),
/**
* ########### <br />
* # Comment # <br />
* ########### <br />
*/
BLOCKED(true, false, " ", " "),
/**
* ############# <br />
* #|¯¯¯¯¯¯¯¯¯|# <br />
* #| Comment |# <br />
* #|_________|# <br />
* ############# <br />
*/
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<String> 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;
}
}

View File

@ -0,0 +1,72 @@
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;
}
}

View File

@ -0,0 +1,761 @@
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<String, Comment> configComments;
final HashMap<String, Comment> defaultComments;
final LinkedHashMap<String, Object> defaults;
final LinkedHashMap<String, Object> 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. <br>
* 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. <br />
* <b>DO NOT USE THIS IN A SYNCHRONIZED LOCK</b>
*
* @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<String, Object> 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<String> 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<String> 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<String> 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<String, Object> 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<String> getKeys(boolean deep) {
LinkedHashSet<String> 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<String, Object> getValues(boolean deep) {
LinkedHashMap<String, Object> result = new LinkedHashMap<>();
int pathIndex = fullPath.lastIndexOf(root.pathChar);
if (deep) {
result.putAll((Map<String, Object>) 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<String, Object>) 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<String, Object>) 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<String, Object>) 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<ConfigSection> getSections(String path) {
ConfigSection rootSection = getConfigurationSection(path);
if (rootSection == null) {
return Collections.emptyList();
}
ArrayList<ConfigSection> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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> T getObject(@NotNull String path, @NotNull Class<T> clazz) {
Object result = get(path);
return clazz.isInstance(result) ? clazz.cast(result) : null;
}
@Nullable
@Override
public <T> T getObject(@NotNull String path, @NotNull Class<T> 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);
}
}

View File

@ -0,0 +1,135 @@
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<Integer> getIntegerList() {
return config.getIntegerList(key);
}
public List<String> 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> T getObject(@NotNull Class<T> clazz) {
return config.getObject(key, clazz);
}
public <T> T getObject(@NotNull Class<T> 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;
}
}

View File

@ -0,0 +1,30 @@
package com.songoda.core.configuration;
import org.bukkit.configuration.ConfigurationSection;
public interface DataStoreObject<T> {
/**
* @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);
}

View File

@ -1,18 +0,0 @@
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<String> comment);
default void setHeaderComment(@Nullable String comment) {
setHeaderComment(() -> comment);
}
@Nullable Supplier<String> getHeaderComment();
@NotNull String generateHeaderCommentLines();
}

View File

@ -1,72 +0,0 @@
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;
}

View File

@ -1,16 +0,0 @@
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<String> comment);
default void setNodeComment(@NotNull String key, @Nullable String comment) {
setNodeComment(key, () -> comment);
}
@Nullable Supplier<String> getNodeComment(@Nullable String key);
}

View File

@ -1,72 +0,0 @@
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<String> comment) {
throw new UnsupportedOperationException("Cannot set comment on a read-only config entry");
}
@Override
@Contract(" -> null")
public Map<Integer, Pair<String, Function<Object, Object>>> getUpgradeSteps() {
return null;
}
@Override
@Contract("_, _, _ -> fail")
public ConfigEntry withUpgradeStep(int version, @Nullable String keyInGivenVersion, @Nullable Function<Object, Object> 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");
}
}

View File

@ -0,0 +1,284 @@
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 <T> DataObject class that is used to store the data
*/
public class SimpleDataStore<T extends DataStoreObject> {
protected final Plugin plugin;
protected final String filename, dirName;
private final Function<ConfigurationSection, T> getFromSection;
protected final HashMap<Object, T> 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<ConfigurationSection, T> 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<ConfigurationSection, T> 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<Object, T> 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 <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
*/
@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 <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
*/
@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 <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
*/
@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 <tt>value.getKey()</tt>, or
* <tt>null</tt> if there was no mapping for <tt>value.getKey()</tt>.
*/
@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<T> 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();
}
}
}

View File

@ -1,19 +0,0 @@
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<String> comment) {
((NodeCommentable) getConfig()).setNodeComment(getKey(), comment);
return this;
}
}

View File

@ -1,6 +1,7 @@
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;
@ -26,10 +27,7 @@ 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;
@ -275,9 +273,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;

View File

@ -12,10 +12,7 @@ import java.util.List;
/**
* Edit a string list
*
* @deprecated Needs a recode in another package
*/
@Deprecated
public class ConfigEditorListEditorGui extends SimplePagedGui {
final ConfigEditorGui current;

View File

@ -2,6 +2,7 @@ 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;
@ -18,10 +19,7 @@ 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<String, MemoryConfiguration> configs = new LinkedHashMap<>();
@ -35,18 +33,14 @@ 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<Config> more = plugin.getConfigs();
// if (more != null && !more.isEmpty()) {
// for (Config cfg : more) {
// configs.put(cfg.getFile().getName(), cfg);
// }
// }
configs.put(plugin.getCoreConfig().getFile().getName(), plugin.getCoreConfig());
List<Config> more = plugin.getExtraConfig();
if (more != null && !more.isEmpty()) {
for (Config cfg : more) {
configs.put(cfg.getFile().getName(), cfg);
}
}
init();
}

View File

@ -1,245 +0,0 @@
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<String, ConfigEntry> 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()}.<br>
* <br>
* As this is intended to keep the {@link org.bukkit.plugin.java.JavaPlugin#onEnable()} method clean,
* it catches all exceptions and logs them instead.<br>
* <br>
* 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<String> 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<Object, Object>> 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);
}
}
}

View File

@ -1,73 +0,0 @@
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<String, Supplier<String>> nodeComments;
public YamlCommentRepresenter(DumperOptions dumperOptions, Map<String, Supplier<String>> 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<String> innerValue = this.nodeComments.get(key);
if (innerValue != null) {
scalarNode.setBlockComments(Collections.singletonList(new CommentLine(new CommentEvent(CommentType.BLOCK, " " + innerValue.get(), null, null))));
}
}
}

View File

@ -1,90 +0,0 @@
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<Integer, Pair<@Nullable String, @Nullable Function<@Nullable Object, @Nullable Object>>> 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<Integer, Pair<@Nullable String, @Nullable Function<@Nullable Object, @Nullable Object>>> 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);
}
}

View File

@ -1,371 +0,0 @@
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<String, Object> values;
protected final @NotNull Map<String, Supplier<String>> nodeComments;
protected @Nullable Supplier<String> headerComment;
public YamlConfiguration() {
this(new LinkedHashMap<>(), new LinkedHashMap<>());
}
protected YamlConfiguration(@NotNull Map<String, Object> values, @NotNull Map<String, Supplier<String>> 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<String, ?> 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<String> getKeys(String key) {
if (key == null) {
return Collections.emptySet();
}
if (key.equals("")) {
return Collections.unmodifiableSet(this.values.keySet());
}
Map<String, ?> 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<Integer> newValue = new ArrayList<>(((short[]) value).length);
for (Short s : (short[]) value) {
newValue.add(s.intValue());
}
value = newValue;
} else if (value instanceof byte[]) {
List<Integer> 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<Double> 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<String> 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<String, ?> 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<String> comment) {
this.headerComment = comment;
}
@Override
public @Nullable Supplier<String> 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<String> comment) {
this.nodeComments.put(key, comment);
}
@Override
public @Nullable Supplier<String> 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<String, Object> map, @NotNull String key, @Nullable Object value) {
String[] fullKeyPath = key.split("\\.");
Map<String, ?> innerMap = getInnerMap(map, Arrays.copyOf(fullKeyPath, fullKeyPath.length - 1), true);
return ((Map<String, Object>) innerMap).put(fullKeyPath[fullKeyPath.length - 1], value);
}
protected static Object getInnerValueForKey(@NotNull Map<String, Object> map, @NotNull String key) {
String[] fullKeyPath = key.split("\\.");
Map<String, ?> 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<String, ?> getInnerMap(@NotNull Map<String, ?> map, @NotNull String[] keys, boolean createMissingMaps) {
if (keys.length == 0) {
return map;
}
int currentKeyIndex = 0;
Map<String, ?> currentMap = map;
while (true) {
Object currentValue = currentMap.get(keys[currentKeyIndex]);
if (currentValue == null) {
if (!createMissingMaps) {
return null;
}
currentValue = new HashMap<>();
((Map<String, Object>) 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<String, Object>) currentMap).put(keys[currentKeyIndex], currentValue);
}
if (currentKeyIndex == keys.length - 1) {
return (Map<String, ?>) currentValue;
}
currentMap = (Map<String, ?>) currentValue;
++currentKeyIndex;
}
}
/**
* This takes a map and removes all keys that have a value of null.<br>
* Additionally, if the value is a {@link Map}, it will be recursively cleaned too.<br>
* {@link Map}s that are or get empty, will be removed (recursively).<br>
*/
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);
}
}
}
}

View File

@ -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();
}
}

View File

@ -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.ConfigEntry;
import com.songoda.core.configuration.songoda.SongodaYamlConfig;
import com.songoda.core.configuration.Config;
import com.songoda.core.configuration.ConfigSection;
import com.songoda.core.gui.methods.Clickable;
import com.songoda.core.utils.TextUtils;
import org.bukkit.Bukkit;
@ -16,7 +16,6 @@ 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;
@ -46,89 +45,60 @@ public class CustomizableGui extends Gui {
localeFolder.mkdir();
}
SongodaYamlConfig config = new SongodaYamlConfig(new File(plugin.getDataFolder(), "gui/" + guiKey + ".yml"));
try {
config.load();
} catch (IOException ex) {
// FIXME
throw new RuntimeException(ex);
}
Config config = new Config(plugin, "gui/" + guiKey + ".yml");
config.load();
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.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.");
config.saveChanges();
}
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.");
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.");
try {
config.save();
} catch (IOException ex) {
// FIXME
throw new RuntimeException(ex);
config.saveChanges();
}
CustomContent customContent = loadedGuis.computeIfAbsent(guiKey, g -> new CustomContent(guiKey));
loadedGuis.put(guiKey, customContent);
this.customContent = customContent;
int rows = config.getReadEntry("overrides.__ROWS__").getIntOr(-1);
int rows = config.getInt("overrides.__ROWS__", -1);
if (rows != -1) {
customContent.setRows(rows);
}
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());
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);
}
} else {
customContent.addButton(overrideKey,
position.getStringOr("-1"),
title.getString(),
lore.getStringList(),
item.getMaterial());
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);
}
}
for (String disabled : disabledGuis.getStringListOr(Collections.emptyList())) {
for (String disabled : config.getStringList("disabled")) {
customContent.disableButton(disabled);
}
} else {

View File

@ -1,496 +1,495 @@
//
//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<String, String> 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<Locale> loadAllLocales(JavaPlugin plugin) {
// File localeFolder = new File(plugin.getDataFolder(), "locales/");
// List<Locale> 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<String> getLocales(Plugin plugin) {
// File localeFolder = new File(plugin.getDataFolder(), "locales/");
// List<String> 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<String> 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<String> 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<String> 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<String, String> 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<Locale> loadAllLocales(JavaPlugin plugin) {
File localeFolder = new File(plugin.getDataFolder(), "locales/");
List<Locale> 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<String> getLocales(Plugin plugin) {
File localeFolder = new File(plugin.getDataFolder(), "locales/");
List<String> 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<String> 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<String> 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<String> 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();
}
}
}

View File

@ -1,102 +0,0 @@
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<String> downloadMissingTranslations(File targetDirectory) throws IOException {
List<String> availableLanguages = this.fetchAvailableLanguageFiles();
if (availableLanguages == null) {
return Collections.emptyList();
}
Files.createDirectories(targetDirectory.toPath());
List<String> 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<String> fetchAvailableLanguageFiles() throws IOException {
String projectLanguageIndex = fetchProjectFile("_index.txt");
if (projectLanguageIndex == null) {
return null;
}
List<String> 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);
}
}

View File

@ -1,115 +0,0 @@
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<SongodaYamlConfig> 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<String> 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<String> 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;
}
}

View File

@ -1,19 +0,0 @@
package com.songoda.core.utils;
public class Pair<T, U> {
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;
}
}

View File

@ -1,49 +0,0 @@
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());
}
}

View File

@ -1,114 +0,0 @@
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<Path> 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)));
}
}

View File

@ -1,246 +0,0 @@
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<Path> 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));
}
}

View File

@ -1,258 +0,0 @@
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<String> 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");
}
}

View File

@ -1,613 +0,0 @@
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<String, Object> expectedValues = new HashMap<String, Object>() {{
put("number", 27);
put("foo", new HashMap<String, Object>() {{
put("bar", "baz");
}});
put("bar", new HashMap<String, Object>() {{
put("foo", new HashMap<String, Object>() {{
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<String> 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();
}
}
}

View File

@ -1,160 +0,0 @@
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<Path> 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<String> 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<String> 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<String> 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<String> 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<String> 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);
}
}