Convert config stuff and fix issues

This commit is contained in:
Norbi Peti 2023-02-23 00:54:29 +01:00
parent 65be3b2df9
commit 482df40992
5 changed files with 576 additions and 518 deletions

View file

@ -34,6 +34,7 @@
<executions>
<execution>
<id>compile</id>
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
</goals>

View file

@ -6,8 +6,6 @@ import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.architecture.Component.Companion.updateConfig
import buttondevteam.lib.chat.Command2MC
import buttondevteam.lib.chat.ICommand2MC
import lombok.AccessLevel
import lombok.Getter
import org.bukkit.configuration.InvalidConfigurationException
import org.bukkit.configuration.file.FileConfiguration
import org.bukkit.configuration.file.YamlConfiguration
@ -16,22 +14,19 @@ import java.io.File
import java.io.IOException
import java.util.*
import java.util.function.Consumer
import java.util.function.Function
@HasConfig(global = true)
abstract class ButtonPlugin : JavaPlugin() {
protected val iConfig = IHaveConfig { saveConfig() }
private var yaml: CommentedConfiguration? = null
@Getter(AccessLevel.PROTECTED)
private val data //TODO
protected val data //TODO
: IHaveConfig? = null
/**
* Used to unregister components in the right order - and to reload configs
*/
@Getter
private val componentStack = Stack<Component<*>>()
val componentStack = Stack<Component<*>>()
protected abstract fun pluginEnable()
/**
@ -111,9 +106,12 @@ abstract class ButtonPlugin : JavaPlugin() {
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", ""),
*Arrays.stream<String>(value.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray())
.map<String> { str: String -> "# " + str.trim { it <= ' ' } }.toArray<String> { _Dummy_.__Array__() })
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()
)
return true
}
@ -148,7 +146,7 @@ abstract class ButtonPlugin : JavaPlugin() {
val command2MC = Command2MC()
fun configGenAllowed(obj: Any): Boolean {
return !Optional.ofNullable(obj.javaClass.getAnnotation(ConfigOpts::class.java))
.map(Function<ConfigOpts, Boolean> { obj: ConfigOpts -> obj.disableConfigGen() }).orElse(false)
.map { it.disableConfigGen }.orElse(false)
}
}
}

View file

@ -11,38 +11,34 @@ import org.bukkit.event.Listener
import org.bukkit.plugin.java.JavaPlugin
import java.util.*
import java.util.function.Consumer
import java.util.function.Function
import java.util.stream.Collectors
/**
* Configuration is based on class name
*/
@HasConfig(global = false) //Used for obtaining javadoc
abstract class Component<TP : JavaPlugin?> {
@Getter
private var enabled = false
var isEnabled = false
@Getter
private var plugin: TP = null
var plugin: TP? = null
@Getter
private val config = IHaveConfig(null)
val config = IHaveConfig(null)
@Getter
private val data //TODO
: IHaveConfig? = null
@JvmField
val shouldBeEnabled = config.getData("enabled",
Optional.ofNullable(javaClass.getAnnotation(ComponentMetadata::class.java)).map(Function<ComponentMetadata, Boolean> { obj: ComponentMetadata -> obj.enabledByDefault() }).orElse(true))
val shouldBeEnabled: ConfigData<Boolean> = config.getData("enabled",
Optional.ofNullable(javaClass.getAnnotation(ComponentMetadata::class.java)).map { it.enabledByDefault }
.orElse(true))
fun log(message: String) {
plugin!!.logger.info("[" + className + "] " + message)
plugin!!.logger.info("[$className] $message")
}
fun logWarn(message: String) {
plugin!!.logger.warning("[" + className + "] " + message)
plugin!!.logger.warning("[$className] $message")
}
/**
@ -85,7 +81,7 @@ abstract class Component<TP : JavaPlugin?> {
fun registerCommand(command: ICommand2MC) {
if (plugin is ButtonPlugin) command.registerToPlugin(plugin as ButtonPlugin)
command.registerToComponent(this)
ButtonPlugin.getCommand2MC().registerCommand(command)
ButtonPlugin.command2MC.registerCommand(command)
}
/**
@ -107,18 +103,23 @@ abstract class Component<TP : JavaPlugin?> {
* @return A map containing configs
*/
fun getConfigMap(key: String?, defaultProvider: Map<String, Consumer<IHaveConfig?>>): Map<String, IHaveConfig> {
val c: ConfigurationSection = getConfig().getConfig()
val c: ConfigurationSection = config.config
var cs = c.getConfigurationSection(key)
if (cs == null) cs = c.createSection(key)
val res = cs!!.getValues(false).entries.stream().filter { (_, value): Map.Entry<String?, Any?> -> value is ConfigurationSection }
.collect(Collectors.toMap<Map.Entry<String?, Any?>, String, IHaveConfig>(Function<Map.Entry<String?, Any?>, String> { (key1, value) -> java.util.Map.Entry.key }, Function<Map.Entry<String?, Any?>, IHaveConfig> { (_, value): Map.Entry<String?, Any?> ->
val conf = IHaveConfig { getPlugin().saveConfig() }
conf.reset(value as ConfigurationSection?)
conf
}))
if (res.size == 0) {
val res = cs!!.getValues(false).entries.stream()
.filter { (_, value): Map.Entry<String?, Any?> -> value is ConfigurationSection }
.collect(
Collectors.toMap<Map.Entry<String?, Any?>, String, IHaveConfig>(
{ it.key },
{ (_, value): Map.Entry<String?, Any?> ->
val conf = IHaveConfig { plugin!!.saveConfig() }
conf.reset(value as ConfigurationSection?)
conf
})
)
if (res.isEmpty()) {
for ((key1, value) in defaultProvider) {
val conf = IHaveConfig { getPlugin().saveConfig() }
val conf = IHaveConfig { plugin!!.saveConfig() }
conf.reset(cs.createSection(key1))
value.accept(conf)
res[key1] = conf
@ -128,7 +129,7 @@ abstract class Component<TP : JavaPlugin?> {
}
private val className: String
private get() = javaClass.simpleName
get() = javaClass.simpleName
companion object {
private val components = HashMap<Class<out Component<*>>, Component<out JavaPlugin>>()
@ -143,7 +144,7 @@ abstract class Component<TP : JavaPlugin?> {
* @return Whether the component is registered successfully (it may have failed to enable)
*/
@JvmStatic
fun <T : JavaPlugin?> registerComponent(plugin: T, component: Component<T>): Boolean {
fun <T : JavaPlugin> registerComponent(plugin: T, component: Component<T>): Boolean {
return registerUnregisterComponent(plugin, component, true)
}
@ -156,29 +157,37 @@ abstract class Component<TP : JavaPlugin?> {
* @return Whether the component is unregistered successfully (it also got disabled)
*/
@JvmStatic
fun <T : JavaPlugin?> unregisterComponent(plugin: T, component: Component<T>): Boolean {
fun <T : JavaPlugin> unregisterComponent(plugin: T, component: Component<T>): Boolean {
return registerUnregisterComponent(plugin, component, false)
}
fun <T : JavaPlugin?> registerUnregisterComponent(plugin: T, component: Component<T>, register: Boolean): Boolean {
fun <T : JavaPlugin> registerUnregisterComponent(
plugin: T,
component: Component<T>,
register: Boolean
): Boolean {
return try {
val metaAnn = component.javaClass.getAnnotation(ComponentMetadata::class.java)
if (metaAnn != null) {
val dependencies: Array<Class<out Component<*>>> = metaAnn.depends()
val dependencies = metaAnn.depends
for (dep in dependencies) { //TODO: Support dependencies at enable/disable as well
if (!components.containsKey(dep)) {
plugin!!.logger.warning("Failed to " + (if (register) "" else "un") + "register component " + component.className + " as a required dependency is missing/disabled: " + dep.simpleName)
if (!components.containsKey(dep.java)) {
plugin.logger.warning("Failed to " + (if (register) "" else "un") + "register component " + component.className + " as a required dependency is missing/disabled: " + dep.simpleName)
return false
}
}
}
if (register) {
if (components.containsKey(component.javaClass)) {
TBMCCoreAPI.SendException("Failed to register component " + component.className, IllegalArgumentException("The component is already registered!"), plugin)
TBMCCoreAPI.SendException(
"Failed to register component " + component.className,
IllegalArgumentException("The component is already registered!"),
plugin
)
return false
}
component.plugin = plugin
component.config.saveAction = Runnable { plugin!!.saveConfig() }
component.plugin = plugin // TODO: Perhaps construct a new object with these initialized
component.config.saveAction = Runnable { plugin.saveConfig() }
updateConfig(plugin, component)
component.register(plugin)
components[component.javaClass] = component
@ -188,7 +197,11 @@ abstract class Component<TP : JavaPlugin?> {
setComponentEnabled(component, true)
true
} catch (e: Exception) {
TBMCCoreAPI.SendException("Failed to enable component " + component.className + "!", e, component)
TBMCCoreAPI.SendException(
"Failed to enable component " + component.className + "!",
e,
component
)
true
} catch (e: NoClassDefFoundError) {
TBMCCoreAPI.SendException("Failed to enable component " + component.className + "!", e, component)
@ -197,14 +210,22 @@ abstract class Component<TP : JavaPlugin?> {
}
} else {
if (!components.containsKey(component.javaClass)) return true //Already unregistered
if (component.enabled) {
if (component.isEnabled) {
try {
setComponentEnabled(component, false)
} catch (e: Exception) {
TBMCCoreAPI.SendException("Failed to disable component " + component.className + "!", e, component)
TBMCCoreAPI.SendException(
"Failed to disable component " + component.className + "!",
e,
component
)
return false //If failed to disable, won't unregister either
} catch (e: NoClassDefFoundError) {
TBMCCoreAPI.SendException("Failed to disable component " + component.className + "!", e, component)
TBMCCoreAPI.SendException(
"Failed to disable component " + component.className + "!",
e,
component
)
return false
}
}
@ -228,10 +249,10 @@ abstract class Component<TP : JavaPlugin?> {
@Throws(UnregisteredComponentException::class)
fun setComponentEnabled(component: Component<*>, enabled: Boolean) {
if (!components.containsKey(component.javaClass)) throw UnregisteredComponentException(component)
if (component.enabled == enabled) return //Don't do anything
if (enabled.also { component.enabled = it }) {
if (component.isEnabled == enabled) return //Don't do anything
if (enabled.also { component.isEnabled = it }) {
try {
updateConfig(component.getPlugin(), component)
updateConfig(component.plugin!!, component)
component.enable()
if (ButtonPlugin.configGenAllowed(component)) {
IHaveConfig.pregenConfig(component, null)
@ -253,7 +274,7 @@ abstract class Component<TP : JavaPlugin?> {
}
} else {
component.disable()
ButtonPlugin.getCommand2MC().unregisterCommands(component)
ButtonPlugin.command2MC.unregisterCommands(component)
}
}

View file

@ -1,252 +1,260 @@
package buttondevteam.lib.architecture;
package buttondevteam.lib.architecture
import buttondevteam.core.MainPlugin;
import buttondevteam.lib.ChromaUtils;
import lombok.*;
import org.bukkit.Bukkit;
import org.bukkit.configuration.Configuration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.scheduler.BukkitTask;
import java.lang.reflect.Array;
import java.util.HashMap;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import buttondevteam.core.MainPlugin
import buttondevteam.lib.ChromaUtils
import buttondevteam.lib.architecture.IHaveConfig.getConfig
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
/**
* Use the getter/setter constructor if {@link T} isn't a primitive type or String.<br>
* Use {@link Component#getConfig()} or {@link ButtonPlugin#getIConfig()} then {@link IHaveConfig#getData(String, Object)} to get an instance.
* Use the getter/setter constructor if [T] isn't a primitive type or String.<br></br>
* Use [Component.getConfig] or [ButtonPlugin.getIConfig] then [IHaveConfig.getData] to get an instance.
*/
public class ConfigData<T> {
private static final HashMap<Configuration, SaveTask> saveTasks = new HashMap<>();
/**
* May be null for testing
*/
private IHaveConfig config;
@Getter
@Setter(AccessLevel.PACKAGE)
private String path;
protected final T def;
private final Object primitiveDef;
/**
* The parameter is of a primitive type as returned by {@link YamlConfiguration#get(String)}
*/
private final Function<Object, T> getter;
/**
* The result should be a primitive type or string that can be retrieved correctly later
*/
private final Function<T, Object> setter;
open class ConfigData<T> internal constructor(
config: IHaveConfig?,
path: String?,
def: T,
primitiveDef: Any?,
getter: Function<Any?, T>?,
setter: Function<T, Any?>?
) {
/**
* May be null for testing
*/
private val config: IHaveConfig?
/**
* The config value should not change outside this instance
*/
private T value;
@Getter
@Setter(AccessLevel.PACKAGE)
private val path: String?
protected val def: T?
private val primitiveDef: Any?
ConfigData(IHaveConfig config, String path, T def, Object primitiveDef, Function<Object, T> getter, Function<T, Object> setter) {
if (def == null) {
if (primitiveDef == null)
throw new IllegalArgumentException("Either def or primitiveDef must be set.");
if (getter == null)
throw new IllegalArgumentException("A getter and setter must be present when using primitiveDef.");
def = getter.apply(primitiveDef);
} else if (primitiveDef == null)
if (setter == null)
primitiveDef = def;
else
primitiveDef = setter.apply(def);
if ((getter == null) != (setter == null))
throw new IllegalArgumentException("Both setters and getters must be present (or none if def is primitive).");
this.config = config;
this.path = path;
this.def = def;
this.primitiveDef = primitiveDef;
this.getter = getter;
this.setter = setter;
get(); //Generate config automatically
}
/**
* The parameter is of a primitive type as returned by [YamlConfiguration.get]
*/
private val getter: Function<Any?, T>?
@Override
public String toString() {
return "ConfigData{" + "path='" + path + '\'' + ", value=" + value + '}';
}
/**
* The result should be a primitive type or string that can be retrieved correctly later
*/
private val setter: Function<T, Any?>?
void reset() {
value = null;
}
/**
* The config value should not change outside this instance
*/
private var value: T? = null
@SuppressWarnings("unchecked")
public T get() {
if (value != null) return value; //Speed things up
var config = this.config.getConfig();
Object val;
if (config == null || !config.isSet(path)) { //Call set() if config == null
val = primitiveDef;
if ((def == null || this instanceof 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
} else
val = config.get(path); //config==null: testing
if (val == null) //If it's set to null explicitly
val = primitiveDef;
BiFunction<Object, Object, Object> convert = (_val, _def) -> {
if (_def instanceof Number) //If we expect a number
if (_val instanceof Number)
_val = ChromaUtils.convertNumber((Number) _val,
(Class<? extends Number>) _def.getClass());
else
_val = _def; //If we didn't get a number, return default (which is a number)
else if (_val instanceof List && _def != null && _def.getClass().isArray())
_val = ((List<T>) _val).toArray((T[]) Array.newInstance(_def.getClass().getComponentType(), 0));
return _val;
};
if (getter != null) {
val = convert.apply(val, primitiveDef);
T hmm = getter.apply(val);
if (hmm == null) hmm = def; //Set if the getter returned null
return hmm;
}
val = convert.apply(val, def);
return value = (T) val; //Always cache, if not cached yet
}
init {
var def: T? = def
var primitiveDef = primitiveDef
if (def == null) {
requireNotNull(primitiveDef) { "Either def or primitiveDef must be set." }
requireNotNull(getter) { "A getter and setter must be present when using primitiveDef." }
def = getter.apply(primitiveDef)
} else if (primitiveDef == null) primitiveDef = if (setter == null) def else setter.apply(def)
require(getter == null == (setter == null)) { "Both setters and getters must be present (or none if def is primitive)." }
this.config = config
this.path = path
this.def = def
this.primitiveDef = primitiveDef
this.getter = getter
this.setter = setter
get() //Generate config automatically
}
public void set(T value) {
if (this instanceof ReadOnlyConfigData)
return; //Safety for Discord channel/role data
Object val;
if (setter != null && value != null)
val = setter.apply(value);
else val = value;
if (config.getConfig() != null)
setInternal(val);
this.value = value;
}
override fun toString(): String {
return "ConfigData{path='$path', value=$value}"
}
private void setInternal(Object val) {
config.getConfig().set(path, val);
signalChange(config);
}
fun reset() {
value = null
}
static void signalChange(IHaveConfig config) {
var cc = config.getConfig();
var sa = config.getSaveAction();
if (!saveTasks.containsKey(cc.getRoot())) {
synchronized (saveTasks) {
saveTasks.put(cc.getRoot(), new SaveTask(Bukkit.getScheduler().runTaskLaterAsynchronously(MainPlugin.Instance, () -> {
synchronized (saveTasks) {
saveTasks.remove(cc.getRoot());
sa.run();
}
}, 100), sa));
}
}
}
fun get(): T? {
if (value != null) return value //Speed things up
val config = config!!.getConfig<Any>()
var `val`: Any?
if (config == null || !config.isSet(path)) { //Call set() if config == null
`val` = 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
} 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
}
}
@AllArgsConstructor
private static class SaveTask {
BukkitTask task;
Runnable saveAction;
}
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`)
this.value = value
}
public static boolean saveNow(Configuration config) {
synchronized (saveTasks) {
SaveTask st = saveTasks.get(config);
if (st != null) {
st.task.cancel();
saveTasks.remove(config);
st.saveAction.run();
return true;
}
}
return false;
}
private fun setInternal(`val`: Any?) {
config!!.getConfig<Any>().set(path, `val`)
signalChange(config)
}
public static <T> ConfigData.ConfigDataBuilder<T> builder(IHaveConfig config, String path) {
return new ConfigDataBuilder<T>(config, path);
}
@AllArgsConstructor
private class SaveTask {
var task: BukkitTask? = null
var saveAction: Runnable? = null
}
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public static class ConfigDataBuilder<T> {
private final IHaveConfig config;
private final String path;
private T def;
private Object primitiveDef;
private Function<Object, T> getter;
private Function<T, Object> setter;
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
class ConfigDataBuilder<T> {
private val config: IHaveConfig? = null
private val path: String? = null
private var def: T? = null
private var primitiveDef: Any? = null
private var getter: Function<Any?, T?>? = null
private var setter: Function<T?, Any?>? = null
/**
* The default value to use, as used in code. If not a primitive type, use the {@link #getter(Function)} and {@link #setter(Function)} methods.
* <br/>
* To set the value as it is stored, use {@link #primitiveDef(Object)}.
*
* @param def The default value
* @return This builder
*/
public ConfigDataBuilder<T> def(T def) {
this.def = def;
return this;
}
/**
* The default value to use, as used in code. If not a primitive type, use the [.getter] and [.setter] methods.
* <br></br>
* To set the value as it is stored, use [.primitiveDef].
*
* @param def The default value
* @return This builder
*/
fun def(def: T): ConfigDataBuilder<T> {
this.def = def
return this
}
/**
* The default value to use, as stored in yaml. Must be a primitive type. Make sure to use the {@link #getter(Function)} and {@link #setter(Function)} methods.
* <br/>
* To set the value as used in the code, use {@link #def(Object)}.
*
* @param primitiveDef The default value
* @return This builder
*/
public ConfigDataBuilder<T> primitiveDef(Object primitiveDef) {
this.primitiveDef = primitiveDef;
return this;
}
/**
* The default value to use, as stored in yaml. Must be a primitive type. Make sure to use the [.getter] and [.setter] methods.
* <br></br>
* To set the value as used in the code, use [.def].
*
* @param primitiveDef The default value
* @return This builder
*/
fun primitiveDef(primitiveDef: Any?): ConfigDataBuilder<T> {
this.primitiveDef = primitiveDef
return this
}
/**
* A function to use to obtain the runtime object from the yaml representation (usually string).
* The {@link #setter(Function)} must also be set.
*
* @param getter A function that receives the primitive type and returns the runtime type
* @return This builder
*/
public ConfigDataBuilder<T> getter(Function<Object, T> getter) {
this.getter = getter;
return this;
}
/**
* A function to use to obtain the runtime object from the yaml representation (usually string).
* The [.setter] must also be set.
*
* @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> {
this.getter = getter
return this
}
/**
* A function to use to obtain the yaml representation (usually string) from the runtime object.
* The {@link #getter(Function)} must also be set.
*
* @param setter A function that receives the runtime type and returns the primitive type
* @return This builder
*/
public ConfigDataBuilder<T> setter(Function<T, Object> setter) {
this.setter = setter;
return this;
}
/**
* A function to use to obtain the yaml representation (usually string) from the runtime object.
* The [.getter] must also be set.
*
* @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> {
this.setter = setter
return this
}
/**
* Builds a modifiable config representation. Use if you want to change the value <i>in code</i>.
*
* @return A ConfigData instance.
*/
public ConfigData<T> build() {
ConfigData<T> config = new ConfigData<>(this.config, path, def, primitiveDef, getter, setter);
this.config.onConfigBuild(config);
return config;
}
/**
* Builds a modifiable config representation. Use if you want to change the value *in code*.
*
* @return A ConfigData instance.
*/
fun build(): ConfigData<T?> {
val config = ConfigData(config, path, def, primitiveDef, getter, setter)
this.config!!.onConfigBuild(config)
return config
}
/**
* Builds a read-only config representation. Use if you only want the value to be changed <i>in the config</i>.
*
* @return A ReadOnlyConfigData instance.
*/
public ReadOnlyConfigData<T> buildReadOnly() {
ReadOnlyConfigData<T> config = new ReadOnlyConfigData<>(this.config, path, def, primitiveDef, getter, setter);
this.config.onConfigBuild(config);
return config;
}
/**
* Builds a read-only config representation. Use if you only want the value to be changed *in the config*.
*
* @return A ReadOnlyConfigData instance.
*/
fun buildReadOnly(): ReadOnlyConfigData<T?> {
val config = ReadOnlyConfigData(config, path, def, primitiveDef, getter, setter)
this.config!!.onConfigBuild(config)
return config
}
public String toString() {return "ConfigData.ConfigDataBuilder(config=" + this.config + ", path=" + this.path + ", def=" + this.def + ", primitiveDef=" + this.primitiveDef + ", getter=" + this.getter + ", setter=" + this.setter + ")";}
}
override fun toString(): String {
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>()
val sa = config.saveAction
if (!saveTasks.containsKey(cc.getRoot())) {
synchronized(saveTasks) {
saveTasks.put(
cc.getRoot(),
SaveTask(Bukkit.getScheduler().runTaskLaterAsynchronously(MainPlugin.Instance, {
synchronized(
saveTasks
) {
saveTasks.remove(cc.getRoot())
sa!!.run()
}
}, 100), sa)
)
}
}
}
@JvmStatic
fun saveNow(config: Configuration): Boolean {
synchronized(saveTasks) {
val st = saveTasks[config]
if (st != null) {
st.task!!.cancel()
saveTasks.remove(config)
st.saveAction!!.run()
return true
}
}
return false
}
fun <T> builder(config: IHaveConfig?, path: String?): ConfigDataBuilder<T> {
return ConfigDataBuilder(config, path)
}
}
}

View file

@ -1,256 +1,286 @@
package buttondevteam.lib.architecture;
package buttondevteam.lib.architecture
import buttondevteam.core.MainPlugin;
import buttondevteam.lib.TBMCCoreAPI;
import lombok.Getter;
import lombok.Setter;
import lombok.val;
import org.bukkit.Bukkit;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.plugin.java.JavaPlugin;
import javax.annotation.Nullable;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import buttondevteam.core.MainPlugin
import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.architecture.ConfigData.ConfigDataBuilder
import lombok.Getter
import org.bukkit.Bukkit
import org.bukkit.configuration.ConfigurationSection
import org.bukkit.plugin.java.JavaPlugin
import java.lang.reflect.InvocationTargetException
import java.util.*
import java.util.function.Function
import java.util.function.Predicate
import java.util.function.Supplier
import java.util.stream.Collectors
/**
* A config system
* May be used in testing.
*
* @param saveAction What to do to save the config to disk. Don't use get methods until it's non-null.
*/
public final class IHaveConfig {
private final HashMap<String, ConfigData<?>> datamap = new HashMap<>();
/**
* Returns the Bukkit ConfigurationSection. Use {@link #signalChange()} after changing it.
*/
@Getter
private ConfigurationSection config;
@Getter
@Setter
private Runnable saveAction;
class IHaveConfig(var saveAction: Runnable?) { // TODO: Make non-nullable after adding component builder
private val datamap = HashMap<String, ConfigData<*>>()
/**
* May be used in testing.
*
* @param saveAction What to do to save the config to disk. Don't use get methods until it's non-null.
*/
public IHaveConfig(Runnable saveAction) {
this.saveAction = saveAction;
}
/**
* Returns the Bukkit ConfigurationSection. Use [.signalChange] after changing it.
*/
@Getter
private var config: ConfigurationSection? = null
/**
* Gets a config object for the given path. The def or primitiveDef must be set. If a getter is present, a setter must be present as well.
*
* @param path The dot-separated path relative to this config instance
* @param <T> The runtime type of the config value
* @return A ConfigData builder to set how to obtain the value
*/
public <T> ConfigData.ConfigDataBuilder<T> getConfig(String path) {
return ConfigData.builder(this, path);
}
/**
* Gets a config object for the given path. The def or primitiveDef must be set. If a getter is present, a setter must be present as well.
*
* @param path The dot-separated path relative to this config instance
* @param <T> The runtime type of the config value
* @return A ConfigData builder to set how to obtain the value
</T> */
fun <T> getConfig(path: String?): ConfigDataBuilder<T> {
return ConfigData.builder(this, path)
}
void onConfigBuild(ConfigData<?> config) {
datamap.put(config.getPath(), config);
}
fun onConfigBuild(config: ConfigData<*>) {
datamap[config.path] = config
}
/**
* This method overload should only be used with primitives or String.
*
* @param path The path in config to use
* @param def The value to use by default
* @param <T> The type of this variable (only use primitives or String)
* @return The data object that can be used to get or set the value
*/
@SuppressWarnings("unchecked")
public <T> ConfigData<T> getData(String path, T def) {
ConfigData<?> data = datamap.get(path);
if (data == null) datamap.put(path, data = new ConfigData<>(this, path, def, def, null, null));
return (ConfigData<T>) data;
}
/**
* This method overload should only be used with primitives or String.
*
* @param path The path in config to use
* @param def The value to use by default
* @param <T> The type of this variable (only use primitives or String)
* @return The data object that can be used to get or set the value
</T> */
fun <T> getData(path: String, def: T): ConfigData<T> {
var data = datamap[path]
if (data == null) datamap[path] = ConfigData(this, path, def, def, null, null).also { data = it }
@Suppress("UNCHECKED_CAST")
return data as ConfigData<T>
}
/**
* This method overload may be used with any class.
*
* @param path The path in config to use
* @param def The value to use by default
* @param getter A function that converts a primitive representation to the correct value
* @param setter A function that converts a value to a primitive representation
* @param <T> The type of this variable (can be any class)
* @return The data object that can be used to get or set the value
*/
@SuppressWarnings("unchecked")
public <T> ConfigData<T> getData(String path, T def, Function<Object, T> getter, Function<T, Object> setter) {
ConfigData<?> data = datamap.get(path);
if (data == null)
datamap.put(path, data = new ConfigData<>(this, path, def, setter.apply(def), getter, setter));
return (ConfigData<T>) data;
}
/**
* This method overload may be used with any class.
*
* @param path The path in config to use
* @param def The value to use by default
* @param getter A function that converts a primitive representation to the correct value
* @param setter A function that converts a value to a primitive representation
* @param <T> The type of this variable (can be any class)
* @return The data object that can be used to get or set the value
</T> */
fun <T> getData(path: String, def: T, getter: Function<Any?, T>?, setter: Function<T, Any?>): ConfigData<T> {
var data = datamap[path]
if (data == null) datamap[path] =
ConfigData(this, path, def, setter.apply(def), getter, setter).also { data = it }
@Suppress("UNCHECKED_CAST")
return data as ConfigData<T>
}
/**
* This method overload may be used with any class. The given default value will be run through the getter.
*
* @param path The path in config to use
* @param primitiveDef The <b>primitive</b> value to use by default
* @param getter A function that converts a primitive representation to the correct value
* @param setter A function that converts a value to a primitive representation
* @param <T> The type of this variable (can be any class)
* @return The data object that can be used to get or set the value
*/
@SuppressWarnings("unchecked")
public <T> ConfigData<T> getDataPrimDef(String path, Object primitiveDef, Function<Object, T> getter, Function<T, Object> setter) {
ConfigData<?> data = datamap.get(path);
if (data == null)
datamap.put(path, data = new ConfigData<>(this, path, getter.apply(primitiveDef), primitiveDef, getter, setter));
return (ConfigData<T>) data;
}
/**
* This method overload may be used with any class. The given default value will be run through the getter.
*
* @param path The path in config to use
* @param primitiveDef The **primitive** value to use by default
* @param getter A function that converts a primitive representation to the correct value
* @param setter A function that converts a value to a primitive representation
* @param <T> The type of this variable (can be any class)
* @return The data object that can be used to get or set the value
</T> */
fun <T> getDataPrimDef(
path: String,
primitiveDef: Any?,
getter: Function<Any?, T>,
setter: Function<T, Any?>?
): ConfigData<T> {
var data = datamap[path]
if (data == null) datamap[path] =
ConfigData(this, path, getter.apply(primitiveDef), primitiveDef, getter, setter).also { data = it }
@Suppress("UNCHECKED_CAST")
return data as ConfigData<T>
}
/**
* This method overload may be used with any class. The given default value will be run through the getter.
*
* @param path The path in config to use
* @param primitiveDef The <b>primitive</b> value to use by default
* @param getter A function that converts a primitive representation to the correct value
* @param setter A function that converts a value to a primitive representation
* @param <T> The type of this variable (can be any class)
* @return The data object that can be used to get or set the value
*/
@SuppressWarnings("unchecked")
public <T> ReadOnlyConfigData<T> getReadOnlyDataPrimDef(String path, Object primitiveDef, Function<Object, T> getter, Function<T, Object> setter) {
ConfigData<?> data = datamap.get(path);
if (data == null)
datamap.put(path, data = new ReadOnlyConfigData<>(this, path, getter.apply(primitiveDef), primitiveDef, getter, setter));
return (ReadOnlyConfigData<T>) data;
}
/**
* This method overload may be used with any class. The given default value will be run through the getter.
*
* @param path The path in config to use
* @param primitiveDef The **primitive** value to use by default
* @param getter A function that converts a primitive representation to the correct value
* @param setter A function that converts a value to a primitive representation
* @param <T> The type of this variable (can be any class)
* @return The data object that can be used to get or set the value
</T> */
fun <T> getReadOnlyDataPrimDef(
path: String,
primitiveDef: Any?,
getter: Function<Any?, T>,
setter: Function<T, Any?>?
): ReadOnlyConfigData<T> {
var data = datamap[path]
if (data == null) datamap[path] =
ReadOnlyConfigData(this, path, getter.apply(primitiveDef), primitiveDef, getter, setter).also { data = it }
@Suppress("UNCHECKED_CAST")
return data as ReadOnlyConfigData<T>
}
/**
* This method overload should only be used with primitves or String.
*
* @param path The path in config to use
* @param def The value to use by default
* @param <T> The type of this variable (only use primitives or String)
* @return The data object that can be used to get or set the value
*/
@SuppressWarnings("unchecked")
public <T> ConfigData<T> getData(String path, Supplier<T> def) {
ConfigData<?> data = datamap.get(path);
if (data == null) {
val defval = def.get();
datamap.put(path, data = new ConfigData<>(this, path, defval, defval, null, null));
}
return (ConfigData<T>) data;
}
/**
* This method overload should only be used with primitves or String.
*
* @param path The path in config to use
* @param def The value to use by default
* @param <T> The type of this variable (only use primitives or String)
* @return The data object that can be used to get or set the value
</T> */
fun <T> getData(path: String, def: Supplier<T>): ConfigData<T> {
var data = datamap[path]
if (data == null) {
val defval = def.get()
datamap[path] = ConfigData(this, path, defval, defval, null, null).also { data = it }
}
@Suppress("UNCHECKED_CAST")
return data as ConfigData<T>
}
/**
* This method overload may be used with any class.
*
* @param path The path in config to use
* @param def The value to use by default
* @param getter A function that converts a primitive representation to the correct value
* @param setter A function that converts a value to a primitive representation
* @param <T> The type of this variable (can be any class)
* @return The data object that can be used to get or set the value
*/
@SuppressWarnings("unchecked")
public <T> ConfigData<T> getData(String path, Supplier<T> def, Function<Object, T> getter, Function<T, Object> setter) {
ConfigData<?> data = datamap.get(path);
if (data == null) {
val defval = def.get();
datamap.put(path, data = new ConfigData<>(this, path, defval, setter.apply(defval), getter, setter));
}
return (ConfigData<T>) data;
}
/**
* This method overload may be used with any class.
*
* @param path The path in config to use
* @param def The value to use by default
* @param getter A function that converts a primitive representation to the correct value
* @param setter A function that converts a value to a primitive representation
* @param <T> The type of this variable (can be any class)
* @return The data object that can be used to get or set the value
</T> */
fun <T> getData(
path: String,
def: Supplier<T>,
getter: Function<Any?, T>?,
setter: Function<T, Any?>
): ConfigData<T> {
var data = datamap[path]
if (data == null) {
val defval = def.get()
datamap[path] = ConfigData(this, path, defval, setter.apply(defval), getter, setter).also { data = it }
}
@Suppress("UNCHECKED_CAST")
return data as ConfigData<T>
}
/**
* This method overload should only be used with primitves or String.
*
* @param path The path in config to use
* @param <T> The type of this variable (only use primitives or String)
* @return The data object that can be used to get or set the value
*/
@SuppressWarnings("unchecked")
public <T> ListConfigData<T> getListData(String path) {
ConfigData<?> data = datamap.get(path);
if (data == null)
datamap.put(path, data = new ListConfigData<>(this, path, new ListConfigData.List<T>()));
return (ListConfigData<T>) data;
}
/**
* This method overload should only be used with primitves or String.
*
* @param path The path in config to use
* @param <T> The type of this variable (only use primitives or String)
* @return The data object that can be used to get or set the value
</T> */
fun <T> getListData(path: String): ListConfigData<T> {
var data = datamap[path]
if (data == null) datamap[path] = ListConfigData(this, path, ListConfigData.List<T>()).also { data = it }
@Suppress("UNCHECKED_CAST")
return data as ListConfigData<T>
}
/**
* Schedules a save operation. Use after changing the ConfigurationSection directly.
*/
public void signalChange() {
ConfigData.signalChange(this);
}
/**
* Schedules a save operation. Use after changing the ConfigurationSection directly.
*/
fun signalChange() {
ConfigData.signalChange(this)
}
/**
* Clears all caches and loads everything from yaml.
*/
public void reset(ConfigurationSection config) {
this.config = config;
datamap.forEach((path, data) -> data.reset());
}
/**
* Clears all caches and loads everything from yaml.
*/
fun reset(config: ConfigurationSection?) {
this.config = config
datamap.forEach { (path: String?, data: ConfigData<*>) -> data.reset() }
}
/**
* Generates the config YAML.
*
* @param obj The object which has config methods
* @param configMap The result from {@link Component#getConfigMap(String, Map)}. May be null.
*/
public static void pregenConfig(Object obj, @Nullable Map<String, IHaveConfig> configMap) {
val ms = obj.getClass().getDeclaredMethods();
for (val m : ms) {
if (!m.getReturnType().getName().equals(ConfigData.class.getName())) continue;
final String mName;
{
var name = m.getName();
var ind = name.lastIndexOf('$');
if (ind == -1) mName = name;
else mName = name.substring(ind + 1);
}
try {
m.setAccessible(true);
List<ConfigData<?>> configList;
if (m.getParameterCount() == 0) {
configList = Collections.singletonList((ConfigData<?>) m.invoke(obj));
} else if (m.getParameterCount() == 1 && m.getParameterTypes()[0] == IHaveConfig.class) {
if (configMap == null) continue; //Hope it will get called with the param later
configList = configMap.entrySet().stream().map(kv ->
{
try {
return (ConfigData<?>) m.invoke(obj, kv.getValue());
} catch (IllegalAccessException | InvocationTargetException e) {
String msg = "Failed to pregenerate " + mName + " for " + obj + " using config " + kv.getKey() + "!";
if (obj instanceof Component<?>)
TBMCCoreAPI.SendException(msg, e, (Component<?>) obj);
else if (obj instanceof JavaPlugin)
TBMCCoreAPI.SendException(msg, e, (JavaPlugin) obj);
else
TBMCCoreAPI.SendException(msg, e, false, Bukkit.getLogger()::warning);
return null;
}
}).filter(Objects::nonNull).collect(Collectors.toList());
} else {
if (TBMCCoreAPI.IsTestServer())
MainPlugin.Instance.getLogger().warning("Method " + mName + " returns a config but its parameters are unknown: " + Arrays.toString(m.getParameterTypes()));
continue;
}
for (val c : configList) {
if (c.getPath().length() == 0)
c.setPath(mName);
else if (!c.getPath().equals(mName))
MainPlugin.Instance.getLogger().warning("Config name does not match: " + c.getPath() + " instead of " + mName);
c.get(); //Saves the default value if needed - also checks validity
}
} catch (Exception e) {
String msg = "Failed to pregenerate " + mName + " for " + obj + "!";
if (obj instanceof Component<?>)
TBMCCoreAPI.SendException(msg, e, (Component<?>) obj);
else if (obj instanceof JavaPlugin)
TBMCCoreAPI.SendException(msg, e, (JavaPlugin) obj);
else
TBMCCoreAPI.SendException(msg, e, false, Bukkit.getLogger()::warning);
}
}
}
companion object {
/**
* Generates the config YAML.
*
* @param obj The object which has config methods
* @param configMap The result from [Component.getConfigMap]. May be null.
*/
fun pregenConfig(obj: Any, configMap: Map<String, IHaveConfig?>?) {
val ms = obj.javaClass.declaredMethods
for (m in ms) {
if (m.returnType.name != ConfigData::class.java.name) continue
val mName: String
run {
val name = m.name
val ind = name.lastIndexOf('$')
mName = if (ind == -1) name else name.substring(ind + 1)
}
try {
m.isAccessible = true
var configList: List<ConfigData<*>>
configList = if (m.parameterCount == 0) {
listOf(m.invoke(obj) as ConfigData<*>)
} else if (m.parameterCount == 1 && m.parameterTypes[0] == IHaveConfig::class.java) {
if (configMap == null) continue //Hope it will get called with the param later
configMap.entries.stream().map { (key, value): Map.Entry<String, IHaveConfig?> ->
try {
return@map m.invoke(obj, value) as ConfigData<*>
} catch (e: IllegalAccessException) {
val msg = "Failed to pregenerate $mName for $obj using config $key!"
if (obj is Component<*>) TBMCCoreAPI.SendException(
msg,
e,
obj
) else if (obj is JavaPlugin) TBMCCoreAPI.SendException(
msg,
e,
obj
) else TBMCCoreAPI.SendException(msg, e, false) { msg: String? ->
Bukkit.getLogger().warning(msg)
}
return@map null
} catch (e: InvocationTargetException) {
val msg = "Failed to pregenerate $mName for $obj using config $key!"
if (obj is Component<*>) TBMCCoreAPI.SendException(
msg,
e,
obj
) else if (obj is JavaPlugin) TBMCCoreAPI.SendException(
msg,
e,
obj
) else TBMCCoreAPI.SendException(msg, e, false) { msg: String? ->
Bukkit.getLogger().warning(msg)
}
return@map null
}
}
.filter(Predicate<ConfigData<Any?>> { obj: ConfigData<Any?>? -> Objects.nonNull(obj) })
.collect(Collectors.toList())
} else {
if (TBMCCoreAPI.IsTestServer()) MainPlugin.Instance.logger.warning(
"Method " + mName + " returns a config but its parameters are unknown: " + Arrays.toString(
m.parameterTypes
)
)
continue
}
for (c in configList) {
if (c.path.length == 0) c.setPath(mName) else if (c.path != mName) MainPlugin.Instance.logger.warning(
"Config name does not match: " + c.path + " instead of " + mName
)
c.get() //Saves the default value if needed - also checks validity
}
} catch (e: Exception) {
val msg = "Failed to pregenerate $mName for $obj!"
if (obj is Component<*>) TBMCCoreAPI.SendException(
msg,
e,
obj
) else if (obj is JavaPlugin) TBMCCoreAPI.SendException(msg, e, obj) else TBMCCoreAPI.SendException(
msg,
e,
false
) { msg: String? -> Bukkit.getLogger().warning(msg) }
}
}
}
}
}