[CORDA-926]: Parsing NodeConfiguration will now fail if unknown properties are present. (#2484)

This commit is contained in:
Michele Sollecito
2018-03-01 14:57:36 +00:00
committed by GitHub
parent 754b87d547
commit b580a2ac30
26 changed files with 278 additions and 86 deletions

View File

@ -29,6 +29,8 @@ import kotlin.reflect.jvm.jvmErasure
@Target(AnnotationTarget.PROPERTY)
annotation class OldConfig(val value: String)
const val CUSTOM_NODE_PROPERTIES_ROOT = "custom"
// TODO Move other config parsing to use parseAs and remove this
operator fun <T : Any> Config.getValue(receiver: Any, metadata: KProperty<*>): T {
return getValueInternal(metadata.name, metadata.returnType)
@ -37,9 +39,24 @@ operator fun <T : Any> Config.getValue(receiver: Any, metadata: KProperty<*>): T
fun <T : Any> Config.parseAs(clazz: KClass<T>): T {
require(clazz.isData) { "Only Kotlin data classes can be parsed. Offending: ${clazz.qualifiedName}" }
val constructor = clazz.primaryConstructor!!
val args = constructor.parameters
.filterNot { it.isOptional && !hasPath(it.name!!) }
.associateBy({ it }) { param ->
val parameters = constructor.parameters
val parameterNames = parameters.flatMap { param ->
mutableSetOf<String>().apply {
param.name?.let(this::add)
clazz.memberProperties.singleOrNull { it.name == param.name }?.let { matchingProperty ->
matchingProperty.annotations.filterIsInstance<OldConfig>().map { it.value }.forEach { this.add(it) }
}
}
}
val unknownConfigurationKeys = this.entrySet()
.mapNotNull { it.key.split(".").firstOrNull() }
.filterNot { it == CUSTOM_NODE_PROPERTIES_ROOT }
.filterNot(parameterNames::contains)
.toSortedSet()
if (unknownConfigurationKeys.isNotEmpty()) {
throw UnknownConfigurationKeysException.of(unknownConfigurationKeys)
}
val args = parameters.filterNot { it.isOptional && !hasPath(it.name!!) }.associateBy({ it }) { param ->
// Get the matching property for this parameter
val property = clazz.memberProperties.first { it.name == param.name }
val path = defaultToOldPath(property)
@ -48,6 +65,20 @@ fun <T : Any> Config.parseAs(clazz: KClass<T>): T {
return constructor.callBy(args)
}
class UnknownConfigurationKeysException private constructor(val unknownKeys: Set<String>) : IllegalArgumentException(message(unknownKeys)) {
init {
require(unknownKeys.isNotEmpty()) { "Absence of unknown keys should not raise UnknownConfigurationKeysException." }
}
companion object {
fun of(offendingKeys: Set<String>): UnknownConfigurationKeysException = UnknownConfigurationKeysException(offendingKeys)
private fun message(offendingKeys: Set<String>) = "Unknown configuration keys: ${offendingKeys.joinToString(", ", "[", "]")}."
}
}
inline fun <reified T : Any> Config.parseAs(): T = parseAs(T::class)
fun Config.toProperties(): Properties {

View File

@ -90,15 +90,17 @@ class NetworkBootstrapper {
}
private fun generateDirectoriesIfNeeded(directory: Path) {
val confFiles = directory.list { it.filter { it.toString().endsWith(".conf") }.toList() }
val confFiles = directory.list { it.filter { it.toString().endsWith("_node.conf") }.toList() }
val webServerConfFiles = directory.list { it.filter { it.toString().endsWith("_web-server.conf") }.toList() }
if (confFiles.isEmpty()) return
println("Node config files found in the root directory - generating node directories")
val cordaJar = extractCordaJarTo(directory)
for (confFile in confFiles) {
val nodeName = confFile.fileName.toString().removeSuffix(".conf")
val nodeName = confFile.fileName.toString().removeSuffix("_node.conf")
println("Generating directory for $nodeName")
val nodeDir = (directory / nodeName).createDirectories()
confFile.moveTo(nodeDir / "node.conf", StandardCopyOption.REPLACE_EXISTING)
webServerConfFiles.firstOrNull { directory.relativize(it).toString().removeSuffix("_web-server.conf") == nodeName }?.moveTo(nodeDir / "web-server.conf", StandardCopyOption.REPLACE_EXISTING)
Files.copy(cordaJar, (nodeDir / "corda.jar"), StandardCopyOption.REPLACE_EXISTING)
}
Files.delete(cordaJar)

View File

@ -200,6 +200,21 @@ class ConfigParsingTest {
assertThat(DataWithCompanion(3).toConfig()).isEqualTo(config("value" to 3))
}
@Test
fun `unknown configuration keys raise exception`() {
// intentional typo here, parsing should throw rather than sneakily return default value
val knownKey = "mandatory"
val unknownKey = "optioal"
val configuration = config(knownKey to "hello", unknownKey to "world")
assertThatThrownBy { configuration.parseAs<TypedConfiguration>() }.isInstanceOfSatisfying(UnknownConfigurationKeysException::class.java) { exception ->
assertThat(exception.unknownKeys).contains(unknownKey)
assertThat(exception.unknownKeys).doesNotContain(knownKey)
}
}
private inline fun <reified S : SingleData<V>, reified L : ListData<V>, V : Any> testPropertyType(
value1: V,
value2: V,
@ -243,6 +258,7 @@ class ConfigParsingTest {
val values: List<T>
}
data class TypedConfiguration(private val mandatory: String, private val optional: String = "optional")
data class StringData(override val value: String) : SingleData<String>
data class StringListData(override val values: List<String>) : ListData<String>
data class StringSetData(val values: Set<String>)