Converted ConfigData and started others

- Bumped Spigot version to 1.18.1, so we get config comment support natively
- Made getters perform a basic casting by default, this was done by the
ConfigData code before
This commit is contained in:
Norbi Peti 2023-03-04 19:50:49 +01:00
parent 89b7246f4f
commit be5f9ded60
6 changed files with 137 additions and 383 deletions

View file

@ -169,7 +169,7 @@
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.12.2-R0.1-SNAPSHOT</version>
<version>1.18.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -18,7 +18,7 @@ import java.util.function.Consumer
@HasConfig(global = true)
abstract class ButtonPlugin : JavaPlugin() {
protected val iConfig = IHaveConfig { saveConfig() }
private var yaml: CommentedConfiguration? = null
private var yaml: YamlConfiguration? = null
protected val data //TODO
: IHaveConfig? = null
@ -39,7 +39,7 @@ abstract class ButtonPlugin : JavaPlugin() {
*/
protected fun pluginPreDisable() {}
override fun onEnable() {
if (!loadConfig()) {
if (!reloadIConfig()) {
logger.warning("Please fix the issues and restart the server to load the plugin.")
return
}
@ -52,12 +52,12 @@ abstract class ButtonPlugin : JavaPlugin() {
IHaveConfig.pregenConfig(this, null)
}
private fun loadConfig(): Boolean {
val config = config ?: return false
private fun reloadIConfig(): Boolean {
val config = config
var section = config.getConfigurationSection("global")
if (section == null) section = config.createSection("global")
iConfig.reset(section)
return true
return configLoaded // If loading fails, getConfig() returns a temporary instance
}
override fun onDisable() {
@ -78,7 +78,7 @@ abstract class ButtonPlugin : JavaPlugin() {
fun tryReloadConfig(): Boolean {
if (!justReload()) return false
loadConfig()
reloadIConfig()
componentStack.forEach(Consumer { c: Component<*>? -> updateConfig(this, c!!) })
return true
}
@ -89,7 +89,7 @@ abstract class ButtonPlugin : JavaPlugin() {
return false
}
val file = File(dataFolder, "config.yml")
val yaml = CommentedConfiguration(file)
val yaml = YamlConfiguration()
if (file.exists()) {
try {
yaml.load(file)
@ -102,16 +102,17 @@ abstract class ButtonPlugin : JavaPlugin() {
e.printStackTrace()
return false
}
this.yaml = yaml
} else {
return false
}
this.yaml = yaml
val res = getTextResource("configHelp.yml") ?: return true
val yc = YamlConfiguration.loadConfiguration(res)
for ((key, value) in yc.getValues(true)) if (value is String) yaml.addComment(key.replace(
".generalDescriptionInsteadOfAConfig",
""
),
*value.split("\n").map { str -> "# " + str.trim { it <= ' ' } }.toTypedArray()
)
for ((key, value) in yc.getValues(true))
if (value is String) yaml.setComments(
key.replace(".generalDescriptionInsteadOfAConfig", ""),
value.split("\n").map { str -> "# " + str.trim { it <= ' ' } }
)
return true
}
@ -121,13 +122,11 @@ abstract class ButtonPlugin : JavaPlugin() {
}
override fun saveConfig() {
try {
if (yaml != null) yaml!!.save()
} catch (e: Exception) {
TBMCCoreAPI.SendException("Failed to save config", e, this)
}
if (configLoaded) super.saveConfig()
}
val configLoaded get() = yaml != null
/**
* Registers command and sets its plugin.
*

View file

@ -1,229 +0,0 @@
package buttondevteam.lib.architecture;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.configuration.file.YamlConstructor;
import org.bukkit.configuration.file.YamlRepresenter;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.representer.Representer;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.HashMap;
/**
* A copy of Towny's CommentedConfiguration: https://github.com/TownyAdvanced/Towny/blob/master/src/com/palmergames/bukkit/config/CommentedConfiguration.java
* Modified to remove dependency on the FileMgmt class
*
* @author dumptruckman &amp; Articdive
*/
public class CommentedConfiguration extends YamlConfiguration {
private HashMap<String, String> comments;
private File file;
private final DumperOptions yamlOptions = new DumperOptions();
private final Representer yamlRepresenter = new YamlRepresenter();
private final Yaml yaml = new Yaml(new YamlConstructor(), yamlRepresenter, yamlOptions);
public CommentedConfiguration(File file) {
super();
comments = new HashMap<>();
this.file = file;
}
public boolean load() {
boolean loaded = true;
try {
this.load(file);
} catch (InvalidConfigurationException | IOException e) {
loaded = false;
}
return loaded;
}
public void save() throws IOException {
boolean saved = true;
// Save the config just like normal
try {
this.save(file);
} catch (Exception e) {
saved = false;
}
// if there's comments to add and it saved fine, we need to add comments
if (!comments.isEmpty() && saved) {
// String array of each line in the config file
String[] yamlContents = Files.readAllLines(file.toPath()).toArray(new String[0]);
// This will hold the newly formatted line
StringBuilder newContents = new StringBuilder();
// This holds the current path the lines are at in the config
String currentPath = "";
// This flags if the line is a node or unknown text.
boolean node;
// The depth of the path. (number of words separated by periods - 1)
int depth = 0;
// Loop through the config lines
for (String line : yamlContents) {
// If the line is a node (and not something like a list value)
if (line.contains(": ") || (line.length() > 1 && line.charAt(line.length() - 1) == ':')) {
// This is a node so flag it as one
node = true;
// Grab the index of the end of the node name
int index;
index = line.indexOf(": ");
if (index < 0) {
index = line.length() - 1;
}
// If currentPath is empty, store the node name as the currentPath. (this is only on the first iteration, i think)
if (currentPath.isEmpty()) {
currentPath = line.substring(0, index);
} else {
// Calculate the whitespace preceding the node name
int whiteSpace = 0;
for (int n = 0; n < line.length(); n++) {
if (line.charAt(n) == ' ') {
whiteSpace++;
} else {
break;
}
}
// Find out if the current depth (whitespace * 2) is greater/lesser/equal to the previous depth
if (whiteSpace / 2 > depth) {
// Path is deeper. Add a . and the node name
currentPath += "." + line.substring(whiteSpace, index);
depth++;
} else if (whiteSpace / 2 < depth) {
// Path is shallower, calculate current depth from whitespace (whitespace / 2) and subtract that many levels from the currentPath
int newDepth = whiteSpace / 2;
for (int i = 0; i < depth - newDepth; i++) {
currentPath = currentPath.replace(currentPath.substring(currentPath.lastIndexOf(".")), "");
}
// Grab the index of the final period
int lastIndex = currentPath.lastIndexOf(".");
if (lastIndex < 0) {
// if there isn't a final period, set the current path to nothing because we're at root
currentPath = "";
} else {
// If there is a final period, replace everything after it with nothing
currentPath = currentPath.replace(currentPath.substring(currentPath.lastIndexOf(".")), "");
currentPath += ".";
}
// Add the new node name to the path
currentPath += line.substring(whiteSpace, index);
// Reset the depth
depth = newDepth;
} else {
// Path is same depth, replace the last path node name to the current node name
int lastIndex = currentPath.lastIndexOf(".");
if (lastIndex < 0) {
// if there isn't a final period, set the current path to nothing because we're at root
currentPath = "";
} else {
// If there is a final period, replace everything after it with nothing
currentPath = currentPath.replace(currentPath.substring(currentPath.lastIndexOf(".")), "");
currentPath += ".";
}
//currentPath = currentPath.replace(currentPath.substring(currentPath.lastIndexOf(".")), "");
currentPath += line.substring(whiteSpace, index);
}
}
} else {
node = false;
}
if (node) {
// If there's a comment for the current path, retrieve it and flag that path as already commented
String comment = comments.get(currentPath);
if (comment != null) {
// Add the comment to the beginning of the current line
line = comment + System.getProperty("line.separator") + line + System.getProperty("line.separator");
} else {
// Add a new line as it is a node, but has no comment
line += System.getProperty("line.separator");
}
}
// Add the (modified) line to the total config String
if (!node) {
newContents.append(line).append(System.getProperty("line.separator"));
} else {
newContents.append(line);
}
}
/*
* Due to a Bukkit Bug with the Configuration
* we just need to remove any extra comments at the start of a file.
*/
while (newContents.toString().startsWith(" " + System.getProperty("line.separator"))) {
newContents = new StringBuilder(newContents.toString().replaceFirst(" " + System.getProperty("line.separator"), ""));
}
Files.write(file.toPath(), newContents.toString().getBytes(StandardCharsets.UTF_8));
}
}
/**
* Adds a comment just before the specified path. The comment can be
* multiple lines. An empty string will indicate a blank line.
*
* @param path Configuration path to add comment.
* @param commentLines Comments to add. One String per line.
*/
public void addComment(String path, String... commentLines) {
StringBuilder commentstring = new StringBuilder();
StringBuilder leadingSpaces = new StringBuilder();
for (int n = 0; n < path.length(); n++) {
if (path.charAt(n) == '.') {
leadingSpaces.append(" ");
}
}
for (String line : commentLines) {
if (!line.isEmpty()) {
line = leadingSpaces + line;
} else {
line = " ";
}
if (commentstring.length() > 0) {
commentstring.append(System.getProperty("line.separator"));
}
commentstring.append(line);
}
comments.put(path, commentstring.toString());
}
@Override
public String saveToString() {
yamlOptions.setIndent(options().indent());
yamlOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
yamlOptions.setWidth(10000);
yamlRepresenter.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
String dump = yaml.dump(getValues(false));
if (dump.equals(BLANK_CONFIG)) {
dump = "";
}
return dump;
}
}

View file

@ -6,7 +6,6 @@ import lombok.*
import org.bukkit.Bukkit
import org.bukkit.configuration.Configuration
import org.bukkit.scheduler.BukkitTask
import java.util.function.BiFunction
import java.util.function.Function
/**
@ -21,10 +20,10 @@ open class ConfigData<T> internal constructor(
val path: String,
def: T?,
primitiveDef: Any?,
private val getter: Function<Any?, T>?,
private val setter: Function<T, Any?>?
private val getter: Function<Any?, T>,
private val setter: Function<T, Any?>
) {
private val def: Any?
private val pdef: Any?
/**
* The config value should not change outside this instance
@ -32,9 +31,8 @@ open class ConfigData<T> internal constructor(
private var value: T? = null
init {
this.def = primitiveDef ?: def?.let { setter?.apply(it) }
this.pdef = primitiveDef ?: def?.let { setter.apply(it) }
?: throw IllegalArgumentException("Either def or primitiveDef must be set. A getter and setter must be present when using primitiveDef.")
require(getter == null == (setter == null)) { "Both setters and getters must be present (or none if def is primitive)." }
get() //Generate config automatically
}
@ -50,67 +48,46 @@ open class ConfigData<T> internal constructor(
if (value != null) return value //Speed things up
val config = config?.config
var `val`: Any?
if (config == null || !config.isSet(path)) { //Call set() if config == null
`val` = primitiveDef // TODO: primitiveDef --> def, def --> getter(primitiveDef)
if ((def == null || this is ReadOnlyConfigData<*>) && config != null) //In Discord's case def may be null
setInternal(primitiveDef) //If read-only then we still need to save the default value so it can be set
else set(def) //Save default value - def is always set
if (config == null || !config.isSet(path)) {
`val` = pdef
setInternal(pdef) // Save default value even if read-only
} else `val` = config.get(path) //config==null: testing
if (`val` == null) //If it's set to null explicitly
`val` = primitiveDef
val convert = BiFunction { _val: Any?, _def: Any? ->
if (_def is Number) //If we expect a number
_val = if (_val is Number) ChromaUtils.convertNumber(
_val as Number?,
_def.javaClass as Class<out Number?>
) else _def //If we didn't get a number, return default (which is a number)
else if (_val is List<*> && _def != null && _def.javaClass.isArray) _val = (_val as List<T>).toArray<T>(
java.lang.reflect.Array.newInstance(
_def.javaClass.componentType,
0
) as Array<T>
)
_val
}
if (getter != null) {
`val` = convert.apply(`val`, primitiveDef)
var hmm: T? = getter.apply(`val`)
if (hmm == null) hmm = def //Set if the getter returned null
return hmm
}
`val` = convert.apply(`val`, def)
return `val` as T?. also {
value = it //Always cache, if not cached yet
`val` = pdef
fun convert(_val: Any?, _pdef: Any?): Any? {
return if (_pdef is Number) //If we expect a number
if (_val is Number)
ChromaUtils.convertNumber(_val as Number?, _pdef.javaClass as Class<out Number?>)
else _pdef //If we didn't get a number, return default (which is a number)
else if (_val is List<*> && _pdef != null && _pdef.javaClass.isArray)
_val.toTypedArray()
else _val
}
return getter.apply(convert(`val`, pdef)).also { value = it }
}
fun set(value: T?) {
if (this is ReadOnlyConfigData<*>) return //Safety for Discord channel/role data
val `val`: Any?
`val` = if (setter != null && value != null) setter.apply(value) else value
if (config!!.getConfig<Any>() != null) setInternal(`val`)
val `val` = value?.let { setter.apply(value) }
setInternal(`val`)
this.value = value
}
private fun setInternal(`val`: Any?) {
config!!.getConfig<Any>().set(path, `val`)
if (config == null) return
config.config.set(path, `val`)
signalChange(config)
}
@AllArgsConstructor
private class SaveTask {
var task: BukkitTask? = null
var saveAction: Runnable? = null
}
private class SaveTask(val task: BukkitTask, val saveAction: Runnable)
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
class ConfigDataBuilder<T> {
private val config: IHaveConfig? = null
private val path: String? = null
class ConfigDataBuilder<T> internal constructor(private val config: IHaveConfig, private val path: String) {
private var def: T? = null
private var primitiveDef: Any? = null
private var getter: Function<Any?, T?>? = null
private var setter: Function<T?, Any?>? = null
@Suppress("UNCHECKED_CAST")
private var getter: Function<Any?, T> = Function { it as T }
private var setter: Function<T, Any?> = Function { it }
/**
* The default value to use, as used in code. If not a primitive type, use the [.getter] and [.setter] methods.
@ -145,7 +122,7 @@ open class ConfigData<T> internal constructor(
* @param getter A function that receives the primitive type and returns the runtime type
* @return This builder
*/
fun getter(getter: Function<Any?, T>?): ConfigDataBuilder<T> {
fun getter(getter: Function<Any?, T>): ConfigDataBuilder<T> {
this.getter = getter
return this
}
@ -157,7 +134,7 @@ open class ConfigData<T> internal constructor(
* @param setter A function that receives the runtime type and returns the primitive type
* @return This builder
*/
fun setter(setter: Function<T, Any?>?): ConfigDataBuilder<T> {
fun setter(setter: Function<T, Any?>): ConfigDataBuilder<T> {
this.setter = setter
return this
}
@ -167,9 +144,9 @@ open class ConfigData<T> internal constructor(
*
* @return A ConfigData instance.
*/
fun build(): ConfigData<T?> {
fun build(): ConfigData<T> {
val config = ConfigData(config, path, def, primitiveDef, getter, setter)
this.config!!.onConfigBuild(config)
this.config.onConfigBuild(config)
return config
}
@ -178,26 +155,26 @@ open class ConfigData<T> internal constructor(
*
* @return A ReadOnlyConfigData instance.
*/
fun buildReadOnly(): ReadOnlyConfigData<T?> {
fun buildReadOnly(): ReadOnlyConfigData<T> {
val config = ReadOnlyConfigData(config, path, def, primitiveDef, getter, setter)
this.config!!.onConfigBuild(config)
this.config.onConfigBuild(config)
return config
}
override fun toString(): String {
return "ConfigData.ConfigDataBuilder(config=" + config + ", path=" + path + ", def=" + def + ", primitiveDef=" + primitiveDef + ", getter=" + getter + ", setter=" + setter + ")"
return "ConfigData.ConfigDataBuilder(config=$config, path=$path, def=$def, primitiveDef=$primitiveDef, getter=$getter, setter=$setter)"
}
}
companion object {
private val saveTasks = HashMap<Configuration, SaveTask>()
fun signalChange(config: IHaveConfig?) {
val cc = config!!.getConfig<Any>()
fun signalChange(config: IHaveConfig) {
val cc = config.config
val sa = config.saveAction
if (!saveTasks.containsKey(cc.getRoot())) {
if (!saveTasks.containsKey(cc.root)) {
synchronized(saveTasks) {
saveTasks.put(
cc.getRoot(),
cc.root,
SaveTask(Bukkit.getScheduler().runTaskLaterAsynchronously(MainPlugin.Instance, {
synchronized(
saveTasks
@ -216,16 +193,16 @@ open class ConfigData<T> internal constructor(
synchronized(saveTasks) {
val st = saveTasks[config]
if (st != null) {
st.task!!.cancel()
st.task.cancel()
saveTasks.remove(config)
st.saveAction!!.run()
st.saveAction.run()
return true
}
}
return false
}
fun <T> builder(config: IHaveConfig?, path: String?): ConfigDataBuilder<T> {
fun <T> builder(config: IHaveConfig, path: String): ConfigDataBuilder<T> {
return ConfigDataBuilder(config, path)
}
}

View file

@ -1,13 +1,12 @@
package buttondevteam.lib.architecture;
package buttondevteam.lib.architecture
import java.util.function.Function;
import java.util.function.Function
public class ReadOnlyConfigData<T> extends ConfigData<T> {
ReadOnlyConfigData(IHaveConfig config, String path, T def, Object primitiveDef, Function<Object, T> getter, Function<T, Object> setter) {
super(config, path, def, primitiveDef, getter, setter);
}
ReadOnlyConfigData(IHaveConfig config, String path, T def, Object primitiveDef) {
super(config, path, def, primitiveDef, null, null);
}
}
class ReadOnlyConfigData<T> internal constructor(
config: IHaveConfig?,
path: String,
def: T?,
primitiveDef: Any?,
getter: Function<Any?, T>,
setter: Function<T, Any?>
) : ConfigData<T>(config, path, def, primitiveDef, getter, setter)

View file

@ -1,67 +1,75 @@
package buttondevteam.lib.chat.commands;
package buttondevteam.lib.chat.commands
import buttondevteam.core.MainPlugin;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.chat.Command2Sender;
import buttondevteam.lib.chat.ICommand2;
import lombok.val;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import buttondevteam.core.MainPlugin
import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.chat.Command2Sender
import buttondevteam.lib.chat.ICommand2
import org.bukkit.configuration.ConfigurationSection
import org.bukkit.configuration.file.YamlConfiguration
import java.io.IOException
import java.io.InputStreamReader
import java.lang.reflect.Method
/**
* Deals with reading the commands.yml file from the plugin. The file is generated by ButtonProcessor at compile-time.
* Only used when registering commands.
*/
public class CommandArgumentHelpManager<TC extends ICommand2<TP>, TP extends Command2Sender> {
private ConfigurationSection commandConfig;
class CommandArgumentHelpManager<TC : ICommand2<TP>, TP : Command2Sender>(command: TC) {
private var commandConfig: ConfigurationSection? = null
/**
* Read the yaml file for the given command class.
*
* @param command The command object to use
*/
public CommandArgumentHelpManager(TC command) {
val commandClass = command.getClass();
// It will load it for each class, but it would be complicated to solve that
// Most plugins don't have a lot of command classes anyway
try (val str = commandClass.getResourceAsStream("/commands.yml")) {
if (str == null) {
TBMCCoreAPI.SendException("Error while getting command data!", new Exception("Resource not found!"), MainPlugin.Instance);
return;
}
val config = YamlConfiguration.loadConfiguration(new InputStreamReader(str));
commandConfig = config.getConfigurationSection(commandClass.getCanonicalName().replace('$', '.'));
if (commandConfig == null) {
MainPlugin.Instance.getLogger().warning("Failed to get command data for " + commandClass + "! Make sure to use 'clean install' when building the project.");
}
} catch (IOException e) {
TBMCCoreAPI.SendException("Error while getting command data!", e, MainPlugin.Instance);
}
}
/**
* Read the yaml file for the given command class.
*
* @param command The command object to use
*/
init {
val commandClass = command.javaClass
// It will load it for each class, but it would be complicated to solve that
// Most plugins don't have a lot of command classes anyway
try {
commandClass.getResourceAsStream("/commands.yml").use { str ->
if (str == null) {
TBMCCoreAPI.SendException(
"Error while getting command data!",
Exception("Resource not found!"),
MainPlugin.Instance
)
return@use
}
val config = YamlConfiguration.loadConfiguration(InputStreamReader(str))
commandConfig = config.getConfigurationSection(commandClass.canonicalName.replace('$', '.'))
if (commandConfig == null) {
MainPlugin.Instance.logger.warning("Failed to get command data for $commandClass! Make sure to use 'clean install' when building the project.")
}
}
} catch (e: IOException) {
TBMCCoreAPI.SendException("Error while getting command data!", e, MainPlugin.Instance)
}
}
/**
* Returns a parameter help string for the given subcommand method by reading it from the plugin.
*
* @param method The subcommand method
* @return The parameter part of the usage string for the command
*/
public String getParameterHelpForMethod(Method method) {
val cs = commandConfig.getConfigurationSection(method.getName());
if (cs == null) {
MainPlugin.Instance.getLogger().warning("Failed to get command data for " + method + "! Make sure to use 'clean install' when building the project.");
return null;
}
val mname = cs.getString("method");
val params = cs.getString("params");
int i = mname.indexOf('('); //Check only the name - the whole method is still stored for backwards compatibility and in case it may be useful
if (i != -1 && method.getName().equals(mname.substring(0, i)) && params != null) {
return params;
} else
TBMCCoreAPI.SendException("Error while getting command data for " + method + "!", new Exception("Method '" + method + "' != " + mname + " or params is " + params), MainPlugin.Instance);
return null;
}
}
/**
* Returns a parameter help string for the given subcommand method by reading it from the plugin.
*
* @param method The subcommand method
* @return The parameter part of the usage string for the command
*/
fun getParameterHelpForMethod(method: Method): String? {
val cs = commandConfig?.getConfigurationSection(method.name)
if (cs == null) {
MainPlugin.Instance.logger.warning("Failed to get command data for $method! Make sure to use 'clean install' when building the project.")
return null
}
val mname = cs.getString("method")
val params = cs.getString("params")
val i =
mname.indexOf('(') //Check only the name - the whole method is still stored for backwards compatibility and in case it may be useful
if (i != -1 && method.name == mname.substring(0, i) && params != null) {
return params
} else TBMCCoreAPI.SendException(
"Error while getting command data for $method!",
Exception("Method '$method' != $mname or params is $params"),
MainPlugin.Instance
)
return null
}
}