Improved config handling, updated read-only and list configs

- The read-only configs are no longer using a separate class, it seems
unnecessary
- The list configs got refactored so that the entire list doesn't get
recreated each time it changes
- Also added support for getters/setters for the list config, just to be
consistent
This commit is contained in:
Norbi Peti 2023-03-17 16:45:38 +01:00
parent 99958e74f9
commit 8d8708d14b
5 changed files with 240 additions and 294 deletions

View file

@ -2,6 +2,7 @@ package buttondevteam.lib.architecture
import buttondevteam.core.MainPlugin
import buttondevteam.lib.ChromaUtils
import buttondevteam.lib.architecture.config.IConfigData
import org.bukkit.Bukkit
import org.bukkit.configuration.Configuration
import org.bukkit.scheduler.BukkitTask
@ -14,14 +15,14 @@ import java.util.function.Function
* @param getter The parameter is of a primitive type as returned by [Configuration.get]
* @param setter The result should be a primitive type or string that can be retrieved correctly later
*/
open class ConfigData<T> internal constructor(
private val config: IHaveConfig?,
val path: String,
def: T?,
class ConfigData<T> internal constructor(
val config: IHaveConfig?,
override val path: String,
primitiveDef: Any?,
private val getter: Function<Any?, T>,
private val setter: Function<T, Any?>
) {
private val setter: Function<T, Any?>,
private val readOnly: Boolean
) : IConfigData<T> {
private val pdef: Any?
/**
@ -30,7 +31,7 @@ open class ConfigData<T> internal constructor(
private var value: T? = null
init {
this.pdef = primitiveDef ?: def?.let { setter.apply(it) }
this.pdef = primitiveDef
?: throw IllegalArgumentException("Either def or primitiveDef must be set. A getter and setter must be present when using primitiveDef.")
get() //Generate config automatically
}
@ -39,11 +40,11 @@ open class ConfigData<T> internal constructor(
return "ConfigData{path='$path', value=$value}"
}
fun reset() {
override fun reset() {
value = null
}
fun get(): T? {
override fun get(): T? {
if (value != null) return value //Speed things up
val config = config?.config
var `val`: Any?
@ -65,8 +66,8 @@ open class ConfigData<T> internal constructor(
return getter.apply(convert(`val`, pdef)).also { value = it }
}
fun set(value: T?) {
if (this is ReadOnlyConfigData<*>) return //Safety for Discord channel/role data
override fun set(value: T?) {
if (readOnly) return //Safety for Discord channel/role data
val `val` = value?.let { setter.apply(value) }
setInternal(`val`)
this.value = value
@ -80,88 +81,30 @@ open class ConfigData<T> internal constructor(
private class SaveTask(val task: BukkitTask, val saveAction: Runnable)
class ConfigDataBuilder<T> internal constructor(private val config: IHaveConfig, private val path: String) {
private var def: T? = null
private var primitiveDef: Any? = null
@Suppress("UNCHECKED_CAST")
private var getter: Function<Any?, T> = Function { it as T }
private var setter: Function<T, Any?> = Function { it }
/**
* The default value to use, as used in code. If not a primitive type, use the [.getter] and [.setter] methods.
* <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 [.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 [.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 [.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
}
class ConfigDataBuilder<T> internal constructor(
private val config: IHaveConfig,
private val path: String,
private val primitiveDef: Any?,
private val getter: Function<Any?, T>,
private val setter: Function<T, Any?>
) {
/**
* 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)
fun build(readOnly: Boolean = false): ConfigData<T> {
val config = ConfigData(config, path, primitiveDef, getter, setter, readOnly)
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
}
override fun toString(): String {
return "ConfigData.ConfigDataBuilder(config=$config, path=$path, def=$def, primitiveDef=$primitiveDef, getter=$getter, setter=$setter)"
fun buildList(readOnly: Boolean = false): ListConfigData<T> {
if (primitiveDef is List<*>) {
val config = ListConfigData(config, path, primitiveDef, getter, setter, readOnly)
this.config.onConfigBuild(config)
return config
}
}
}

View file

@ -3,6 +3,7 @@ package buttondevteam.lib.architecture
import buttondevteam.core.MainPlugin
import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.architecture.ConfigData.ConfigDataBuilder
import buttondevteam.lib.architecture.config.IConfigData
import org.bukkit.Bukkit
import org.bukkit.configuration.ConfigurationSection
import org.bukkit.plugin.java.JavaPlugin
@ -16,90 +17,72 @@ 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.
*/
class IHaveConfig(var saveAction: Runnable?) { // TODO: Make non-nullable after adding component builder
private val datamap = HashMap<String, ConfigData<*>>()
class IHaveConfig(
/**
* The way the underlying configuration gets saved to disk
*/
val saveAction: Runnable,
/**
* Returns the Bukkit ConfigurationSection. Use [.signalChange] after changing it.
*/
var config: ConfigurationSection? = null // TODO: Make non-nullable after removing reset() method
val config: ConfigurationSection
) {
private val datamap = HashMap<String, IConfigData<*>>()
/**
* 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.
* You may use this method with any data type, but always provide getters and setters that convert to primitive types
* if you're not using primitive types directly.
* These primitive types are strings, numbers, characters, booleans and lists of these things.
*
* @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> {
* @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 primDef Whether the default value is a primitive value that needs to be converted to the correct type using the getter
* @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
*/
fun <T> getConfig(
path: String,
def: T,
getter: Function<Any?, T>? = null,
setter: Function<T, Any?>? = null
): ConfigDataBuilder<T> {
return ConfigData.builder(this, path)
}
fun onConfigBuild(config: ConfigData<*>) {
fun onConfigBuild(config: IConfigData<*>) {
datamap[config.path] = config
}
/**
* This method overload should only be used with primitives or String.
* You may use this method with any data type, but always provide getters and setters that convert to primitive types
* if you're not using primitive types directly.
* These primitive types are strings, numbers, characters, booleans and lists of these things.
*
* @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 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 primDef Whether the default value is a primitive value that needs to be converted to the correct type using the getter
* @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 **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(
@Suppress("UNCHECKED_CAST")
fun <T> getData(
path: String,
primitiveDef: Any?,
getter: Function<Any?, T>,
setter: Function<T, Any?>?
def: T,
getter: Function<Any?, T>? = null,
setter: Function<T, Any?>? = null,
readOnly: Boolean = false
): 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>
return getData(path, getter ?: Function { it as T }, setter ?: Function { it }, def)
}
/**
* This method overload may be used with any class. The given default value will be run through the getter.
* You may use this method with any data type and provide a primitive default value.
* These primitive types are strings, numbers, characters, booleans and lists of these things.
*
* @param path The path in config to use
* @param primitiveDef The **primitive** value to use by default
@ -108,17 +91,17 @@ class IHaveConfig(var saveAction: Runnable?) { // TODO: Make non-nullable after
* @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(
@Suppress("UNCHECKED_CAST")
fun <T> getData(
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>
setter: Function<T, Any?>,
primitiveDef: Any?,
readOnly: Boolean = false
): ConfigData<T> {
val data =
datamap[path] ?: ConfigData(this, path, primitiveDef, getter, setter, readOnly).also { datamap[path] = it }
return data as ConfigData<T>
}
/**
@ -139,31 +122,6 @@ class IHaveConfig(var saveAction: Runnable?) { // TODO: Make non-nullable after
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
</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.
*

View file

@ -1,124 +1,172 @@
package buttondevteam.lib.architecture;
package buttondevteam.lib.architecture
import lombok.val;
import org.jetbrains.annotations.NotNull;
import buttondevteam.lib.architecture.config.IConfigData
import java.util.function.Function
import java.util.function.Predicate
import java.util.function.UnaryOperator
import kotlin.collections.List as KList
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
class ListConfigData<T> internal constructor(
config: IHaveConfig?,
path: String,
primitiveDef: kotlin.collections.List<*>,
private val elementGetter: Function<Any?, T>,
private val elementSetter: Function<T, Any?>,
readOnly: Boolean
) : IConfigData<ListConfigData<T>.List> {
val listConfig: ConfigData<List> =
ConfigData(config, path, primitiveDef, { List((it as KList<*>).toMutableList()) }, { it }, readOnly)
public class ListConfigData<T> extends ConfigData<ListConfigData.List<T>> {
@SuppressWarnings("unchecked")
ListConfigData(IHaveConfig config, String path, List<T> def) {
super(config, path, def, new ArrayList<>(def), list -> {
var l = new List<>((ArrayList<T>) list);
l.listConfig = def.listConfig;
return l;
}, ArrayList::new);
def.listConfig = this; //Can't make the List class non-static or pass this in the super() constructor
}
override val path: String get() = listConfig.path
public static class List<T> extends ArrayList<T> {
private ListConfigData<T> listConfig;
override fun reset() {
listConfig.reset()
}
public List(@NotNull Collection<? extends T> c) {
super(c);
}
override fun get(): List? {
return listConfig.get()
}
public List() {
}
override fun set(value: List?) {
listConfig.set(value)
}
private void update() {
listConfig.set(this); //Update the config model and start save task if needed
}
inner class List(backingList: MutableList<Any?>) : MutableList<T> {
private val primitiveList = backingList
override val size: Int get() = primitiveList.size
private fun update() {
val config = listConfig.config
if (config != null) {
ConfigData.signalChange(config) //Update the config model and start save task if needed
}
}
@Override
public T set(int index, T element) {
T ret = super.set(index, element);
update();
return ret;
}
override fun set(index: Int, element: T): T {
val ret = primitiveList.set(index, elementSetter.apply(element))
update()
return elementGetter.apply(ret)
}
@Override
public boolean add(T t) {
val ret = super.add(t);
update();
return ret;
}
override fun add(element: T): Boolean {
val ret = primitiveList.add(elementSetter.apply(element))
update()
return ret
}
@Override
public void add(int index, T element) {
super.add(index, element);
update();
}
override fun add(index: Int, element: T) {
primitiveList.add(index, elementSetter.apply(element))
update()
}
@Override
public T remove(int index) {
T ret = super.remove(index);
update();
return ret;
}
override fun removeAt(index: Int): T {
val ret = primitiveList.removeAt(index)
update()
return elementGetter.apply(ret)
}
@Override
public boolean remove(Object o) {
val ret = super.remove(o);
update();
return ret;
}
override fun subList(fromIndex: Int, toIndex: Int): MutableList<T> =
List(primitiveList.subList(fromIndex, toIndex))
@Override
public boolean addAll(Collection<? extends T> c) {
val ret = super.addAll(c);
update();
return ret;
}
override fun remove(element: T): Boolean {
val ret = primitiveList.remove(elementSetter.apply(element))
update()
return ret
}
@Override
public boolean addAll(int index, Collection<? extends T> c) {
val ret = super.addAll(index, c);
update();
return ret;
}
override fun addAll(elements: Collection<T>): Boolean {
val ret = primitiveList.addAll(elements.map { elementSetter.apply(it) })
update()
return ret
}
@Override
protected void removeRange(int fromIndex, int toIndex) {
super.removeRange(fromIndex, toIndex);
update();
}
override fun addAll(index: Int, elements: Collection<T>): Boolean {
val ret = primitiveList.addAll(index, elements.map { elementSetter.apply(it) })
update()
return ret
}
@Override
public boolean removeAll(Collection<?> c) {
val ret = super.removeAll(c);
update();
return ret;
}
override fun removeAll(elements: Collection<T>): Boolean {
val ret = primitiveList.removeAll(elements.map { elementSetter.apply(it) })
update()
return ret
}
@Override
public boolean retainAll(Collection<?> c) {
val ret = super.retainAll(c);
update();
return ret;
}
override fun retainAll(elements: Collection<T>): Boolean {
val ret = primitiveList.retainAll(elements.map { elementSetter.apply(it) })
update()
return ret
}
@Override
public boolean removeIf(Predicate<? super T> filter) {
val ret = super.removeIf(filter);
update();
return ret;
}
override fun removeIf(filter: Predicate<in T>): Boolean {
val ret = primitiveList.removeIf { filter.test(elementGetter.apply(it)) }
update()
return ret
}
@Override
public void replaceAll(UnaryOperator<T> operator) {
super.replaceAll(operator);
update();
}
override fun replaceAll(operator: UnaryOperator<T>) {
primitiveList.replaceAll { elementSetter.apply(operator.apply(elementGetter.apply(it))) }
update()
}
@Override
public void sort(Comparator<? super T> c) {
super.sort(c);
update();
}
}
override fun sort(c: Comparator<in T>) {
primitiveList.sortWith { o1, o2 -> c.compare(elementGetter.apply(o1), elementGetter.apply(o2)) }
update()
}
override fun clear() {
primitiveList.clear()
update()
}
override fun get(index: Int): T = elementGetter.apply(primitiveList[index])
override fun isEmpty(): Boolean = primitiveList.isEmpty()
override fun lastIndexOf(element: T): Int = primitiveList.lastIndexOf(elementSetter.apply(element))
override fun indexOf(element: T): Int = primitiveList.indexOf(elementSetter.apply(element))
override fun containsAll(elements: Collection<T>): Boolean =
primitiveList.containsAll(elements.map { elementSetter.apply(it) })
override fun contains(element: T): Boolean = primitiveList.contains(elementSetter.apply(element))
override fun iterator(): MutableIterator<T> {
return object : MutableIterator<T> {
private val iterator = primitiveList.iterator()
override fun hasNext(): Boolean = iterator.hasNext()
override fun next(): T = elementGetter.apply(iterator.next())
override fun remove() {
iterator.remove()
update()
}
}
}
override fun listIterator(): MutableListIterator<T> {
return listIterator(0)
}
override fun listIterator(index: Int): MutableListIterator<T> {
return object : MutableListIterator<T> {
private val iterator = primitiveList.listIterator(index)
override fun hasNext(): Boolean = iterator.hasNext()
override fun next(): T = elementGetter.apply(iterator.next())
override fun remove() {
iterator.remove()
update()
}
override fun hasPrevious(): Boolean = iterator.hasPrevious()
override fun nextIndex(): Int = iterator.nextIndex()
override fun previous(): T = elementGetter.apply(iterator.previous())
override fun previousIndex(): Int = iterator.previousIndex()
override fun add(element: T) {
iterator.add(elementSetter.apply(element))
update()
}
override fun set(element: T) {
iterator.set(elementSetter.apply(element))
update()
}
}
}
}
}

View file

@ -1,12 +0,0 @@
package buttondevteam.lib.architecture
import java.util.function.Function
class ReadOnlyConfigData<T> internal constructor(
config: IHaveConfig?,
path: String,
def: T?,
primitiveDef: Any?,
getter: Function<Any?, T>,
setter: Function<T, Any?>
) : ConfigData<T>(config, path, def, primitiveDef, getter, setter)

View file

@ -0,0 +1,9 @@
package buttondevteam.lib.architecture.config
interface IConfigData<T> {
fun reset()
fun get(): T?
fun set(value: T?)
val path: String
}