ENT-4494 Harmonize configuration classes.

This commit is contained in:
Christian Sailer 2020-03-10 14:21:40 +00:00
parent 68bb7a0e7b
commit 53d92771bb
3 changed files with 87 additions and 20 deletions

View File

@ -60,6 +60,7 @@ interface CertificateStore : Iterable<Pair<String, X509Certificate>> {
forEach { (alias, certificate) -> action.invoke(alias, certificate) } forEach { (alias, certificate) -> action.invoke(alias, certificate) }
} }
fun aliases(): List<String> = value.internal.aliases().toList()
/** /**
* @throws IllegalArgumentException if no certificate for the alias is found, or if the certificate is not an [X509Certificate]. * @throws IllegalArgumentException if no certificate for the alias is found, or if the certificate is not an [X509Certificate].
*/ */

View File

@ -1,4 +1,5 @@
@file:JvmName("ConfigUtilities") @file:JvmName("ConfigUtilities")
@file:Suppress("LongParameterList")
package net.corda.nodeapi.internal.config package net.corda.nodeapi.internal.config
@ -52,21 +53,35 @@ const val CUSTOM_NODE_PROPERTIES_ROOT = "custom"
// This is to enable constructs like: // This is to enable constructs like:
// `val keyStorePassword: String by config` // `val keyStorePassword: String by config`
operator fun <T : Any> Config.getValue(receiver: Any, metadata: KProperty<*>): T { operator fun <T : Any> Config.getValue(receiver: Any, metadata: KProperty<*>): T {
return getValueInternal(metadata.name, metadata.returnType, UnknownConfigKeysPolicy.IGNORE::handle, nestedPath = null, baseDirectory = null) return getValueInternal(
metadata.name,
metadata.returnType,
UnknownConfigKeysPolicy.IGNORE::handle,
nestedPath = null,
baseDirectory = null
)
} }
// Problems: // Problems:
// - Forces you to have a primary constructor with all fields of name and type matching the configuration file structure. // - Forces you to have a primary constructor with all fields of name and type matching the configuration file structure.
// - Encourages weak bean-like types. // - Encourages weak bean-like types.
// - Cannot support a many-to-one relationship between configuration file structures and configuration domain type. This is essential for versioning of the configuration files. // - Cannot support a many-to-one relationship between configuration file structures and configuration domain type. This is essential for
// versioning of the configuration files.
// - It's complicated and based on reflection, meaning problems with it are typically found at runtime. // - It's complicated and based on reflection, meaning problems with it are typically found at runtime.
// - It doesn't support validation errors in a structured way. If something goes wrong, it throws exceptions, which doesn't support good usability practices like displaying all the errors at once. // - It doesn't support validation errors in a structured way. If something goes wrong, it throws exceptions, which doesn't support good
fun <T : Any> Config.parseAs(clazz: KClass<T>, onUnknownKeys: ((Set<String>, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle, // usability practices like displaying all the errors at once.
nestedPath: String? = null, baseDirectory: Path? = null): T { fun <T : Any> Config.parseAs(
clazz: KClass<T>,
onUnknownKeys: ((Set<String>, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle,
nestedPath: String? = null,
baseDirectory: Path? = null
): T {
// Use custom parser if provided, instead of treating the object as data class. // Use custom parser if provided, instead of treating the object as data class.
clazz.findAnnotation<CustomConfigParser>()?.let { return uncheckedCast(it.parser.createInstance().parse(this)) } clazz.findAnnotation<CustomConfigParser>()?.let { return uncheckedCast(it.parser.createInstance().parse(this)) }
require(clazz.isData) { "Only Kotlin data classes or class annotated with CustomConfigParser can be parsed. Offending: ${clazz.qualifiedName}" } require(clazz.isData) {
"Only Kotlin data classes or class annotated with CustomConfigParser can be parsed. Offending: ${clazz.qualifiedName}"
}
val constructor = clazz.primaryConstructor!! val constructor = clazz.primaryConstructor!!
val parameters = constructor.parameters val parameters = constructor.parameters
val parameterNames = parameters.flatMap { param -> val parameterNames = parameters.flatMap { param ->
@ -104,24 +119,46 @@ class UnknownConfigurationKeysException private constructor(val unknownKeys: Set
companion object { companion object {
fun of(offendingKeys: Set<String>): UnknownConfigurationKeysException = UnknownConfigurationKeysException(offendingKeys) fun of(offendingKeys: Set<String>): UnknownConfigurationKeysException = UnknownConfigurationKeysException(offendingKeys)
private fun message(offendingKeys: Set<String>) = "Unknown configuration keys: ${offendingKeys.joinToString(", ", "[", "]")}." private fun message(offendingKeys: Set<String>) = "Unknown configuration keys: " +
"${offendingKeys.joinToString(", ", "[", "]")}."
} }
} }
inline fun <reified T : Any> Config.parseAs(noinline onUnknownKeys: ((Set<String>, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle): T = parseAs(T::class, onUnknownKeys) inline fun <reified T : Any> Config.parseAs(
noinline onUnknownKeys: ((Set<String>, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle
): T = parseAs(T::class, onUnknownKeys)
fun Config.toProperties(): Properties { fun Config.toProperties(): Properties {
return entrySet().associateByTo( return entrySet().associateByTo(
Properties(), Properties(),
{ ConfigUtil.splitPath(it.key).joinToString(".") }, { ConfigUtil.splitPath(it.key).joinToString(".") },
{ it.value.unwrapped().toString() }) { it.value.unwrapped() })
} }
private fun <T : Any> Config.getValueInternal(path: String, type: KType, onUnknownKeys: ((Set<String>, logger: Logger) -> Unit), nestedPath: String?, baseDirectory: Path?): T { private fun <T : Any> Config.getValueInternal(
return uncheckedCast(if (type.arguments.isEmpty()) getSingleValue(path, type, onUnknownKeys, nestedPath, baseDirectory) else getCollectionValue(path, type, onUnknownKeys, nestedPath, baseDirectory)) path: String,
type: KType,
onUnknownKeys: ((Set<String>, logger: Logger) -> Unit),
nestedPath: String?,
baseDirectory: Path?
): T {
return uncheckedCast(
if (type.arguments.isEmpty()) {
getSingleValue(path, type, onUnknownKeys, nestedPath, baseDirectory)
} else {
getCollectionValue(path, type, onUnknownKeys, nestedPath, baseDirectory)
}
)
} }
private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set<String>, logger: Logger) -> Unit, nestedPath: String?, baseDirectory: Path?): Any? { @Suppress("ComplexMethod")
private fun Config.getSingleValue(
path: String,
type: KType,
onUnknownKeys: (Set<String>, logger: Logger) -> Unit,
nestedPath: String?,
baseDirectory: Path?
): Any? {
if (type.isMarkedNullable && !hasPath(path)) return null if (type.isMarkedNullable && !hasPath(path)) return null
val typeClass = type.jvmErasure val typeClass = type.jvmErasure
return try { return try {
@ -153,7 +190,12 @@ private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set
else -> if (typeClass.java.isEnum) { else -> if (typeClass.java.isEnum) {
parseEnum(typeClass.java, getString(path)) parseEnum(typeClass.java, getString(path))
} else { } else {
getConfig(path).parseAs(typeClass, onUnknownKeys, nestedPath?.let { "$it.$path" } ?: path, baseDirectory = baseDirectory) getConfig(path).parseAs(
typeClass,
onUnknownKeys,
nestedPath?.let { "$it.$path" } ?: path,
baseDirectory = baseDirectory
)
} }
} }
} catch (e: ConfigException.Missing) { } catch (e: ConfigException.Missing) {
@ -164,7 +206,8 @@ private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set
private fun resolvePath(pathAsString: String, baseDirectory: Path?): Path { private fun resolvePath(pathAsString: String, baseDirectory: Path?): Path {
val path = Paths.get(pathAsString) val path = Paths.get(pathAsString)
return if (baseDirectory != null) { return if (baseDirectory != null) {
// if baseDirectory been specified try resolving path against it. Note if `pathFromConfig` is an absolute path - this instruction has no effect. // if baseDirectory been specified try resolving path against it. Note if `pathFromConfig` is an absolute path - this instruction
// has no effect.
baseDirectory.resolve(path) baseDirectory.resolve(path)
} else { } else {
path path
@ -178,10 +221,18 @@ private fun ConfigException.Missing.relative(path: String, nestedPath: String?):
} }
} }
private fun Config.getCollectionValue(path: String, type: KType, onUnknownKeys: (Set<String>, logger: Logger) -> Unit, nestedPath: String?, baseDirectory: Path?): Collection<Any> { @Suppress("ComplexMethod")
private fun Config.getCollectionValue(
path: String,
type: KType,
onUnknownKeys: (Set<String>, logger: Logger) -> Unit,
nestedPath: String?,
baseDirectory: Path?
): Collection<Any> {
val typeClass = type.jvmErasure val typeClass = type.jvmErasure
require(typeClass == List::class || typeClass == Set::class) { "$typeClass is not supported" } require(typeClass == List::class || typeClass == Set::class) { "$typeClass is not supported" }
val elementClass = type.arguments[0].type?.jvmErasure ?: throw IllegalArgumentException("Cannot work with star projection: $type") val elementClass = type.arguments[0].type?.jvmErasure
?: throw IllegalArgumentException("Cannot work with star projection: $type")
if (!hasPath(path)) { if (!hasPath(path)) {
return if (typeClass == List::class) emptyList() else emptySet() return if (typeClass == List::class) emptyList() else emptySet()
} }
@ -240,7 +291,13 @@ private fun <T : Enum<T>> enumBridge(clazz: Class<T>, name: String): T {
*/ */
fun Any.toConfig(): Config = ConfigValueFactory.fromMap(toConfigMap()).toConfig() fun Any.toConfig(): Config = ConfigValueFactory.fromMap(toConfigMap()).toConfig()
fun Any?.toConfigValue(): ConfigValue = if (this is ConfigValue) this else if (this != null) ConfigValueFactory.fromAnyRef(convertValue(this)) else ConfigValueFactory.fromAnyRef(null) fun Any?.toConfigValue(): ConfigValue = if (this is ConfigValue) {
this
} else if (this != null) {
ConfigValueFactory.fromAnyRef(convertValue(this))
} else {
ConfigValueFactory.fromAnyRef(null)
}
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
// Reflect over the fields of the receiver and generate a value Map that can use to create Config object. // Reflect over the fields of the receiver and generate a value Map that can use to create Config object.
@ -253,7 +310,8 @@ private fun Any.toConfigMap(): Map<String, Any> {
val configValue = if (value is String || value is Boolean || value is Number) { val configValue = if (value is String || value is Boolean || value is Number) {
// These types are supported by Config as use as is // These types are supported by Config as use as is
value value
} else if (value is Temporal || value is NetworkHostAndPort || value is CordaX500Name || value is Path || value is URL || value is UUID || value is X500Principal) { } else if (value is Temporal || value is NetworkHostAndPort || value is CordaX500Name ||
value is Path || value is URL || value is UUID || value is X500Principal) {
// These types make sense to be represented as Strings and the exact inverse parsing function for use in parseAs // These types make sense to be represented as Strings and the exact inverse parsing function for use in parseAs
value.toString() value.toString()
} else if (value is Enum<*>) { } else if (value is Enum<*>) {
@ -278,7 +336,8 @@ private fun convertValue(value: Any): Any {
return if (value is String || value is Boolean || value is Number) { return if (value is String || value is Boolean || value is Number) {
// These types are supported by Config as use as is // These types are supported by Config as use as is
value value
} else if (value is Temporal || value is NetworkHostAndPort || value is CordaX500Name || value is Path || value is URL || value is UUID || value is X500Principal) { } else if (value is Temporal || value is NetworkHostAndPort || value is CordaX500Name ||
value is Path || value is URL || value is UUID || value is X500Principal) {
// These types make sense to be represented as Strings and the exact inverse parsing function for use in parseAs // These types make sense to be represented as Strings and the exact inverse parsing function for use in parseAs
value.toString() value.toString()
} else if (value is Enum<*>) { } else if (value is Enum<*>) {

View File

@ -4,6 +4,7 @@ interface SslConfiguration {
val keyStore: FileBasedCertificateStoreSupplier? val keyStore: FileBasedCertificateStoreSupplier?
val trustStore: FileBasedCertificateStoreSupplier? val trustStore: FileBasedCertificateStoreSupplier?
val useOpenSsl: Boolean
companion object { companion object {
@ -19,4 +20,10 @@ interface MutualSslConfiguration : SslConfiguration {
override val trustStore: FileBasedCertificateStoreSupplier override val trustStore: FileBasedCertificateStoreSupplier
} }
private class MutualSslOptions(override val keyStore: FileBasedCertificateStoreSupplier, override val trustStore: FileBasedCertificateStoreSupplier) : MutualSslConfiguration private class MutualSslOptions(override val keyStore: FileBasedCertificateStoreSupplier,
override val trustStore: FileBasedCertificateStoreSupplier) : MutualSslConfiguration {
override val useOpenSsl: Boolean = false
}
const val DEFAULT_SSL_HANDSHAKE_TIMEOUT_MILLIS = 60000L // Set at least 3 times higher than sun.security.provider.certpath.URICertStore.DEFAULT_CRL_CONNECT_TIMEOUT which is 15 sec