Converted FullNodeConfiguration into a data class and added ability to parse Configs into data classes

This commit is contained in:
Shams Asari
2017-04-07 11:23:25 +01:00
parent 868a490bba
commit c17fe29a62
32 changed files with 544 additions and 301 deletions

View File

@ -12,6 +12,7 @@ import net.corda.core.flows.FlowException
import net.corda.core.serialization.*
import net.corda.core.toFuture
import net.corda.core.toObservable
import net.corda.nodeapi.config.OldConfig
import org.apache.commons.fileupload.MultipartStream
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@ -24,7 +25,11 @@ val rpcLog: Logger by lazy { LoggerFactory.getLogger("net.corda.rpc") }
/** Used in the RPC wire protocol to wrap an observation with the handle of the observable it's intended for. */
data class MarshalledObservation(val forHandle: Int, val what: Notification<*>)
data class User(val username: String, val password: String, val permissions: Set<String>) {
data class User(
@OldConfig("user")
val username: String,
val password: String,
val permissions: Set<String>) {
override fun toString(): String = "${javaClass.simpleName}($username, permissions=$permissions)"
}

View File

@ -2,73 +2,125 @@ package net.corda.nodeapi.config
import com.google.common.net.HostAndPort
import com.typesafe.config.Config
import com.typesafe.config.ConfigUtil
import net.corda.core.noneOrSingle
import org.slf4j.LoggerFactory
import java.net.Proxy
import java.net.URL
import java.nio.file.Path
import java.nio.file.Paths
import java.time.Instant
import java.time.LocalDate
import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.jvm.javaType
import kotlin.reflect.KType
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.jvmErasure
private fun <T : Enum<T>> enumBridge(clazz: Class<T>, enumValueString: String): T {
return java.lang.Enum.valueOf(clazz, enumValueString)
}
private class DummyEnum : Enum<DummyEnum>("", 0)
@Target(AnnotationTarget.PROPERTY)
annotation class OldConfig(val value: String)
@Suppress("UNCHECKED_CAST", "PLATFORM_CLASS_MAPPED_TO_KOTLIN")
// TODO Move other config parsing to use parseAs and remove this
operator fun <T> Config.getValue(receiver: Any, metadata: KProperty<*>): T {
if (metadata.returnType.isMarkedNullable && !hasPath(metadata.name)) {
return null as T
}
val returnType = metadata.returnType.javaType
return when (metadata.returnType.javaType) {
String::class.java -> getString(metadata.name) as T
Int::class.java -> getInt(metadata.name) as T
Integer::class.java -> getInt(metadata.name) as T
Long::class.java -> getLong(metadata.name) as T
Double::class.java -> getDouble(metadata.name) as T
Boolean::class.java -> getBoolean(metadata.name) as T
LocalDate::class.java -> LocalDate.parse(getString(metadata.name)) as T
Instant::class.java -> Instant.parse(getString(metadata.name)) as T
HostAndPort::class.java -> HostAndPort.fromString(getString(metadata.name)) as T
Path::class.java -> Paths.get(getString(metadata.name)) as T
URL::class.java -> URL(getString(metadata.name)) as T
Properties::class.java -> getProperties(metadata.name) as T
else -> {
if (returnType is Class<*> && Enum::class.java.isAssignableFrom(returnType)) {
return enumBridge(returnType as Class<DummyEnum>, getString(metadata.name)) as T
return getValueInternal(metadata.name, metadata.returnType)
}
fun <T : Any> Config.parseAs(clazz: KClass<T>): T {
require(clazz.isData) { "Only Kotlin data classes can be parsed" }
val constructor = clazz.primaryConstructor!!
val args = constructor.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)
getValueInternal<Any>(path, param.type)
}
throw IllegalArgumentException("Unsupported type ${metadata.returnType}")
return constructor.callBy(args)
}
inline fun <reified T : Any> Config.parseAs(): T = parseAs(T::class)
fun Config.toProperties(): Properties {
return entrySet().associateByTo(
Properties(),
{ ConfigUtil.splitPath(it.key).joinToString(".") },
{ it.value.unwrapped().toString() })
}
@Suppress("UNCHECKED_CAST")
private fun <T> Config.getValueInternal(path: String, type: KType): T {
return (if (type.arguments.isEmpty()) getSingleValue(path, type) else getCollectionValue(path, type)) as T
}
private fun Config.getSingleValue(path: String, type: KType): Any? {
if (type.isMarkedNullable && !hasPath(path)) return null
val typeClass = type.jvmErasure
return when (typeClass) {
String::class -> getString(path)
Int::class -> getInt(path)
Long::class -> getLong(path)
Double::class -> getDouble(path)
Boolean::class -> getBoolean(path)
LocalDate::class -> LocalDate.parse(getString(path))
Instant::class -> Instant.parse(getString(path))
HostAndPort::class -> HostAndPort.fromString(getString(path))
Path::class -> Paths.get(getString(path))
URL::class -> URL(getString(path))
Properties::class -> getConfig(path).toProperties()
else -> if (typeClass.java.isEnum) {
parseEnum(typeClass.java, getString(path))
} else {
getConfig(path).parseAs(typeClass)
}
}
}
/**
* Helper class for optional configurations
*/
class OptionalConfig<out T>(val conf: Config, val lambda: () -> T) {
operator fun getValue(receiver: Any, metadata: KProperty<*>): T {
return if (conf.hasPath(metadata.name)) conf.getValue(receiver, metadata) else lambda()
private fun Config.getCollectionValue(path: String, type: KType): Collection<Any> {
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")
if (!hasPath(path)) {
return if (typeClass == List::class) emptyList() else emptySet()
}
val values: List<Any> = when (elementClass) {
String::class -> getStringList(path)
Int::class -> getIntList(path)
Long::class -> getLongList(path)
Double::class -> getDoubleList(path)
Boolean::class -> getBooleanList(path)
LocalDate::class -> getStringList(path).map(LocalDate::parse)
Instant::class -> getStringList(path).map(Instant::parse)
HostAndPort::class -> getStringList(path).map(HostAndPort::fromString)
Path::class -> getStringList(path).map { Paths.get(it) }
URL::class -> getStringList(path).map(::URL)
Properties::class -> getConfigList(path).map(Config::toProperties)
else -> if (elementClass.java.isEnum) {
getStringList(path).map { parseEnum(elementClass.java, it) }
} else {
getConfigList(path).map { it.parseAs(elementClass) }
}
}
return if (typeClass == Set::class) values.toSet() else values
}
fun <T> Config.getOrElse(lambda: () -> T): OptionalConfig<T> = OptionalConfig(this, lambda)
fun Config.getProperties(path: String): Properties {
val obj = this.getObject(path)
val props = Properties()
for ((property, objectValue) in obj.entries) {
props.setProperty(property, objectValue.unwrapped().toString())
private fun Config.defaultToOldPath(property: KProperty<*>): String {
if (!hasPath(property.name)) {
val oldConfig = property.annotations.filterIsInstance<OldConfig>().noneOrSingle()
if (oldConfig != null && hasPath(oldConfig.value)) {
logger.warn("Config key ${oldConfig.value} has been deprecated and will be removed in a future release. " +
"Use ${property.name} instead")
return oldConfig.value
}
}
return props
return property.name
}
@Suppress("UNCHECKED_CAST")
inline fun <reified T : Any> Config.getListOrElse(path: String, default: Config.() -> List<T>): List<T> {
return if (hasPath(path)) {
(if (T::class == String::class) getStringList(path) else getConfigList(path)) as List<T>
} else {
this.default()
}
}
private fun parseEnum(enumType: Class<*>, name: String): Enum<*> = enumBridge(enumType as Class<Proxy.Type>, name) // Any enum will do
private fun <T : Enum<T>> enumBridge(clazz: Class<T>, name: String): T = java.lang.Enum.valueOf(clazz, name)
private val logger = LoggerFactory.getLogger("net.corda.nodeapi.config")

View File

@ -0,0 +1,238 @@
package net.corda.nodeapi.config
import com.google.common.net.HostAndPort
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory.empty
import com.typesafe.config.ConfigRenderOptions.defaults
import com.typesafe.config.ConfigValueFactory
import net.corda.core.div
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import java.net.URL
import java.nio.file.Path
import java.nio.file.Paths
import java.time.Instant
import java.time.LocalDate
import java.util.*
import kotlin.reflect.full.primaryConstructor
class ConfigParsingTest {
@Test
fun `String`() {
testPropertyType<StringData, StringListData, String>("hello world!", "bye")
}
@Test
fun `Int`() {
testPropertyType<IntData, IntListData, Int>(1, 2)
}
@Test
fun `Long`() {
testPropertyType<LongData, LongListData, Long>(Long.MAX_VALUE, Long.MIN_VALUE)
}
@Test
fun `Double`() {
testPropertyType<DoubleData, DoubleListData, Double>(1.2, 3.4)
}
@Test
fun `Boolean`() {
testPropertyType<BooleanData, BooleanListData, Boolean>(true, false)
}
@Test
fun `Enum`() {
testPropertyType<EnumData, EnumListData, TestEnum>(TestEnum.Value2, TestEnum.Value1, valuesToString = true)
}
@Test
fun `LocalDate`() {
testPropertyType<LocalDateData, LocalDateListData, LocalDate>(LocalDate.now(), LocalDate.now().plusDays(1), valuesToString = true)
}
@Test
fun `Instant`() {
testPropertyType<InstantData, InstantListData, Instant>(Instant.now(), Instant.now().plusMillis(100), valuesToString = true)
}
@Test
fun `HostAndPort`() {
testPropertyType<HostAndPortData, HostAndPortListData, HostAndPort>(
HostAndPort.fromParts("localhost", 2223),
HostAndPort.fromParts("localhost", 2225),
valuesToString = true)
}
@Test
fun `Path`() {
val path = Paths.get("tmp") / "test"
testPropertyType<PathData, PathListData, Path>(path, path / "file", valuesToString = true)
}
@Test
fun `URL`() {
testPropertyType<URLData, URLListData, URL>(URL("http://localhost:1234"), URL("http://localhost:1235"), valuesToString = true)
}
@Test
fun `flat Properties`() {
val config = config("value" to mapOf("key" to "prop"))
assertThat(config.parseAs<PropertiesData>().value).isEqualTo(Properties().apply { this["key"] = "prop" })
}
@Test
fun `Properties key with dot`() {
val config = config("value" to mapOf("key.key2" to "prop"))
assertThat(config.parseAs<PropertiesData>().value).isEqualTo(Properties().apply { this["key.key2"] = "prop" })
}
@Test
fun `nested Properties`() {
val config = config("value" to mapOf("first" to mapOf("second" to "prop")))
assertThat(config.parseAs<PropertiesData>().value).isEqualTo(Properties().apply { this["first.second"] = "prop" })
}
@Test
fun `List of Properties`() {
val config = config("values" to listOf(emptyMap(), mapOf("key" to "prop")))
assertThat(config.parseAs<PropertiesListData>().values).containsExactly(
Properties(),
Properties().apply { this["key"] = "prop" })
}
@Test
fun `Set`() {
val config = config("values" to listOf("a", "a", "b"))
assertThat(config.parseAs<StringSetData>().values).containsOnly("a", "b")
assertThat(empty().parseAs<StringSetData>().values).isEmpty()
}
@Test
fun `multi property data class`() {
val data = config(
"b" to true,
"i" to 123,
"l" to listOf("a", "b"))
.parseAs<MultiPropertyData>()
assertThat(data.i).isEqualTo(123)
assertThat(data.b).isTrue()
assertThat(data.l).containsExactly("a", "b")
}
@Test
fun `nested data classes`() {
val config = config(
"first" to mapOf(
"value" to "nested"))
val data = NestedData(StringData("nested"))
assertThat(config.parseAs<NestedData>()).isEqualTo(data)
}
@Test
fun `List of data classes`() {
val config = config(
"list" to listOf(
mapOf("value" to "1"),
mapOf("value" to "2")))
val data = DataListData(listOf(StringData("1"), StringData("2")))
assertThat(config.parseAs<DataListData>()).isEqualTo(data)
}
@Test
fun `default value property`() {
assertThat(config("a" to 3).parseAs<DefaultData>()).isEqualTo(DefaultData(3, 2))
assertThat(config("a" to 3, "defaultOfTwo" to 3).parseAs<DefaultData>()).isEqualTo(DefaultData(3, 3))
}
@Test
fun `nullable property`() {
assertThat(empty().parseAs<NullableData>().nullable).isNull()
assertThat(config("nullable" to null).parseAs<NullableData>().nullable).isNull()
assertThat(config("nullable" to "not null").parseAs<NullableData>().nullable).isEqualTo("not null")
}
@Test
fun `old config property`() {
assertThat(config("oldValue" to "old").parseAs<OldData>().newValue).isEqualTo("old")
assertThat(config("newValue" to "new").parseAs<OldData>().newValue).isEqualTo("new")
}
private inline fun <reified S : SingleData<V>, reified L : ListData<V>, V : Any> testPropertyType(
value1: V,
value2: V,
valuesToString: Boolean = false) {
testSingleProperty<S, V>(value1, valuesToString)
testListProperty<L, V>(value1, value2, valuesToString)
}
private inline fun <reified T : SingleData<V>, V : Any> testSingleProperty(value: V, valueToString: Boolean) {
val constructor = T::class.primaryConstructor!!
val config = config("value" to if (valueToString) value.toString() else value)
val data = constructor.call(value)
assertThat(config.parseAs<T>().value).isEqualTo(data.value)
}
private inline fun <reified T : ListData<V>, V : Any> testListProperty(value1: V, value2: V, valuesToString: Boolean) {
val rawValues = listOf(value1, value2)
val configValues = if (valuesToString) rawValues.map(Any::toString) else rawValues
val constructor = T::class.primaryConstructor!!
for (n in 0..2) {
val config = config("values" to configValues.take(n))
val data = constructor.call(rawValues.take(n))
assertThat(config.parseAs<T>().values).isEqualTo(data.values)
}
assertThat(empty().parseAs<T>().values).isEmpty()
}
private fun config(vararg values: Pair<String, *>): Config {
val config = ConfigValueFactory.fromMap(mapOf(*values))
println(config.render(defaults().setOriginComments(false)))
return config.toConfig()
}
private interface SingleData<out T> {
val value: T
}
private interface ListData<out T> {
val values: List<T>
}
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>)
data class IntData(override val value: Int) : SingleData<Int>
data class IntListData(override val values: List<Int>) : ListData<Int>
data class LongData(override val value: Long) : SingleData<Long>
data class LongListData(override val values: List<Long>) : ListData<Long>
data class DoubleData(override val value: Double) : SingleData<Double>
data class DoubleListData(override val values: List<Double>) : ListData<Double>
data class BooleanData(override val value: Boolean) : SingleData<Boolean>
data class BooleanListData(override val values: List<Boolean>) : ListData<Boolean>
data class EnumData(override val value: TestEnum) : SingleData<TestEnum>
data class EnumListData(override val values: List<TestEnum>) : ListData<TestEnum>
data class LocalDateData(override val value: LocalDate) : SingleData<LocalDate>
data class LocalDateListData(override val values: List<LocalDate>) : ListData<LocalDate>
data class InstantData(override val value: Instant) : SingleData<Instant>
data class InstantListData(override val values: List<Instant>) : ListData<Instant>
data class HostAndPortData(override val value: HostAndPort) : SingleData<HostAndPort>
data class HostAndPortListData(override val values: List<HostAndPort>) : ListData<HostAndPort>
data class PathData(override val value: Path) : SingleData<Path>
data class PathListData(override val values: List<Path>) : ListData<Path>
data class URLData(override val value: URL) : SingleData<URL>
data class URLListData(override val values: List<URL>) : ListData<URL>
data class PropertiesData(override val value: Properties) : SingleData<Properties>
data class PropertiesListData(override val values: List<Properties>) : ListData<Properties>
data class MultiPropertyData(val i: Int, val b: Boolean, val l: List<String>)
data class NestedData(val first: StringData)
data class DataListData(val list: List<StringData>)
data class DefaultData(val a: Int, val defaultOfTwo: Int = 2)
data class NullableData(val nullable: String?)
data class OldData(
@OldConfig("oldValue")
val newValue: String)
enum class TestEnum { Value1, Value2 }
}