diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt index fd38af42bb..f9f2e0bde9 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt @@ -49,8 +49,10 @@ interface ConfigParser { const val CUSTOM_NODE_PROPERTIES_ROOT = "custom" // TODO Move other config parsing to use parseAs and remove this +// This is to enable constructs like: +// `val keyStorePassword: String by config` operator fun Config.getValue(receiver: Any, metadata: KProperty<*>): T { - return getValueInternal(metadata.name, metadata.returnType, UnknownConfigKeysPolicy.IGNORE::handle) + return getValueInternal(metadata.name, metadata.returnType, UnknownConfigKeysPolicy.IGNORE::handle, nestedPath = null, baseDirectory = null) } // Problems: @@ -59,7 +61,8 @@ operator fun Config.getValue(receiver: Any, metadata: KProperty<*>): T // - 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 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. -fun Config.parseAs(clazz: KClass, onUnknownKeys: ((Set, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle, nestedPath: String? = null): T { +fun Config.parseAs(clazz: KClass, onUnknownKeys: ((Set, 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. clazz.findAnnotation()?.let { return uncheckedCast(it.parser.createInstance().parse(this)) } @@ -85,7 +88,7 @@ fun Config.parseAs(clazz: KClass, onUnknownKeys: ((Set, log // Get the matching property for this parameter val property = clazz.memberProperties.first { it.name == param.name } val path = defaultToOldPath(property) - getValueInternal(path, param.type, onUnknownKeys, nestedPath) + getValueInternal(path, param.type, onUnknownKeys, nestedPath, baseDirectory) } try { return constructor.callBy(args) @@ -114,11 +117,11 @@ fun Config.toProperties(): Properties { { it.value.unwrapped().toString() }) } -private fun Config.getValueInternal(path: String, type: KType, onUnknownKeys: ((Set, logger: Logger) -> Unit), nestedPath: String? = null): T { - return uncheckedCast(if (type.arguments.isEmpty()) getSingleValue(path, type, onUnknownKeys, nestedPath) else getCollectionValue(path, type, onUnknownKeys, nestedPath)) +private fun Config.getValueInternal(path: String, type: KType, onUnknownKeys: ((Set, 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, logger: Logger) -> Unit, nestedPath: String? = null): Any? { +private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set, logger: Logger) -> Unit, nestedPath: String?, baseDirectory: Path?): Any? { if (type.isMarkedNullable && !hasPath(path)) return null val typeClass = type.jvmErasure return try { @@ -132,7 +135,10 @@ private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set Duration::class -> getDuration(path) Instant::class -> Instant.parse(getString(path)) NetworkHostAndPort::class -> NetworkHostAndPort.parse(getString(path)) - Path::class -> Paths.get(getString(path)) + Path::class -> { + val pathAsString = getString(path) + resolvePath(pathAsString, baseDirectory) + } URL::class -> URL(getString(path)) UUID::class -> UUID.fromString(getString(path)) X500Principal::class -> X500Principal(getString(path)) @@ -147,7 +153,7 @@ private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set else -> if (typeClass.java.isEnum) { parseEnum(typeClass.java, getString(path)) } else { - getConfig(path).parseAs(typeClass, onUnknownKeys, nestedPath?.let { "$it.$path" } ?: path) + getConfig(path).parseAs(typeClass, onUnknownKeys, nestedPath?.let { "$it.$path" } ?: path, baseDirectory = baseDirectory) } } } catch (e: ConfigException.Missing) { @@ -155,6 +161,16 @@ private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set } } +private fun resolvePath(pathAsString: String, baseDirectory: Path?): Path { + val path = Paths.get(pathAsString) + 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. + baseDirectory.resolve(path) + } else { + path + } +} + private fun ConfigException.Missing.relative(path: String, nestedPath: String?): ConfigException.Missing { return when { nestedPath != null -> throw ConfigException.Missing("$nestedPath.$path", this) @@ -162,7 +178,7 @@ private fun ConfigException.Missing.relative(path: String, nestedPath: String?): } } -private fun Config.getCollectionValue(path: String, type: KType, onUnknownKeys: (Set, logger: Logger) -> Unit, nestedPath: String? = null): Collection { +private fun Config.getCollectionValue(path: String, type: KType, onUnknownKeys: (Set, logger: Logger) -> Unit, nestedPath: String?, baseDirectory: Path?): Collection { val typeClass = type.jvmErasure 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") @@ -179,7 +195,7 @@ private fun Config.getCollectionValue(path: String, type: KType, onUnknownKeys: LocalDate::class -> getStringList(path).map(LocalDate::parse) Instant::class -> getStringList(path).map(Instant::parse) NetworkHostAndPort::class -> getStringList(path).map(NetworkHostAndPort.Companion::parse) - Path::class -> getStringList(path).map { Paths.get(it) } + Path::class -> getStringList(path).map { resolvePath(it, baseDirectory) } URL::class -> getStringList(path).map(::URL) X500Principal::class -> getStringList(path).map(::X500Principal) UUID::class -> getStringList(path).map { UUID.fromString(it) } @@ -188,7 +204,7 @@ private fun Config.getCollectionValue(path: String, type: KType, onUnknownKeys: else -> if (elementClass.java.isEnum) { getStringList(path).map { parseEnum(elementClass.java, it) } } else { - getConfigList(path).map { it.parseAs(elementClass, onUnknownKeys) } + getConfigList(path).map { it.parseAs(elementClass, onUnknownKeys, baseDirectory = baseDirectory) } } } } catch (e: ConfigException.Missing) { diff --git a/node/src/main/kotlin/net/corda/node/services/config/schema/v1/V1NodeConfigurationSpec.kt b/node/src/main/kotlin/net/corda/node/services/config/schema/v1/V1NodeConfigurationSpec.kt index 0845879268..9eafb50bb2 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/schema/v1/V1NodeConfigurationSpec.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/schema/v1/V1NodeConfigurationSpec.kt @@ -69,10 +69,11 @@ internal object V1NodeConfigurationSpec : Configuration.Specification(NodeConfigurationImpl( - baseDirectory = configuration[baseDirectory], + baseDirectory = baseDirectoryPath, myLegalName = configuration[myLegalName], emailAddress = configuration[emailAddress], p2pAddress = configuration[p2pAddress], @@ -116,7 +117,7 @@ internal object V1NodeConfigurationSpec : Configuration.Specification