From 106ca040af5a64355aff179b5f0e3578c10e91fd Mon Sep 17 00:00:00 2001 From: Robinson Date: Wed, 16 Aug 2023 19:43:33 -0500 Subject: [PATCH] Fixed issues with array/list expansion for loading, cli/sys/env. Requirement for cli/sys/env is that it MUST match case --- src/dorkbox/config/ConfigProcessor.kt | 574 +++++++++++++++++--------- src/dorkbox/config/ConfigProp.kt | 42 +- test/dorkbox/Test.kt | 217 +++++++++- 3 files changed, 598 insertions(+), 235 deletions(-) diff --git a/src/dorkbox/config/ConfigProcessor.kt b/src/dorkbox/config/ConfigProcessor.kt index 14b8866..d6ba318 100644 --- a/src/dorkbox/config/ConfigProcessor.kt +++ b/src/dorkbox/config/ConfigProcessor.kt @@ -28,14 +28,13 @@ import dorkbox.json.Json.Companion.isShort import dorkbox.json.Json.Companion.isString import dorkbox.json.OutputType import dorkbox.os.OS +import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.File import java.lang.IllegalArgumentException import java.lang.reflect.Modifier import java.util.* -import kotlin.collections.AbstractList import kotlin.collections.ArrayList -import kotlin.math.max import kotlin.reflect.KClass import kotlin.reflect.KProperty import kotlin.reflect.KVisibility @@ -131,12 +130,14 @@ class ConfigProcessor dorkbox.updates.Updates.add(ConfigProcessor::class.java, "23475d7cdfef4c1e9c38c310420086ca", version) } - private fun createConfigMap(config: T, objectType: KClass): Map { + private fun createConfigMap(config: T): MutableMap { + val klass = config::class + // this creates an EASY-to-use map of all arguments we have val argumentMap = mutableMapOf() // get all the members of this class. - for (member in objectType.declaredMemberProperties) { + for (member in klass.declaredMemberProperties) { @Suppress("UNCHECKED_CAST") assignFieldsToMap( argMap = argumentMap, @@ -184,7 +185,7 @@ class ConfigProcessor jsonName += "[$index]" } - val prop = ConfigProp(parentConf, parentObj, field, arrayName, index) + val prop = ConfigProp(jsonName, parentConf, parentObj, field, arrayName, index, false) val type = obj::class.java if (isString(type) || @@ -202,6 +203,9 @@ class ConfigProcessor argMap[jsonName] = prop } else if (type.isArray) { + // assign the actual array object. this makes access MUCH easier later on! + argMap[jsonName] = ConfigProp(jsonName, parentConf, parentObj, field, jsonName, index, true) + // iterate over the array, but assign the index with the name. @Suppress("UNCHECKED_CAST") val collection = obj as Array @@ -217,6 +221,9 @@ class ConfigProcessor index = i) } } else if (Collection::class.java.isAssignableFrom(type)) { + // assign the actual array object. this makes access MUCH easier later on! + argMap[jsonName] = ConfigProp(jsonName, parentConf, parentObj, field, jsonName, index, true) + // iterate over the collection, but assign the index with the name. @Suppress("UNCHECKED_CAST") val collection = obj as Collection @@ -277,6 +284,16 @@ class ConfigProcessor */ val json = Json() + /** + * Specify a logger to output errors, if any (instead of silently ignoring them) + */ + var logger: Logger? = null + set(value) { + field = value + json.logger = LoggerFactory.getLogger("test") + } + + @Suppress("UNCHECKED_CAST") private val objectType: KClass = configObject::class as KClass @@ -294,16 +311,14 @@ class ConfigProcessor */ var arguments: List = mutableListOf() - private val config: T = configObject - private var processed = false /** * this creates an EASY-to-use map of all arguments we have * * our configMap will ALWAYS modify the default (or rather, incoming object)!! */ - private val configMap: Map = createConfigMap(configObject, objectType) + private val configMap = createConfigMap(configObject) // DEEP COPY of the original values from the file, so when saving, we know what the // overridden values are and can skip saving them @@ -391,8 +406,9 @@ class ConfigProcessor } else { null } - } catch (ignored: Exception) { + } catch (exception: Exception) { // there was a problem parsing the config + logger?.error("Error loading JSON data", exception) null } @@ -403,7 +419,7 @@ class ConfigProcessor this.configString = configString load(configObject) - return process(configObject) + return postProcess() } /** @@ -421,8 +437,9 @@ class ConfigProcessor try { json.fromJson(objectType.java, fileContents) } - catch (ignored: Exception) { + catch (exception: Exception) { // there was a problem parsing the config + logger?.error("Error loading JSON data", exception) null } } @@ -441,7 +458,7 @@ class ConfigProcessor this.configFile = configFile load(configObject) - return process(configObject) + return postProcess() } /** @@ -458,8 +475,9 @@ class ConfigProcessor } else { null } - } catch (ignored: Exception) { + } catch (exception: Exception) { // there was a problem parsing the config + logger?.error("Error loading JSON data", exception) null } @@ -488,8 +506,9 @@ class ConfigProcessor try { json.fromJson(objectType.java, fileContents) } - catch (ignored: Exception) { + catch (exception: Exception) { // there was a problem parsing the config + logger?.error("Error loading JSON data", exception) null } } @@ -512,62 +531,148 @@ class ConfigProcessor } + // support + // thing[0].flag = true (array + list) + // thing[0].bah.flag = true (array + list) + @Suppress("UNCHECKED_CAST") @Synchronized private fun load(configObject: T) { - // we must make sure to modify the ORIGINAL object (which has already been set) - // if we are invoked multiple times, then we might "undo" the overloaded value, but then "redo" it later. - val incomingDataConfigMap = createConfigMap(configObject, objectType) - incomingDataConfigMap.forEach { (k,v) -> - // when setting a district array of length > 1, this is causing errors (since the "default" has no length, and we do. + val incomingDataConfigMap = createConfigMap(configObject) - // if we are an array/list/object -- then we REPLACE it (not set an element of it at a specific spot) - val current = configMap[k] - if (current != null) { - current.set(v.get()) - } else { - configMap as MutableMap - configMap.put(k, v) + // step 1 is to expand arrays... + // step 2 is to assign values (that are not an array themself) + val configuredArrays = mutableSetOf("") + + incomingDataConfigMap.values.forEach { prop -> + // have to expand arrays here! + + var p: ConfigProp? = prop + while (p != null && p.isSupported()) { + val parent = p.parentObj + + if (parent is Array<*>) { + // do we need to GROW the array? (we never shrink, we only grow) + val arrayName = p.collectionName + + if (!configuredArrays.contains(arrayName)) { + configuredArrays.add(arrayName) + + // expand, and get the new array (or original one) + val newArray = expandForProp(p, configMap) + + val affectedProps = incomingDataConfigMap.filterKeys { it.startsWith("$arrayName[") } + + affectedProps.forEach { (k, v) -> + val newProp = ConfigProp(k, v.parentConf, newArray, v.member, arrayName, v.index, v.ignore) + newProp.set(v.get()) + configMap[k] = newProp + } + } + } else if (Collection::class.java.isAssignableFrom(parent::class.java)) { + // do we add items to the original collection (we never shrink, we only grow) + val arrayName = p.collectionName + + if (!configuredArrays.contains(arrayName)) { + configuredArrays.add(arrayName) + + // get the original list (because we have to ADD to it!) + val origList = configMap[arrayName]!!.get()!! + + val affectedProps = incomingDataConfigMap.filterKeys { it.startsWith("$arrayName[") } + + affectedProps.forEach { (k, v) -> + // do we have an existing value for the affected property? + val existing = configMap[k] + if (existing == null) { + // we don't exist at all, so we have to add it. + // when we add it, we have to add ALL properties of the new object, so that all references are to the same base object + var affectedProp: ConfigProp? = v + while (affectedProp != null && affectedProp.parentObj != parent) { + affectedProp = affectedProp.parentConf + } + affectedProp!! + + val affectedObj = affectedProp.get() + if (origList is ArrayList<*>) { + (origList as ArrayList).add(affectedObj) + } else if (origList is MutableList<*>) { + (origList as MutableList).add(v) + } + + if (affectedObj != null) { + val affectedObjConfigMap = createConfigMap(affectedObj) + affectedObjConfigMap.forEach { (aK,aV) -> + // place all the new object property objects into the new ap (they are assign from the old map -- and so they reference the correct objects + + val newK = affectedProp.key + "." + aV.key + val newProp = ConfigProp(newK, affectedProp, aV.parentObj, aV.member, affectedProp.collectionName, aV.index, aV.ignore) + + configMap[newK] = newProp + } + } + } else { + existing.set(v.get()) + } + } + } + } + + p = p.parentConf } } - } - /** - * Processes and populates the values of the config object, if any - */ - @Synchronized - private fun process(configObject: T): ConfigProcessor { - if (!processed) { - processed = true - // only do this once - origCopyConfig = json.fromJson(objectType.java, json.toJson(configObject))!! - origCopyConfigMap = createConfigMap(origCopyConfig, objectType) + incomingDataConfigMap.values.forEach { prop -> + val parent = prop.parentObj + if (prop.isSupported() && + parent !is Array<*> && + !Collection::class.java.isAssignableFrom(parent::class.java)) { + + // it's not an array or collection, so we just assign the value + + // we have to just assign the value to our "original" value + val original = configMap[prop.key] + + if (original != null) { + original.set(prop.get()) + } else { + throw Exception("Unable to assign an incoming property to the original configuration when it doesn't exist.") + } + } } - return postProcess() + + // now setup the "original" objects. ORIGINAL means... + // 1) the original, passed in config object IFF no other config data was loaded + // 2) the file (as an object) + // 3) the text (as an object) + origCopyConfig = json.fromJson(objectType.java, json.toJson(configObject))!! + origCopyConfigMap = createConfigMap(origCopyConfig) } + /** * Processes and populates the values of the config object, if any */ @Synchronized fun process(): ConfigProcessor { - if (!processed) { - processed = true - // only do this once - origCopyConfig = json.fromJson(objectType.java, json.toJson(configObject))!! - origCopyConfigMap = createConfigMap(origCopyConfig, objectType) - } - // when processing data, the FILE, if present, is always the FIRST to load. configFile?.also { load(it) } // the string will be used to MODIFY what was set by the FILE (if present), configString?.also { load(it) } + if (configFile == null && configString == null) { + // now setup the "original" objects. ORIGINAL means... + // 1) the original, passed in config object IFF no other config data was loaded + // 2) the file (as an object) + // 3) the text (as an object) + origCopyConfig = json.fromJson(objectType.java, json.toJson(configObject))!! + origCopyConfigMap = createConfigMap(origCopyConfig) + } + return postProcess() } - @Suppress("UNCHECKED_CAST") private fun postProcess() : ConfigProcessor { // this permits a bash/batch/CLI invocation of "get xyz" or "set xyz" so we can be interactive with the CLI // if there is no get/set CLI argument, then this does nothing. @@ -588,106 +693,38 @@ class ConfigProcessor /** - * we have to check to see if we must grow the arrays, because if we ADD an element to an array based on the DEFAULT, meaning + * FOR CLI/SYS/ENV... we have to check to see if we must grow the arrays, because if we ADD an element to an array based on the DEFAULT, meaning * that the default array is size 3, and we add an element at position 4 -- we want to make sure that saving/printing, etc all work properly. */ run { val configuredArrays = mutableSetOf("") + val newProps = mutableMapOf() - configMap.forEach { (_, prop) -> - val returnType = prop.returnType + configMap.forEach { (arg, prop) -> + if (!prop.isSupported()) { + return@forEach + } - if (prop.isSupported()) { - val parent = prop.parent + val parent = prop.parentObj - if (parent is Array<*> || - parent is ArrayList<*> || - parent is AbstractList<*> - ) { + if (parent is Array<*> || + parent is ArrayList<*> || + parent is MutableList<*> + ) { - // do we need to GROW the array? (we never shrink, we only grow) - val arrayName = prop.collectionName + // do we need to GROW the array? (we never shrink, we only grow) + val arrayName = prop.collectionName - if (configuredArrays.contains(arrayName)) { - return@forEach - } + if (!configuredArrays.contains(arrayName)) { configuredArrays.add(arrayName) - val foundArgs = commandLineArguments.filter { it.startsWith("$arrayName[") } - - // if any of the foundArgs INDEX is larger than our array, we have to grow to that size. - val largestIndex: Int - - val array = - if (parent is Array<*>) { - largestIndex = parent.size - 1 - parent - } else if (parent is ArrayList<*>) { - largestIndex = parent.size - 1 - parent as ArrayList - } else if (parent is MutableList<*>) { - largestIndex = parent.size - 1 - parent as MutableList - } - else { - throw IllegalArgumentException("Unknown array type: ${parent.javaClass}") - } - - - val len = "$arrayName[".length - val cleaned = foundArgs.map { arg -> - val last = arg.indexOfFirst { it == ']' } - arg.substring(len, last).toInt() - }.maxOf { it } - val largest = max(largestIndex, cleaned) - - if (largest > largestIndex) { - val newObj = if (array is Array<*>) { - // we have to grow (offset by 1 again because of size vs index difference - val newArray = array.copyOf(largest + 1) - - // now we have to set the index so we can set it later - prop.parentConf!!.set(newArray) - newArray - } else { - array - } - - - // we must - // 1) assign what the parent obj is for the array members - // 2) create the new ones - for (index in 0 .. largest) { - val keyName = "$arrayName[$index]" - // prop.member DOES NOT MATTER for arrays BECAUSE we ignore it when get/set! - newProps[keyName] = ConfigProp(prop.parentConf, newObj, prop.member, arrayName, index) - } - - for (index in largestIndex + 1 .. largest) { - val keyName = "$arrayName[$index]" - // prop.member DOES NOT MATTER for arrays BECAUSE we ignore it when get/set! - val newProp = newProps[keyName]!! - - - if (parent is ArrayList<*>) { - (parent as ArrayList).add(defaultType(returnType)) - } - else if (parent is MutableList<*>) { - (parent as MutableList).add(defaultType(returnType)) - } - - // we have to set a default value!!! - newProp.set(defaultType(returnType)) - newProp.override = true // cannot use the method because we have to forceSet it! - } - } + // this checks CLI, SYS, ENV for array expansion + expandArrays(prop, newProps) } } } - configMap as MutableMap - - newProps.forEach { (k, v) -> + newProps.forEach { (k,v) -> configMap[k] = v } } @@ -704,91 +741,73 @@ class ConfigProcessor configMap.forEach { (arg, prop) -> val returnType = prop.returnType - if (prop.isSupported()) { - //// - // CLI CHECK IF PROPERTY EXIST (explicit check for arg=value) - // if arg is found, no more processing happens - //// - var foundArg = commandLineArguments.firstOrNull { it.startsWith("$arg=") } + if (!prop.isSupported()) { + LoggerFactory.getLogger(ConfigProcessor::class.java).error("${prop.member.name} (${returnType.javaObjectType.simpleName}) overloading is not supported. Ignoring") + return@forEach + } + + + //// + // CLI CHECK IF PROPERTY EXIST (explicit check for arg=value) + // if arg is found, no more processing happens + //// + var foundArg = commandLineArguments.firstOrNull { it.startsWith("$arg=") } + if (foundArg != null) { + // we know that split[0] == 'arg=' because we already checked for that + val overriddenValue = foundArg.split(regexEquals)[1].trim().getType(returnType) + + // This will only be saved if different, so we can figure out what the values are on save (and not save + // overridden properties, that are unchanged) + prop.override(overriddenValue) + + arguments.remove(foundArg) + return@forEach + } + + //// + // CLI CHECK IF PROPERTY EXIST (check if 'arg' exists, and is a boolean) + // if arg is found, no more processing happens + //// + if (returnType.isSubclassOf(Boolean::class)) { + // this is a boolean type? if present then we make it TRUE + foundArg = commandLineArguments.firstOrNull { it.startsWith(arg) } if (foundArg != null) { - // we know that split[0] == 'arg=' because we already checked for that - val overriddenValue = foundArg.split(regexEquals)[1].trim().getType(returnType) // This will only be saved if different, so we can figure out what the values are on save (and not save // overridden properties, that are unchanged) - prop.override(overriddenValue) + prop.override(true) arguments.remove(foundArg) return@forEach } - - //// - // CLI CHECK IF PROPERTY EXIST (check if 'arg' exists, and is a boolean) - // if arg is found, no more processing happens - //// - if (returnType.isSubclassOf(Boolean::class)) { - // this is a boolean type? if present then we make it TRUE - foundArg = commandLineArguments.firstOrNull { it.startsWith(arg) } - if (foundArg != null) { - - // This will only be saved if different, so we can figure out what the values are on save (and not save - // overridden properties, that are unchanged) - prop.override(true) - - arguments.remove(foundArg) - return@forEach - } - } + } - //// - // SYSTEM PROPERTY CHECK - //// - var sysProperty: String? = OS.getProperty(arg)?.trim() + //// + // SYSTEM PROPERTY CHECK + //// + val sysProperty: String? = OS.getProperty(arg)?.trim() + if (!sysProperty.isNullOrEmpty()) { + val overriddenValue = sysProperty.getType(returnType) - // try lowercase - if (sysProperty.isNullOrEmpty()) { - sysProperty = OS.getProperty(arg.lowercase(Locale.getDefault()))?.trim() - } - // try uppercase - if (sysProperty.isNullOrEmpty()) { - sysProperty = OS.getProperty(arg.uppercase(Locale.getDefault()))?.trim() - } + // This will only be saved if different, so we can figure out what the values are on save (and not save + // overridden properties, that are unchanged) + prop.override(overriddenValue) - if (!sysProperty.isNullOrEmpty()) { - val overriddenValue = sysProperty.getType(returnType) - - // This will only be saved if different, so we can figure out what the values are on save (and not save - // overridden properties, that are unchanged) - prop.override(overriddenValue) - - return@forEach - } + return@forEach + } - //// - // ENVIRONMENT VARIABLE CHECK - //// - var envProperty = OS.getEnv(environmentVarPrefix + arg)?.trim() + //// + // ENVIRONMENT VARIABLE CHECK + //// + val envProperty = OS.getEnv(environmentVarPrefix + arg)?.trim() + if (!envProperty.isNullOrEmpty()) { + // This will only be saved if different, so we can figure out what the values are on save (and not save + // overridden properties, that are unchanged) + prop.override(envProperty) - // try lowercase - if (envProperty.isNullOrEmpty()) { - envProperty = OS.getEnv(environmentVarPrefix + arg.lowercase(Locale.getDefault()))?.trim() - } - // try uppercase - if (envProperty.isNullOrEmpty()) { - envProperty = OS.getEnv(environmentVarPrefix + arg.uppercase(Locale.getDefault()))?.trim() - } - - if (!envProperty.isNullOrEmpty()) { - // This will only be saved if different, so we can figure out what the values are on save (and not save - // overridden properties, that are unchanged) - prop.override(envProperty) - - return@forEach - } - } else { - LoggerFactory.getLogger(ConfigProcessor::class.java).error("${prop.member.name} (${returnType.javaObjectType.simpleName}) overloading is not supported. Ignoring") + return@forEach } } @@ -798,6 +817,155 @@ class ConfigProcessor return this } + @Suppress("UNCHECKED_CAST") + private fun expandForProp(prop: ConfigProp, configMap: MutableMap): Array { + + // do we need to GROW the array? (we never shrink, we only grow) + val arrayName = prop.collectionName + + // if we have more args found that we have, then we have to grow the array + + val len = "$arrayName[".length + val affectedProps = configMap.filterKeys { it.startsWith("$arrayName[") } + val maxFound = affectedProps.map { arg -> + val last = arg.key.indexOfFirst { it == ']' } + arg.key.substring(len, last).toInt() + } + + val max = if (maxFound.isEmpty()) { + 0 + } else { + // +1 because we go from index -> size + maxFound.maxOf { it } + 1 + } + + val array = prop.parentObj as Array + val largestIndex = array.size + + val originalProp = configMap[arrayName] as ConfigProp + val originalArray = originalProp.get() as Array + + if (largestIndex > max) { + val newArray = originalArray.copyOf(largestIndex) + + // replace the array with the copy + originalProp.set(newArray) + + // reset all the original "parent objects" to be the new one + affectedProps.forEach { (k, v) -> + configMap[k] = ConfigProp(k, v.parentConf, newArray, v.member, arrayName, v.index, v.ignore) + } + + return newArray + } + + return originalArray + } + + @Suppress("UNCHECKED_CAST") + private fun expandArrays(prop: ConfigProp, newProps: MutableMap) { + + val returnType = prop.returnType + val parent = prop.parentObj + + // do we need to GROW the array? (we never shrink, we only grow) + val arrayName = prop.collectionName + val maxFound = mutableListOf() + + run { + // CLI + var foundArgs: Collection = commandLineArguments.filter { it.startsWith("$arrayName[") } + var len = "$arrayName[".length + foundArgs.map { arg -> + val last = arg.indexOfFirst { it == ']' } + arg.substring(len, last).toInt() + }.also { maxFound.addAll(it) } + + + // SYS + foundArgs = OS.getProperties().map { it.key }.filter { it.startsWith("$environmentVarPrefix$arrayName[") } + len = "$environmentVarPrefix$arrayName[".length + foundArgs.map { arg -> + val last = arg.indexOfFirst { it == ']' } + arg.substring(len, last).toInt() + }.also { maxFound.addAll(it) } + + + // ENV + foundArgs = OS.getEnv().map { it.key }.filter { it.startsWith("$environmentVarPrefix$arrayName[") } + foundArgs.map { arg -> + val last = arg.indexOfFirst { it == ']' } + arg.substring(len, last).toInt() + }.also { maxFound.addAll(it) } + } + + + + val max = if (maxFound.isEmpty()) { + 0 + } else { + // +1 because we go from index -> size + maxFound.maxOf { it } + 1 + } + + // if any of the foundArgs INDEX is larger than our array, we have to grow to that size. + val largestIndex: Int = + if (parent is Array<*>) { + parent.size + } else if (parent is ArrayList<*>) { + parent.size + } else if (parent is MutableList<*>) { + parent.size + } + else { + throw IllegalArgumentException("Unknown array type: ${parent.javaClass}") + } + + + // slightly different than expandForProp() because we are not expanding for found props, but for + // CLI/SYS/ENV + if (max > largestIndex) { + val newObj = if (parent is Array<*>) { + val newArray = parent.copyOf(max) + + // now we have to set the index so we can set it later + prop.parentConf!!.set(newArray) + newArray + } else { + parent + } + + // we must + // 1) assign what the parent obj is for the array members + // 2) create the new ones + for (index in 0 until max) { + val keyName = "$arrayName[$index]" + // prop.member DOES NOT MATTER for arrays BECAUSE we ignore it when get/set! + newProps[keyName] = ConfigProp(keyName, prop.parentConf, newObj, prop.member, arrayName, index, prop.ignore) + } + + for (index in largestIndex until max) { + val keyName = "$arrayName[$index]" + // prop.member DOES NOT MATTER for arrays BECAUSE we ignore it when get/set! + val newProp = newProps[keyName]!! + + if (parent is ArrayList<*>) { + (parent as ArrayList).add(defaultType(returnType)) + } + else if (parent is MutableList<*>) { + (parent as MutableList).add(defaultType(returnType)) + } else { + // array + // we have to set a default value!!! + newProp.set(defaultType(returnType)) + } + + newProp.override = true // cannot use the method because we have to forceSet it! + } + } + } + + /** * @return the JSON string representing the ORIGINAL configuration (does not include overridden properties) */ @@ -806,7 +974,7 @@ class ConfigProcessor // we use configCopy to save the state of everything as a snapshot (and then we serialize it) origCopyConfigMap.forEach { (k,v) -> val configured = configMap[k] - if (configured?.override == false) { + if (configured != null && !configured.ignore && !configured.override) { // this will change what the "original copy" is recorded as having. v.set(configured.get()) } @@ -820,7 +988,7 @@ class ConfigProcessor */ @Synchronized fun json(): String { - return json.toJson(config) + return json.toJson(configObject) } /** diff --git a/src/dorkbox/config/ConfigProp.kt b/src/dorkbox/config/ConfigProp.kt index 539fdab..0fe1329 100644 --- a/src/dorkbox/config/ConfigProp.kt +++ b/src/dorkbox/config/ConfigProp.kt @@ -20,16 +20,13 @@ import java.lang.Exception import kotlin.reflect.KClass import kotlin.reflect.KMutableProperty import kotlin.reflect.KProperty -import kotlin.reflect.jvm.javaType import kotlin.reflect.jvm.jvmErasure -data class ConfigProp(val parentConf: ConfigProp?, val parent: Any, val member: KProperty, val collectionName: String, val index: Int) { - - val isCollection: Boolean = parent is Collection<*> +internal data class ConfigProp(val key: String, val parentConf: ConfigProp?, val parentObj: Any, val member: KProperty, val collectionName: String, val index: Int, val ignore: Boolean) { val returnType: KClass<*> get() { - return when (parent) { + return when (parentObj) { is Array<*> -> { member.returnType.jvmErasure.javaObjectType.componentType.kotlin } @@ -46,22 +43,25 @@ data class ConfigProp(val parentConf: ConfigProp?, val parent: Any, val member: @Synchronized fun isSupported(): Boolean { - return member is KMutableProperty<*> + return !ignore && member is KMutableProperty<*> } @Synchronized fun get(): Any? { - return if (parent is Array<*>) { - parent[index] + return if (parentObj is Array<*>) { + parentObj[index] } - else if (parent is ArrayList<*>) { - parent[index] + else if (parentObj is ArrayList<*>) { + parentObj[index] } - else if (parent is MutableList<*>) { - parent[index] + else if (parentObj is AbstractList<*>) { + parentObj[index] + } + else if (parentObj is MutableList<*>) { + parentObj[index] } else { - member.getter.call(parent) + member.getter.call(parentObj) } } @@ -69,17 +69,17 @@ data class ConfigProp(val parentConf: ConfigProp?, val parent: Any, val member: @Synchronized fun set(value: Any?) { if (member is KMutableProperty<*>) { - if (parent is Array<*>) { - (parent as Array)[index] = value + if (parentObj is Array<*>) { + (parentObj as Array)[index] = value } - else if (parent is ArrayList<*>) { - (parent as ArrayList).set(index, value) + else if (parentObj is ArrayList<*>) { + (parentObj as ArrayList).set(index, value) } - else if (parent is AbstractList<*>) { - (parent as MutableList).set(index, value) + else if (parentObj is MutableList<*>) { + (parentObj as MutableList).set(index, value) } else { - member.setter.call(parent, value) + member.setter.call(parentObj, value) } // if the value is manually "set", then we consider it "not overridden" @@ -107,6 +107,8 @@ data class ConfigProp(val parentConf: ConfigProp?, val parent: Any, val member: if (other !is ConfigProp) return false if (isSupported() != other.isSupported()) return false + if (ignore != other.ignore) return false + if (key != other.key) return false if (get() != other.get()) return false return true } diff --git a/test/dorkbox/Test.kt b/test/dorkbox/Test.kt index 15ce97a..ecf15db 100644 --- a/test/dorkbox/Test.kt +++ b/test/dorkbox/Test.kt @@ -17,8 +17,10 @@ package dorkbox import dorkbox.config.ConfigProcessor import dorkbox.json.annotation.Json +import dorkbox.os.OS import org.junit.Assert import org.junit.Test +import org.slf4j.LoggerFactory class Test { @@ -101,21 +103,24 @@ class Test { Assert.assertTrue(conf.ip == "127.0.0.1") - System.setProperty("server", "true") - val config = ConfigProcessor(conf) - .envPrefix("") - .cliArguments(arrayOf("ip_address=11.12.13.14")) - .process() + OS.setProperty("server", "true") + try { + val config = ConfigProcessor(conf) + .envPrefix("") + .cliArguments(arrayOf("ip_address=11.12.13.14")) + .process() - Assert.assertTrue(conf.ip == "11.12.13.14") - Assert.assertTrue(conf.server) - Assert.assertFalse(conf.client) + Assert.assertTrue(conf.ip == "11.12.13.14") + Assert.assertTrue(conf.server) + Assert.assertFalse(conf.client) - Assert.assertEquals("{\"ip_address\":\"127.0.0.1\",\"server\":false,\"client\":false,\"nested\":[{\"iceCream\":false,\"potatoes\":true}]}", config.originalJson()) - Assert.assertEquals("{\"ip_address\":\"11.12.13.14\",\"server\":true,\"client\":false,\"nested\":[{\"iceCream\":false,\"potatoes\":true}]}", config.json()) - - System.clearProperty("server") + Assert.assertEquals("{\"ip_address\":\"127.0.0.1\",\"server\":false,\"client\":false,\"nested\":[{\"iceCream\":false,\"potatoes\":true}]}", config.originalJson()) + Assert.assertEquals("{\"ip_address\":\"11.12.13.14\",\"server\":true,\"client\":false,\"nested\":[{\"iceCream\":false,\"potatoes\":true}]}", config.json()) + } + finally { + OS.clearProperty("server") + } } @Test @@ -179,11 +184,195 @@ class Test { Assert.assertTrue(conf.ip == "0.0.0.0") Assert.assertTrue(conf.server) Assert.assertTrue(conf.client) + Assert.assertFalse(conf.nested[0].iceCream) Assert.assertEquals("{\"ip_address\":\"0.0.0.0\",\"server\":true,\"client\":true,\"nested\":[{\"iceCream\":false,\"potatoes\":true}]}", config.originalJson()) Assert.assertEquals("{\"ip_address\":\"0.0.0.0\",\"server\":true,\"client\":true,\"nested\":[{\"iceCream\":false,\"potatoes\":true}]}", config.json()) } + @Test + fun updateInvalidTest() { + val conf = Conf() + + Assert.assertTrue(conf.ip == "127.0.0.1") + + val config = ConfigProcessor(conf) + .envPrefix("") + .cliArguments(arrayOf("ip_address=1.2.3.4", "nested[0].iceCream")) + .process() + + + Assert.assertTrue(conf.ip == "1.2.3.4") + Assert.assertFalse(conf.server) + Assert.assertFalse(conf.client) + Assert.assertTrue(conf.nested[0].iceCream) + + + Assert.assertEquals("{\"ip_address\":\"127.0.0.1\",\"server\":false,\"client\":false,\"nested\":[{\"iceCream\":false,\"potatoes\":true}]}", config.originalJson()) + Assert.assertEquals("{\"ip_address\":\"1.2.3.4\",\"server\":false,\"client\":false,\"nested\":[{\"iceCream\":true,\"potatoes\":true}]}", config.json()) + + + // on purpose, there is a field here that doesn't exist + config.logger = LoggerFactory.getLogger("test") + config.loadAndProcess("{does_not_exit:0}") + + Assert.assertTrue(conf.ip == "1.2.3.4") + Assert.assertFalse(conf.server) + Assert.assertFalse(conf.client) + Assert.assertTrue(conf.nested[0].iceCream) + + Assert.assertEquals("{\"ip_address\":\"127.0.0.1\",\"server\":false,\"client\":false,\"nested\":[{\"iceCream\":false,\"potatoes\":true}]}", config.originalJson()) + Assert.assertEquals("{\"ip_address\":\"1.2.3.4\",\"server\":false,\"client\":false,\"nested\":[{\"iceCream\":true,\"potatoes\":true}]}", config.json()) + } + + @Test + fun updateInvalid2Test() { + val conf = Conf() + + Assert.assertTrue(conf.ip == "127.0.0.1") + + val config = ConfigProcessor(conf) + .envPrefix("") + .cliArguments(arrayOf("ip_address=1.2.3.4", "nested[0].iceCream")) + .process() + + + Assert.assertTrue(conf.ip == "1.2.3.4") + Assert.assertFalse(conf.server) + Assert.assertFalse(conf.client) + Assert.assertTrue(conf.nested[0].iceCream) + + + Assert.assertEquals("{\"ip_address\":\"127.0.0.1\",\"server\":false,\"client\":false,\"nested\":[{\"iceCream\":false,\"potatoes\":true}]}", config.originalJson()) + Assert.assertEquals("{\"ip_address\":\"1.2.3.4\",\"server\":false,\"client\":false,\"nested\":[{\"iceCream\":true,\"potatoes\":true}]}", config.json()) + + + // on purpose, there is a field here that doesn't exist + config.json.exceptionOnMissingFields = false + config.logger = LoggerFactory.getLogger("test") + config.loadAndProcess("{does_not_exit:0}") + + // the "default" object should be loaded!!! (but the override should still take effect) + + Assert.assertTrue(conf.ip == "1.2.3.4") + Assert.assertFalse(conf.server) + Assert.assertFalse(conf.client) + Assert.assertTrue(conf.nested[0].iceCream) + + Assert.assertEquals("{\"ip_address\":\"127.0.0.1\",\"server\":false,\"client\":false,\"nested\":[{\"iceCream\":false,\"potatoes\":true}]}", config.originalJson()) + Assert.assertEquals("{\"ip_address\":\"1.2.3.4\",\"server\":false,\"client\":false,\"nested\":[{\"iceCream\":true,\"potatoes\":true}]}", config.json()) + } + + @Test + fun updateArrayTest() { + val conf = ArrayConf() + + Assert.assertArrayEquals(arrayOf(1, 2, 3, 4), conf.ips) + + val config = ConfigProcessor(conf) + .envPrefix("") + .cliArguments(arrayOf("ips[0]=7")) + .process() + + // since we did not PROCESS the cli arguments again -- it means that the original has not been overloaded. To consider overloading stuff, process() must be called + config.load(""" + {"ips":[1,2,3,4,5,6,7,]} + """.trimIndent()) + + Assert.assertArrayEquals(arrayOf(1,2,3,4,5,6,7), conf.ips) + + Assert.assertEquals("{\"ips\":[1,2,3,4,5,6,7]}", config.originalJson()) + Assert.assertEquals("{\"ips\":[1,2,3,4,5,6,7]}", config.json()) + + config.process() // overrides ips[0] -> 7 + Assert.assertEquals("{\"ips\":[1,2,3,4,5,6,7]}", config.originalJson()) + Assert.assertEquals("{\"ips\":[7,2,3,4,5,6,7]}", config.json()) + } + + @Test + fun updateArrayCliTest() { + val conf = ArrayConf() + + Assert.assertArrayEquals(arrayOf(1, 2, 3, 4), conf.ips) + + val config = ConfigProcessor(conf) + .envPrefix("") + .cliArguments(arrayOf("ips[7]=7")) + .process() + + Assert.assertArrayEquals(arrayOf(1,2,3,4,0,0,0,7), conf.ips) + + Assert.assertEquals("{\"ips\":[1,2,3,4]}", config.originalJson()) + Assert.assertEquals("{\"ips\":[1,2,3,4,0,0,0,7]}", config.json()) + } + + @Test + fun updateArraySysTest() { + val conf = ArrayConf() + + Assert.assertArrayEquals(arrayOf(1, 2, 3, 4), conf.ips) + + OS.setProperty("ips[7]", "7") + try { + val config = ConfigProcessor(conf) + .envPrefix("") + .process() + + Assert.assertArrayEquals(arrayOf(1,2,3,4,0,0,0,7), conf.ips) + + Assert.assertEquals("{\"ips\":[1,2,3,4]}", config.originalJson()) + Assert.assertEquals("{\"ips\":[1,2,3,4,0,0,0,7]}", config.json()) + } + finally { + OS.clearProperty("ips[7]") + } + } + + @Test + fun updateArrayEmptyTest() { + val conf = ArrayEmptyConf() + + Assert.assertArrayEquals(arrayOf(), conf.ips) + + val config = ConfigProcessor(conf) + .envPrefix("") + .cliArguments(arrayOf("ips[0]=7")) + .process() + + // since we did not PROCESS the cli arguments again -- it means that the original has not been overloaded. To consider overloading stuff, process() must be called + config.load(""" + {"ips":[1,2,3,4,5,6,7,]} + """.trimIndent()) + + Assert.assertArrayEquals(arrayOf(1,2,3,4,5,6,7), conf.ips) + + Assert.assertEquals("{\"ips\":[1,2,3,4,5,6,7]}", config.originalJson()) + Assert.assertEquals("{\"ips\":[1,2,3,4,5,6,7]}", config.json()) + + config.process() + Assert.assertEquals("{\"ips\":[1,2,3,4,5,6,7]}", config.originalJson()) + Assert.assertEquals("{\"ips\":[7,2,3,4,5,6,7]}", config.json()) + } + + @Test + fun updateArrayListTest() { + val conf = Conf() + + val config = ConfigProcessor(conf) + .envPrefix("") + .cliArguments(arrayOf("nested[0].iceCream=true")) + .process() + + // since we did not PROCESS the cli arguments again -- it means that the original has not been overloaded. To consider overloading stuff, process() must be called + config.load(""" + {"ip_address":"127.0.0.1","server":false,"client":false,"nested":[{"iceCream":false,"potatoes":true},{"iceCream":true,"potatoes":true},{"iceCream":false,"potatoes":false}]} + """.trimIndent()) + + Assert.assertEquals("{\"ip_address\":\"127.0.0.1\",\"server\":false,\"client\":false,\"nested\":[{\"iceCream\":false,\"potatoes\":true},{\"iceCream\":true,\"potatoes\":true},{\"iceCream\":false,\"potatoes\":false}]}", config.originalJson()) + + Assert.assertEquals("{\"ip_address\":\"127.0.0.1\",\"server\":false,\"client\":false,\"nested\":[{\"iceCream\":false,\"potatoes\":true},{\"iceCream\":true,\"potatoes\":true},{\"iceCream\":false,\"potatoes\":false}]}", config.json()) + } + class ListConf { var ips = mutableListOf(1, 2, 3, 4) } @@ -192,6 +381,10 @@ class Test { var ips = arrayOf(1, 2, 3, 4) } + class ArrayEmptyConf { + var ips = arrayOf() + } + class CharArrayConf { var ips = arrayOf('1', '2', '3', '4') }