diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 6f25d0a850..51247be08d 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -30,8 +30,10 @@
-
-
+
+
+
+
diff --git a/common/README.md b/common/README.md
new file mode 100644
index 0000000000..1eecd8139b
--- /dev/null
+++ b/common/README.md
@@ -0,0 +1,9 @@
+# Common libraries
+
+This directory contains modules representing libraries that are reusable in different areas of Corda.
+
+## Rules of the folder
+
+- No dependencies whatsoever on any modules that are not in this directory (no corda-core, test-utils, etc.).
+- No active components, as in, nothing that has a main function in it.
+- Think carefully before using non-internal packages in these libraries.
\ No newline at end of file
diff --git a/common/configuration-parsing/README.md b/common/configuration-parsing/README.md
new file mode 100644
index 0000000000..3d0d2b5745
--- /dev/null
+++ b/common/configuration-parsing/README.md
@@ -0,0 +1,23 @@
+# configuration-parsing
+
+This module provides types and functions to facilitate using Typesafe configuration objects.
+
+## Features
+
+1. A multi-step, structured validation framework for Typesafe configurations, allowing to merge Typesafe and application-level rules.
+2. A parsing framework, allowing to extract domain types from raw configuration objects in a versioned, type-safe fashion.
+3. A configuration description framework, allowing to print the expected schema of a configuration object.
+4. A configuration serialization framework, allowing to output the structure and values of a configuration object, potentially obfuscating sensitive data.
+
+## Concepts
+
+The main idea is to create a `Configuration.Specification` to model the expected structure of a Typesafe configuration.
+The specification is then able to parse, validate, describe and serialize a raw Typesafe configuration.
+
+By using `VersionedConfigurationParser`, it is possible to map specific versions to `Configuration.Specification`s and to parse and validate a raw configuration object based on a version header.
+
+Refer to the following tests to gain an understanding of how the library works:
+
+- net.corda.common.configuration.parsing.internal.versioned.VersionedParsingExampleTest
+- net.corda.common.configuration.parsing.internal.SpecificationTest
+- net.corda.common.configuration.parsing.internal.SchemaTest
\ No newline at end of file
diff --git a/common/configuration-parsing/build.gradle b/common/configuration-parsing/build.gradle
new file mode 100644
index 0000000000..3b44430643
--- /dev/null
+++ b/common/configuration-parsing/build.gradle
@@ -0,0 +1,25 @@
+apply plugin: 'kotlin'
+
+apply plugin: 'net.corda.plugins.publish-utils'
+apply plugin: 'com.jfrog.artifactory'
+
+dependencies {
+ compile group: "org.jetbrains.kotlin", name: "kotlin-stdlib-jdk8", version: kotlin_version
+ compile group: "org.jetbrains.kotlin", name: "kotlin-reflect", version: kotlin_version
+
+ compile group: "com.typesafe", name: "config", version: typesafe_config_version
+
+ compile project(":common-validation")
+
+ testCompile group: "org.jetbrains.kotlin", name: "kotlin-test", version: kotlin_version
+ testCompile group: "junit", name: "junit", version: junit_version
+ testCompile group: "org.assertj", name: "assertj-core", version: assertj_version
+}
+
+jar {
+ baseName 'common-configuration-parsing'
+}
+
+publish {
+ name jar.baseName
+}
\ No newline at end of file
diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Configuration.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Configuration.kt
new file mode 100644
index 0000000000..becab85429
--- /dev/null
+++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Configuration.kt
@@ -0,0 +1,547 @@
+package net.corda.common.configuration.parsing.internal
+
+import com.typesafe.config.Config
+import com.typesafe.config.ConfigException
+import com.typesafe.config.ConfigObject
+import com.typesafe.config.ConfigValue
+import net.corda.common.configuration.parsing.internal.versioned.VersionExtractor
+import net.corda.common.validation.internal.Validated
+import net.corda.common.validation.internal.Validated.Companion.invalid
+import java.time.Duration
+import kotlin.reflect.KClass
+
+/**
+ * Entry point for the [Config] parsing utilities.
+ */
+object Configuration {
+
+ /**
+ * Able to describe a part of a [Config] as a [ConfigValue].
+ * Implemented by [Configuration.Specification], [Configuration.Schema] and [Configuration.Property.Definition] to output values that are masked if declared as sensitive.
+ */
+ interface Describer {
+
+ /**
+ * Describes a [Config] hiding sensitive data.
+ */
+ fun describe(configuration: Config): ConfigValue
+ }
+
+ object Value {
+
+ /**
+ * Defines functions able to extract values from a [Config] in a type-safe fashion.
+ */
+ interface Extractor {
+
+ /**
+ * Returns a value out of a [Config] if all is good. Otherwise, it throws an exception.
+ *
+ * @throws ConfigException.Missing if the [Config] does not specify the value.
+ * @throws ConfigException.WrongType if the [Config] specifies a value of the wrong type.
+ * @throws ConfigException.BadValue if the [Config] specifies a value of the correct type, but this in unacceptable according to application-level validation rules..
+ */
+ @Throws(ConfigException.Missing::class, ConfigException.WrongType::class, ConfigException.BadValue::class)
+ fun valueIn(configuration: Config): TYPE
+
+ /**
+ * Returns whether the value is specified by the [Config].
+ */
+ fun isSpecifiedBy(configuration: Config): Boolean
+
+ /**
+ * Returns a value out of a [Config] if all is good, or null if no value is present. Otherwise, it throws an exception.
+ *
+ * @throws ConfigException.WrongType if the [Config] specifies a value of the wrong type.
+ * @throws ConfigException.BadValue if the [Config] specifies a value of the correct type, but this in unacceptable according to application-level validation rules..
+ */
+ @Throws(ConfigException.WrongType::class, ConfigException.BadValue::class)
+ fun valueInOrNull(configuration: Config): TYPE? {
+
+ return when {
+ isSpecifiedBy(configuration) -> valueIn(configuration)
+ else -> null
+ }
+ }
+ }
+
+ /**
+ * Able to parse a value from a [Config] and [Configuration.Validation.Options], returning a [Valid] result containing either the value itself, or some [Configuration.Validation.Error]s.
+ */
+ interface Parser {
+
+ /**
+ * Returns a [Valid] wrapper either around a valid value extracted from the [Config], or around a set of [Configuration.Validation.Error] with details about what went wrong.
+ */
+ fun parse(configuration: Config, options: Configuration.Validation.Options = Configuration.Validation.Options.defaults): Valid
+ }
+ }
+
+ object Property {
+
+ /**
+ * Configuration property metadata, as in the set of qualifying traits for a [Configuration.Property.Definition].
+ */
+ interface Metadata {
+
+ /**
+ * Property key.
+ */
+ val key: String
+
+ /**
+ * Name of the type for this property..
+ */
+ val typeName: String
+
+ /**
+ * Whether the absence of a value for this property will raise an error.
+ */
+ val isMandatory: Boolean
+
+ /**
+ * Whether the value for this property will be shown by [Configuration.Property.Definition.describe].
+ */
+ val isSensitive: Boolean
+
+ val schema: Schema?
+ }
+
+ /**
+ * Property definition, able to validate, describe and extract values from a [Config] object.
+ */
+ interface Definition : Configuration.Property.Metadata, Configuration.Validator, Configuration.Value.Extractor, Configuration.Describer, Configuration.Value.Parser {
+
+ override fun isSpecifiedBy(configuration: Config): Boolean = configuration.hasPath(key)
+
+ /**
+ * Defines a required property, which must provide a value or produce an error.
+ */
+ interface Required : Definition {
+
+ /**
+ * Returns an optional property with given [defaultValue]. This property does not produce errors in case the value is unspecified, returning the [defaultValue] instead.
+ */
+ fun optional(defaultValue: TYPE? = null): Definition
+ }
+
+ /**
+ * Defines a property that must provide a single value or produce an error in case multiple values are specified for the relevant key.
+ */
+ interface Single : Definition {
+
+ /**
+ * Returns a required property expecting multiple values for the relevant key.
+ */
+ fun list(): Required>
+ }
+
+ /**
+ * Default property definition, required and single-value.
+ */
+ interface Standard : Required, Single {
+
+ /**
+ * Passes the value to a validating mapping function, provided this is valid in the first place.
+ */
+ fun mapValid(mappedTypeName: String, convert: (TYPE) -> Validated): Standard
+
+ /**
+ * Passes the value to a non-validating mapping function, provided this is valid in the first place.
+ */
+ fun map(mappedTypeName: String, convert: (TYPE) -> MAPPED): Standard = mapValid(mappedTypeName) { value -> valid(convert.invoke(value)) }
+ }
+
+ override fun parse(configuration: Config, options: Configuration.Validation.Options): Validated {
+
+ return validate(configuration, options).mapValid { config -> valid(valueIn(config)) }
+ }
+
+ companion object {
+
+ const val SENSITIVE_DATA_PLACEHOLDER = "*****"
+
+ /**
+ * Returns a [Configuration.Property.Definition.Standard] with value of type [Long].
+ */
+ fun long(key: String, sensitive: Boolean = false): Standard = LongProperty(key, sensitive)
+
+ /**
+ * Returns a [Configuration.Property.Definition.Standard] with value of type [Int].
+ */
+ fun int(key: String, sensitive: Boolean = false): Standard = long(key, sensitive).mapValid { value ->
+
+ try {
+ valid(Math.toIntExact(value))
+ } catch (e: ArithmeticException) {
+ invalid(Configuration.Validation.Error.BadValue.of("Provided value exceeds Integer range [${Int.MIN_VALUE}, ${Int.MAX_VALUE}].", key, Int::class.javaObjectType.simpleName))
+ }
+ }
+
+ /**
+ * Returns a [Configuration.Property.Definition.Standard] with value of type [Boolean].
+ */
+ fun boolean(key: String, sensitive: Boolean = false): Standard = StandardProperty(key, Boolean::class.javaObjectType.simpleName, Config::getBoolean, Config::getBooleanList, sensitive)
+
+ /**
+ * Returns a [Configuration.Property.Definition.Standard] with value of type [Double].
+ */
+ fun double(key: String, sensitive: Boolean = false): Standard = StandardProperty(key, Double::class.javaObjectType.simpleName, Config::getDouble, Config::getDoubleList, sensitive)
+
+ /**
+ * Returns a [Configuration.Property.Definition.Standard] with value of type [Float].
+ */
+ fun float(key: String, sensitive: Boolean = false): Standard = double(key, sensitive).mapValid { value ->
+
+ val floatValue = value.toFloat()
+ if (floatValue.isInfinite() || floatValue.isNaN()) {
+ invalid(Configuration.Validation.Error.BadValue.of(key, Float::class.javaObjectType.simpleName, "Provided value exceeds Float range."))
+ } else {
+ valid(value.toFloat())
+ }
+ }
+
+ /**
+ * Returns a [Configuration.Property.Definition.Standard] with value of type [String].
+ */
+ fun string(key: String, sensitive: Boolean = false): Standard = StandardProperty(key, String::class.java.simpleName, Config::getString, Config::getStringList, sensitive)
+
+ /**
+ * Returns a [Configuration.Property.Definition.Standard] with value of type [Duration].
+ */
+ fun duration(key: String, sensitive: Boolean = false): Standard = StandardProperty(key, Duration::class.java.simpleName, Config::getDuration, Config::getDurationList, sensitive)
+
+ /**
+ * Returns a [Configuration.Property.Definition.Standard] with value of type [ConfigObject].
+ * It supports an optional [Configuration.Schema], which is used for validation and more when provided.
+ */
+ fun nestedObject(key: String, schema: Schema? = null, sensitive: Boolean = false): Standard = StandardProperty(key, ConfigObject::class.java.simpleName, Config::getObject, Config::getObjectList, sensitive, schema)
+
+ /**
+ * Returns a [Configuration.Property.Definition.Standard] with value of type [ENUM].
+ * This property expects the exact [ENUM] value specified as text for the relevant key.
+ */
+ fun > enum(key: String, enumClass: KClass, sensitive: Boolean = false): Standard = StandardProperty(key, enumClass.java.simpleName, { conf: Config, propertyKey: String -> conf.getEnum(enumClass.java, propertyKey) }, { conf: Config, propertyKey: String -> conf.getEnumList(enumClass.java, propertyKey) }, sensitive)
+ }
+ }
+ }
+
+ /**
+ * A definition of the expected structure of a [Config] object, able to validate it and describe it while preventing sensitive values from being revealed.
+ */
+ interface Schema : Configuration.Validator, Configuration.Describer {
+
+ /**
+ * Name of the schema.
+ */
+ val name: String?
+
+ /**
+ * A description of the schema definition, with references to nested types.
+ */
+ fun description(): String
+
+ /**
+ * All properties defining this schema.
+ */
+ val properties: Set>
+
+ companion object {
+
+ /**
+ * Constructs a schema with given name and properties.
+ */
+ fun withProperties(name: String? = null, properties: Iterable>): Schema = Schema(name, properties)
+
+ /**
+ * @see [withProperties].
+ */
+ fun withProperties(vararg properties: Property.Definition<*>, name: String? = null): Schema = withProperties(name, properties.toSet())
+
+ /**
+ * Convenient way of creating an [Iterable] of [Property.Definition]s without having to reference the [Property.Definition.Companion] each time.
+ * @see [withProperties].
+ */
+ fun withProperties(name: String? = null, builder: Property.Definition.Companion.() -> Iterable>): Schema = withProperties(name, builder.invoke(Property.Definition))
+ }
+ }
+
+ /**
+ * A [Configuration.Schema] that is also able to parse a raw [Config] object into a [VALUE].
+ * It is an abstract class to allow extension with delegated properties e.g., object Settings: Specification() { val address by string().optional("localhost:8080") }.
+ */
+ abstract class Specification(name: String?, private val prefix: String? = null) : Configuration.Schema, Configuration.Value.Parser {
+
+ private val mutableProperties = mutableSetOf>()
+
+ override val properties: Set> = mutableProperties
+
+ private val schema: Schema by lazy { Schema(name, properties) }
+
+ /**
+ * Returns a delegate for a [Configuration.Property.Definition.Standard] of type [Long].
+ */
+ fun long(key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = PropertyDelegate.long(key, prefix, sensitive) { mutableProperties.add(it) }
+
+ /**
+ * Returns a delegate for a [Configuration.Property.Definition.Standard] of type [Int].
+ */
+ fun int(key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = PropertyDelegate.int(key, prefix, sensitive) { mutableProperties.add(it) }
+
+ /**
+ * Returns a delegate for a [Configuration.Property.Definition.Standard] of type [Boolean].
+ */
+ fun boolean(key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = PropertyDelegate.boolean(key, prefix, sensitive) { mutableProperties.add(it) }
+
+ /**
+ * Returns a delegate for a [Configuration.Property.Definition.Standard] of type [Double].
+ */
+ fun double(key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = PropertyDelegate.double(key, prefix, sensitive) { mutableProperties.add(it) }
+
+ /**
+ * Returns a delegate for a [Configuration.Property.Definition.Standard] of type [Float].
+ */
+ fun float(key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = PropertyDelegate.float(key, prefix, sensitive) { mutableProperties.add(it) }
+
+ /**
+ * Returns a delegate for a [Configuration.Property.Definition.Standard] of type [String].
+ */
+ fun string(key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = PropertyDelegate.string(key, prefix, sensitive) { mutableProperties.add(it) }
+
+ /**
+ * Returns a delegate for a [Configuration.Property.Definition.Standard] of type [Duration].
+ */
+ fun duration(key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = PropertyDelegate.duration(key, prefix, sensitive) { mutableProperties.add(it) }
+
+ /**
+ * Returns a delegate for a [Configuration.Property.Definition.Standard] of type [ConfigObject].
+ * It supports an optional [Configuration.Schema], which is used for validation and more when provided.
+ */
+ fun nestedObject(schema: Schema? = null, key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = PropertyDelegate.nestedObject(schema, key, prefix, sensitive) { mutableProperties.add(it) }
+
+ /**
+ * Returns a delegate for a [Configuration.Property.Definition.Standard] of type [ENUM].
+ * This property expects the exact [ENUM] value specified as text for the relevant key.
+ */
+ fun > enum(key: String? = null, enumClass: KClass, sensitive: Boolean = false): PropertyDelegate.Standard = PropertyDelegate.enum(key, prefix, enumClass, sensitive) { mutableProperties.add(it) }
+
+ override val name: String? get() = schema.name
+
+ override fun description() = schema.description()
+
+ override fun validate(target: Config, options: Validation.Options?) = schema.validate(target, options)
+
+ override fun describe(configuration: Config) = schema.describe(configuration)
+
+ final override fun parse(configuration: Config, options: Configuration.Validation.Options): Valid = validate(configuration, options).mapValid(::parseValid)
+
+ /**
+ * Implement to define further mapping and validation logic, assuming the underlying raw [Config] is correct in terms of this [Configuration.Specification].
+ */
+ protected abstract fun parseValid(configuration: Config): Valid
+ }
+
+ object Validation {
+
+ /**
+ * [Config] validation options.
+ * @property strict whether to raise unknown property keys as errors.
+ */
+ data class Options(val strict: Boolean) {
+
+ companion object {
+
+ /**
+ * Default [Config] validation options, without [strict] parsing enabled.
+ */
+ val defaults: Configuration.Validation.Options = Options(strict = false)
+ }
+ }
+
+ /**
+ * Super-type for the errors raised by the parsing and validation of a [Config] object.
+ *
+ * @property keyName name of the property key this error refers to, if any.
+ * @property typeName name of the type of the property this error refers to, if any.
+ * @property message details about what went wrong during the processing.
+ * @property containingPath containing path of the error, excluding the [keyName].
+ */
+ sealed class Error constructor(open val keyName: String?, open val typeName: String?, open val message: String, val containingPath: List = emptyList()) {
+
+ internal companion object {
+
+ private const val UNKNOWN = ""
+
+ private fun contextualize(keyName: String, containingPath: List): Pair> {
+
+ val keyParts = keyName.split(".")
+ return when {
+ keyParts.size > 1 -> {
+ val fullContainingPath = containingPath + keyParts.subList(0, keyParts.size - 1)
+ val keySegment = keyParts.last()
+ keySegment to fullContainingPath
+ }
+ else -> keyName to containingPath
+ }
+ }
+ }
+
+ /**
+ * Full path for nested property keys, including the [keyName].
+ */
+ val path: List get() = keyName?.let { containingPath + it } ?: containingPath
+
+ /**
+ * [containingPath] joined by "." characters.
+ */
+ val containingPathAsString: String = containingPath.joinToString(".")
+
+ /**
+ * [pathstr] joined by "." characters.
+ */
+ val pathAsString: String = path.joinToString(".")
+
+ internal abstract fun withContainingPath(vararg containingPath: String): Error
+
+ internal abstract fun with(keyName: String = this.keyName ?: UNKNOWN, typeName: String = this.typeName ?: UNKNOWN): Configuration.Validation.Error
+
+ override fun toString(): String {
+
+ return "(keyName='$keyName', typeName='$typeName', path=$path, message='$message')"
+ }
+
+ /**
+ * Raised when a value was found for the relevant [keyName], but the value did not match the declared one for the property.
+ */
+ class WrongType private constructor(override val keyName: String, override val typeName: String, message: String, containingPath: List = emptyList()) : Configuration.Validation.Error(keyName, typeName, message, containingPath) {
+
+ internal companion object {
+
+ internal fun of(message: String, keyName: String = UNKNOWN, typeName: String = UNKNOWN, containingPath: List = emptyList()): WrongType = contextualize(keyName, containingPath).let { (key, path) -> WrongType(key, typeName, message, path) }
+ }
+
+ override fun withContainingPath(vararg containingPath: String) = WrongType(keyName, typeName, message, containingPath.toList() + this.containingPath)
+
+ override fun with(keyName: String, typeName: String): WrongType = WrongType.of(message, keyName, typeName, containingPath)
+ }
+
+ /**
+ * Raised when no value was found for the relevant [keyName], and the property is [Configuration.Property.Definition.Required].
+ */
+ class MissingValue private constructor(override val keyName: String, override val typeName: String, message: String, containingPath: List = emptyList()) : Configuration.Validation.Error(keyName, typeName, message, containingPath) {
+
+ internal companion object {
+
+ internal fun of(message: String, keyName: String = UNKNOWN, typeName: String = UNKNOWN, containingPath: List = emptyList()): MissingValue = contextualize(keyName, containingPath).let { (key, path) -> MissingValue(key, typeName, message, path) }
+ }
+
+ override fun withContainingPath(vararg containingPath: String) = MissingValue(keyName, typeName, message, containingPath.toList() + this.containingPath)
+
+ override fun with(keyName: String, typeName: String): MissingValue = MissingValue.of(message, keyName, typeName, containingPath)
+ }
+
+ /**
+ * Raised when a value was found for the relevant [keyName], it matched the declared raw type for the property, but its value is unacceptable due to application-level validation rules.
+ */
+ class BadValue private constructor(override val keyName: String, override val typeName: String, message: String, containingPath: List = emptyList()) : Configuration.Validation.Error(keyName, typeName, message, containingPath) {
+
+ internal companion object {
+
+ internal fun of(message: String, keyName: String = UNKNOWN, typeName: String = UNKNOWN, containingPath: List = emptyList()): BadValue = contextualize(keyName, containingPath).let { (key, path) -> BadValue(key, typeName, message, path) }
+ }
+
+ override fun withContainingPath(vararg containingPath: String) = BadValue(keyName, typeName, message, containingPath.toList() + this.containingPath)
+
+ override fun with(keyName: String, typeName: String): BadValue = BadValue.of(message, keyName, typeName, containingPath)
+ }
+
+ /**
+ * Raised when the [Config] contains a malformed path.
+ */
+ class BadPath private constructor(override val keyName: String, override val typeName: String, message: String, containingPath: List = emptyList()) : Configuration.Validation.Error(keyName, typeName, message, containingPath) {
+
+ internal companion object {
+
+ internal fun of(message: String, keyName: String = UNKNOWN, typeName: String = UNKNOWN, containingPath: List = emptyList()): BadPath = contextualize(keyName, containingPath).let { (key, path) -> BadPath(key, typeName, message, path) }
+ }
+
+ override fun withContainingPath(vararg containingPath: String) = BadPath(keyName, typeName, message, containingPath.toList() + this.containingPath)
+
+ override fun with(keyName: String, typeName: String): BadPath = BadPath.of(message, keyName, typeName, containingPath)
+ }
+
+ /**
+ * Raised when the [Config] is malformed and cannot be parsed.
+ */
+ class MalformedStructure private constructor(override val keyName: String, override val typeName: String, message: String, containingPath: List = emptyList()) : Configuration.Validation.Error(keyName, typeName, message, containingPath) {
+
+ internal companion object {
+
+ internal fun of(message: String, keyName: String = UNKNOWN, typeName: String = UNKNOWN, containingPath: List = emptyList()): MalformedStructure = contextualize(keyName, containingPath).let { (key, path) -> MalformedStructure(key, typeName, message, path) }
+ }
+
+ override fun withContainingPath(vararg containingPath: String) = MalformedStructure(keyName, typeName, message, containingPath.toList() + this.containingPath)
+
+ override fun with(keyName: String, typeName: String): MalformedStructure = MalformedStructure.of(message, keyName, typeName, containingPath)
+ }
+
+ /**
+ * Raised when a key-value pair appeared in the [Config] object without a matching property in the [Configuration.Schema], and [Configuration.Validation.Options.strict] was enabled.
+ */
+ class Unknown private constructor(override val keyName: String, containingPath: List = emptyList()) : Configuration.Validation.Error(keyName, null, message(keyName), containingPath) {
+
+ internal companion object {
+
+ private fun message(keyName: String) = "Unknown property \"$keyName\"."
+
+ internal fun of(keyName: String = UNKNOWN, containingPath: List = emptyList()): Unknown = contextualize(keyName, containingPath).let { (key, path) -> Unknown(key, path) }
+ }
+
+ override val message = message(pathAsString)
+
+ override fun withContainingPath(vararg containingPath: String) = Unknown(keyName, containingPath.toList() + this.containingPath)
+
+ override fun with(keyName: String, typeName: String): Unknown = Unknown.of(keyName, containingPath)
+ }
+
+ /**
+ * Raised when the specification version found in the [Config] object did not match any known [Configuration.Specification].
+ */
+ class UnsupportedVersion private constructor(val version: Int, containingPath: List = emptyList()) : Configuration.Validation.Error(null, null, "Unknown configuration version $version.", containingPath) {
+
+ internal companion object {
+
+ internal fun of(version: Int): UnsupportedVersion = UnsupportedVersion(version)
+ }
+
+ override fun withContainingPath(vararg containingPath: String) = UnsupportedVersion(version, containingPath.toList() + this.containingPath)
+
+ override fun with(keyName: String, typeName: String): UnsupportedVersion = this
+ }
+ }
+ }
+
+ object Version {
+
+ /**
+ * Defines the contract from extracting a specification version from a [Config] object.
+ */
+ interface Extractor : Configuration.Value.Parser {
+
+ companion object {
+
+ const val DEFAULT_VERSION_VALUE = 1
+
+ /**
+ * Returns a [Configuration.Version.Extractor] that reads the value from given [versionKey], defaulting to [versionDefaultValue] when [versionKey] is unspecified.
+ */
+ fun fromKey(versionKey: String, versionDefaultValue: Int? = DEFAULT_VERSION_VALUE): Configuration.Version.Extractor = VersionExtractor(versionKey, versionDefaultValue)
+ }
+ }
+ }
+
+ /**
+ * Defines the ability to validate a [Config] object, producing a valid [Config] or a set of [Configuration.Validation.Error].
+ */
+ interface Validator : net.corda.common.validation.internal.Validator
+}
\ No newline at end of file
diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Properties.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Properties.kt
new file mode 100644
index 0000000000..17b9fe73a1
--- /dev/null
+++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Properties.kt
@@ -0,0 +1,205 @@
+package net.corda.common.configuration.parsing.internal
+
+import com.typesafe.config.*
+import net.corda.common.validation.internal.Validated
+import net.corda.common.validation.internal.Validated.Companion.invalid
+import net.corda.common.validation.internal.Validated.Companion.valid
+
+internal class LongProperty(key: String, sensitive: Boolean = false) : StandardProperty(key, Long::class.javaObjectType.simpleName, Config::getLong, Config::getLongList, sensitive) {
+
+ override fun validate(target: Config, options: Configuration.Validation.Options?): Valid {
+
+ val validated = super.validate(target, options)
+ if (validated.isValid && target.getValue(key).unwrapped().toString().contains(".")) {
+ return invalid(ConfigException.WrongType(target.origin(), key, Long::class.javaObjectType.simpleName, Double::class.javaObjectType.simpleName).toValidationError(key, typeName))
+ }
+ return validated
+ }
+}
+
+internal open class StandardProperty(override val key: String, typeNameArg: String, private val extractSingleValue: (Config, String) -> TYPE, internal val extractListValue: (Config, String) -> List, override val isSensitive: Boolean = false, final override val schema: Configuration.Schema? = null) : Configuration.Property.Definition.Standard {
+
+ override fun valueIn(configuration: Config) = extractSingleValue.invoke(configuration, key)
+
+ override val typeName: String = schema?.let { "#${it.name ?: "Object@$key"}" } ?: typeNameArg
+
+ override fun mapValid(mappedTypeName: String, convert: (TYPE) -> Valid): Configuration.Property.Definition.Standard = FunctionalProperty(this, mappedTypeName, extractListValue, convert)
+
+ override fun optional(defaultValue: TYPE?): Configuration.Property.Definition = OptionalProperty(this, defaultValue)
+
+ override fun list(): Configuration.Property.Definition.Required> = ListProperty(this)
+
+ override fun describe(configuration: Config): ConfigValue {
+
+ if (isSensitive) {
+ return ConfigValueFactory.fromAnyRef(Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER)
+ }
+ return schema?.describe(configuration.getConfig(key)) ?: ConfigValueFactory.fromAnyRef(valueIn(configuration))
+ }
+
+ override val isMandatory = true
+
+ override fun validate(target: Config, options: Configuration.Validation.Options?): Valid {
+
+ val errors = mutableSetOf()
+ errors += errorsWhenExtractingValue(target)
+ if (errors.isEmpty()) {
+ schema?.let { nestedSchema ->
+ val nestedConfig: Config? = target.getConfig(key)
+ nestedConfig?.let {
+ errors += nestedSchema.validate(nestedConfig, options).errors.map { error -> error.withContainingPath(*key.split(".").toTypedArray()) }
+ }
+ }
+ }
+ return Validated.withResult(target, errors)
+ }
+
+ override fun toString() = "\"$key\": \"$typeName\""
+}
+
+private class ListProperty(delegate: StandardProperty) : RequiredDelegatedProperty, StandardProperty>(delegate) {
+
+ override val typeName: String = "List<${delegate.typeName}>"
+
+ override fun valueIn(configuration: Config): List = delegate.extractListValue.invoke(configuration, key)
+
+ override fun validate(target: Config, options: Configuration.Validation.Options?): Valid {
+
+ val errors = mutableSetOf()
+ errors += errorsWhenExtractingValue(target)
+ if (errors.isEmpty()) {
+ delegate.schema?.let { schema ->
+ errors += valueIn(target).asSequence().map { element -> element as ConfigObject }.map(ConfigObject::toConfig).mapIndexed { index, targetConfig -> schema.validate(targetConfig, options).errors.map { error -> error.withContainingPath(key, "[$index]") } }.reduce { one, other -> one + other }
+ }
+ }
+ return Validated.withResult(target, errors)
+ }
+
+ override fun describe(configuration: Config): ConfigValue {
+
+ if (isSensitive) {
+ return ConfigValueFactory.fromAnyRef(Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER)
+ }
+ return delegate.schema?.let { schema -> ConfigValueFactory.fromAnyRef(valueIn(configuration).asSequence().map { element -> element as ConfigObject }.map(ConfigObject::toConfig).map { schema.describe(it) }.toList()) } ?: ConfigValueFactory.fromAnyRef(valueIn(configuration))
+ }
+}
+
+private class OptionalProperty(delegate: Configuration.Property.Definition.Required, private val defaultValue: TYPE?) : DelegatedProperty>(delegate) {
+
+ override val isMandatory: Boolean = false
+
+ override val typeName: String = "${super.typeName}?"
+
+ override fun describe(configuration: Config) = delegate.describe(configuration)
+
+ override fun valueIn(configuration: Config): TYPE? {
+
+ return when {
+ isSpecifiedBy(configuration) -> delegate.valueIn(configuration)
+ else -> defaultValue
+ }
+ }
+
+ override fun validate(target: Config, options: Configuration.Validation.Options?): Valid {
+
+ val result = delegate.validate(target, options)
+ val error = result.errors.asSequence().filterIsInstance().singleOrNull()
+ return when {
+ error != null -> if (result.errors.size > 1) result else valid(target)
+ else -> result
+ }
+ }
+}
+
+private class FunctionalProperty(delegate: Configuration.Property.Definition.Standard, private val mappedTypeName: String, internal val extractListValue: (Config, String) -> List, private val convert: (TYPE) -> Valid) : RequiredDelegatedProperty>(delegate), Configuration.Property.Definition.Standard {
+
+ override fun valueIn(configuration: Config) = convert.invoke(delegate.valueIn(configuration)).valueOrThrow()
+
+ override val typeName: String = if (super.typeName == "#$mappedTypeName") super.typeName else "$mappedTypeName(${super.typeName})"
+
+ override fun mapValid(mappedTypeName: String, convert: (MAPPED) -> Valid): Configuration.Property.Definition.Standard = FunctionalProperty(delegate, mappedTypeName, extractListValue, { target: TYPE -> this.convert.invoke(target).mapValid(convert) })
+
+ override fun list(): Configuration.Property.Definition.Required> = FunctionalListProperty(this)
+
+ override fun validate(target: Config, options: Configuration.Validation.Options?): Valid {
+
+ val errors = mutableSetOf()
+ errors += delegate.validate(target, options).errors
+ if (errors.isEmpty()) {
+ errors += convert.invoke(delegate.valueIn(target)).mapErrors { error -> error.with(delegate.key, mappedTypeName) }.errors
+ }
+ return Validated.withResult(target, errors)
+ }
+
+ override fun describe(configuration: Config) = delegate.describe(configuration)
+}
+
+private class FunctionalListProperty(delegate: FunctionalProperty) : RequiredDelegatedProperty, FunctionalProperty>(delegate) {
+
+ override val typeName: String = "List<${super.typeName}>"
+
+ override fun valueIn(configuration: Config): List = delegate.extractListValue.invoke(configuration, key).asSequence().map { configObject(key to ConfigValueFactory.fromAnyRef(it)) }.map(ConfigObject::toConfig).map(delegate::valueIn).toList()
+
+ override fun validate(target: Config, options: Configuration.Validation.Options?): Valid {
+
+ val list = try {
+ delegate.extractListValue.invoke(target, key)
+ } catch (e: ConfigException) {
+ if (isErrorExpected(e)) {
+ return invalid(e.toValidationError(key, typeName))
+ } else {
+ throw e
+ }
+ }
+ val errors = list.asSequence().map { configObject(key to ConfigValueFactory.fromAnyRef(it)) }.mapIndexed { index, value -> delegate.validate(value.toConfig(), options).errors.map { error -> error.withContainingPath(key, "[$index]") } }.reduce { one, other -> one + other }.toSet()
+ return Validated.withResult(target, errors)
+ }
+
+ override fun describe(configuration: Config): ConfigValue {
+
+ if (isSensitive) {
+ return ConfigValueFactory.fromAnyRef(Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER)
+ }
+ return delegate.schema?.let { schema -> ConfigValueFactory.fromAnyRef(valueIn(configuration).asSequence().map { element -> element as ConfigObject }.map(ConfigObject::toConfig).map { schema.describe(it) }.toList()) } ?: ConfigValueFactory.fromAnyRef(valueIn(configuration))
+ }
+}
+
+private abstract class DelegatedProperty(protected val delegate: DELEGATE) : Configuration.Property.Metadata by delegate, Configuration.Property.Definition {
+
+ final override fun toString() = "\"$key\": \"$typeName\""
+}
+
+private abstract class RequiredDelegatedProperty>(delegate: DELEGATE) : DelegatedProperty(delegate), Configuration.Property.Definition.Required {
+
+ final override fun optional(defaultValue: TYPE?): Configuration.Property.Definition = OptionalProperty(this, defaultValue)
+}
+
+private fun ConfigException.toValidationError(keyName: String, typeName: String): Configuration.Validation.Error {
+
+ val toError = when (this) {
+ is ConfigException.Missing -> Configuration.Validation.Error.MissingValue.Companion::of
+ is ConfigException.WrongType -> Configuration.Validation.Error.WrongType.Companion::of
+ is ConfigException.BadValue -> Configuration.Validation.Error.BadValue.Companion::of
+ is ConfigException.BadPath -> Configuration.Validation.Error.BadPath.Companion::of
+ is ConfigException.Parse -> Configuration.Validation.Error.MalformedStructure.Companion::of
+ else -> throw IllegalStateException("Unsupported ConfigException of type ${this::class.java.name}", this)
+ }
+ return toError.invoke(message!!, keyName, typeName, emptyList())
+}
+
+private fun Configuration.Property.Definition<*>.errorsWhenExtractingValue(target: Config): Set {
+
+ try {
+ valueIn(target)
+ return emptySet()
+ } catch (exception: ConfigException) {
+ if (isErrorExpected(exception)) {
+ return setOf(exception.toValidationError(key, typeName))
+ }
+ throw exception
+ }
+}
+
+private val expectedExceptionTypes = setOf(ConfigException.Missing::class, ConfigException.WrongType::class, ConfigException.BadValue::class, ConfigException.BadPath::class, ConfigException.Parse::class)
+
+private fun isErrorExpected(error: ConfigException) = expectedExceptionTypes.any { expected -> expected.isInstance(error) }
\ No newline at end of file
diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Schema.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Schema.kt
new file mode 100644
index 0000000000..06e90d34cd
--- /dev/null
+++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Schema.kt
@@ -0,0 +1,75 @@
+package net.corda.common.configuration.parsing.internal
+
+import com.typesafe.config.Config
+import com.typesafe.config.ConfigValue
+import com.typesafe.config.ConfigValueFactory
+import net.corda.common.validation.internal.Validated
+
+internal class Schema(override val name: String?, unorderedProperties: Iterable>) : Configuration.Schema {
+
+ override val properties = unorderedProperties.sortedBy(Configuration.Property.Definition<*>::key).toSet()
+
+ init {
+ val invalid = properties.groupBy(Configuration.Property.Definition<*>::key).mapValues { entry -> entry.value.size }.filterValues { propertiesForKey -> propertiesForKey > 1 }
+ if (invalid.isNotEmpty()) {
+ throw IllegalArgumentException("More than one property was found for keys ${invalid.keys.joinToString(", ", "[", "]")}.")
+ }
+ }
+
+ override fun validate(target: Config, options: Configuration.Validation.Options?): Valid {
+
+ val propertyErrors = properties.flatMap { property -> property.validate(target, options).errors }.toMutableSet()
+ if (options?.strict == true) {
+ val unknownKeys = target.root().keys - properties.map(Configuration.Property.Definition<*>::key)
+ propertyErrors += unknownKeys.map { Configuration.Validation.Error.Unknown.of(it) }
+ }
+ return Validated.withResult(target, propertyErrors)
+ }
+
+ override fun description(): String {
+
+ val description = StringBuilder()
+ val root = properties.asSequence().map { it.key to ConfigValueFactory.fromAnyRef(it.typeName) }.fold(configObject()) { config, (key, value) -> config.withValue(key, value) }
+
+ description.append(root.toConfig().serialize())
+
+ val nestedProperties = (properties + properties.flatMap { it.schema?.properties ?: emptySet() }).asSequence().distinctBy(Configuration.Property.Definition<*>::schema)
+ nestedProperties.forEach { property ->
+ property.schema?.let {
+ description.append(System.lineSeparator())
+ description.append("${property.typeName}: ")
+ description.append(it.description())
+ description.append(System.lineSeparator())
+ }
+ }
+ return description.toString()
+ }
+
+ override fun describe(configuration: Config): ConfigValue {
+
+ return properties.asSequence().map { it.key to it.describe(configuration) }.fold(configObject()) { config, (key, value) -> config.withValue(key, value) }
+ }
+
+ override fun equals(other: Any?): Boolean {
+
+ if (this === other) {
+ return true
+ }
+ if (javaClass != other?.javaClass) {
+ return false
+ }
+
+ other as Schema
+
+ if (properties != other.properties) {
+ return false
+ }
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+
+ return properties.hashCode()
+ }
+}
\ No newline at end of file
diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Specification.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Specification.kt
new file mode 100644
index 0000000000..919263dc16
--- /dev/null
+++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Specification.kt
@@ -0,0 +1,101 @@
+package net.corda.common.configuration.parsing.internal
+
+import com.typesafe.config.ConfigObject
+import java.time.Duration
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KClass
+import kotlin.reflect.KProperty
+
+interface PropertyDelegate {
+
+ operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty>
+
+ interface Required {
+
+ operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty>
+
+ fun optional(defaultValue: TYPE? = null): PropertyDelegate
+ }
+
+ interface Single {
+
+ operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty>
+
+ fun list(): Required>
+ }
+
+ interface Standard : Required, Single {
+
+ override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty>
+
+ fun mapValid(mappedTypeName: String, convert: (TYPE) -> Valid): Standard
+
+ fun map(mappedTypeName: String, convert: (TYPE) -> MAPPED): Standard = mapValid(mappedTypeName) { value -> valid(convert.invoke(value)) }
+ }
+
+ companion object {
+
+ internal fun long(key: String?, prefix: String?, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard = PropertyDelegateImpl(key, prefix, sensitive, addProperty, Configuration.Property.Definition.Companion::long)
+
+ internal fun int(key: String?, prefix: String?, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard = PropertyDelegateImpl(key, prefix, sensitive, addProperty, Configuration.Property.Definition.Companion::int)
+
+ internal fun boolean(key: String?, prefix: String?, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard = PropertyDelegateImpl(key, prefix, sensitive, addProperty, Configuration.Property.Definition.Companion::boolean)
+
+ internal fun double(key: String?, prefix: String?, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard = PropertyDelegateImpl(key, prefix, sensitive, addProperty, Configuration.Property.Definition.Companion::double)
+
+ internal fun float(key: String?, prefix: String?, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard = PropertyDelegateImpl(key, prefix, sensitive, addProperty, Configuration.Property.Definition.Companion::float)
+
+ internal fun string(key: String?, prefix: String?, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard = PropertyDelegateImpl(key, prefix, sensitive, addProperty, Configuration.Property.Definition.Companion::string)
+
+ internal fun duration(key: String?, prefix: String?, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard = PropertyDelegateImpl(key, prefix, sensitive, addProperty, Configuration.Property.Definition.Companion::duration)
+
+ internal fun nestedObject(schema: Configuration.Schema?, key: String?, prefix: String?, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard = PropertyDelegateImpl(key, prefix, sensitive, addProperty, { k, s -> Configuration.Property.Definition.nestedObject(k, schema, s) })
+
+ internal fun > enum(key: String?, prefix: String?, enumClass: KClass, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard = PropertyDelegateImpl(key, prefix, sensitive, addProperty, { k, s -> Configuration.Property.Definition.enum(k, enumClass, s) })
+ }
+}
+
+private class PropertyDelegateImpl(private val key: String?, private val prefix: String?, private val sensitive: Boolean = false, private val addToProperties: (Configuration.Property.Definition<*>) -> Unit, private val construct: (String, Boolean) -> Configuration.Property.Definition.Standard) : PropertyDelegate.Standard {
+
+ override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty> {
+
+ val shortName = key ?: property.name
+ val prop = construct.invoke(prefix?.let { "$prefix.$shortName" } ?: shortName, sensitive).also(addToProperties)
+ return object : ReadOnlyProperty> {
+
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Configuration.Property.Definition.Standard = prop
+ }
+ }
+
+ override fun list(): PropertyDelegate.Required> = ListPropertyDelegateImpl(key, sensitive, addToProperties, { k, s -> construct.invoke(k, s).list() })
+
+ override fun optional(defaultValue: TYPE?): PropertyDelegate = OptionalPropertyDelegateImpl(key, sensitive, addToProperties, { k, s -> construct.invoke(k, s).optional(defaultValue) })
+
+ override fun mapValid(mappedTypeName: String, convert: (TYPE) -> Valid): PropertyDelegate.Standard = PropertyDelegateImpl(key, prefix, sensitive, addToProperties, { k, s -> construct.invoke(k, s).mapValid(mappedTypeName) { value -> convert.invoke(value) } })
+}
+
+private class OptionalPropertyDelegateImpl(private val key: String?, private val sensitive: Boolean = false, private val addToProperties: (Configuration.Property.Definition<*>) -> Unit, private val construct: (String, Boolean) -> Configuration.Property.Definition) : PropertyDelegate {
+
+ override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty> {
+
+ val prop = construct.invoke(key ?: property.name, sensitive).also(addToProperties)
+ return object : ReadOnlyProperty> {
+
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Configuration.Property.Definition = prop
+ }
+ }
+}
+
+private class ListPropertyDelegateImpl(private val key: String?, private val sensitive: Boolean = false, private val addToProperties: (Configuration.Property.Definition<*>) -> Unit, private val construct: (String, Boolean) -> Configuration.Property.Definition.Required) : PropertyDelegate.Required {
+
+ override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty> {
+
+ val prop = construct.invoke(key ?: property.name, sensitive).also(addToProperties)
+ return object : ReadOnlyProperty> {
+
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Configuration.Property.Definition.Required = prop
+ }
+ }
+
+ override fun optional(defaultValue: TYPE?): PropertyDelegate = OptionalPropertyDelegateImpl(key, sensitive, addToProperties, { k, s -> construct.invoke(k, s).optional(defaultValue) })
+}
\ No newline at end of file
diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Utils.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Utils.kt
new file mode 100644
index 0000000000..d75a2b1d08
--- /dev/null
+++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Utils.kt
@@ -0,0 +1,53 @@
+package net.corda.common.configuration.parsing.internal
+
+import com.typesafe.config.*
+import net.corda.common.validation.internal.Validated
+
+inline fun Configuration.Property.Definition.Standard.mapValid(noinline convert: (TYPE) -> Valid): Configuration.Property.Definition.Standard = mapValid(MAPPED::class.java.simpleName, convert)
+
+inline fun , VALUE : Any> Configuration.Specification.enum(key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = enum(key, ENUM::class, sensitive)
+
+inline fun PropertyDelegate.Standard.mapValid(noinline convert: (TYPE) -> Valid): PropertyDelegate.Standard = mapValid(MAPPED::class.java.simpleName, convert)
+
+inline fun PropertyDelegate.Standard.map(noinline convert: (TYPE) -> MAPPED): PropertyDelegate.Standard = map(MAPPED::class.java.simpleName, convert)
+
+operator fun Config.get(property: Configuration.Property.Definition): TYPE = property.valueIn(this)
+
+inline fun Configuration.Specification<*>.nested(specification: Configuration.Specification, key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = nestedObject(schema = specification, key = key, sensitive = sensitive).map(ConfigObject::toConfig).mapValid { value -> specification.parse(value) }
+
+@Suppress("UNCHECKED_CAST")
+internal fun configObject(vararg entries: Pair): ConfigObject {
+
+ var configuration = ConfigFactory.empty()
+ entries.forEach { entry ->
+ val value = entry.second
+ configuration += if (value is Pair<*, *> && value.first is String) {
+ (entry.first to (ConfigFactory.empty() + value as Pair).root())
+ } else {
+ entry
+ }
+ }
+ return configuration.root()
+}
+
+internal operator fun Config.plus(entry: Pair): Config {
+
+ var value = entry.second ?: return this - entry.first
+ if (value is Config) {
+ value = value.root()
+ }
+ return withValue(entry.first, ConfigValueFactory.fromAnyRef(value))
+}
+
+internal operator fun Config.minus(key: String): Config {
+
+ return withoutPath(key)
+}
+
+internal fun Config.serialize(options: ConfigRenderOptions = ConfigRenderOptions.concise().setFormatted(true).setJson(true)): String = root().serialize(options)
+
+internal fun ConfigValue.serialize(options: ConfigRenderOptions = ConfigRenderOptions.concise().setFormatted(true).setJson(true)): String = render(options)
+
+internal typealias Valid = Validated
+
+internal fun valid(target: TYPE) = Validated.valid(target)
\ No newline at end of file
diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionExtractor.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionExtractor.kt
new file mode 100644
index 0000000000..cdba00e7c7
--- /dev/null
+++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionExtractor.kt
@@ -0,0 +1,23 @@
+package net.corda.common.configuration.parsing.internal.versioned
+
+import com.typesafe.config.Config
+import net.corda.common.configuration.parsing.internal.Configuration
+import net.corda.common.configuration.parsing.internal.Valid
+import net.corda.common.configuration.parsing.internal.valid
+
+internal class VersionExtractor(versionKey: String, versionDefaultValue: Int?) : Configuration.Version.Extractor {
+
+ private val spec = Spec(versionKey, versionDefaultValue)
+
+ override fun parse(configuration: Config, options: Configuration.Validation.Options): Valid {
+
+ return spec.parse(configuration)
+ }
+
+ private class Spec(versionKey: String, versionDefaultValue: Int?) : Configuration.Specification("Version") {
+
+ private val version by int(key = versionKey).optional(versionDefaultValue)
+
+ override fun parseValid(configuration: Config) = valid(version.valueIn(configuration))
+ }
+}
\ No newline at end of file
diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionedSpecificationRegistry.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionedSpecificationRegistry.kt
new file mode 100644
index 0000000000..11cff499d8
--- /dev/null
+++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionedSpecificationRegistry.kt
@@ -0,0 +1,30 @@
+package net.corda.common.configuration.parsing.internal.versioned
+
+import com.typesafe.config.Config
+import net.corda.common.configuration.parsing.internal.Configuration
+import net.corda.common.configuration.parsing.internal.Valid
+import net.corda.common.configuration.parsing.internal.valid
+import net.corda.common.validation.internal.Validated.Companion.invalid
+
+class VersionedSpecificationRegistry private constructor(private val versionFromConfig: (Config) -> Valid, private val specifications: Map>) : (Config) -> Valid> {
+
+ companion object {
+
+ fun mapping(versionParser: Configuration.Value.Parser, specifications: Map>) = VersionedSpecificationRegistry({ config -> versionParser.parse(config) }, specifications)
+
+ fun mapping(versionParser: Configuration.Value.Parser, vararg specifications: Pair>) = VersionedSpecificationRegistry({ config -> versionParser.parse(config) }, specifications.toMap())
+
+ fun mapping(versionParser: (Config) -> Valid, specifications: Map>) = VersionedSpecificationRegistry(versionParser, specifications)
+
+ fun mapping(versionParser: (Config) -> Valid, vararg specifications: Pair>) = VersionedSpecificationRegistry(versionParser, specifications.toMap())
+ }
+
+ override fun invoke(configuration: Config): Valid> {
+
+ return versionFromConfig.invoke(configuration).mapValid { version ->
+
+ val value = specifications[version]
+ value?.let { valid(it) } ?: invalid, Configuration.Validation.Error>(Configuration.Validation.Error.UnsupportedVersion.of(version))
+ }
+ }
+}
\ No newline at end of file
diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/Address.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/Address.kt
new file mode 100644
index 0000000000..254d50740d
--- /dev/null
+++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/Address.kt
@@ -0,0 +1,29 @@
+package net.corda.common.configuration.parsing.internal
+
+import net.corda.common.validation.internal.Validated
+
+data class Address(val host: String, val port: Int) {
+
+ init {
+ require(host.isNotBlank())
+ require(port > 0)
+ }
+
+ companion object {
+
+ fun validFromRawValue(rawValue: String, mapError: (String) -> ERROR): Validated {
+
+ val parts = rawValue.split(":")
+ if (parts.size != 2 || parts[0].isBlank() || parts[1].isBlank() || parts[1].toIntOrNull() == null) {
+ return Validated.invalid(sequenceOf("Value format is \":\"").map(mapError).toSet())
+ }
+ val host = parts[0]
+ val port = parts[1].toInt()
+ if (port <= 0) {
+ return Validated.invalid(sequenceOf("Port value must be greater than zero").map(mapError).toSet())
+ }
+
+ return Validated.valid(Address(host, port))
+ }
+ }
+}
\ No newline at end of file
diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/Addresses.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/Addresses.kt
new file mode 100644
index 0000000000..6c60fc796a
--- /dev/null
+++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/Addresses.kt
@@ -0,0 +1,3 @@
+package net.corda.common.configuration.parsing.internal
+
+data class Addresses(val principal: Address, val admin: Address)
\ No newline at end of file
diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyTest.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyTest.kt
new file mode 100644
index 0000000000..d2d35e5de1
--- /dev/null
+++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyTest.kt
@@ -0,0 +1,184 @@
+package net.corda.common.configuration.parsing.internal
+
+import com.typesafe.config.ConfigException
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.junit.Test
+
+class PropertyTest {
+
+ @Test
+ fun present_value_with_correct_type() {
+
+ val key = "a.b.c"
+ val value = 1L
+ val configuration = configObject(key to value).toConfig()
+
+ val property = Configuration.Property.Definition.long(key)
+ println(property)
+
+ assertThat(property.key).isEqualTo(key)
+ assertThat(property.isMandatory).isTrue()
+ assertThat(property.isSpecifiedBy(configuration)).isTrue()
+ assertThat(property.valueIn(configuration)).isEqualTo(value)
+ assertThat(configuration[property]).isEqualTo(value)
+ }
+
+ @Test
+ fun present_value_with_wrong_type() {
+
+ val key = "a.b.c"
+ val value = 1
+ val configuration = configObject(key to value).toConfig()
+
+ val property = Configuration.Property.Definition.boolean(key)
+ println(property)
+
+ assertThat(property.key).isEqualTo(key)
+ assertThat(property.isMandatory).isTrue()
+ assertThat(property.isSpecifiedBy(configuration)).isTrue()
+ assertThatThrownBy { property.valueIn(configuration) }.isInstanceOf(ConfigException.WrongType::class.java)
+ }
+
+ @Test
+ fun present_value_of_list_type() {
+
+ val key = "a.b.c"
+ val value = listOf(1L, 2L, 3L)
+ val configuration = configObject(key to value).toConfig()
+
+ val property = Configuration.Property.Definition.long(key).list()
+ println(property)
+
+ assertThat(property.key).isEqualTo(key)
+ assertThat(property.isMandatory).isTrue()
+ assertThat(property.isSpecifiedBy(configuration)).isTrue()
+ assertThat(property.valueIn(configuration)).isEqualTo(value)
+ }
+
+ @Test
+ fun optional_present_value_of_list_type() {
+
+ val key = "a.b.c"
+ val value = listOf(1L, 2L, 3L)
+ val configuration = configObject(key to value).toConfig()
+
+ val property = Configuration.Property.Definition.long(key).list().optional()
+ println(property)
+
+ assertThat(property.key).isEqualTo(key)
+ assertThat(property.isMandatory).isFalse()
+ assertThat(property.isSpecifiedBy(configuration)).isTrue()
+ assertThat(property.valueIn(configuration)).isEqualTo(value)
+ }
+
+ @Test
+ fun optional_absent_value_of_list_type() {
+
+ val key = "a.b.c"
+ val configuration = configObject(key to null).toConfig()
+
+ val property = Configuration.Property.Definition.long(key).list().optional()
+ println(property)
+
+ assertThat(property.key).isEqualTo(key)
+ assertThat(property.isMandatory).isFalse()
+ assertThat(property.isSpecifiedBy(configuration)).isFalse()
+ assertThat(property.valueIn(configuration)).isNull()
+
+ }
+
+ @Test
+ fun optional_absent_value_of_list_type_with_default_value() {
+
+ val key = "a.b.c"
+ val configuration = configObject(key to null).toConfig()
+
+ val defaultValue = listOf(1L, 2L, 3L)
+ val property = Configuration.Property.Definition.long(key).list().optional(defaultValue)
+ println(property)
+
+ assertThat(property.key).isEqualTo(key)
+ assertThat(property.isMandatory).isFalse()
+ assertThat(property.isSpecifiedBy(configuration)).isFalse()
+ assertThat(property.valueIn(configuration)).isEqualTo(defaultValue)
+ }
+
+ @Test
+ fun absent_value() {
+
+ val key = "a.b.c"
+ val configuration = configObject(key to null).toConfig()
+
+ val property = Configuration.Property.Definition.long(key)
+ println(property)
+
+ assertThat(property.key).isEqualTo(key)
+ assertThat(property.isMandatory).isTrue()
+ assertThat(property.isSpecifiedBy(configuration)).isFalse()
+ assertThatThrownBy { property.valueIn(configuration) }.isInstanceOf(ConfigException.Missing::class.java)
+ }
+
+ @Test
+ fun optional_present_value_with_correct_type() {
+
+ val key = "a.b.c"
+ val value = 1L
+ val configuration = configObject(key to value).toConfig()
+
+ val property = Configuration.Property.Definition.long(key).optional()
+ println(property)
+
+ assertThat(property.key).isEqualTo(key)
+ assertThat(property.isMandatory).isFalse()
+ assertThat(property.isSpecifiedBy(configuration)).isTrue()
+ assertThat(property.valueIn(configuration)).isEqualTo(value)
+ }
+
+ @Test
+ fun optional_present_value_with_wrong_type() {
+
+ val key = "a.b.c"
+ val value = 1
+ val configuration = configObject(key to value).toConfig()
+
+ val property = Configuration.Property.Definition.boolean(key).optional()
+ println(property)
+
+ assertThat(property.key).isEqualTo(key)
+ assertThat(property.isMandatory).isFalse()
+ assertThat(property.isSpecifiedBy(configuration)).isTrue()
+ assertThatThrownBy { property.valueIn(configuration) }.isInstanceOf(ConfigException.WrongType::class.java)
+ }
+
+ @Test
+ fun optional_absent_value() {
+
+ val key = "a.b.c"
+ val configuration = configObject(key to null).toConfig()
+
+ val property = Configuration.Property.Definition.long(key).optional()
+ println(property)
+
+ assertThat(property.key).isEqualTo(key)
+ assertThat(property.isMandatory).isFalse()
+ assertThat(property.isSpecifiedBy(configuration)).isFalse()
+ assertThat(property.valueIn(configuration)).isNull()
+ }
+
+ @Test
+ fun optional_absent_with_default_value() {
+
+ val key = "a.b.c"
+ val configuration = configObject(key to null).toConfig()
+
+ val defaultValue = 23L
+ val property = Configuration.Property.Definition.long(key).optional(defaultValue)
+ println(property)
+
+ assertThat(property.key).isEqualTo(key)
+ assertThat(property.isMandatory).isFalse()
+ assertThat(property.isSpecifiedBy(configuration)).isFalse()
+ assertThat(property.valueIn(configuration)).isEqualTo(defaultValue)
+ }
+}
\ No newline at end of file
diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyValidationTest.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyValidationTest.kt
new file mode 100644
index 0000000000..b26aca0abb
--- /dev/null
+++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyValidationTest.kt
@@ -0,0 +1,337 @@
+package net.corda.common.configuration.parsing.internal
+
+import com.typesafe.config.Config
+import net.corda.common.validation.internal.Validated.Companion.invalid
+import net.corda.common.validation.internal.Validated.Companion.valid
+import net.corda.common.validation.internal.Validator
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+
+class PropertyValidationTest {
+
+ @Test
+ fun absent_value() {
+
+ val key = "a.b.c"
+ val configuration = configObject().toConfig()
+
+ val property: Validator = Configuration.Property.Definition.long(key)
+
+ assertThat(property.validate(configuration).errors).satisfies { errors ->
+
+ assertThat(errors).hasSize(1)
+ assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error ->
+
+ assertThat(error.keyName).isEqualTo(key.split(".").last())
+ assertThat(error.path).containsExactly(*key.split(".").toTypedArray())
+ }
+ }
+ }
+
+ @Test
+ fun missing_value() {
+
+ val key = "a.b.c"
+ val configuration = configObject(key to null).toConfig()
+
+ val property: Validator = Configuration.Property.Definition.long(key)
+
+ assertThat(property.validate(configuration).errors).satisfies { errors ->
+
+ assertThat(errors).hasSize(1)
+ assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error ->
+
+ assertThat(error.keyName).isEqualTo(key.split(".").last())
+ assertThat(error.path).containsExactly(*key.split(".").toTypedArray())
+ }
+ }
+ }
+
+ @Test
+ fun absent_list_value() {
+
+ val key = "a.b.c"
+ val configuration = configObject().toConfig()
+
+ val property: Validator = Configuration.Property.Definition.long(key).list()
+
+ assertThat(property.validate(configuration).errors).satisfies { errors ->
+
+ assertThat(errors).hasSize(1)
+ assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error ->
+
+ assertThat(error.keyName).isEqualTo(key.split(".").last())
+ assertThat(error.path).containsExactly(*key.split(".").toTypedArray())
+ }
+ }
+ }
+
+ @Test
+ fun missing_list_value() {
+
+ val key = "a.b.c"
+ val configuration = configObject(key to null).toConfig()
+
+ val property: Validator = Configuration.Property.Definition.long(key).list()
+
+ assertThat(property.validate(configuration).errors).satisfies { errors ->
+
+ assertThat(errors).hasSize(1)
+ assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error ->
+
+ assertThat(error.keyName).isEqualTo(key.split(".").last())
+ assertThat(error.path).containsExactly(*key.split(".").toTypedArray())
+ }
+ }
+ }
+
+ @Test
+ fun wrong_type() {
+
+ val key = "a.b.c"
+
+ val property: Validator = Configuration.Property.Definition.long(key)
+
+ val configuration = configObject(key to false).toConfig()
+
+ assertThat(property.validate(configuration).errors).satisfies { errors ->
+
+ assertThat(errors).hasSize(1)
+ assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error ->
+
+ assertThat(error.keyName).isEqualTo(key.split(".").last())
+ assertThat(error.path).containsExactly(*key.split(".").toTypedArray())
+ }
+ }
+ }
+
+ @Test
+ fun wrong_floating_numeric_type_when_integer_expected() {
+
+ val key = "a.b.c"
+
+ val property: Validator = Configuration.Property.Definition.long(key)
+
+ val configuration = configObject(key to 1.2).toConfig()
+
+ assertThat(property.validate(configuration).errors).satisfies { errors ->
+
+ assertThat(errors).hasSize(1)
+ assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error ->
+
+ assertThat(error.keyName).isEqualTo(key.split(".").last())
+ assertThat(error.path).containsExactly(*key.split(".").toTypedArray())
+ }
+ }
+ }
+
+ @Test
+ fun integer_numeric_type_when_floating_expected_works() {
+
+ val key = "a.b.c"
+
+ val property: Validator = Configuration.Property.Definition.double(key)
+
+ val configuration = configObject(key to 1).toConfig()
+
+ assertThat(property.validate(configuration).isValid).isTrue()
+ }
+
+ @Test
+ fun wrong_element_type_for_list() {
+
+ val key = "a.b.c"
+
+ val property: Validator = Configuration.Property.Definition.long(key).list()
+
+ val configuration = configObject(key to listOf(false, true)).toConfig()
+
+ assertThat(property.validate(configuration).errors).satisfies { errors ->
+
+ assertThat(errors).hasSize(1)
+ assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error ->
+
+ assertThat(error.keyName).isEqualTo(key.split(".").last())
+ assertThat(error.path).containsExactly(*key.split(".").toTypedArray())
+ }
+ }
+ }
+
+ @Test
+ fun list_type_when_declared_single() {
+
+ val key = "a.b.c"
+
+ val property: Validator = Configuration.Property.Definition.long(key)
+
+ val configuration = configObject(key to listOf(1, 2, 3)).toConfig()
+
+ assertThat(property.validate(configuration).errors).satisfies { errors ->
+
+ assertThat(errors).hasSize(1)
+ assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error ->
+
+ assertThat(error.keyName).isEqualTo(key.split(".").last())
+ assertThat(error.path).containsExactly(*key.split(".").toTypedArray())
+ }
+ }
+ }
+
+ @Test
+ fun single_type_when_declared_list() {
+
+ val key = "a.b.c"
+
+ val property: Validator = Configuration.Property.Definition.long(key).list()
+
+ val configuration = configObject(key to 1).toConfig()
+
+ assertThat(property.validate(configuration).errors).satisfies { errors ->
+
+ assertThat(errors).hasSize(1)
+ assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error ->
+
+ assertThat(error.keyName).isEqualTo(key.split(".").last())
+ assertThat(error.path).containsExactly(*key.split(".").toTypedArray())
+ }
+ }
+ }
+
+ @Test
+ fun wrong_type_in_nested_property() {
+
+ val key = "a.b.c"
+
+ val nestedKey = "d"
+ val nestedPropertySchema = Configuration.Schema.withProperties(Configuration.Property.Definition.long(nestedKey))
+
+ val property: Validator = Configuration.Property.Definition.nestedObject(key, nestedPropertySchema)
+
+ val configuration = configObject(key to configObject(nestedKey to false)).toConfig()
+
+ assertThat(property.validate(configuration).errors).satisfies { errors ->
+
+ assertThat(errors).hasSize(1)
+ assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error ->
+
+ assertThat(error.keyName).isEqualTo(nestedKey)
+ assertThat(error.path).containsExactly(*key.split(".").toTypedArray(), nestedKey)
+ }
+ }
+ }
+
+ @Test
+ fun absent_value_in_nested_property() {
+
+ val key = "a.b.c"
+
+ val nestedKey = "d"
+ val nestedPropertySchema = Configuration.Schema.withProperties(Configuration.Property.Definition.long(nestedKey))
+
+ val property: Validator = Configuration.Property.Definition.nestedObject(key, nestedPropertySchema)
+
+ val configuration = configObject(key to configObject()).toConfig()
+
+ assertThat(property.validate(configuration).errors).satisfies { errors ->
+
+ assertThat(errors).hasSize(1)
+ assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error ->
+
+ assertThat(error.keyName).isEqualTo(nestedKey)
+ assertThat(error.path).containsExactly(*key.split(".").toTypedArray(), nestedKey)
+ }
+ }
+ }
+
+ @Test
+ fun missing_value_in_nested_property() {
+
+ val key = "a.b.c"
+
+ val nestedKey = "d"
+ val nestedPropertySchema = Configuration.Schema.withProperties(Configuration.Property.Definition.long(nestedKey))
+
+ val property: Validator = Configuration.Property.Definition.nestedObject(key, nestedPropertySchema)
+
+ val configuration = configObject(key to configObject(nestedKey to null)).toConfig()
+
+ assertThat(property.validate(configuration).errors).satisfies { errors ->
+
+ assertThat(errors).hasSize(1)
+ assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error ->
+
+ assertThat(error.keyName).isEqualTo(nestedKey)
+ assertThat(error.path).containsExactly(*key.split(".").toTypedArray(), nestedKey)
+ }
+ }
+ }
+
+ @Test
+ fun nested_property_without_schema_does_not_validate() {
+
+ val key = "a.b.c"
+
+ val nestedKey = "d"
+
+ val property: Validator = Configuration.Property.Definition.nestedObject(key)
+
+ val configuration = configObject(key to configObject(nestedKey to false)).toConfig()
+
+ assertThat(property.validate(configuration).isValid).isTrue()
+ }
+
+ @Test
+ fun valid_mapped_property() {
+
+ val key = "a"
+
+ val property: Validator = Configuration.Property.Definition.string(key).mapValid(::parseAddress)
+
+ val host = "localhost"
+ val port = 8080
+ val value = "$host:$port"
+
+ val configuration = configObject(key to value).toConfig()
+
+ assertThat(property.validate(configuration).isValid).isTrue()
+ }
+
+ @Test
+ fun invalid_mapped_property() {
+
+ val key = "a.b.c"
+
+ val property: Validator = Configuration.Property.Definition.string(key).mapValid(::parseAddress)
+
+ val host = "localhost"
+ val port = 8080
+ // No ":" separating the 2 parts.
+ val value = "$host$port"
+
+ val configuration = configObject(key to value).toConfig()
+
+ val result = property.validate(configuration)
+
+ assertThat(result.errors).satisfies { errors ->
+
+ assertThat(errors).hasSize(1)
+ assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.BadValue::class.java) { error ->
+
+ assertThat(error.keyName).isEqualTo(key.split(".").last())
+ assertThat(error.path).containsExactly(*key.split(".").toTypedArray())
+ }
+ }
+ }
+
+ private fun parseAddress(value: String): Valid {
+
+ return try {
+ val parts = value.split(":")
+ val host = parts[0].also { require(it.isNotBlank()) }
+ val port = parts[1].toInt().also { require(it > 0) }
+ valid(Address(host, port))
+ } catch (e: Exception) {
+ return invalid(Configuration.Validation.Error.BadValue.of("Value must be of format \"host(String):port(Int > 0)\" e.g., \"127.0.0.1:8080\""))
+ }
+ }
+}
\ No newline at end of file
diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SchemaTest.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SchemaTest.kt
new file mode 100644
index 0000000000..d468e68361
--- /dev/null
+++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SchemaTest.kt
@@ -0,0 +1,203 @@
+package net.corda.common.configuration.parsing.internal
+
+import com.typesafe.config.ConfigObject
+import com.typesafe.config.ConfigValueFactory
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+
+class SchemaTest {
+
+ @Test
+ fun validation_with_nested_properties() {
+
+ val prop1 = "prop1"
+ val prop1Value = "value1"
+
+ val prop2 = "prop2"
+ val prop2Value = 3L
+
+ val prop3 = "prop3"
+ val prop4 = "prop4"
+ val prop4Value = true
+ val prop5 = "prop5"
+ val prop5Value = -17.3
+ val prop3Value = configObject(prop4 to prop4Value, prop5 to prop5Value)
+
+ val configuration = configObject(prop1 to prop1Value, prop2 to prop2Value, prop3 to prop3Value).toConfig()
+ println(configuration.serialize())
+
+ val fooConfigSchema = Configuration.Schema.withProperties(name = "Foo") { setOf(boolean("prop4"), double("prop5")) }
+ val barConfigSchema = Configuration.Schema.withProperties(name = "Bar") { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema)) }
+
+ val result = barConfigSchema.validate(configuration)
+ println(barConfigSchema.description())
+
+ assertThat(result.isValid).isTrue()
+ }
+
+ @Test
+ fun validation_with_unknown_properties() {
+
+ val prop1 = "prop1"
+ val prop1Value = "value1"
+
+ val prop2 = "prop2"
+ val prop2Value = 3L
+
+ val prop3 = "prop3"
+ val prop4 = "prop4"
+ val prop4Value = true
+ val prop5 = "prop5"
+ val prop5Value = -17.3
+ // Here "prop6" is not known to the schema.
+ val prop3Value = configObject(prop4 to prop4Value, "prop6" to "value6", prop5 to prop5Value)
+
+ // Here "prop4" is not known to the schema.
+ val configuration = configObject(prop1 to prop1Value, prop2 to prop2Value, prop3 to prop3Value, "prop4" to "value4").toConfig()
+ println(configuration.serialize())
+
+ val fooConfigSchema = Configuration.Schema.withProperties { setOf(boolean("prop4"), double("prop5")) }
+ val barConfigSchema = Configuration.Schema.withProperties { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema)) }
+
+ val strictErrors = barConfigSchema.validate(configuration, Configuration.Validation.Options(strict = true)).errors
+
+ assertThat(strictErrors).hasSize(2)
+ assertThat(strictErrors.filter { error -> error.keyName == "prop4" }).hasSize(1)
+ assertThat(strictErrors.filter { error -> error.keyName == "prop6" }).hasSize(1)
+
+ val errors = barConfigSchema.validate(configuration, Configuration.Validation.Options(strict = false)).errors
+
+ assertThat(errors).isEmpty()
+
+ val errorsWithDefaultOptions = barConfigSchema.validate(configuration).errors
+
+ assertThat(errorsWithDefaultOptions).isEmpty()
+ }
+
+ @Test
+ fun validation_with_unknown_properties_non_strict() {
+
+ val prop1 = "prop1"
+ val prop1Value = "value1"
+
+ val prop2 = "prop2"
+ val prop2Value = 3L
+
+ val prop3 = "prop3"
+ val prop4 = "prop4"
+ val prop4Value = true
+ val prop5 = "prop5"
+ val prop5Value = -17.3
+ // Here "prop6" is not known to the schema, but it is not in strict mode.
+ val prop3Value = configObject(prop4 to prop4Value, "prop6" to "value6", prop5 to prop5Value)
+
+ // Here "prop4" is not known to the schema, but it is not in strict mode.
+ val configuration = configObject(prop1 to prop1Value, prop2 to prop2Value, prop3 to prop3Value, "prop4" to "value4").toConfig()
+ println(configuration.serialize())
+
+ val fooConfigSchema = Configuration.Schema.withProperties { setOf(boolean("prop4"), double("prop5")) }
+ val barConfigSchema = Configuration.Schema.withProperties { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema)) }
+
+ val result = barConfigSchema.validate(configuration)
+
+ assertThat(result.isValid).isTrue()
+ }
+
+ @Test
+ fun validation_with_wrong_nested_properties() {
+
+ val prop1 = "prop1"
+ val prop1Value = "value1"
+
+ val prop2 = "prop2"
+ // This value is wrong, should be an Int.
+ val prop2Value = false
+
+ val prop3 = "prop3"
+ val prop4 = "prop4"
+ // This value is wrong, should be a Boolean.
+ val prop4Value = 44444
+ val prop5 = "prop5"
+ val prop5Value = -17.3
+ val prop3Value = configObject(prop4 to prop4Value, prop5 to prop5Value)
+
+ val configuration = configObject(prop1 to prop1Value, prop2 to prop2Value, prop3 to prop3Value).toConfig()
+ println(configuration.serialize())
+
+ val fooConfigSchema = Configuration.Schema.withProperties { setOf(boolean("prop4"), double("prop5")) }
+ val barConfigSchema = Configuration.Schema.withProperties { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema)) }
+
+ val errors = barConfigSchema.validate(configuration).errors
+ errors.forEach(::println)
+
+ assertThat(errors).hasSize(2)
+ }
+
+ @Test
+ fun describe_with_nested_properties_does_not_show_sensitive_values() {
+
+ val prop1 = "prop1"
+ val prop1Value = "value1"
+
+ val prop2 = "prop2"
+ val prop2Value = 3L
+
+ val prop3 = "prop3"
+ val prop4 = "prop4"
+ val prop4Value = true
+ val prop5 = "prop5"
+ val prop5Value = "sensitive!"
+ val prop3Value = configObject(prop4 to prop4Value, prop5 to prop5Value)
+
+ val configuration = configObject(prop1 to prop1Value, prop2 to prop2Value, prop3 to prop3Value).toConfig()
+
+ val fooConfigSchema = Configuration.Schema.withProperties(name = "Foo") { setOf(boolean("prop4"), string("prop5", sensitive = true)) }
+ val barConfigSchema = Configuration.Schema.withProperties(name = "Bar") { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema)) }
+
+ val printedConfiguration = barConfigSchema.describe(configuration)
+
+ val description = printedConfiguration.serialize().also { println(it) }
+
+ val descriptionObj = (printedConfiguration as ConfigObject).toConfig()
+
+ assertThat(descriptionObj.getAnyRef("prop3.prop5")).isEqualTo(Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER)
+ assertThat(description).doesNotContain(prop5Value)
+ }
+
+ @Test
+ fun describe_with_nested_properties_list_does_not_show_sensitive_values() {
+
+ val prop1 = "prop1"
+ val prop1Value = "value1"
+
+ val prop2 = "prop2"
+ val prop2Value = 3L
+
+ val prop3 = "prop3"
+ val prop4 = "prop4"
+ val prop4Value = true
+ val prop5 = "prop5"
+ val prop5Value = "sensitive!"
+ val prop3Value = ConfigValueFactory.fromIterable(listOf(configObject(prop4 to prop4Value, prop5 to prop5Value), configObject(prop4 to prop4Value, prop5 to prop5Value)))
+
+ val configuration = configObject(prop1 to prop1Value, prop2 to prop2Value, prop3 to prop3Value).toConfig()
+
+ val fooConfigSchema = Configuration.Schema.withProperties(name = "Foo") { setOf(boolean("prop4"), string("prop5", sensitive = true)) }
+ val barConfigSchema = Configuration.Schema.withProperties(name = "Bar") { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema).list()) }
+
+ val printedConfiguration = barConfigSchema.describe(configuration)
+
+ val description = printedConfiguration.serialize().also { println(it) }
+
+ val descriptionObj = (printedConfiguration as ConfigObject).toConfig()
+
+ assertThat(descriptionObj.getObjectList("prop3")).satisfies { objects ->
+
+ objects.forEach { obj ->
+
+ assertThat(obj.toConfig().getString("prop5")).isEqualTo(Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER)
+ }
+ }
+ assertThat(description).doesNotContain(prop5Value)
+ }
+}
\ No newline at end of file
diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SpecificationTest.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SpecificationTest.kt
new file mode 100644
index 0000000000..a94489c99e
--- /dev/null
+++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SpecificationTest.kt
@@ -0,0 +1,113 @@
+package net.corda.common.configuration.parsing.internal
+
+import com.typesafe.config.Config
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+
+class SpecificationTest {
+
+ private object RpcSettingsSpec : Configuration.Specification("RpcSettings") {
+
+ private object AddressesSpec : Configuration.Specification("Addresses") {
+
+ val principal by string().mapValid(::parseAddress)
+ val admin by string().mapValid(::parseAddress)
+
+ override fun parseValid(configuration: Config) = valid(Addresses(configuration[principal], configuration[admin]))
+
+ private fun parseAddress(rawValue: String): Valid {
+
+ return Address.validFromRawValue(rawValue) { error -> Configuration.Validation.Error.BadValue.of(error) }
+ }
+ }
+
+ val useSsl by boolean()
+ val addresses by nested(AddressesSpec)
+
+ override fun parseValid(configuration: Config) = valid(RpcSettingsImpl(configuration[addresses], configuration[useSsl]))
+ }
+
+ @Test
+ fun parse() {
+
+ val useSslValue = true
+ val principalAddressValue = Address("localhost", 8080)
+ val adminAddressValue = Address("127.0.0.1", 8081)
+ val addressesValue = configObject("principal" to "${principalAddressValue.host}:${principalAddressValue.port}", "admin" to "${adminAddressValue.host}:${adminAddressValue.port}")
+ val configuration = configObject("useSsl" to useSslValue, "addresses" to addressesValue).toConfig()
+
+ val rpcSettings = RpcSettingsSpec.parse(configuration)
+
+ assertThat(rpcSettings.isValid).isTrue()
+ assertThat(rpcSettings.valueOrThrow()).satisfies { value ->
+
+ assertThat(value.useSsl).isEqualTo(useSslValue)
+ assertThat(value.addresses).satisfies { addresses ->
+
+ assertThat(addresses.principal).isEqualTo(principalAddressValue)
+ assertThat(addresses.admin).isEqualTo(adminAddressValue)
+ }
+ }
+ }
+
+ @Test
+ fun validate() {
+
+ val principalAddressValue = Address("localhost", 8080)
+ val adminAddressValue = Address("127.0.0.1", 8081)
+ val addressesValue = configObject("principal" to "${principalAddressValue.host}:${principalAddressValue.port}", "admin" to "${adminAddressValue.host}:${adminAddressValue.port}")
+ // Here "useSsl" shouldn't be `null`, hence causing the validation to fail.
+ val configuration = configObject("useSsl" to null, "addresses" to addressesValue).toConfig()
+
+ val rpcSettings = RpcSettingsSpec.parse(configuration)
+
+ assertThat(rpcSettings.errors).hasSize(1)
+ assertThat(rpcSettings.errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error ->
+
+ assertThat(error.path).containsExactly("useSsl")
+ }
+ }
+
+ @Test
+ fun validate_with_domain_specific_errors() {
+
+ val useSslValue = true
+ val principalAddressValue = Address("localhost", 8080)
+ val adminAddressValue = Address("127.0.0.1", 8081)
+ // Here, for the "principal" property, the value is incorrect, as the port value is unacceptable.
+ val addressesValue = configObject("principal" to "${principalAddressValue.host}:-10", "admin" to "${adminAddressValue.host}:${adminAddressValue.port}")
+ val configuration = configObject("useSsl" to useSslValue, "addresses" to addressesValue).toConfig()
+
+ val rpcSettings = RpcSettingsSpec.parse(configuration)
+
+ assertThat(rpcSettings.errors).hasSize(1)
+ assertThat(rpcSettings.errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.BadValue::class.java) { error ->
+
+ assertThat(error.path).containsExactly("addresses", "principal")
+ assertThat(error.keyName).isEqualTo("principal")
+ assertThat(error.typeName).isEqualTo(Address::class.java.simpleName)
+ }
+ }
+
+ @Test
+ fun chained_delegated_properties_are_not_added_multiple_times() {
+
+ val spec = object : Configuration.Specification?>("Test") {
+
+ @Suppress("unused")
+ val myProp by string().list().optional()
+
+ override fun parseValid(configuration: Config) = valid(configuration[myProp])
+ }
+
+ assertThat(spec.properties).hasSize(1)
+ }
+
+ private interface RpcSettings {
+
+ val addresses: Addresses
+ val useSsl: Boolean
+ }
+
+ private data class RpcSettingsImpl(override val addresses: Addresses, override val useSsl: Boolean) : RpcSettings
+}
\ No newline at end of file
diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/TestUtils.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/TestUtils.kt
new file mode 100644
index 0000000000..bf2c2f92bf
--- /dev/null
+++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/TestUtils.kt
@@ -0,0 +1,19 @@
+package net.corda.common.configuration.parsing.internal
+
+import com.typesafe.config.Config
+import net.corda.common.validation.internal.Validated
+
+internal val extractMissingVersion: Configuration.Value.Parser = extractVersion(null)
+
+internal fun extractVersion(value: Int?) = extractValidValue(value)
+
+internal fun extractPresentVersion(value: Int) = extractValidValue(value)
+
+internal fun extractValidValue(value: VALUE) = extractValue(Validated.valid(value))
+
+internal fun extractValueWithErrors(errors: Set) = extractValue(Validated.invalid(errors))
+
+internal fun