1169 lines
41 KiB
Kotlin
1169 lines
41 KiB
Kotlin
/*
|
|
* Copyright 2024 dorkbox, llc
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package dorkbox.config
|
|
|
|
import dorkbox.json.Json
|
|
import dorkbox.json.Json.Companion.isBoolean
|
|
import dorkbox.json.Json.Companion.isByte
|
|
import dorkbox.json.Json.Companion.isChar
|
|
import dorkbox.json.Json.Companion.isDouble
|
|
import dorkbox.json.Json.Companion.isFloat
|
|
import dorkbox.json.Json.Companion.isInt
|
|
import dorkbox.json.Json.Companion.isLong
|
|
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.reflect.Modifier
|
|
import java.util.*
|
|
import kotlin.reflect.KClass
|
|
import kotlin.reflect.KProperty
|
|
import kotlin.reflect.KVisibility
|
|
import kotlin.reflect.full.declaredMemberProperties
|
|
import kotlin.reflect.full.isSubclassOf
|
|
import kotlin.reflect.jvm.javaField
|
|
import kotlin.system.exitProcess
|
|
|
|
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
|
/**
|
|
* We want to support configuration properties, such that ANYTHING in the config file can ALSO be passed in on the command line or as an env/system property
|
|
*
|
|
* ```
|
|
* overloaded: commandline > system property > environment variable
|
|
*
|
|
* baseline: default object or json string or json file
|
|
* ```
|
|
*
|
|
* If the json file AND string are specified, the string will be used for loading, and the file used for saving.
|
|
*
|
|
* Once a property has been defined, it cannot be overloaded again via a different method, as specified in the above hierarchy,
|
|
*
|
|
* During a save operation, overloaded values (via CLI/ENV) will be ignored unless they were manually changed to something
|
|
* different (in the application)
|
|
*
|
|
**********************************************
|
|
* SYSTEM PROPERTIES AND ENVIRONMENT VARIABLES
|
|
**********************************************
|
|
* For setting configuration properties via system properties or environment variables.... it is possible that a standard system
|
|
* property or environment variable name might conflict...
|
|
*
|
|
* (In this example, "PATH" environment variable)
|
|
* The system uses PATH, but if we want to use path for something different, we can via setting the prefix.
|
|
* For example, setting the prefix to "CONFIG__" means that for us to set the `path` property, we set it via
|
|
*
|
|
*```
|
|
* CONFIG__path="/home/blah/whatever"
|
|
*```
|
|
* And this way, **OUR** configuration path name does not conflict with the system path.
|
|
*
|
|
***********************************
|
|
* COMMAND LINE INTERFACE ARGUMENTS
|
|
***********************************
|
|
* When specifying a CLI property,
|
|
* - if it is a boolean value, the existence of the value means "true"
|
|
* - <'do_a_thing'>
|
|
* - overrides the 'do_a_thing' to be "true"
|
|
* - if it is any other type, you must specify the property name + value
|
|
* - <'server_ip=1.2.3.4'>
|
|
* - overrides the 'server_ip' to the string value of '1.2.3.4'
|
|
* - <'do_a_thing=true'>
|
|
* - overrides the 'do_a_thing' to be "true"
|
|
*
|
|
*
|
|
* Additionally, there is the ability to `get` or `set` configuration properties via the CLI, which will appropriately
|
|
* - <'get' 'property'>
|
|
* - get a value following the override/default rules
|
|
* - print the value to the console output
|
|
* - exit the application
|
|
*
|
|
* - <'set' 'property' 'value'>
|
|
* - set a value overriding what the system properties/environment properties, string, or file.
|
|
* - print the old value to the console output
|
|
* - save() the new change to file (if possible)
|
|
* - exit the application
|
|
*
|
|
* This is very useful when wanting to set or retrieve properties from the commandline
|
|
*/
|
|
class ConfigProcessor<T : Any>
|
|
|
|
|
|
/**
|
|
* Specify the object that will be modified based on:
|
|
* - JSON string
|
|
* - JSON file
|
|
* - Command Line arguments
|
|
* - System properties
|
|
* - Environment variablse
|
|
*/
|
|
(private val configObject: T) {
|
|
|
|
companion object {
|
|
private val regexEquals = "=".toRegex()
|
|
private val locale = Locale.getDefault()
|
|
|
|
/**
|
|
* Gets the version number.
|
|
*/
|
|
const val version = "2.9.1"
|
|
|
|
init {
|
|
// Add this project to the updates system, which verifies this class + UUID + version information
|
|
dorkbox.updates.Updates.add(ConfigProcessor::class.java, "23475d7cdfef4c1e9c38c310420086ca", version)
|
|
}
|
|
|
|
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 klass.declaredMemberProperties) {
|
|
@Suppress("UNCHECKED_CAST")
|
|
assignFieldsToMap(
|
|
argMap = argumentMap,
|
|
field = member as KProperty<Any>,
|
|
parentConf = null,
|
|
parentObj = config,
|
|
obj = member.getter.call(config)!!,
|
|
prefix = "",
|
|
arrayName = "",
|
|
index = -1
|
|
)
|
|
}
|
|
|
|
return argumentMap
|
|
}
|
|
|
|
// the class is treated as lowercase, but the VALUE of properties is treated as case-sensitive
|
|
private fun assignFieldsToMap(argMap: MutableMap<String, ConfigProp>, field: KProperty<Any>,
|
|
parentConf: ConfigProp?, parentObj: Any, obj: Any,
|
|
prefix: String, arrayName: String, index: Int = -1)
|
|
|
|
{
|
|
val javaField = field.javaField!!
|
|
if (Modifier.isTransient(javaField.modifiers)) {
|
|
// ignore transient fields!
|
|
return
|
|
}
|
|
|
|
require(obj::class.java.typeName != "java.util.Arrays\$ArrayList") {
|
|
"Cannot modify an effectively immutable type! This is likely listOf() insteadof mutableListOf(), or Arrays.toList() insteadof ArrayList()"
|
|
}
|
|
|
|
val annotation = field.javaField?.annotations?.filterIsInstance<dorkbox.json.annotation.Json>()?.firstOrNull()
|
|
if (annotation?.ignore == true) {
|
|
return
|
|
}
|
|
|
|
var jsonName = (annotation?.name ?: field.name)
|
|
jsonName = when {
|
|
prefix.isEmpty() -> jsonName
|
|
else ->"$prefix.$jsonName"
|
|
}
|
|
|
|
if (index > -1) {
|
|
jsonName += "[$index]"
|
|
}
|
|
|
|
val prop = ConfigProp(jsonName, parentConf, parentObj, field, arrayName, index, false)
|
|
|
|
val type = obj::class.java
|
|
if (isString(type) ||
|
|
isInt(type) ||
|
|
isBoolean(type) ||
|
|
isFloat(type) ||
|
|
isLong(type) ||
|
|
isDouble(type) ||
|
|
isShort(type) ||
|
|
isByte(type) ||
|
|
isChar(type) ||
|
|
|
|
Enum::class.java.isAssignableFrom(type))
|
|
{
|
|
|
|
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>
|
|
collection.forEachIndexed { i, any ->
|
|
assignFieldsToMap(
|
|
argMap = argMap,
|
|
field = field,
|
|
parentConf = prop,
|
|
parentObj = obj,
|
|
obj = any,
|
|
prefix = prefix,
|
|
arrayName = jsonName,
|
|
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>
|
|
collection.forEachIndexed { i, any ->
|
|
assignFieldsToMap(
|
|
argMap = argMap,
|
|
field = field,
|
|
parentConf = prop,
|
|
parentObj = obj,
|
|
obj = any,
|
|
prefix = prefix,
|
|
arrayName = jsonName,
|
|
index = i)
|
|
}
|
|
} else if (annotation != null) {
|
|
// if there is an annotation on the field, we add it as the object!
|
|
// (BUT ONLY if it's not a collection or array!)
|
|
argMap[jsonName] = prop
|
|
} else {
|
|
val kClass = obj::class
|
|
// get all the members of this class.
|
|
for (member in kClass.declaredMemberProperties) {
|
|
if (member.visibility != KVisibility.PRIVATE) {
|
|
@Suppress("UNCHECKED_CAST")
|
|
assignFieldsToMap(
|
|
argMap = argMap,
|
|
field = member as KProperty<Any>,
|
|
parentConf = prop,
|
|
parentObj = obj,
|
|
obj = member.getter.call(obj)!!,
|
|
prefix = jsonName,
|
|
arrayName = "",
|
|
index = -1
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun consoleLog(message: String) {
|
|
println(message)
|
|
|
|
}
|
|
private fun consoleLog(argProp: ConfigProp?) {
|
|
if (argProp != null) {
|
|
println(argProp.get().toString())
|
|
} else {
|
|
println()
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* This is exposed in order to customize JSON parsing/writing behavior
|
|
*/
|
|
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>
|
|
|
|
// these are the defaults if nothing is set!
|
|
private var environmentVarPrefix = ""
|
|
private var commandLineArguments = Array(0) { "" }
|
|
private var configString: String? = null
|
|
private var saveFile: File? = null
|
|
private var configFile: File? = null
|
|
private var saveLogic: () -> Unit = { savePretty() }
|
|
|
|
|
|
/**
|
|
* The cleaned CLI arguments which no longer have elements used to modify configuration properties
|
|
*/
|
|
var arguments: List<String> = mutableListOf()
|
|
|
|
|
|
|
|
/**
|
|
* 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 = 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
|
|
// NOTE: overridden values are set via CLI/Sys/Env!!
|
|
private var origCopyConfig: T? = null
|
|
private var origCopyConfigMap: Map<String, ConfigProp>? = null
|
|
|
|
|
|
init {
|
|
// we want to default to the standard JSON output format
|
|
json.outputType = OutputType.json
|
|
}
|
|
|
|
/**
|
|
* Registers a serializer to use for the specified type instead of the default behavior of serializing all of an objects
|
|
* fields.
|
|
*/
|
|
@Synchronized
|
|
fun <T2: Any> setSerializer(type: Class<T2>, serializer: dorkbox.json.JsonSerializer<T2>): ConfigProcessor<T> {
|
|
json.setSerializer(type, serializer)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Specify the environment variable prefix, for customizing how system properties or environment variables are parsed.
|
|
*
|
|
* The system properties and environment variables are augmented by the "environmentVarPrefix" string.
|
|
*/
|
|
@Synchronized
|
|
fun envPrefix(environmentVarPrefix: String): ConfigProcessor<T> {
|
|
this.environmentVarPrefix = environmentVarPrefix
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Specify the command line arguments, if desired, to be used to overlaod
|
|
*/
|
|
@Synchronized
|
|
fun cliArguments(commandLineArguments: Array<String>): ConfigProcessor<T> {
|
|
this.commandLineArguments = commandLineArguments
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Specify the file to save to (useful in the case where the file saved to and the file loaded from are different)
|
|
*/
|
|
@Synchronized
|
|
fun saveFile(saveFile: File?): ConfigProcessor<T> {
|
|
this.saveFile = saveFile
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Specify the file to save to (useful in the case where the file saved to and the file loaded from are different)
|
|
*/
|
|
@Synchronized
|
|
fun saveFile(saveFileName: String): ConfigProcessor<T> {
|
|
this.saveFile = File(saveFileName)
|
|
System.err.println(saveFile!!.absolutePath)
|
|
return this
|
|
}
|
|
|
|
|
|
/**
|
|
* Specify the save logic. By default, will attempt to save to the saveFile (if available), then will attempt
|
|
* to save to the configFile (if available).
|
|
*/
|
|
@Synchronized
|
|
fun saveFile(save: () -> Unit): ConfigProcessor<T> {
|
|
this.saveLogic = save
|
|
return this
|
|
}
|
|
|
|
private fun loadOrigCopyConfig(configObject: T) {
|
|
// 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)
|
|
val origCopyConfig = json.fromExplicit(objectType.java, json.to(configObject))!!
|
|
this.origCopyConfig = origCopyConfig
|
|
origCopyConfigMap = createConfigMap(origCopyConfig)
|
|
}
|
|
|
|
|
|
/**
|
|
* Specify the baseline data (as a JSON string) used to populate the values of the config object.
|
|
*
|
|
* If the specified string DOES NOT load, then it will not be used or processed!
|
|
*/
|
|
@Synchronized
|
|
fun loadAndProcess(configString: String): ConfigProcessor<T> {
|
|
val configObject =
|
|
try {
|
|
if (configString.isNotBlank()) {
|
|
json.fromExplicit(objectType.java, configString)
|
|
} else {
|
|
null
|
|
}
|
|
} catch (exception: Exception) {
|
|
// there was a problem parsing the config
|
|
logger?.error("Error loading JSON data", exception)
|
|
null
|
|
}
|
|
|
|
if (configObject == null) {
|
|
return this
|
|
}
|
|
|
|
this.configString = configString
|
|
load(configObject)
|
|
|
|
return postProcess()
|
|
}
|
|
|
|
/**
|
|
* Specify the baseline data (as a JSON file) used to populate the values of the config object.
|
|
*
|
|
* If the specified file DOES NOT load, then it will not be used or processed!
|
|
*/
|
|
@Suppress("DuplicatedCode")
|
|
@Synchronized
|
|
fun loadAndProcess(configFile: File): ConfigProcessor<T> {
|
|
val configObject =
|
|
if (configFile.canRead()) {
|
|
// get the text from the file
|
|
val fileContents = configFile.readText(Charsets.UTF_8)
|
|
if (fileContents.isNotEmpty()) {
|
|
try {
|
|
json.fromExplicit(objectType.java, fileContents)
|
|
}
|
|
catch (exception: Exception) {
|
|
// there was a problem parsing the config
|
|
logger?.error("Error loading JSON data", exception)
|
|
null
|
|
}
|
|
}
|
|
else {
|
|
null
|
|
}
|
|
}
|
|
else {
|
|
null
|
|
}
|
|
|
|
if (configObject == null) {
|
|
return this
|
|
}
|
|
|
|
this.configFile = configFile
|
|
load(configObject)
|
|
|
|
return postProcess()
|
|
}
|
|
|
|
/**
|
|
* Specify the baseline data (as a JSON string) used to populate the values of the config object.
|
|
*
|
|
* If the specified string DOES NOT load, then it will not be used!
|
|
*/
|
|
@Synchronized
|
|
fun load(configString: String): Boolean {
|
|
val configObject =
|
|
try {
|
|
if (configString.isNotBlank()) {
|
|
json.fromExplicit(objectType.java, configString)
|
|
} else {
|
|
null
|
|
}
|
|
} catch (exception: Exception) {
|
|
// there was a problem parsing the config
|
|
logger?.error("Error loading JSON data", exception)
|
|
null
|
|
}
|
|
|
|
if (configObject == null) {
|
|
return false
|
|
}
|
|
|
|
this.configString = configString
|
|
load(configObject)
|
|
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Specify the baseline data (as a JSON file) used to populate the values of the config object.
|
|
*
|
|
* If the specified file DOES NOT load, then it will not be used!
|
|
*/
|
|
@Suppress("DuplicatedCode")
|
|
@Synchronized
|
|
fun load(configFile: File): Boolean {
|
|
val configObject =
|
|
if (configFile.canRead()) {
|
|
// get the text from the file
|
|
val fileContents = configFile.readText(Charsets.UTF_8)
|
|
if (fileContents.isNotEmpty()) {
|
|
try {
|
|
json.fromExplicit(objectType.java, fileContents)
|
|
}
|
|
catch (exception: Exception) {
|
|
// there was a problem parsing the config
|
|
logger?.error("Error loading JSON data", exception)
|
|
null
|
|
}
|
|
}
|
|
else {
|
|
null
|
|
}
|
|
}
|
|
else {
|
|
null
|
|
}
|
|
|
|
if (configObject == null) {
|
|
return false
|
|
}
|
|
|
|
this.configFile = configFile
|
|
load(configObject)
|
|
|
|
return true
|
|
}
|
|
|
|
|
|
// support
|
|
// thing[0].flag = true (array + list)
|
|
// thing[0].bah.flag = true (array + list)
|
|
@Suppress("UNCHECKED_CAST", "UNUSED_DESTRUCTURED_PARAMETER_ENTRY")
|
|
@Synchronized
|
|
private fun load(configObject: T) {
|
|
val incomingDataConfigMap = createConfigMap(configObject)
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
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.")
|
|
}
|
|
}
|
|
}
|
|
|
|
loadOrigCopyConfig(configObject)
|
|
}
|
|
|
|
|
|
/**
|
|
* Processes and populates the values of the config object, if any
|
|
*/
|
|
@Synchronized
|
|
fun process(): ConfigProcessor<T> {
|
|
// 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) {
|
|
loadOrigCopyConfig(configObject)
|
|
}
|
|
|
|
return postProcess()
|
|
}
|
|
|
|
@Suppress("UNUSED_DESTRUCTURED_PARAMETER_ENTRY")
|
|
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.
|
|
if (manageGetAndSet()) {
|
|
exitProcess(0)
|
|
}
|
|
|
|
// always save the LOADED (file/string) config!
|
|
saveLogic()
|
|
|
|
|
|
// now we have to see if there are any OVERLOADED properties
|
|
|
|
|
|
// when we are processing the command line arguments, we can be processing them MULTIPLE times, for example if
|
|
// we initially load data from a file, then we load data from a remote server
|
|
val arguments = commandLineArguments.toMutableList()
|
|
|
|
|
|
/**
|
|
* 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 { (arg, prop) ->
|
|
if (!prop.isSupported()) {
|
|
return@forEach
|
|
}
|
|
|
|
val parent = prop.parentObj
|
|
|
|
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
|
|
|
|
if (!configuredArrays.contains(arrayName)) {
|
|
configuredArrays.add(arrayName)
|
|
|
|
// this checks CLI, SYS, ENV for array expansion
|
|
expandArrays(prop, newProps)
|
|
}
|
|
}
|
|
}
|
|
|
|
newProps.forEach { (k,v) ->
|
|
configMap[k] = v
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Overloaded properties can be of the form
|
|
* For strings (and all other supported types)
|
|
* config.z="flag" (for a string)
|
|
* config.x="true" (for a boolean)
|
|
*
|
|
* Additional support for booleans
|
|
* config.x (absent any value is a 'true')
|
|
*/
|
|
configMap.forEach { (arg, prop) ->
|
|
val returnType = prop.returnType
|
|
|
|
if (!prop.isSupported()) {
|
|
if (!prop.ignore) {
|
|
// only show an error if we are not the proper type. We explicitly ignore BASE collection/array types - so properly ignore those
|
|
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) {
|
|
|
|
// 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
|
|
////
|
|
val sysProperty: String? = OS.getProperty(arg)?.trim()
|
|
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
|
|
}
|
|
|
|
|
|
////
|
|
// 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)
|
|
|
|
return@forEach
|
|
}
|
|
}
|
|
|
|
// we also will save out the processed arguments, so if we want to use a "cleaned" list of CLI arguments, we can
|
|
this.arguments = arguments
|
|
|
|
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", "CascadeIf")
|
|
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)
|
|
*/
|
|
@Synchronized
|
|
fun originalJson(): String {
|
|
if (origCopyConfigMap == null) {
|
|
// we might not have loaded the file yet (because invalid configs/etc), so make sure it's loaded!
|
|
loadOrigCopyConfig(configObject)
|
|
}
|
|
|
|
// 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 != null && !configured.ignore && !configured.override) {
|
|
// this will change what the "original copy" is recorded as having.
|
|
v.set(configured.get())
|
|
}
|
|
}
|
|
|
|
return json.to(origCopyConfig!!)
|
|
}
|
|
|
|
/**
|
|
* @return the JSON string representing this configuration INCLUDING the overridden properties
|
|
*/
|
|
@Synchronized
|
|
fun json(): String {
|
|
return json.to(configObject)
|
|
}
|
|
|
|
/**
|
|
* Saves the baseline (ie: not the overridden values) JSON representation of this configuration to file, if possible
|
|
*
|
|
* @return the JSON string representing the ORIGINAL configuration (does not include overridden properties)
|
|
*/
|
|
@Synchronized
|
|
fun save(): String {
|
|
val configToString = originalJson()
|
|
|
|
if (saveFile != null) {
|
|
saveFile!!.writeText(configToString, Charsets.UTF_8)
|
|
}
|
|
else if (configFile != null) {
|
|
configFile!!.writeText(configToString, Charsets.UTF_8)
|
|
}
|
|
|
|
return configToString
|
|
}
|
|
|
|
/**
|
|
* Saves the baseline (ie: not the overridden values) JSON representation of this configuration to file, if possible
|
|
*
|
|
* @return the JSON string representing the ORIGINAL configuration (does not include overridden properties)
|
|
*/
|
|
@Synchronized
|
|
fun savePretty(): String {
|
|
val configToString = json.prettyPrint(originalJson())
|
|
|
|
if (saveFile != null) {
|
|
saveFile!!.writeText(configToString, Charsets.UTF_8)
|
|
}
|
|
else if (configFile != null) {
|
|
configFile!!.writeText(configToString, Charsets.UTF_8)
|
|
}
|
|
|
|
return configToString
|
|
}
|
|
|
|
/**
|
|
* Allows the ability to `get` or `set` configuration properties. Will call System.exit() if a get/set was done
|
|
*
|
|
* @return false if nothing is to be done, true if the system should exit
|
|
*/
|
|
private fun manageGetAndSet(): Boolean {
|
|
if ((commandLineArguments.isEmpty())) {
|
|
// nothing to do
|
|
return false
|
|
}
|
|
|
|
val args = commandLineArguments.map { it.lowercase(locale) }
|
|
val getIndex = args.indexOf("get")
|
|
val setIndex = args.indexOf("set")
|
|
|
|
if (getIndex > -1) {
|
|
val propIndex = getIndex + 1
|
|
if (propIndex > commandLineArguments.size - 1) {
|
|
consoleLog("Must specify property to get. For Example: 'get server.ip'")
|
|
return true
|
|
}
|
|
|
|
val propToQuery = commandLineArguments[propIndex]
|
|
val prop = configMap[propToQuery]
|
|
consoleLog(prop)
|
|
return true
|
|
} else if (setIndex > -1) {
|
|
val propIndex = setIndex + 1
|
|
if (propIndex > commandLineArguments.size - 1) {
|
|
consoleLog("Must specify property to set. For Example: 'set server.ip 127.0.0.1'")
|
|
return true
|
|
}
|
|
|
|
val valueIndex = setIndex + 2
|
|
if (valueIndex > commandLineArguments.size - 1) {
|
|
consoleLog("Must specify property value to set. For Example: 'set server.ip 127.0.0.1'")
|
|
return true
|
|
}
|
|
|
|
val propToSet = commandLineArguments[propIndex]
|
|
val valueToSet = commandLineArguments[valueIndex]
|
|
|
|
val prop = configMap[propToSet]
|
|
if (prop != null) {
|
|
// we output the OLD value, in case we want it from the CLI
|
|
val oldValue = prop.get()
|
|
|
|
prop.set(valueToSet)
|
|
|
|
// we ALWAYS want to re-save this file back
|
|
save()
|
|
|
|
consoleLog(oldValue.toString())
|
|
return true
|
|
} else {
|
|
// prop wasn't found
|
|
consoleLog("")
|
|
return true
|
|
}
|
|
}
|
|
|
|
// no get/set found
|
|
return false
|
|
}
|
|
|
|
override fun equals(other: Any?): Boolean {
|
|
if (other !is ConfigProcessor<*>) return false
|
|
if (configMap.size != other.configMap.size) return false
|
|
|
|
configMap.forEach { (k,v) ->
|
|
if (v != other.configMap[k]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// test maps in both directions
|
|
other.configMap.forEach { (k,v) ->
|
|
if (v != configMap[k]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* This is extremely slow, it is not recommended using this
|
|
*/
|
|
override fun hashCode(): Int {
|
|
return json().hashCode()
|
|
}
|
|
|
|
/**
|
|
* this returns the JSON string **WITH** overridden values!
|
|
*/
|
|
override fun toString(): String {
|
|
return json()
|
|
}
|
|
|
|
private fun String.getType(propertyType: Any): Any {
|
|
return when (propertyType) {
|
|
Boolean::class -> this.toBoolean()
|
|
Byte::class -> this.toByte()
|
|
Char::class -> this[0]
|
|
Double::class -> this.toDouble()
|
|
Float::class -> this.toFloat()
|
|
Int::class -> this.toInt()
|
|
Long::class -> this.toLong()
|
|
Short::class -> this.toShort()
|
|
else -> this
|
|
}
|
|
}
|
|
|
|
private fun defaultType(propertyType: Any): Any {
|
|
return when (propertyType) {
|
|
String::class -> ""
|
|
Boolean::class -> false
|
|
Byte::class -> 0.toByte()
|
|
Char::class -> Character.MIN_VALUE
|
|
Double::class -> 0.0
|
|
Float::class -> 0.0f
|
|
Int::class -> 0
|
|
Long::class -> 0L
|
|
Short::class -> 0.toShort()
|
|
else -> this
|
|
}
|
|
}
|
|
}
|