Fixed issues with array/list expansion for loading, cli/sys/env.

Requirement for cli/sys/env is that it MUST match case
master
Robinson 2023-08-16 19:43:33 -05:00
parent c3b23990bd
commit 106ca040af
No known key found for this signature in database
GPG Key ID: 8E7DB78588BD6F5C
3 changed files with 598 additions and 235 deletions

View File

@ -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<T : Any>
dorkbox.updates.Updates.add(ConfigProcessor::class.java, "23475d7cdfef4c1e9c38c310420086ca", version)
}
private fun <T: Any> createConfigMap(config: T, objectType: KClass<T>): Map<String, ConfigProp> {
private fun <T: Any> createConfigMap(config: T): MutableMap<String, ConfigProp> {
val klass = config::class
// this creates an EASY-to-use map of all arguments we have
val argumentMap = mutableMapOf<String, ConfigProp>()
// 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<T : Any>
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<T : Any>
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<Any>
@ -217,6 +221,9 @@ class ConfigProcessor<T : Any>
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<Any>
@ -277,6 +284,16 @@ class ConfigProcessor<T : Any>
*/
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<T> = configObject::class as KClass<T>
@ -294,16 +311,14 @@ class ConfigProcessor<T : Any>
*/
var arguments: List<String> = 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<String, ConfigProp> = 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<T : Any>
} 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<T : Any>
this.configString = configString
load(configObject)
return process(configObject)
return postProcess()
}
/**
@ -421,8 +437,9 @@ class ConfigProcessor<T : Any>
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<T : Any>
this.configFile = configFile
load(configObject)
return process(configObject)
return postProcess()
}
/**
@ -458,8 +475,9 @@ class ConfigProcessor<T : Any>
} 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<T : Any>
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<T : Any>
}
// 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<Any?>).add(affectedObj)
} else if (origList is MutableList<*>) {
(origList as MutableList<Any?>).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<T> {
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<T> {
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<T> {
// 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<T : Any>
/**
* 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<String, ConfigProp>()
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<Any?>
} else if (parent is MutableList<*>) {
largestIndex = parent.size - 1
parent as MutableList<Any?>
}
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<Any>).add(defaultType(returnType))
}
else if (parent is MutableList<*>) {
(parent as MutableList<Any>).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<T : Any>
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<T : Any>
return this
}
@Suppress("UNCHECKED_CAST")
private fun expandForProp(prop: ConfigProp, configMap: MutableMap<String, ConfigProp>): Array<Any?> {
// 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<Any?>
val largestIndex = array.size
val originalProp = configMap[arrayName] as ConfigProp
val originalArray = originalProp.get() as Array<Any?>
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<String, ConfigProp>) {
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<Int>()
run {
// CLI
var foundArgs: Collection<String> = 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<Any>).add(defaultType(returnType))
}
else if (parent is MutableList<*>) {
(parent as MutableList<Any>).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<T : Any>
// 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<T : Any>
*/
@Synchronized
fun json(): String {
return json.toJson(config)
return json.toJson(configObject)
}
/**

View File

@ -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<Any>, 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<Any>, 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<Any?>)[index] = value
if (parentObj is Array<*>) {
(parentObj as Array<Any?>)[index] = value
}
else if (parent is ArrayList<*>) {
(parent as ArrayList<Any?>).set(index, value)
else if (parentObj is ArrayList<*>) {
(parentObj as ArrayList<Any?>).set(index, value)
}
else if (parent is AbstractList<*>) {
(parent as MutableList<Any?>).set(index, value)
else if (parentObj is MutableList<*>) {
(parentObj as MutableList<Any?>).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
}

View File

@ -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<Int>(), 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<Int>()
}
class CharArrayConf {
var ips = arrayOf('1', '2', '3', '4')
}