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> <executions>
<execution> <execution>
<id>compile</id> <id>compile</id>
<phase>process-sources</phase>
<goals> <goals>
<goal>compile</goal> <goal>compile</goal>
</goals> </goals>

View file

@ -6,8 +6,6 @@ import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.architecture.Component.Companion.updateConfig import buttondevteam.lib.architecture.Component.Companion.updateConfig
import buttondevteam.lib.chat.Command2MC import buttondevteam.lib.chat.Command2MC
import buttondevteam.lib.chat.ICommand2MC import buttondevteam.lib.chat.ICommand2MC
import lombok.AccessLevel
import lombok.Getter
import org.bukkit.configuration.InvalidConfigurationException import org.bukkit.configuration.InvalidConfigurationException
import org.bukkit.configuration.file.FileConfiguration import org.bukkit.configuration.file.FileConfiguration
import org.bukkit.configuration.file.YamlConfiguration import org.bukkit.configuration.file.YamlConfiguration
@ -16,22 +14,19 @@ import java.io.File
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*
import java.util.function.Consumer import java.util.function.Consumer
import java.util.function.Function
@HasConfig(global = true) @HasConfig(global = true)
abstract class ButtonPlugin : JavaPlugin() { abstract class ButtonPlugin : JavaPlugin() {
protected val iConfig = IHaveConfig { saveConfig() } protected val iConfig = IHaveConfig { saveConfig() }
private var yaml: CommentedConfiguration? = null private var yaml: CommentedConfiguration? = null
@Getter(AccessLevel.PROTECTED) protected val data //TODO
private val data //TODO
: IHaveConfig? = null : IHaveConfig? = null
/** /**
* Used to unregister components in the right order - and to reload configs * Used to unregister components in the right order - and to reload configs
*/ */
@Getter val componentStack = Stack<Component<*>>()
private val componentStack = Stack<Component<*>>()
protected abstract fun pluginEnable() protected abstract fun pluginEnable()
/** /**
@ -111,9 +106,12 @@ abstract class ButtonPlugin : JavaPlugin() {
this.yaml = yaml this.yaml = yaml
val res = getTextResource("configHelp.yml") ?: return true val res = getTextResource("configHelp.yml") ?: return true
val yc = YamlConfiguration.loadConfiguration(res) val yc = YamlConfiguration.loadConfiguration(res)
for ((key, value) in yc.getValues(true)) if (value is String) yaml.addComment(key.replace(".generalDescriptionInsteadOfAConfig", ""), for ((key, value) in yc.getValues(true)) if (value is String) yaml.addComment(key.replace(
*Arrays.stream<String>(value.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) ".generalDescriptionInsteadOfAConfig",
.map<String> { str: String -> "# " + str.trim { it <= ' ' } }.toArray<String> { _Dummy_.__Array__() }) ""
),
*value.split("\n").map { str -> "# " + str.trim { it <= ' ' } }.toTypedArray()
)
return true return true
} }
@ -148,7 +146,7 @@ abstract class ButtonPlugin : JavaPlugin() {
val command2MC = Command2MC() val command2MC = Command2MC()
fun configGenAllowed(obj: Any): Boolean { fun configGenAllowed(obj: Any): Boolean {
return !Optional.ofNullable(obj.javaClass.getAnnotation(ConfigOpts::class.java)) 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 org.bukkit.plugin.java.JavaPlugin
import java.util.* import java.util.*
import java.util.function.Consumer import java.util.function.Consumer
import java.util.function.Function
import java.util.stream.Collectors import java.util.stream.Collectors
/** /**
* Configuration is based on class name * Configuration is based on class name
*/ */
@HasConfig(global = false) //Used for obtaining javadoc @HasConfig(global = false) //Used for obtaining javadoc
abstract class Component<TP : JavaPlugin?> { abstract class Component<TP : JavaPlugin?> {
@Getter var isEnabled = false
private var enabled = false
@Getter var plugin: TP? = null
private var plugin: TP = null
@Getter val config = IHaveConfig(null)
private val config = IHaveConfig(null)
@Getter @Getter
private val data //TODO private val data //TODO
: IHaveConfig? = null : IHaveConfig? = null
@JvmField @JvmField
val shouldBeEnabled = config.getData("enabled", val shouldBeEnabled: ConfigData<Boolean> = config.getData("enabled",
Optional.ofNullable(javaClass.getAnnotation(ComponentMetadata::class.java)).map(Function<ComponentMetadata, Boolean> { obj: ComponentMetadata -> obj.enabledByDefault() }).orElse(true)) Optional.ofNullable(javaClass.getAnnotation(ComponentMetadata::class.java)).map { it.enabledByDefault }
.orElse(true))
fun log(message: String) { fun log(message: String) {
plugin!!.logger.info("[" + className + "] " + message) plugin!!.logger.info("[$className] $message")
} }
fun logWarn(message: String) { 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) { fun registerCommand(command: ICommand2MC) {
if (plugin is ButtonPlugin) command.registerToPlugin(plugin as ButtonPlugin) if (plugin is ButtonPlugin) command.registerToPlugin(plugin as ButtonPlugin)
command.registerToComponent(this) 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 * @return A map containing configs
*/ */
fun getConfigMap(key: String?, defaultProvider: Map<String, Consumer<IHaveConfig?>>): Map<String, IHaveConfig> { 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) var cs = c.getConfigurationSection(key)
if (cs == null) cs = c.createSection(key) if (cs == null) cs = c.createSection(key)
val res = cs!!.getValues(false).entries.stream().filter { (_, value): Map.Entry<String?, Any?> -> value is ConfigurationSection } val res = cs!!.getValues(false).entries.stream()
.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?> -> .filter { (_, value): Map.Entry<String?, Any?> -> value is ConfigurationSection }
val conf = IHaveConfig { getPlugin().saveConfig() } .collect(
conf.reset(value as ConfigurationSection?) Collectors.toMap<Map.Entry<String?, Any?>, String, IHaveConfig>(
conf { it.key },
})) { (_, value): Map.Entry<String?, Any?> ->
if (res.size == 0) { val conf = IHaveConfig { plugin!!.saveConfig() }
conf.reset(value as ConfigurationSection?)
conf
})
)
if (res.isEmpty()) {
for ((key1, value) in defaultProvider) { for ((key1, value) in defaultProvider) {
val conf = IHaveConfig { getPlugin().saveConfig() } val conf = IHaveConfig { plugin!!.saveConfig() }
conf.reset(cs.createSection(key1)) conf.reset(cs.createSection(key1))
value.accept(conf) value.accept(conf)
res[key1] = conf res[key1] = conf
@ -128,7 +129,7 @@ abstract class Component<TP : JavaPlugin?> {
} }
private val className: String private val className: String
private get() = javaClass.simpleName get() = javaClass.simpleName
companion object { companion object {
private val components = HashMap<Class<out Component<*>>, Component<out JavaPlugin>>() 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) * @return Whether the component is registered successfully (it may have failed to enable)
*/ */
@JvmStatic @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) 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) * @return Whether the component is unregistered successfully (it also got disabled)
*/ */
@JvmStatic @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) 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 { return try {
val metaAnn = component.javaClass.getAnnotation(ComponentMetadata::class.java) val metaAnn = component.javaClass.getAnnotation(ComponentMetadata::class.java)
if (metaAnn != null) { 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 for (dep in dependencies) { //TODO: Support dependencies at enable/disable as well
if (!components.containsKey(dep)) { 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) plugin.logger.warning("Failed to " + (if (register) "" else "un") + "register component " + component.className + " as a required dependency is missing/disabled: " + dep.simpleName)
return false return false
} }
} }
} }
if (register) { if (register) {
if (components.containsKey(component.javaClass)) { 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 return false
} }
component.plugin = plugin component.plugin = plugin // TODO: Perhaps construct a new object with these initialized
component.config.saveAction = Runnable { plugin!!.saveConfig() } component.config.saveAction = Runnable { plugin.saveConfig() }
updateConfig(plugin, component) updateConfig(plugin, component)
component.register(plugin) component.register(plugin)
components[component.javaClass] = component components[component.javaClass] = component
@ -188,7 +197,11 @@ abstract class Component<TP : JavaPlugin?> {
setComponentEnabled(component, true) setComponentEnabled(component, true)
true true
} catch (e: Exception) { } catch (e: Exception) {
TBMCCoreAPI.SendException("Failed to enable component " + component.className + "!", e, component) TBMCCoreAPI.SendException(
"Failed to enable component " + component.className + "!",
e,
component
)
true true
} catch (e: NoClassDefFoundError) { } catch (e: NoClassDefFoundError) {
TBMCCoreAPI.SendException("Failed to enable component " + component.className + "!", e, component) TBMCCoreAPI.SendException("Failed to enable component " + component.className + "!", e, component)
@ -197,14 +210,22 @@ abstract class Component<TP : JavaPlugin?> {
} }
} else { } else {
if (!components.containsKey(component.javaClass)) return true //Already unregistered if (!components.containsKey(component.javaClass)) return true //Already unregistered
if (component.enabled) { if (component.isEnabled) {
try { try {
setComponentEnabled(component, false) setComponentEnabled(component, false)
} catch (e: Exception) { } 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 return false //If failed to disable, won't unregister either
} catch (e: NoClassDefFoundError) { } 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 return false
} }
} }
@ -228,10 +249,10 @@ abstract class Component<TP : JavaPlugin?> {
@Throws(UnregisteredComponentException::class) @Throws(UnregisteredComponentException::class)
fun setComponentEnabled(component: Component<*>, enabled: Boolean) { fun setComponentEnabled(component: Component<*>, enabled: Boolean) {
if (!components.containsKey(component.javaClass)) throw UnregisteredComponentException(component) if (!components.containsKey(component.javaClass)) throw UnregisteredComponentException(component)
if (component.enabled == enabled) return //Don't do anything if (component.isEnabled == enabled) return //Don't do anything
if (enabled.also { component.enabled = it }) { if (enabled.also { component.isEnabled = it }) {
try { try {
updateConfig(component.getPlugin(), component) updateConfig(component.plugin!!, component)
component.enable() component.enable()
if (ButtonPlugin.configGenAllowed(component)) { if (ButtonPlugin.configGenAllowed(component)) {
IHaveConfig.pregenConfig(component, null) IHaveConfig.pregenConfig(component, null)
@ -253,7 +274,7 @@ abstract class Component<TP : JavaPlugin?> {
} }
} else { } else {
component.disable() 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.core.MainPlugin
import buttondevteam.lib.ChromaUtils; import buttondevteam.lib.ChromaUtils
import lombok.*; import buttondevteam.lib.architecture.IHaveConfig.getConfig
import org.bukkit.Bukkit; import lombok.*
import org.bukkit.configuration.Configuration; import org.bukkit.Bukkit
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.Configuration
import org.bukkit.scheduler.BukkitTask; import org.bukkit.scheduler.BukkitTask
import java.util.function.BiFunction
import java.lang.reflect.Array; import java.util.function.Function
import java.util.HashMap;
import java.util.List;
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 the getter/setter constructor if [T] isn't a primitive type or String.<br></br>
* Use {@link Component#getConfig()} or {@link ButtonPlugin#getIConfig()} then {@link IHaveConfig#getData(String, Object)} to get an instance. * Use [Component.getConfig] or [ButtonPlugin.getIConfig] then [IHaveConfig.getData] to get an instance.
*/ */
public class ConfigData<T> { open class ConfigData<T> internal constructor(
private static final HashMap<Configuration, SaveTask> saveTasks = new HashMap<>(); config: IHaveConfig?,
/** path: String?,
* May be null for testing def: T,
*/ primitiveDef: Any?,
private IHaveConfig config; getter: Function<Any?, T>?,
@Getter setter: Function<T, Any?>?
@Setter(AccessLevel.PACKAGE) ) {
private String path; /**
protected final T def; * May be null for testing
private final Object primitiveDef; */
/** private val config: IHaveConfig?
* 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;
/** @Getter
* The config value should not change outside this instance @Setter(AccessLevel.PACKAGE)
*/ private val path: String?
private T value; 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) { * The parameter is of a primitive type as returned by [YamlConfiguration.get]
if (primitiveDef == null) */
throw new IllegalArgumentException("Either def or primitiveDef must be set."); private val getter: Function<Any?, T>?
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
}
@Override /**
public String toString() { * The result should be a primitive type or string that can be retrieved correctly later
return "ConfigData{" + "path='" + path + '\'' + ", value=" + value + '}'; */
} 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") init {
public T get() { var def: T? = def
if (value != null) return value; //Speed things up var primitiveDef = primitiveDef
var config = this.config.getConfig(); if (def == null) {
Object val; requireNotNull(primitiveDef) { "Either def or primitiveDef must be set." }
if (config == null || !config.isSet(path)) { //Call set() if config == null requireNotNull(getter) { "A getter and setter must be present when using primitiveDef." }
val = primitiveDef; def = getter.apply(primitiveDef)
if ((def == null || this instanceof ReadOnlyConfigData) && config != null) //In Discord's case def may be null } else if (primitiveDef == null) primitiveDef = if (setter == null) def else setter.apply(def)
setInternal(primitiveDef); //If read-only then we still need to save the default value so it can be set require(getter == null == (setter == null)) { "Both setters and getters must be present (or none if def is primitive)." }
else this.config = config
set(def); //Save default value - def is always set this.path = path
} else this.def = def
val = config.get(path); //config==null: testing this.primitiveDef = primitiveDef
if (val == null) //If it's set to null explicitly this.getter = getter
val = primitiveDef; this.setter = setter
BiFunction<Object, Object, Object> convert = (_val, _def) -> { get() //Generate config automatically
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
}
public void set(T value) { override fun toString(): String {
if (this instanceof ReadOnlyConfigData) return "ConfigData{path='$path', value=$value}"
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;
}
private void setInternal(Object val) { fun reset() {
config.getConfig().set(path, val); value = null
signalChange(config); }
}
static void signalChange(IHaveConfig config) { fun get(): T? {
var cc = config.getConfig(); if (value != null) return value //Speed things up
var sa = config.getSaveAction(); val config = config!!.getConfig<Any>()
if (!saveTasks.containsKey(cc.getRoot())) { var `val`: Any?
synchronized (saveTasks) { if (config == null || !config.isSet(path)) { //Call set() if config == null
saveTasks.put(cc.getRoot(), new SaveTask(Bukkit.getScheduler().runTaskLaterAsynchronously(MainPlugin.Instance, () -> { `val` = primitiveDef
synchronized (saveTasks) { if ((def == null || this is ReadOnlyConfigData<*>) && config != null) //In Discord's case def may be null
saveTasks.remove(cc.getRoot()); setInternal(primitiveDef) //If read-only then we still need to save the default value so it can be set
sa.run(); else set(def) //Save default value - def is always set
} } else `val` = config.get(path) //config==null: testing
}, 100), sa)); 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 fun set(value: T?) {
private static class SaveTask { if (this is ReadOnlyConfigData<*>) return //Safety for Discord channel/role data
BukkitTask task; val `val`: Any?
Runnable saveAction; `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) { private fun setInternal(`val`: Any?) {
synchronized (saveTasks) { config!!.getConfig<Any>().set(path, `val`)
SaveTask st = saveTasks.get(config); signalChange(config)
if (st != null) { }
st.task.cancel();
saveTasks.remove(config);
st.saveAction.run();
return true;
}
}
return false;
}
public static <T> ConfigData.ConfigDataBuilder<T> builder(IHaveConfig config, String path) { @AllArgsConstructor
return new ConfigDataBuilder<T>(config, path); private class SaveTask {
} var task: BukkitTask? = null
var saveAction: Runnable? = null
}
@RequiredArgsConstructor(access = AccessLevel.PACKAGE) @RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public static class ConfigDataBuilder<T> { class ConfigDataBuilder<T> {
private final IHaveConfig config; private val config: IHaveConfig? = null
private final String path; private val path: String? = null
private T def; private var def: T? = null
private Object primitiveDef; private var primitiveDef: Any? = null
private Function<Object, T> getter; private var getter: Function<Any?, T?>? = null
private Function<T, Object> setter; 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. * The default value to use, as used in code. If not a primitive type, use the [.getter] and [.setter] methods.
* <br/> * <br></br>
* To set the value as it is stored, use {@link #primitiveDef(Object)}. * To set the value as it is stored, use [.primitiveDef].
* *
* @param def The default value * @param def The default value
* @return This builder * @return This builder
*/ */
public ConfigDataBuilder<T> def(T def) { fun def(def: T): ConfigDataBuilder<T> {
this.def = def; this.def = def
return this; 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. * 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></br>
* To set the value as used in the code, use {@link #def(Object)}. * To set the value as used in the code, use [.def].
* *
* @param primitiveDef The default value * @param primitiveDef The default value
* @return This builder * @return This builder
*/ */
public ConfigDataBuilder<T> primitiveDef(Object primitiveDef) { fun primitiveDef(primitiveDef: Any?): ConfigDataBuilder<T> {
this.primitiveDef = primitiveDef; this.primitiveDef = primitiveDef
return this; return this
} }
/** /**
* A function to use to obtain the runtime object from the yaml representation (usually string). * A function to use to obtain the runtime object from the yaml representation (usually string).
* The {@link #setter(Function)} must also be set. * The [.setter] must also be set.
* *
* @param getter A function that receives the primitive type and returns the runtime type * @param getter A function that receives the primitive type and returns the runtime type
* @return This builder * @return This builder
*/ */
public ConfigDataBuilder<T> getter(Function<Object, T> getter) { fun getter(getter: Function<Any?, T>?): ConfigDataBuilder<T> {
this.getter = getter; this.getter = getter
return this; return this
} }
/** /**
* A function to use to obtain the yaml representation (usually string) from the runtime object. * A function to use to obtain the yaml representation (usually string) from the runtime object.
* The {@link #getter(Function)} must also be set. * The [.getter] must also be set.
* *
* @param setter A function that receives the runtime type and returns the primitive type * @param setter A function that receives the runtime type and returns the primitive type
* @return This builder * @return This builder
*/ */
public ConfigDataBuilder<T> setter(Function<T, Object> setter) { fun setter(setter: Function<T, Any?>?): ConfigDataBuilder<T> {
this.setter = setter; this.setter = setter
return this; return this
} }
/** /**
* Builds a modifiable config representation. Use if you want to change the value <i>in code</i>. * Builds a modifiable config representation. Use if you want to change the value *in code*.
* *
* @return A ConfigData instance. * @return A ConfigData instance.
*/ */
public ConfigData<T> build() { fun build(): ConfigData<T?> {
ConfigData<T> config = new ConfigData<>(this.config, path, def, primitiveDef, getter, setter); val config = ConfigData(config, path, def, primitiveDef, getter, setter)
this.config.onConfigBuild(config); this.config!!.onConfigBuild(config)
return config; return config
} }
/** /**
* Builds a read-only config representation. Use if you only want the value to be changed <i>in the config</i>. * Builds a read-only config representation. Use if you only want the value to be changed *in the config*.
* *
* @return A ReadOnlyConfigData instance. * @return A ReadOnlyConfigData instance.
*/ */
public ReadOnlyConfigData<T> buildReadOnly() { fun buildReadOnly(): ReadOnlyConfigData<T?> {
ReadOnlyConfigData<T> config = new ReadOnlyConfigData<>(this.config, path, def, primitiveDef, getter, setter); val config = ReadOnlyConfigData(config, path, def, primitiveDef, getter, setter)
this.config.onConfigBuild(config); this.config!!.onConfigBuild(config)
return 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.core.MainPlugin
import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCCoreAPI
import lombok.Getter; import buttondevteam.lib.architecture.ConfigData.ConfigDataBuilder
import lombok.Setter; import lombok.Getter
import lombok.val; import org.bukkit.Bukkit
import org.bukkit.Bukkit; import org.bukkit.configuration.ConfigurationSection
import org.bukkit.configuration.ConfigurationSection; import org.bukkit.plugin.java.JavaPlugin
import org.bukkit.plugin.java.JavaPlugin; import java.lang.reflect.InvocationTargetException
import java.util.*
import javax.annotation.Nullable; import java.util.function.Function
import java.lang.reflect.InvocationTargetException; import java.util.function.Predicate
import java.util.*; import java.util.function.Supplier
import java.util.function.Function; import java.util.stream.Collectors
import java.util.function.Supplier;
import java.util.stream.Collectors;
/** /**
* A config system * 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 { class IHaveConfig(var saveAction: Runnable?) { // TODO: Make non-nullable after adding component builder
private final HashMap<String, ConfigData<?>> datamap = new HashMap<>(); private val datamap = HashMap<String, ConfigData<*>>()
/**
* Returns the Bukkit ConfigurationSection. Use {@link #signalChange()} after changing it.
*/
@Getter
private ConfigurationSection config;
@Getter
@Setter
private Runnable saveAction;
/** /**
* May be used in testing. * Returns the Bukkit ConfigurationSection. Use [.signalChange] after changing it.
* */
* @param saveAction What to do to save the config to disk. Don't use get methods until it's non-null. @Getter
*/ private var config: ConfigurationSection? = null
public IHaveConfig(Runnable saveAction) {
this.saveAction = saveAction;
}
/** /**
* 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. * 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 path The dot-separated path relative to this config instance
* @param <T> The runtime type of the config value * @param <T> The runtime type of the config value
* @return A ConfigData builder to set how to obtain the value * @return A ConfigData builder to set how to obtain the value
*/ </T> */
public <T> ConfigData.ConfigDataBuilder<T> getConfig(String path) { fun <T> getConfig(path: String?): ConfigDataBuilder<T> {
return ConfigData.builder(this, path); return ConfigData.builder(this, path)
} }
void onConfigBuild(ConfigData<?> config) { fun onConfigBuild(config: ConfigData<*>) {
datamap.put(config.getPath(), config); datamap[config.path] = config
} }
/** /**
* This method overload should only be used with primitives or String. * This method overload should only be used with primitives or String.
* *
* @param path The path in config to use * @param path The path in config to use
* @param def The value to use by default * @param def The value to use by default
* @param <T> The type of this variable (only use primitives or String) * @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 * @return The data object that can be used to get or set the value
*/ </T> */
@SuppressWarnings("unchecked") fun <T> getData(path: String, def: T): ConfigData<T> {
public <T> ConfigData<T> getData(String path, T def) { var data = datamap[path]
ConfigData<?> data = datamap.get(path); if (data == null) datamap[path] = ConfigData(this, path, def, def, null, null).also { data = it }
if (data == null) datamap.put(path, data = new ConfigData<>(this, path, def, def, null, null)); @Suppress("UNCHECKED_CAST")
return (ConfigData<T>) data; return data as ConfigData<T>
} }
/** /**
* This method overload may be used with any class. * This method overload may be used with any class.
* *
* @param path The path in config to use * @param path The path in config to use
* @param def The value to use by default * @param def The value to use by default
* @param getter A function that converts a primitive representation to the correct value * @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 setter A function that converts a value to a primitive representation
* @param <T> The type of this variable (can be any class) * @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 * @return The data object that can be used to get or set the value
*/ </T> */
@SuppressWarnings("unchecked") fun <T> getData(path: String, def: T, getter: Function<Any?, T>?, setter: Function<T, Any?>): ConfigData<T> {
public <T> ConfigData<T> getData(String path, T def, Function<Object, T> getter, Function<T, Object> setter) { var data = datamap[path]
ConfigData<?> data = datamap.get(path); if (data == null) datamap[path] =
if (data == null) ConfigData(this, path, def, setter.apply(def), getter, setter).also { data = it }
datamap.put(path, data = new ConfigData<>(this, path, def, setter.apply(def), getter, setter)); @Suppress("UNCHECKED_CAST")
return (ConfigData<T>) data; return data as ConfigData<T>
} }
/** /**
* This method overload may be used with any class. The given default value will be run through the getter. * 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 path The path in config to use
* @param primitiveDef The <b>primitive</b> value to use by default * @param primitiveDef The **primitive** value to use by default
* @param getter A function that converts a primitive representation to the correct value * @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 setter A function that converts a value to a primitive representation
* @param <T> The type of this variable (can be any class) * @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 * @return The data object that can be used to get or set the value
*/ </T> */
@SuppressWarnings("unchecked") fun <T> getDataPrimDef(
public <T> ConfigData<T> getDataPrimDef(String path, Object primitiveDef, Function<Object, T> getter, Function<T, Object> setter) { path: String,
ConfigData<?> data = datamap.get(path); primitiveDef: Any?,
if (data == null) getter: Function<Any?, T>,
datamap.put(path, data = new ConfigData<>(this, path, getter.apply(primitiveDef), primitiveDef, getter, setter)); setter: Function<T, Any?>?
return (ConfigData<T>) data; ): 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. * 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 path The path in config to use
* @param primitiveDef The <b>primitive</b> value to use by default * @param primitiveDef The **primitive** value to use by default
* @param getter A function that converts a primitive representation to the correct value * @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 setter A function that converts a value to a primitive representation
* @param <T> The type of this variable (can be any class) * @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 * @return The data object that can be used to get or set the value
*/ </T> */
@SuppressWarnings("unchecked") fun <T> getReadOnlyDataPrimDef(
public <T> ReadOnlyConfigData<T> getReadOnlyDataPrimDef(String path, Object primitiveDef, Function<Object, T> getter, Function<T, Object> setter) { path: String,
ConfigData<?> data = datamap.get(path); primitiveDef: Any?,
if (data == null) getter: Function<Any?, T>,
datamap.put(path, data = new ReadOnlyConfigData<>(this, path, getter.apply(primitiveDef), primitiveDef, getter, setter)); setter: Function<T, Any?>?
return (ReadOnlyConfigData<T>) data; ): 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. * This method overload should only be used with primitves or String.
* *
* @param path The path in config to use * @param path The path in config to use
* @param def The value to use by default * @param def The value to use by default
* @param <T> The type of this variable (only use primitives or String) * @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 * @return The data object that can be used to get or set the value
*/ </T> */
@SuppressWarnings("unchecked") fun <T> getData(path: String, def: Supplier<T>): ConfigData<T> {
public <T> ConfigData<T> getData(String path, Supplier<T> def) { var data = datamap[path]
ConfigData<?> data = datamap.get(path); if (data == null) {
if (data == null) { val defval = def.get()
val defval = def.get(); datamap[path] = ConfigData(this, path, defval, defval, null, null).also { data = it }
datamap.put(path, data = new ConfigData<>(this, path, defval, defval, null, null)); }
} @Suppress("UNCHECKED_CAST")
return (ConfigData<T>) data; return data as ConfigData<T>
} }
/** /**
* This method overload may be used with any class. * This method overload may be used with any class.
* *
* @param path The path in config to use * @param path The path in config to use
* @param def The value to use by default * @param def The value to use by default
* @param getter A function that converts a primitive representation to the correct value * @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 setter A function that converts a value to a primitive representation
* @param <T> The type of this variable (can be any class) * @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 * @return The data object that can be used to get or set the value
*/ </T> */
@SuppressWarnings("unchecked") fun <T> getData(
public <T> ConfigData<T> getData(String path, Supplier<T> def, Function<Object, T> getter, Function<T, Object> setter) { path: String,
ConfigData<?> data = datamap.get(path); def: Supplier<T>,
if (data == null) { getter: Function<Any?, T>?,
val defval = def.get(); setter: Function<T, Any?>
datamap.put(path, data = new ConfigData<>(this, path, defval, setter.apply(defval), getter, setter)); ): ConfigData<T> {
} var data = datamap[path]
return (ConfigData<T>) data; 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. * This method overload should only be used with primitves or String.
* *
* @param path The path in config to use * @param path The path in config to use
* @param <T> The type of this variable (only use primitives or String) * @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 * @return The data object that can be used to get or set the value
*/ </T> */
@SuppressWarnings("unchecked") fun <T> getListData(path: String): ListConfigData<T> {
public <T> ListConfigData<T> getListData(String path) { var data = datamap[path]
ConfigData<?> data = datamap.get(path); if (data == null) datamap[path] = ListConfigData(this, path, ListConfigData.List<T>()).also { data = it }
if (data == null) @Suppress("UNCHECKED_CAST")
datamap.put(path, data = new ListConfigData<>(this, path, new ListConfigData.List<T>())); return data as ListConfigData<T>
return (ListConfigData<T>) data; }
}
/** /**
* Schedules a save operation. Use after changing the ConfigurationSection directly. * Schedules a save operation. Use after changing the ConfigurationSection directly.
*/ */
public void signalChange() { fun signalChange() {
ConfigData.signalChange(this); ConfigData.signalChange(this)
} }
/** /**
* Clears all caches and loads everything from yaml. * Clears all caches and loads everything from yaml.
*/ */
public void reset(ConfigurationSection config) { fun reset(config: ConfigurationSection?) {
this.config = config; this.config = config
datamap.forEach((path, data) -> data.reset()); datamap.forEach { (path: String?, data: ConfigData<*>) -> data.reset() }
} }
/** companion object {
* Generates the config YAML. /**
* * 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. * @param obj The object which has config methods
*/ * @param configMap The result from [Component.getConfigMap]. May be null.
public static void pregenConfig(Object obj, @Nullable Map<String, IHaveConfig> configMap) { */
val ms = obj.getClass().getDeclaredMethods(); fun pregenConfig(obj: Any, configMap: Map<String, IHaveConfig?>?) {
for (val m : ms) { val ms = obj.javaClass.declaredMethods
if (!m.getReturnType().getName().equals(ConfigData.class.getName())) continue; for (m in ms) {
final String mName; if (m.returnType.name != ConfigData::class.java.name) continue
{ val mName: String
var name = m.getName(); run {
var ind = name.lastIndexOf('$'); val name = m.name
if (ind == -1) mName = name; val ind = name.lastIndexOf('$')
else mName = name.substring(ind + 1); mName = if (ind == -1) name else name.substring(ind + 1)
} }
try { try {
m.setAccessible(true); m.isAccessible = true
List<ConfigData<?>> configList; var configList: List<ConfigData<*>>
if (m.getParameterCount() == 0) { configList = if (m.parameterCount == 0) {
configList = Collections.singletonList((ConfigData<?>) m.invoke(obj)); listOf(m.invoke(obj) as ConfigData<*>)
} else if (m.getParameterCount() == 1 && m.getParameterTypes()[0] == IHaveConfig.class) { } else if (m.parameterCount == 1 && m.parameterTypes[0] == IHaveConfig::class.java) {
if (configMap == null) continue; //Hope it will get called with the param later if (configMap == null) continue //Hope it will get called with the param later
configList = configMap.entrySet().stream().map(kv -> configMap.entries.stream().map { (key, value): Map.Entry<String, IHaveConfig?> ->
{ try {
try { return@map m.invoke(obj, value) as ConfigData<*>
return (ConfigData<?>) m.invoke(obj, kv.getValue()); } catch (e: IllegalAccessException) {
} catch (IllegalAccessException | InvocationTargetException e) { val msg = "Failed to pregenerate $mName for $obj using config $key!"
String msg = "Failed to pregenerate " + mName + " for " + obj + " using config " + kv.getKey() + "!"; if (obj is Component<*>) TBMCCoreAPI.SendException(
if (obj instanceof Component<?>) msg,
TBMCCoreAPI.SendException(msg, e, (Component<?>) obj); e,
else if (obj instanceof JavaPlugin) obj
TBMCCoreAPI.SendException(msg, e, (JavaPlugin) obj); ) else if (obj is JavaPlugin) TBMCCoreAPI.SendException(
else msg,
TBMCCoreAPI.SendException(msg, e, false, Bukkit.getLogger()::warning); e,
return null; obj
} ) else TBMCCoreAPI.SendException(msg, e, false) { msg: String? ->
}).filter(Objects::nonNull).collect(Collectors.toList()); Bukkit.getLogger().warning(msg)
} else { }
if (TBMCCoreAPI.IsTestServer()) return@map null
MainPlugin.Instance.getLogger().warning("Method " + mName + " returns a config but its parameters are unknown: " + Arrays.toString(m.getParameterTypes())); } catch (e: InvocationTargetException) {
continue; val msg = "Failed to pregenerate $mName for $obj using config $key!"
} if (obj is Component<*>) TBMCCoreAPI.SendException(
for (val c : configList) { msg,
if (c.getPath().length() == 0) e,
c.setPath(mName); obj
else if (!c.getPath().equals(mName)) ) else if (obj is JavaPlugin) TBMCCoreAPI.SendException(
MainPlugin.Instance.getLogger().warning("Config name does not match: " + c.getPath() + " instead of " + mName); msg,
c.get(); //Saves the default value if needed - also checks validity e,
} obj
} catch (Exception e) { ) else TBMCCoreAPI.SendException(msg, e, false) { msg: String? ->
String msg = "Failed to pregenerate " + mName + " for " + obj + "!"; Bukkit.getLogger().warning(msg)
if (obj instanceof Component<?>) }
TBMCCoreAPI.SendException(msg, e, (Component<?>) obj); return@map null
else if (obj instanceof JavaPlugin) }
TBMCCoreAPI.SendException(msg, e, (JavaPlugin) obj); }
else .filter(Predicate<ConfigData<Any?>> { obj: ConfigData<Any?>? -> Objects.nonNull(obj) })
TBMCCoreAPI.SendException(msg, e, false, Bukkit.getLogger()::warning); .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) }
}
}
}
}
} }