/* * 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 /** * 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 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 klass.declaredMemberProperties) { @Suppress("UNCHECKED_CAST") assignFieldsToMap( argMap = argumentMap, field = member as KProperty, 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, field: KProperty, 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()?.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 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 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, 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 = configObject::class as KClass // 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 = 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? = 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 setSerializer(type: Class, serializer: dorkbox.json.JsonSerializer): ConfigProcessor { 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 { this.environmentVarPrefix = environmentVarPrefix return this } /** * Specify the command line arguments, if desired, to be used to overlaod */ @Synchronized fun cliArguments(commandLineArguments: Array): ConfigProcessor { 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 { 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 { 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 { 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 { 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 { 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).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 } } 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 { // 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 { // 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() 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): 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", "CascadeIf") 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) */ @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 } } }