Merge pull request #1510 from corda/merges/29_10_2018_13_40

This commit is contained in:
Michele Sollecito 2018-10-29 15:18:31 +00:00 committed by GitHub
commit 3139f430d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 2618 additions and 301 deletions

227
.idea/compiler.xml generated
View File

@ -4,35 +4,15 @@
<bytecodeTargetLevel target="1.8">
<module name="api-scanner_main" target="1.8" />
<module name="api-scanner_test" target="1.8" />
<module name="attachment-demo_integrationTest" target="1.8" />
<module name="attachment-demo_main" target="1.8" />
<module name="attachment-demo_test" target="1.8" />
<module name="avalanche_main" target="1.8" />
<module name="avalanche_test" target="1.8" />
<module name="bank-of-corda-demo_integrationTest" target="1.8" />
<module name="bank-of-corda-demo_main" target="1.8" />
<module name="bank-of-corda-demo_test" target="1.8" />
<module name="behave-tools_main" target="1.8" />
<module name="behave-tools_test" target="1.8" />
<module name="behave_behave" target="1.8" />
<module name="behave_main" target="1.8" />
<module name="behave_scenario" target="1.8" />
<module name="behave_test" target="1.8" />
<module name="blobinspector_main" target="1.8" />
<module name="blobinspector_test" target="1.8" />
<module name="bootstrapper_main" target="1.8" />
<module name="bootstrapper_test" target="1.8" />
<module name="bridge_integrationTest" target="1.8" />
<module name="bridge_main" target="1.8" />
<module name="bridge_test" target="1.8" />
<module name="bridgecapsule_main" target="1.6" />
<module name="bridgecapsule_smokeTest" target="1.6" />
<module name="bridgecapsule_test" target="1.6" />
<module name="buildSrc_main" target="1.8" />
<module name="buildSrc_test" target="1.8" />
<module name="business-network-demo_integrationTest" target="1.8" />
<module name="business-network-demo_main" target="1.8" />
<module name="business-network-demo_test" target="1.8" />
<module name="canonicalizer_main" target="1.8" />
<module name="canonicalizer_test" target="1.8" />
<module name="capsule-crr-submission_main" target="1.8" />
@ -43,37 +23,13 @@
<module name="capsule-hsm-crl-generator_test" target="1.8" />
<module name="capsule-hsm_main" target="1.8" />
<module name="capsule-hsm_test" target="1.8" />
<module name="cli_main" target="1.8" />
<module name="cli_test" target="1.8" />
<module name="client_main" target="1.8" />
<module name="client_test" target="1.8" />
<module name="cliutils_main" target="1.8" />
<module name="cliutils_test" target="1.8" />
<module name="com.r3.corda_buildSrc_main" target="1.8" />
<module name="com.r3.corda_buildSrc_test" target="1.8" />
<module name="com.r3.corda_canonicalizer_main" target="1.8" />
<module name="com.r3.corda_canonicalizer_test" target="1.8" />
<module name="common_main" target="1.8" />
<module name="common_test" target="1.8" />
<module name="confidential-identities_main" target="1.8" />
<module name="confidential-identities_test" target="1.8" />
<module name="consensus-benchmark_main" target="1.8" />
<module name="consensus-benchmark_test" target="1.8" />
<module name="contract_main" target="1.8" />
<module name="contract_test" target="1.8" />
<module name="contracts-states_integrationTest" target="1.8" />
<module name="contracts-states_main" target="1.8" />
<module name="contracts-states_test" target="1.8" />
<module name="corda-core_integrationTest" target="1.8" />
<module name="corda-core_smokeTest" target="1.8" />
<module name="corda-enterprise-client_main" target="1.8" />
<module name="corda-enterprise-client_test" target="1.8" />
<module name="corda-enterprise-testing_main" target="1.8" />
<module name="corda-enterprise-testing_test" target="1.8" />
<module name="corda-enterprise-tools_main" target="1.8" />
<module name="corda-enterprise-tools_test" target="1.8" />
<module name="corda-enterprise_main" target="1.8" />
<module name="corda-enterprise_test" target="1.8" />
<module name="corda-finance_integrationTest" target="1.8" />
<module name="corda-project-testing_main" target="1.8" />
<module name="corda-project-testing_test" target="1.8" />
@ -81,227 +37,68 @@
<module name="corda-project-tools_test" target="1.8" />
<module name="corda-project_main" target="1.8" />
<module name="corda-project_test" target="1.8" />
<module name="corda-utils_integrationTest" target="1.8" />
<module name="corda-utils_main" target="1.8" />
<module name="corda-utils_test" target="1.8" />
<module name="corda-webserver_integrationTest" target="1.8" />
<module name="corda-webserver_main" target="1.8" />
<module name="corda-webserver_test" target="1.8" />
<module name="cordapp-configuration_main" target="1.8" />
<module name="cordapp-configuration_test" target="1.8" />
<module name="cordapp_integrationTest" target="1.8" />
<module name="cordapp_main" target="1.8" />
<module name="cordapp_test" target="1.8" />
<module name="cordform-common_main" target="1.8" />
<module name="cordform-common_test" target="1.8" />
<module name="cordformation_main" target="1.8" />
<module name="cordformation_runnodes" target="1.8" />
<module name="cordformation_test" target="1.8" />
<module name="core-deterministic-testing_main" target="1.8" />
<module name="core-deterministic-testing_test" target="1.8" />
<module name="core-deterministic_main" target="1.8" />
<module name="core-deterministic_test" target="1.8" />
<module name="core_extraResource" target="1.8" />
<module name="core_integrationTest" target="1.8" />
<module name="core_main" target="1.8" />
<module name="core_smokeTest" target="1.8" />
<module name="core_test" target="1.8" />
<module name="data_main" target="1.8" />
<module name="data_test" target="1.8" />
<module name="dbmigration_main" target="1.8" />
<module name="dbmigration_test" target="1.8" />
<module name="demobench_main" target="1.8" />
<module name="demobench_test" target="1.8" />
<module name="dist_binFiles" target="1.8" />
<module name="dist_installerFiles" target="1.8" />
<module name="dist_licenseFiles" target="1.8" />
<module name="dist_main" target="1.8" />
<module name="dist_readmeFiles" target="1.8" />
<module name="dist_startupScripts" target="1.8" />
<module name="dist_test" target="1.8" />
<module name="djvm_main" target="1.8" />
<module name="djvm_test" target="1.8" />
<module name="docs_main" target="1.8" />
<module name="docs_source_example-code_integrationTest" target="1.8" />
<module name="docs_source_example-code_main" target="1.8" />
<module name="docs_source_example-code_test" target="1.8" />
<module name="docs_test" target="1.8" />
<module name="example-code_integrationTest" target="1.8" />
<module name="example-code_main" target="1.8" />
<module name="example-code_test" target="1.8" />
<module name="experimental-behave_behave" target="1.8" />
<module name="experimental-behave_main" target="1.8" />
<module name="experimental-behave_smokeTest" target="1.8" />
<module name="experimental-behave_test" target="1.8" />
<module name="experimental-kryo-hook_main" target="1.8" />
<module name="experimental-kryo-hook_test" target="1.8" />
<module name="experimental_main" target="1.8" />
<module name="experimental_rpc-worker_main" target="1.8" />
<module name="experimental_rpc-worker_test" target="1.8" />
<module name="experimental_test" target="1.8" />
<module name="explorer-capsule_main" target="1.6" />
<module name="explorer-capsule_test" target="1.6" />
<module name="explorer_main" target="1.8" />
<module name="explorer_test" target="1.8" />
<module name="finance_integrationTest" target="1.8" />
<module name="finance_main" target="1.8" />
<module name="finance_test" target="1.8" />
<module name="flow-hook_main" target="1.8" />
<module name="flow-hook_test" target="1.8" />
<module name="flow-worker_integrationTest" target="1.8" />
<module name="flow-worker_main" target="1.8" />
<module name="flow-worker_test" target="1.8" />
<module name="flows_integrationTest" target="1.8" />
<module name="flows_main" target="1.8" />
<module name="flows_test" target="1.8" />
<module name="gradle-plugins-cordapp_main" target="1.8" />
<module name="gradle-plugins-cordapp_test" target="1.8" />
<module name="graphs_main" target="1.8" />
<module name="graphs_test" target="1.8" />
<module name="ha-testing_integrationTest" target="1.8" />
<module name="ha-testing_main" target="1.8" />
<module name="ha-testing_test" target="1.8" />
<module name="hsm-tool_main" target="1.8" />
<module name="hsm-tool_test" target="1.8" />
<module name="intellij-plugin_main" target="1.8" />
<module name="intellij-plugin_test" target="1.8" />
<module name="irs-demo-cordapp_integrationTest" target="1.8" />
<module name="irs-demo-cordapp_main" target="1.8" />
<module name="irs-demo-cordapp_main~1" target="1.8" />
<module name="irs-demo-cordapp_test" target="1.8" />
<module name="irs-demo-cordapp_test~1" target="1.8" />
<module name="irs-demo-web_main" target="1.8" />
<module name="irs-demo-web_test" target="1.8" />
<module name="irs-demo_integrationTest" target="1.8" />
<module name="irs-demo_main" target="1.8" />
<module name="irs-demo_systemTest" target="1.8" />
<module name="irs-demo_test" target="1.8" />
<module name="isolated_main" target="1.8" />
<module name="isolated_test" target="1.8" />
<module name="jackson_main" target="1.8" />
<module name="jackson_test" target="1.8" />
<module name="jarfilter_main" target="1.8" />
<module name="jarfilter_test" target="1.8" />
<module name="jdk8u-deterministic_main" target="1.8" />
<module name="jdk8u-deterministic_test" target="1.8" />
<module name="jfx_integrationTest" target="1.8" />
<module name="jfx_main" target="1.8" />
<module name="jfx_test" target="1.8" />
<module name="jmeter_main" target="1.8" />
<module name="jmeter_test" target="1.8" />
<module name="jpa_main" target="1.8" />
<module name="jpa_test" target="1.8" />
<module name="kryo-hook_main" target="1.8" />
<module name="kryo-hook_test" target="1.8" />
<module name="launcher_main" target="1.8" />
<module name="launcher_test" target="1.8" />
<module name="loadtest_main" target="1.8" />
<module name="loadtest_test" target="1.8" />
<module name="mock_main" target="1.8" />
<module name="mock_test" target="1.8" />
<module name="net.corda-bank-of-corda-demo_integrationTest" target="1.8" />
<module name="net.corda-corda-project_main" target="1.8" />
<module name="net.corda-corda-project_test" target="1.8" />
<module name="net.corda-sandbox_main" target="1.8" />
<module name="net.corda-sandbox_test" target="1.8" />
<module name="net.corda-verifier_main" target="1.8" />
<module name="net.corda-verifier_test" target="1.8" />
<module name="mysql_main" target="1.8" />
<module name="mysql_test" target="1.8" />
<module name="net.corda_buildSrc_main" target="1.8" />
<module name="net.corda_buildSrc_test" target="1.8" />
<module name="net.corda_canonicalizer_main" target="1.8" />
<module name="net.corda_canonicalizer_test" target="1.8" />
<module name="network-bootstrapper_main" target="1.8" />
<module name="network-bootstrapper_test" target="1.8" />
<module name="network-verifier_main" target="1.8" />
<module name="network-verifier_test" target="1.8" />
<module name="network-visualiser_main" target="1.8" />
<module name="network-visualiser_test" target="1.8" />
<module name="node-api_main" target="1.8" />
<module name="node-api_test" target="1.8" />
<module name="node-capsule_main" target="1.6" />
<module name="node-capsule_test" target="1.6" />
<module name="node-driver_integrationTest" target="1.8" />
<module name="node-driver_main" target="1.8" />
<module name="node-driver_test" target="1.8" />
<module name="node-schemas_main" target="1.8" />
<module name="node-schemas_test" target="1.8" />
<module name="node_integrationTest" target="1.8" />
<module name="node_main" target="1.8" />
<module name="node_smokeTest" target="1.8" />
<module name="node_test" target="1.8" />
<module name="notary-bft-smart_main" target="1.8" />
<module name="notary-bft-smart_test" target="1.8" />
<module name="notary-demo_main" target="1.8" />
<module name="notary-demo_test" target="1.8" />
<module name="notary-healthcheck-client_main" target="1.8" />
<module name="notary-healthcheck-client_test" target="1.8" />
<module name="notary-healthcheck-cordapp_integrationTest" target="1.8" />
<module name="notary-healthcheck-cordapp_main" target="1.8" />
<module name="notary-healthcheck-cordapp_test" target="1.8" />
<module name="notary-healthcheck_main" target="1.8" />
<module name="notary-healthcheck_test" target="1.8" />
<module name="notary-raft_main" target="1.8" />
<module name="notary-raft_test" target="1.8" />
<module name="notary_main" target="1.8" />
<module name="notary_test" target="1.8" />
<module name="notarytest_main" target="1.8" />
<module name="notarytest_test" target="1.8" />
<module name="perftestcordapp_integrationTest" target="1.8" />
<module name="perftestcordapp_main" target="1.8" />
<module name="perftestcordapp_test" target="1.8" />
<module name="publish-utils_main" target="1.8" />
<module name="publish-utils_test" target="1.8" />
<module name="qa-behave_main" target="1.8" />
<module name="qa-behave_test" target="1.8" />
<module name="qa_main" target="1.8" />
<module name="qa_test" target="1.8" />
<module name="quasar-hook_main" target="1.8" />
<module name="quasar-hook_test" target="1.8" />
<module name="quasar-utils_main" target="1.8" />
<module name="quasar-utils_test" target="1.8" />
<module name="registration-tool_main" target="1.8" />
<module name="registration-tool_test" target="1.8" />
<module name="rpc-proxy_main" target="1.8" />
<module name="rpc-proxy_rpcProxy" target="1.8" />
<module name="rpc-proxy_smokeTest" target="1.8" />
<module name="rpc-proxy_test" target="1.8" />
<module name="rpc-worker_integrationTest" target="1.8" />
<module name="rpc-worker_main" target="1.8" />
<module name="rpc-worker_test" target="1.8" />
<module name="rpc_integrationTest" target="1.8" />
<module name="rpc_main" target="1.8" />
<module name="rpc_smokeTest" target="1.8" />
<module name="rpc_test" target="1.8" />
<module name="samples_main" target="1.8" />
<module name="samples_test" target="1.8" />
<module name="sandbox_main" target="1.8" />
<module name="sandbox_test" target="1.8" />
<module name="serialization-deterministic_main" target="1.8" />
<module name="serialization-deterministic_test" target="1.8" />
<module name="serialization_main" target="1.8" />
<module name="serialization_test" target="1.8" />
<module name="sgx-hsm-tool_main" target="1.8" />
<module name="sgx-hsm-tool_test" target="1.8" />
<module name="shell-cli_integrationTest" target="1.8" />
<module name="shell-cli_main" target="1.8" />
<module name="shell-cli_test" target="1.8" />
<module name="shell_integrationTest" target="1.8" />
<module name="shell_main" target="1.8" />
<module name="shell_test" target="1.8" />
<module name="simm-valuation-demo_integrationTest" target="1.8" />
<module name="simm-valuation-demo_main" target="1.8" />
<module name="simm-valuation-demo_scenario" target="1.8" />
<module name="simm-valuation-demo_scenarioTest" target="1.8" />
<module name="simm-valuation-demo_test" target="1.8" />
<module name="smoke-test-utils_main" target="1.8" />
<module name="smoke-test-utils_test" target="1.8" />
<module name="source-example-code_integrationTest" target="1.8" />
<module name="source-example-code_main" target="1.8" />
<module name="source-example-code_test" target="1.8" />
<module name="test-cli_main" target="1.8" />
<module name="test-cli_test" target="1.8" />
<module name="test-common_main" target="1.8" />
<module name="test-common_test" target="1.8" />
<module name="test-utils_integrationTest" target="1.8" />
<module name="test-utils_main" target="1.8" />
<module name="test-utils_test" target="1.8" />
<module name="testing-node-driver_integrationTest" target="1.8" />
<module name="testing-node-driver_main" target="1.8" />
<module name="testing-node-driver_test" target="1.8" />
@ -313,13 +110,8 @@
<module name="testing-test-utils_test" target="1.8" />
<module name="testing_main" target="1.8" />
<module name="testing_test" target="1.8" />
<module name="tools-blobinspector_main" target="1.8" />
<module name="tools-blobinspector_test" target="1.8" />
<module name="tools_main" target="1.8" />
<module name="tools_test" target="1.8" />
<module name="trader-demo_integrationTest" target="1.8" />
<module name="trader-demo_main" target="1.8" />
<module name="trader-demo_test" target="1.8" />
<module name="unwanteds_main" target="1.8" />
<module name="unwanteds_test" target="1.8" />
<module name="verifier_integrationTest" target="1.8" />
@ -328,18 +120,11 @@
<module name="verify-enclave_integrationTest" target="1.8" />
<module name="verify-enclave_main" target="1.8" />
<module name="verify-enclave_test" target="1.8" />
<module name="web_main" target="1.8" />
<module name="web_test" target="1.8" />
<module name="webcapsule_main" target="1.6" />
<module name="webcapsule_test" target="1.6" />
<module name="webserver-webcapsule_main" target="1.8" />
<module name="webserver-webcapsule_test" target="1.8" />
<module name="webserver_integrationTest" target="1.8" />
<module name="webserver_main" target="1.8" />
<module name="webserver_test" target="1.8" />
</bytecodeTargetLevel>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_STRING" value="-parameters" />
</component>
</project>
</project>

9
common/README.md Normal file
View File

@ -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.

View File

@ -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

View File

@ -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
}

View File

@ -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<TYPE> {
/**
* 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<VALUE> {
/**
* 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<VALUE>
}
}
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<TYPE> : Configuration.Property.Metadata, Configuration.Validator, Configuration.Value.Extractor<TYPE>, Configuration.Describer, Configuration.Value.Parser<TYPE> {
override fun isSpecifiedBy(configuration: Config): Boolean = configuration.hasPath(key)
/**
* Defines a required property, which must provide a value or produce an error.
*/
interface Required<TYPE> : Definition<TYPE> {
/**
* 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<TYPE?>
}
/**
* 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<TYPE> : Definition<TYPE> {
/**
* Returns a required property expecting multiple values for the relevant key.
*/
fun list(): Required<List<TYPE>>
}
/**
* Default property definition, required and single-value.
*/
interface Standard<TYPE> : Required<TYPE>, Single<TYPE> {
/**
* Passes the value to a validating mapping function, provided this is valid in the first place.
*/
fun <MAPPED : Any> mapValid(mappedTypeName: String, convert: (TYPE) -> Validated<MAPPED, Validation.Error>): Standard<MAPPED>
/**
* Passes the value to a non-validating mapping function, provided this is valid in the first place.
*/
fun <MAPPED : Any> map(mappedTypeName: String, convert: (TYPE) -> MAPPED): Standard<MAPPED> = mapValid(mappedTypeName) { value -> valid(convert.invoke(value)) }
}
override fun parse(configuration: Config, options: Configuration.Validation.Options): Validated<TYPE, Validation.Error> {
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<Long> = LongProperty(key, sensitive)
/**
* Returns a [Configuration.Property.Definition.Standard] with value of type [Int].
*/
fun int(key: String, sensitive: Boolean = false): Standard<Int> = long(key, sensitive).mapValid { value ->
try {
valid(Math.toIntExact(value))
} catch (e: ArithmeticException) {
invalid<Int, Configuration.Validation.Error>(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<Boolean> = 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<Double> = 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<Float> = double(key, sensitive).mapValid { value ->
val floatValue = value.toFloat()
if (floatValue.isInfinite() || floatValue.isNaN()) {
invalid<Float, Configuration.Validation.Error>(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<String> = 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<Duration> = 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<ConfigObject> = 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 : Enum<ENUM>> enum(key: String, enumClass: KClass<ENUM>, sensitive: Boolean = false): Standard<ENUM> = 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<Property.Definition<*>>
companion object {
/**
* Constructs a schema with given name and properties.
*/
fun withProperties(name: String? = null, properties: Iterable<Property.Definition<*>>): 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<Property.Definition<*>>): 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<VALUE>(name: String?, private val prefix: String? = null) : Configuration.Schema, Configuration.Value.Parser<VALUE> {
private val mutableProperties = mutableSetOf<Property.Definition<*>>()
override val properties: Set<Property.Definition<*>> = 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<Long> = 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<Int> = 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<Boolean> = 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<Double> = 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<Float> = 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<String> = 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<Duration> = 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<ConfigObject> = 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 : Enum<ENUM>> enum(key: String? = null, enumClass: KClass<ENUM>, sensitive: Boolean = false): PropertyDelegate.Standard<ENUM> = 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<VALUE> = 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<VALUE>
}
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<String> = emptyList()) {
internal companion object {
private const val UNKNOWN = "<unknown>"
private fun contextualize(keyName: String, containingPath: List<String>): Pair<String, List<String>> {
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<String> 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<String> = emptyList()) : Configuration.Validation.Error(keyName, typeName, message, containingPath) {
internal companion object {
internal fun of(message: String, keyName: String = UNKNOWN, typeName: String = UNKNOWN, containingPath: List<String> = 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<String> = emptyList()) : Configuration.Validation.Error(keyName, typeName, message, containingPath) {
internal companion object {
internal fun of(message: String, keyName: String = UNKNOWN, typeName: String = UNKNOWN, containingPath: List<String> = 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<String> = emptyList()) : Configuration.Validation.Error(keyName, typeName, message, containingPath) {
internal companion object {
internal fun of(message: String, keyName: String = UNKNOWN, typeName: String = UNKNOWN, containingPath: List<String> = 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<String> = emptyList()) : Configuration.Validation.Error(keyName, typeName, message, containingPath) {
internal companion object {
internal fun of(message: String, keyName: String = UNKNOWN, typeName: String = UNKNOWN, containingPath: List<String> = 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<String> = emptyList()) : Configuration.Validation.Error(keyName, typeName, message, containingPath) {
internal companion object {
internal fun of(message: String, keyName: String = UNKNOWN, typeName: String = UNKNOWN, containingPath: List<String> = 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<String> = 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<String> = 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<String> = 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<Int?> {
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<Config, Configuration.Validation.Error, Configuration.Validation.Options>
}

View File

@ -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<Long>(key, Long::class.javaObjectType.simpleName, Config::getLong, Config::getLongList, sensitive) {
override fun validate(target: Config, options: Configuration.Validation.Options?): Valid<Config> {
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<TYPE>(override val key: String, typeNameArg: String, private val extractSingleValue: (Config, String) -> TYPE, internal val extractListValue: (Config, String) -> List<TYPE>, override val isSensitive: Boolean = false, final override val schema: Configuration.Schema? = null) : Configuration.Property.Definition.Standard<TYPE> {
override fun valueIn(configuration: Config) = extractSingleValue.invoke(configuration, key)
override val typeName: String = schema?.let { "#${it.name ?: "Object@$key"}" } ?: typeNameArg
override fun <MAPPED : Any> mapValid(mappedTypeName: String, convert: (TYPE) -> Valid<MAPPED>): Configuration.Property.Definition.Standard<MAPPED> = FunctionalProperty(this, mappedTypeName, extractListValue, convert)
override fun optional(defaultValue: TYPE?): Configuration.Property.Definition<TYPE?> = OptionalProperty(this, defaultValue)
override fun list(): Configuration.Property.Definition.Required<List<TYPE>> = 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<Config> {
val errors = mutableSetOf<Configuration.Validation.Error>()
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<TYPE>(delegate: StandardProperty<TYPE>) : RequiredDelegatedProperty<List<TYPE>, StandardProperty<TYPE>>(delegate) {
override val typeName: String = "List<${delegate.typeName}>"
override fun valueIn(configuration: Config): List<TYPE> = delegate.extractListValue.invoke(configuration, key)
override fun validate(target: Config, options: Configuration.Validation.Options?): Valid<Config> {
val errors = mutableSetOf<Configuration.Validation.Error>()
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<TYPE>(delegate: Configuration.Property.Definition.Required<TYPE>, private val defaultValue: TYPE?) : DelegatedProperty<TYPE?, Configuration.Property.Definition.Required<TYPE>>(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<Config> {
val result = delegate.validate(target, options)
val error = result.errors.asSequence().filterIsInstance<Configuration.Validation.Error.MissingValue>().singleOrNull()
return when {
error != null -> if (result.errors.size > 1) result else valid(target)
else -> result
}
}
}
private class FunctionalProperty<TYPE, MAPPED : Any>(delegate: Configuration.Property.Definition.Standard<TYPE>, private val mappedTypeName: String, internal val extractListValue: (Config, String) -> List<TYPE>, private val convert: (TYPE) -> Valid<MAPPED>) : RequiredDelegatedProperty<MAPPED, Configuration.Property.Definition.Standard<TYPE>>(delegate), Configuration.Property.Definition.Standard<MAPPED> {
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 <M : Any> mapValid(mappedTypeName: String, convert: (MAPPED) -> Valid<M>): Configuration.Property.Definition.Standard<M> = FunctionalProperty(delegate, mappedTypeName, extractListValue, { target: TYPE -> this.convert.invoke(target).mapValid(convert) })
override fun list(): Configuration.Property.Definition.Required<List<MAPPED>> = FunctionalListProperty(this)
override fun validate(target: Config, options: Configuration.Validation.Options?): Valid<Config> {
val errors = mutableSetOf<Configuration.Validation.Error>()
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<RAW, TYPE : Any>(delegate: FunctionalProperty<RAW, TYPE>) : RequiredDelegatedProperty<List<TYPE>, FunctionalProperty<RAW, TYPE>>(delegate) {
override val typeName: String = "List<${super.typeName}>"
override fun valueIn(configuration: Config): List<TYPE> = 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<Config> {
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<TYPE, DELEGATE : Configuration.Property.Metadata>(protected val delegate: DELEGATE) : Configuration.Property.Metadata by delegate, Configuration.Property.Definition<TYPE> {
final override fun toString() = "\"$key\": \"$typeName\""
}
private abstract class RequiredDelegatedProperty<TYPE, DELEGATE : Configuration.Property.Definition.Required<*>>(delegate: DELEGATE) : DelegatedProperty<TYPE, DELEGATE>(delegate), Configuration.Property.Definition.Required<TYPE> {
final override fun optional(defaultValue: TYPE?): Configuration.Property.Definition<TYPE?> = 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<Configuration.Validation.Error> {
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) }

View File

@ -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.Property.Definition<*>>) : 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<Config> {
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()
}
}

View File

@ -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<TYPE> {
operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty<Any?, Configuration.Property.Definition<TYPE>>
interface Required<TYPE> {
operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty<Any?, Configuration.Property.Definition.Required<TYPE>>
fun optional(defaultValue: TYPE? = null): PropertyDelegate<TYPE?>
}
interface Single<TYPE> {
operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty<Any?, Configuration.Property.Definition.Single<TYPE>>
fun list(): Required<List<TYPE>>
}
interface Standard<TYPE> : Required<TYPE>, Single<TYPE> {
override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty<Any?, Configuration.Property.Definition.Standard<TYPE>>
fun <MAPPED : Any> mapValid(mappedTypeName: String, convert: (TYPE) -> Valid<MAPPED>): Standard<MAPPED>
fun <MAPPED : Any> map(mappedTypeName: String, convert: (TYPE) -> MAPPED): Standard<MAPPED> = mapValid(mappedTypeName) { value -> valid(convert.invoke(value)) }
}
companion object {
internal fun long(key: String?, prefix: String?, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard<Long> = 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<Int> = 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<Boolean> = 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<Double> = 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<Float> = 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<String> = 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<Duration> = 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<ConfigObject> = PropertyDelegateImpl(key, prefix, sensitive, addProperty, { k, s -> Configuration.Property.Definition.nestedObject(k, schema, s) })
internal fun <ENUM : Enum<ENUM>> enum(key: String?, prefix: String?, enumClass: KClass<ENUM>, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard<ENUM> = PropertyDelegateImpl(key, prefix, sensitive, addProperty, { k, s -> Configuration.Property.Definition.enum(k, enumClass, s) })
}
}
private class PropertyDelegateImpl<TYPE>(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<TYPE>) : PropertyDelegate.Standard<TYPE> {
override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty<Any?, Configuration.Property.Definition.Standard<TYPE>> {
val shortName = key ?: property.name
val prop = construct.invoke(prefix?.let { "$prefix.$shortName" } ?: shortName, sensitive).also(addToProperties)
return object : ReadOnlyProperty<Any?, Configuration.Property.Definition.Standard<TYPE>> {
override fun getValue(thisRef: Any?, property: KProperty<*>): Configuration.Property.Definition.Standard<TYPE> = prop
}
}
override fun list(): PropertyDelegate.Required<List<TYPE>> = ListPropertyDelegateImpl(key, sensitive, addToProperties, { k, s -> construct.invoke(k, s).list() })
override fun optional(defaultValue: TYPE?): PropertyDelegate<TYPE?> = OptionalPropertyDelegateImpl(key, sensitive, addToProperties, { k, s -> construct.invoke(k, s).optional(defaultValue) })
override fun <MAPPED : Any> mapValid(mappedTypeName: String, convert: (TYPE) -> Valid<MAPPED>): PropertyDelegate.Standard<MAPPED> = PropertyDelegateImpl(key, prefix, sensitive, addToProperties, { k, s -> construct.invoke(k, s).mapValid(mappedTypeName) { value -> convert.invoke(value) } })
}
private class OptionalPropertyDelegateImpl<TYPE>(private val key: String?, private val sensitive: Boolean = false, private val addToProperties: (Configuration.Property.Definition<*>) -> Unit, private val construct: (String, Boolean) -> Configuration.Property.Definition<TYPE?>) : PropertyDelegate<TYPE?> {
override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty<Any?, Configuration.Property.Definition<TYPE?>> {
val prop = construct.invoke(key ?: property.name, sensitive).also(addToProperties)
return object : ReadOnlyProperty<Any?, Configuration.Property.Definition<TYPE?>> {
override fun getValue(thisRef: Any?, property: KProperty<*>): Configuration.Property.Definition<TYPE?> = prop
}
}
}
private class ListPropertyDelegateImpl<TYPE>(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<TYPE>) : PropertyDelegate.Required<TYPE> {
override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty<Any?, Configuration.Property.Definition.Required<TYPE>> {
val prop = construct.invoke(key ?: property.name, sensitive).also(addToProperties)
return object : ReadOnlyProperty<Any?, Configuration.Property.Definition.Required<TYPE>> {
override fun getValue(thisRef: Any?, property: KProperty<*>): Configuration.Property.Definition.Required<TYPE> = prop
}
}
override fun optional(defaultValue: TYPE?): PropertyDelegate<TYPE?> = OptionalPropertyDelegateImpl(key, sensitive, addToProperties, { k, s -> construct.invoke(k, s).optional(defaultValue) })
}

View File

@ -0,0 +1,53 @@
package net.corda.common.configuration.parsing.internal
import com.typesafe.config.*
import net.corda.common.validation.internal.Validated
inline fun <TYPE, reified MAPPED : Any> Configuration.Property.Definition.Standard<TYPE>.mapValid(noinline convert: (TYPE) -> Valid<MAPPED>): Configuration.Property.Definition.Standard<MAPPED> = mapValid(MAPPED::class.java.simpleName, convert)
inline fun <reified ENUM : Enum<ENUM>, VALUE : Any> Configuration.Specification<VALUE>.enum(key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard<ENUM> = enum(key, ENUM::class, sensitive)
inline fun <TYPE, reified MAPPED : Any> PropertyDelegate.Standard<TYPE>.mapValid(noinline convert: (TYPE) -> Valid<MAPPED>): PropertyDelegate.Standard<MAPPED> = mapValid(MAPPED::class.java.simpleName, convert)
inline fun <TYPE, reified MAPPED : Any> PropertyDelegate.Standard<TYPE>.map(noinline convert: (TYPE) -> MAPPED): PropertyDelegate.Standard<MAPPED> = map(MAPPED::class.java.simpleName, convert)
operator fun <TYPE> Config.get(property: Configuration.Property.Definition<TYPE>): TYPE = property.valueIn(this)
inline fun <reified NESTED : Any> Configuration.Specification<*>.nested(specification: Configuration.Specification<NESTED>, key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard<NESTED> = nestedObject(schema = specification, key = key, sensitive = sensitive).map(ConfigObject::toConfig).mapValid { value -> specification.parse(value) }
@Suppress("UNCHECKED_CAST")
internal fun configObject(vararg entries: Pair<String, Any?>): 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<String, Any?>).root())
} else {
entry
}
}
return configuration.root()
}
internal operator fun Config.plus(entry: Pair<String, Any?>): 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<TARGET> = Validated<TARGET, Configuration.Validation.Error>
internal fun <TYPE> valid(target: TYPE) = Validated.valid<TYPE, Configuration.Validation.Error>(target)

View File

@ -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<Int?> {
return spec.parse(configuration)
}
private class Spec(versionKey: String, versionDefaultValue: Int?) : Configuration.Specification<Int?>("Version") {
private val version by int(key = versionKey).optional(versionDefaultValue)
override fun parseValid(configuration: Config) = valid(version.valueIn(configuration))
}
}

View File

@ -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<VALUE> private constructor(private val versionFromConfig: (Config) -> Valid<Int>, private val specifications: Map<Int, Configuration.Specification<VALUE>>) : (Config) -> Valid<Configuration.Specification<VALUE>> {
companion object {
fun <V> mapping(versionParser: Configuration.Value.Parser<Int>, specifications: Map<Int, Configuration.Specification<V>>) = VersionedSpecificationRegistry({ config -> versionParser.parse(config) }, specifications)
fun <V> mapping(versionParser: Configuration.Value.Parser<Int>, vararg specifications: Pair<Int, Configuration.Specification<V>>) = VersionedSpecificationRegistry({ config -> versionParser.parse(config) }, specifications.toMap())
fun <V> mapping(versionParser: (Config) -> Valid<Int>, specifications: Map<Int, Configuration.Specification<V>>) = VersionedSpecificationRegistry(versionParser, specifications)
fun <V> mapping(versionParser: (Config) -> Valid<Int>, vararg specifications: Pair<Int, Configuration.Specification<V>>) = VersionedSpecificationRegistry(versionParser, specifications.toMap())
}
override fun invoke(configuration: Config): Valid<Configuration.Specification<VALUE>> {
return versionFromConfig.invoke(configuration).mapValid { version ->
val value = specifications[version]
value?.let { valid(it) } ?: invalid<Configuration.Specification<VALUE>, Configuration.Validation.Error>(Configuration.Validation.Error.UnsupportedVersion.of(version))
}
}
}

View File

@ -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 <ERROR> validFromRawValue(rawValue: String, mapError: (String) -> ERROR): Validated<Address, ERROR> {
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 \"<host(String)>:<port:(Int)>\"").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))
}
}
}

View File

@ -0,0 +1,3 @@
package net.corda.common.configuration.parsing.internal
data class Addresses(val principal: Address, val admin: Address)

View File

@ -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)
}
}

View File

@ -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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Address> {
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\""))
}
}
}

View File

@ -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)
}
}

View File

@ -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>("RpcSettings") {
private object AddressesSpec : Configuration.Specification<Addresses>("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<Address> {
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<RpcSettings>(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<List<String>?>("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
}

View File

@ -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<Int?> = extractVersion(null)
internal fun extractVersion(value: Int?) = extractValidValue(value)
internal fun extractPresentVersion(value: Int) = extractValidValue(value)
internal fun <VALUE> extractValidValue(value: VALUE) = extractValue(Validated.valid(value))
internal fun <VALUE> extractValueWithErrors(errors: Set<Configuration.Validation.Error>) = extractValue<VALUE>(Validated.invalid(errors))
internal fun <VALUE> extractValue(value: Valid<VALUE>) = object : Configuration.Value.Parser<VALUE> {
override fun parse(configuration: Config, options: Configuration.Validation.Options): Valid<VALUE> = value
}

View File

@ -0,0 +1,25 @@
package net.corda.common.configuration.parsing.internal
import com.typesafe.config.ConfigFactory
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
class UtilsTest {
@Test
fun serialize_deserialize_configuration() {
var rawConfiguration = ConfigFactory.empty()
rawConfiguration += "key" to "value"
rawConfiguration += "key1.key2" to configObject("key3" to "value2", "key4" to configObject("key5" to -2.0, "key6" to false))
rawConfiguration += "key7" to listOf("Hey!", true, 17, 0.0, configObject("key8" to listOf(-12.0, "HH", false), "key9" to "blah"))
val serialized = rawConfiguration.serialize()
println(serialized)
val deserialized = ConfigFactory.parseString(serialized)
assertThat(deserialized).isEqualTo(rawConfiguration)
}
}

View File

@ -0,0 +1,63 @@
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.configObject
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
class VersionExtractorTest {
private val versionExtractor = Configuration.Version.Extractor.fromKey("configuration.metadata.version")
private val extractVersion: (Config) -> Valid<Int?> = { config -> versionExtractor.parse(config) }
@Test
fun version_header_extraction_present() {
val versionValue = Configuration.Version.Extractor.DEFAULT_VERSION_VALUE + 1
val rawConfiguration = configObject("configuration" to configObject("metadata" to configObject("version" to versionValue), "node" to configObject("p2pAddress" to "localhost:8080"))).toConfig()
val version = extractVersion.invoke(rawConfiguration).valueOrThrow()
assertThat(version).isEqualTo(versionValue)
}
@Test
fun version_header_extraction_no_metadata() {
val rawConfiguration = configObject("configuration" to configObject("node" to configObject("p2pAddress" to "localhost:8080"))).toConfig()
val version = extractVersion.invoke(rawConfiguration).valueOrThrow()
assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE)
}
@Test
fun version_header_extraction_no_key() {
val rawConfiguration = configObject("configuration" to configObject("metadata" to configObject(), "node" to configObject("p2pAddress" to "localhost:8080"))).toConfig()
val version = extractVersion.invoke(rawConfiguration).valueOrThrow()
assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE)
}
@Test
fun version_header_extraction_no_value() {
val rawConfiguration = configObject("configuration" to configObject("metadata" to configObject("version" to null), "node" to configObject("p2pAddress" to "localhost:8080"))).toConfig()
val version = extractVersion.invoke(rawConfiguration).valueOrThrow()
assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE)
}
@Test
fun version_header_extraction_no_configuration() {
val rawConfiguration = configObject().toConfig()
val version = extractVersion.invoke(rawConfiguration).valueOrThrow()
assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE)
}
}

View File

@ -0,0 +1,131 @@
package net.corda.common.configuration.parsing.internal.versioned
import com.typesafe.config.Config
import net.corda.common.configuration.parsing.internal.*
import net.corda.common.validation.internal.Validated
import net.corda.common.validation.internal.Validated.Companion.invalid
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
class VersionedParsingExampleTest {
@Test
fun correct_parsing_function_is_used_for_present_version() {
val versionParser = Configuration.Version.Extractor.fromKey("configuration.metadata.version", null)
val extractVersion: (Config) -> Valid<Int> = { config -> versionParser.parseRequired(config) }
val parseConfiguration = VersionedSpecificationRegistry.mapping(extractVersion, 1 to RpcSettingsSpec.V1, 2 to RpcSettingsSpec.V2)
val principalAddressValue = Address("localhost", 8080)
val adminAddressValue = Address("127.0.0.1", 8081)
val configurationV1 = configObject("configuration.metadata.version" to 1, "principalHost" to principalAddressValue.host, "principalPort" to principalAddressValue.port, "adminHost" to adminAddressValue.host, "adminPort" to adminAddressValue.port).toConfig().also { println(it.serialize()) }
val rpcSettingsFromVersion1Conf = parseConfiguration.invoke(configurationV1).mapValid { it.parse(configurationV1) }
assertResult(rpcSettingsFromVersion1Conf, principalAddressValue, adminAddressValue)
val addressesValue = configObject("principal" to "${principalAddressValue.host}:${principalAddressValue.port}", "admin" to "${adminAddressValue.host}:${adminAddressValue.port}")
val configurationV2 = configObject("configuration.metadata.version" to 2, "configuration.value.addresses" to addressesValue).toConfig().also { println(it.serialize()) }
val rpcSettingsFromVersion2Conf = parseConfiguration.invoke(configurationV2).mapValid { it.parse(configurationV2) }
assertResult(rpcSettingsFromVersion2Conf, principalAddressValue, adminAddressValue)
}
@Test
fun default_value_is_used_for_absent_version() {
val defaultVersion = 2
val versionParser = Configuration.Version.Extractor.fromKey("configuration.metadata.version", defaultVersion)
val extractVersion: (Config) -> Valid<Int> = { config -> versionParser.parseRequired(config) }
val parseConfiguration = VersionedSpecificationRegistry.mapping(extractVersion, 1 to RpcSettingsSpec.V1, 2 to RpcSettingsSpec.V2)
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 configurationV2 = configObject("configuration.value.addresses" to addressesValue).toConfig().also { println(it.serialize()) }
val rpcSettingsFromVersion2Conf = parseConfiguration.invoke(configurationV2).mapValid { it.parse(configurationV2) }
assertResult(rpcSettingsFromVersion2Conf, principalAddressValue, adminAddressValue)
}
private fun assertResult(result: Valid<RpcSettings>, principalAddressValue: Address, adminAddressValue: Address) {
assertThat(result.isValid).isTrue()
assertThat(result.valueOrThrow()).satisfies { value ->
assertThat(value.principal).isEqualTo(principalAddressValue)
assertThat(value.admin).isEqualTo(adminAddressValue)
}
}
private data class RpcSettings(val principal: Address, val admin: Address)
private object RpcSettingsSpec {
private fun addressFor(host: String, port: Int): Valid<Address> {
return try {
require(host.isNotBlank())
require(port > 0)
Validated.valid(Address(host, port))
} catch (e: Exception) {
return Validated.invalid(Configuration.Validation.Error.BadValue.of(host, Address::class.java.simpleName, "Value must be of format \"host(String):port(Int > 0)\" e.g., \"127.0.0.1:8080\""))
}
}
object V1 : Configuration.Specification<RpcSettings>("RpcSettings") {
private val principalHost by string()
private val principalPort by int()
private val adminHost by string()
private val adminPort by int()
override fun parseValid(configuration: Config): Valid<RpcSettings> {
val principalHost = configuration[principalHost]
val principalPort = configuration[principalPort]
val adminHost = configuration[adminHost]
val adminPort = configuration[adminPort]
val principalAddress = addressFor(principalHost, principalPort)
val adminAddress = addressFor(adminHost, adminPort)
return if (principalAddress.isValid && adminAddress.isValid) {
return valid(RpcSettings(principalAddress.value, adminAddress.value))
} else {
invalid(principalAddress.errors + adminAddress.errors)
}
}
}
object V2 : Configuration.Specification<RpcSettings>("RpcSettings", prefix = "configuration.value") {
private object AddressesSpec : Configuration.Specification<Addresses>("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<Address> {
return Address.validFromRawValue(rawValue) { error -> Configuration.Validation.Error.BadValue.of(error) }
}
}
private val addresses by nested(AddressesSpec)
override fun parseValid(configuration: Config): Valid<RpcSettings> {
val addresses = configuration[addresses]
return valid(RpcSettings(addresses.principal, addresses.admin))
}
}
}
}
private fun Configuration.Version.Extractor.parseRequired(config: Config, options: Configuration.Validation.Options = Configuration.Validation.Options.defaults) = parse(config, options).map { it ?: throw IllegalStateException("Absent version value.") }

View File

@ -0,0 +1,3 @@
# common-validation
This module provides basic types to facilitate multi-step validation of typed values.

View File

@ -0,0 +1,19 @@
apply plugin: 'kotlin'
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
testCompile project(":test-utils")
}
jar {
baseName 'common-validation'
}
publish {
name jar.baseName
}

View File

@ -0,0 +1,156 @@
package net.corda.common.validation.internal
import java.util.Collections.emptySet
/**
* A monad, providing information about validation result.
* It wraps either a valid [TARGET] or a set of [ERROR].
*/
interface Validated<TARGET, ERROR> {
/**
* The valid [TARGET] value.
*
* @throws IllegalStateException if accessed in presence of validation errors.
*/
val value: TARGET
/**
* The errors produced during validation, if any.
*/
val errors: Set<ERROR>
/**
* Whether a valid [TARGET] value is present.
*/
val isValid: Boolean get() = errors.isEmpty()
/**
* Whether there were validation errors.
*/
val isInvalid: Boolean get() = !isValid
/**
* Returns the underlying value as optional, with a null result instead of an exception if validation rules were violated.
*/
val optional: TARGET? get() = if (isValid) value else null
/**
* Returns a valid [TARGET] if no validation errors are present. Otherwise, it throws the exception produced by [exceptionOnErrors], defaulting to [IllegalStateException].
*
* @throws IllegalStateException or the result of [exceptionOnErrors] if there are errors.
*/
fun valueOrThrow(exceptionOnErrors: (Set<ERROR>) -> Exception = { errors -> IllegalStateException(errors.joinToString(System.lineSeparator())) }): TARGET
/**
* Applies the [convert] function to the [TARGET] value, if valid. Otherwise, returns a [Validated] monad with a [MAPPED] generic type and the current errors set.
*/
fun <MAPPED> map(convert: (TARGET) -> MAPPED): Validated<MAPPED, ERROR>
/**
* Applies the [convert] function to the [TARGET] value, if valid, returning its [Validated] output. Otherwise, returns a [Validated] monad with a [MAPPED] generic type and the current errors set.
*/
fun <MAPPED> mapValid(convert: (TARGET) -> Validated<MAPPED, ERROR>): Validated<MAPPED, ERROR>
/**
* Applies the [convertError] function to the errors set, if not empty. Otherwise, returns a [Validated] wrapper with a [MAPPED_ERROR] generic type.
*/
fun <MAPPED_ERROR> mapErrors(convertError: (ERROR) -> MAPPED_ERROR): Validated<TARGET, MAPPED_ERROR>
/**
* Performs the given [action] if the underlying value is valid.
* @return itself for fluent chained invocation.
*/
fun doIfValid(action: (TARGET) -> Unit): Validated<TARGET, ERROR> {
if (isValid) {
action.invoke(value)
}
return this
}
/**
* Performs the given [action] if the underlying value is invalid.
* @return itself for fluent chained invocation.
*/
fun doOnErrors(action: (Set<ERROR>) -> Unit): Validated<TARGET, ERROR> {
if (isInvalid) {
action.invoke(errors)
}
return this
}
companion object {
/**
* Constructs a [Validated] wrapper with given valid [target] value and no errors.
*/
fun <T, E> valid(target: T): Validated.Result<T, E> = Validated.Result.Successful(target)
/**
* Constructs an invalid [Validated] wrapper with given errors and no value.
*/
fun <T, E> invalid(errors: Set<E>): Validated.Result<T, E> = Validated.Result.Unsuccessful(errors)
/**
* @see invalid
*/
fun <T, E> invalid(vararg errors: E): Validated.Result<T, E> = invalid(errors.toSet())
/**
* Constructs a [Validated] wrapper with valid [T] value if [errors] is empty. Otherwise, it constructs an invalid wrapper with no value.
*
* @see valid
* @see invalid
*/
fun <T, E> withResult(target: T, errors: Set<E>): Validated<T, E> = if (errors.isEmpty()) valid(target) else invalid(errors)
}
/**
* Models the result of validating a [TARGET] value, producing [ERROR]s if rules are violated.
*/
sealed class Result<TARGET, ERROR> : Validated<TARGET, ERROR> {
/**
* A successful validation result, containing a valid [TARGET] value and no [ERROR]s.
*/
class Successful<TARGET, ERROR>(override val value: TARGET) : Result<TARGET, ERROR>(), Validated<TARGET, ERROR> {
override val errors: Set<ERROR> = emptySet<ERROR>()
override fun valueOrThrow(exceptionOnErrors: (Set<ERROR>) -> Exception) = value
override fun <MAPPED> map(convert: (TARGET) -> MAPPED): Validated<MAPPED, ERROR> {
return valid(convert.invoke(value))
}
override fun <MAPPED> mapValid(convert: (TARGET) -> Validated<MAPPED, ERROR>): Validated<MAPPED, ERROR> {
return convert.invoke(value)
}
override fun <MAPPED_ERROR> mapErrors(convertError: (ERROR) -> MAPPED_ERROR): Validated<TARGET, MAPPED_ERROR> {
return valid(value)
}
}
/**
* An unsuccessful validation result, containing [ERROR]s and no valid [TARGET] value.
*/
class Unsuccessful<TARGET, ERROR>(override val errors: Set<ERROR>) : Result<TARGET, ERROR>(), Validated<TARGET, ERROR> {
init {
require(errors.isNotEmpty())
}
override val value: TARGET get() = throw IllegalStateException("Invalid state.")
override fun valueOrThrow(exceptionOnErrors: (Set<ERROR>) -> Exception) = throw exceptionOnErrors.invoke(errors)
override fun <MAPPED> map(convert: (TARGET) -> MAPPED): Validated<MAPPED, ERROR> {
return invalid(errors)
}
override fun <MAPPED> mapValid(convert: (TARGET) -> Validated<MAPPED, ERROR>): Validated<MAPPED, ERROR> {
return invalid(errors)
}
override fun <MAPPED_ERROR> mapErrors(convertError: (ERROR) -> MAPPED_ERROR): Validated<TARGET, MAPPED_ERROR> {
return invalid(errors.asSequence().map(convertError).toSet())
}
}
}
}

View File

@ -0,0 +1,12 @@
package net.corda.common.validation.internal
/**
* Defines validation behaviour for [TARGET] value and given [OPTIONS], raising [ERROR]s if rules are violated.
*/
interface Validator<TARGET : Any, ERROR : Any, OPTIONS> {
/**
* Validates [target] using given [options], producing a [Validated] monad wrapping either a valid [target] or a set of [ERROR]s.
*/
fun validate(target: TARGET, options: OPTIONS? = null): Validated<TARGET, ERROR>
}

View File

@ -1,20 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="info">
<Configuration status="debug">
<Properties>
<Property name="log-path">${sys:log-path:-logs}</Property>
<Property name="log-name">node-${hostName}</Property>
<Property name="archive">${log-path}/archive</Property>
<Property name="consoleLogLevel">${sys:consoleLogLevel:-error}</Property>
<Property name="defaultLogLevel">${sys:defaultLogLevel:-info}</Property>
<Property name="defaultLogLevel">${sys:log4j2.level:-info}</Property>
<Property name="consoleLogLevel">${sys:consoleLogLevel:-$defaultLogLevel}</Property>
<Property name="fileLogLevel">${sys:fileLogLevel:-$defaultLogLevel}</Property>
</Properties>
<ThresholdFilter level="trace"/>
<Appenders>
<Console name="Console-Appender" target="SYSTEM_OUT">
<PatternLayout>
<ScriptPatternSelector defaultPattern="%highlight{[%level{length=5}] %date{HH:mm:ssZ} [%t] %c{2}.%method - %msg%n %throwable{0}}{INFO=white,WARN=red,FATAL=bright red}">
<ScriptPatternSelector defaultPattern="%highlight{[%level{length=5}] %date{HH:mm:ssZ} [%t] %c{2}.%method - %msg%n%throwable{short.message}}{INFO=white,WARN=red,FATAL=bright red}">
<Script name="MDCSelector" language="javascript"><![CDATA[
result = null;
if (!logEvent.getContextData().size() == 0) {
@ -25,15 +24,14 @@
result;
]]>
</Script>
<PatternMatch key="WithMDC" pattern="%highlight{[%level{length=5}] %date{HH:mm:ssZ} [%t] %c{2}.%method - %msg %X%n}{INFO=white,WARN=red,FATAL=bright red}"/>
<PatternMatch key="WithMDC" pattern="%highlight{[%level{length=5}] %date{HH:mm:ssZ} [%t] %c{2}.%method - %msg %X%n%throwable{short.message}}{INFO=white,WARN=red,FATAL=bright red}"/>
</ScriptPatternSelector>
</PatternLayout>
<ThresholdFilter level="trace"/>
</Console>
<!-- Required for printBasicInfo -->
<Console name="Console-Appender-Println" target="SYSTEM_OUT">
<PatternLayout pattern="%msg%n%throwable{0}" />
<PatternLayout pattern="%msg%n%throwable{short.message}" />
</Console>
<!-- Will generate up to 100 log files for a given day. During every rollover it will delete
@ -42,7 +40,21 @@
fileName="${log-path}/${log-name}.log"
filePattern="${archive}/${log-name}.%date{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="[%-5level] %date{ISO8601}{UTC}Z [%t] %c{2}.%method - %msg %X%n"/>
<PatternLayout>
<ScriptPatternSelector defaultPattern="[%-5level] %date{ISO8601}{UTC}Z [%t] %c{2}.%method - %msg%n">
<Script name="MDCSelector" language="javascript"><![CDATA[
result = null;
if (!logEvent.getContextData().size() == 0) {
result = "WithMDC";
} else {
result = null;
}
result;
]]>
</Script>
<PatternMatch key="WithMDC" pattern="[%-5level] %date{ISO8601}{UTC}Z [%t] %c{2}.%method - %msg %X%n"/>
</ScriptPatternSelector>
</PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy/>
@ -66,7 +78,7 @@
<Loggers>
<Root level="${defaultLogLevel}">
<AppenderRef ref="Console-Appender" level="${consoleLogLevel}"/>
<AppenderRef ref="RollingFile-Appender" />
<AppenderRef ref="RollingFile-Appender" level="${fileLogLevel}"/>
</Root>
<Logger name="BasicInfo" additivity="false">
<AppenderRef ref="Console-Appender-Println"/>

View File

@ -7,6 +7,8 @@ release, see :doc:`upgrade-notes`.
Unreleased
----------
* New "validate-configuration" sub-command to `corda.jar`, allowing to validate the actual node configuration without starting the node.
* Introduced new optional network bootstrapper command line option (--minimum-platform-version) to set as a network parameter
* Introduce minimum and target platform version for CorDapps.

View File

@ -77,6 +77,9 @@ Parameters:
``install-shell-extensions``: Install ``corda`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
``validate-configuration``: Validates the actual configuration without starting the node.
.. _enabling-remote-debugging:
Enabling remote debugging

View File

@ -53,6 +53,12 @@ operator fun <T : Any> Config.getValue(receiver: Any, metadata: KProperty<*>): T
return getValueInternal(metadata.name, metadata.returnType, UnknownConfigKeysPolicy.IGNORE::handle)
}
// Problems:
// - Forces you to have a primary constructor with all fields of name and type matching the configuration file structure.
// - 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.
// - 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 <T : Any> Config.parseAs(clazz: KClass<T>, onUnknownKeys: ((Set<String>, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle, nestedPath: String? = null): T {
// Use custom parser if provided, instead of treating the object as data class.
clazz.findAnnotation<CustomConfigParser>()?.let { return uncheckedCast(it.parser.createInstance().parse(this)) }

View File

@ -72,6 +72,7 @@ dependencies {
compile project(':client:rpc')
compile project(':tools:shell')
compile project(':tools:cliutils')
compile project(':common-validation')
// Log4J: logging framework (with SLF4J bindings)
compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"

View File

@ -2,11 +2,9 @@ package net.corda.node
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions
import net.corda.core.internal.div
import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.NodeConfigurationImpl
import net.corda.node.services.config.parseAsNodeConfiguration
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
import picocli.CommandLine.Option
@ -39,20 +37,9 @@ open class SharedNodeCmdLineOptions {
)
var devMode: Boolean? = null
open fun loadConfig(): NodeConfiguration {
return getRawConfig().parseAsNodeConfiguration(unknownConfigKeysPolicy::handle)
}
open fun parseConfiguration(configuration: Config): NodeConfiguration = configuration.parseAsNodeConfiguration(unknownConfigKeysPolicy::handle)
protected fun getRawConfig(): Config {
val rawConfig = ConfigHelper.loadConfig(
baseDirectory,
configFile
)
if (devMode == true) {
println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}")
}
return rawConfig
}
open fun rawConfiguration(): Config = ConfigHelper.loadConfig(baseDirectory, configFile)
fun copyFrom(other: SharedNodeCmdLineOptions) {
baseDirectory = other.baseDirectory
@ -63,8 +50,8 @@ open class SharedNodeCmdLineOptions {
}
class InitialRegistrationCmdLineOptions : SharedNodeCmdLineOptions() {
override fun loadConfig(): NodeConfiguration {
return getRawConfig().parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config ->
override fun parseConfiguration(configuration: Config): NodeConfiguration {
return super.parseConfiguration(configuration).also { config ->
require(!config.devMode) { "Registration cannot occur in development mode" }
require(config.compatibilityZoneURL != null || config.networkServices != null) {
"compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode."
@ -134,15 +121,8 @@ open class NodeCmdLineOptions : SharedNodeCmdLineOptions() {
)
var networkRootTrustStorePassword: String? = null
override fun loadConfig(): NodeConfiguration {
val rawConfig = ConfigHelper.loadConfig(
baseDirectory,
configFile,
configOverrides = ConfigFactory.parseMap(mapOf("noLocalShell" to this.noLocalShell) +
if (sshdServer) mapOf("sshd" to mapOf("port" to sshdServerPort.toString())) else emptyMap<String, Any>() +
if (devMode != null) mapOf("devMode" to this.devMode) else emptyMap())
)
return rawConfig.parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config ->
override fun parseConfiguration(configuration: Config): NodeConfiguration {
return super.parseConfiguration(configuration).also { config ->
if (isRegistration) {
require(!config.devMode) { "Registration cannot occur in development mode" }
require(config.compatibilityZoneURL != null || config.networkServices != null) {
@ -151,6 +131,18 @@ open class NodeCmdLineOptions : SharedNodeCmdLineOptions() {
}
}
}
override fun rawConfiguration(): Config {
val configOverrides = mutableMapOf<String, Any>()
configOverrides += "noLocalShell" to noLocalShell
if (sshdServer) {
configOverrides += "sshd" to mapOf("port" to sshdServerPort.toString())
}
devMode?.let {
configOverrides += "devMode" to it
}
return ConfigHelper.loadConfig(baseDirectory, configFile, configOverrides = ConfigFactory.parseMap(configOverrides))
}
}

View File

@ -1,6 +1,5 @@
package net.corda.node.internal
import com.typesafe.config.ConfigException
import io.netty.channel.unix.Errors
import net.corda.cliutils.*
import net.corda.core.crypto.Crypto
@ -15,15 +14,14 @@ import net.corda.node.*
import net.corda.node.internal.Node.Companion.isInvalidJavaVersion
import net.corda.node.internal.cordapp.MultipleCordappsForFlowException
import net.corda.node.internal.subcommands.*
import net.corda.node.internal.subcommands.ValidateConfigurationCli.Companion.logConfigurationErrors
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.shouldStartLocalShell
import net.corda.node.services.config.shouldStartSSHDaemon
import net.corda.node.utilities.registration.NodeRegistrationException
import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException
import net.corda.nodeapi.internal.persistence.DatabaseMigrationException
import net.corda.nodeapi.internal.persistence.oracleJdbcDriverSerialFilter
import net.corda.tools.shell.InteractiveShell
import org.fusesource.jansi.Ansi
@ -59,15 +57,18 @@ abstract class NodeCliCommand(alias: String, description: String, val startup: N
/** Main corda entry point. */
open class NodeStartupCli : CordaCliWrapper("corda", "Runs a Corda Node") {
open val startup = NodeStartup()
@Mixin
val cmdLineOptions = NodeCmdLineOptions()
private val networkCacheCli by lazy { ClearNetworkCacheCli(startup) }
private val justGenerateNodeInfoCli by lazy { GenerateNodeInfoCli(startup) }
private val justGenerateRpcSslCertsCli by lazy { GenerateRpcSslCertsCli(startup) }
private val initialRegistrationCli by lazy { InitialRegistrationCli(startup) }
private val validateConfigurationCli by lazy { ValidateConfigurationCli() }
override fun initLogging() = this.initLogging(cmdLineOptions.baseDirectory)
override fun additionalSubCommands() = setOf(networkCacheCli, justGenerateNodeInfoCli, justGenerateRpcSslCertsCli, initialRegistrationCli)
override fun additionalSubCommands() = setOf(networkCacheCli, justGenerateNodeInfoCli, justGenerateRpcSslCertsCli, initialRegistrationCli, validateConfigurationCli)
override fun runProgram(): Int {
return when {
@ -106,9 +107,6 @@ open class NodeStartupCli : CordaCliWrapper("corda", "Runs a Corda Node") {
})
}
}
@Mixin
val cmdLineOptions = NodeCmdLineOptions()
}
/** This class provides a common set of functionality for starting a Node from command line arguments. */
@ -142,13 +140,7 @@ open class NodeStartup : NodeStartupLogging {
Node.printBasicNodeInfo(LOGS_CAN_BE_FOUND_IN_STRING, System.getProperty("log-path"))
// Step 5. Load and validate node configuration.
val configuration = (attempt { cmdLineOptions.loadConfig() }.doOnException(handleConfigurationLoadingError(cmdLineOptions.configFile)) as? Try.Success)?.let(Try.Success<NodeConfiguration>::value)
?: return ExitCodes.FAILURE
val errors = configuration.validate()
if (errors.isNotEmpty()) {
logger.error("Invalid node configuration. Errors were:${System.lineSeparator()}${errors.joinToString(System.lineSeparator())}")
return ExitCodes.FAILURE
}
val configuration = cmdLineOptions.nodeConfiguration().doOnErrors { errors -> logConfigurationErrors(errors, cmdLineOptions.configFile) }.optional ?: return ExitCodes.FAILURE
// Step 6. Configuring special serialisation requirements, i.e., bft-smart relies on Java serialization.
attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success
@ -441,23 +433,6 @@ interface NodeStartupLogging {
else -> error.logAsUnexpected("Exception during node startup")
}
}
fun handleConfigurationLoadingError(configFile: Path) = { error: Exception ->
when (error) {
is UnknownConfigurationKeysException -> error.logAsExpected()
is ConfigException.IO -> error.logAsExpected(configFileNotFoundMessage(configFile), ::println)
else -> error.logAsUnexpected("Unexpected error whilst reading node configuration")
}
}
private fun configFileNotFoundMessage(configFile: Path): String {
return """
Unable to load the node config file from '$configFile'.
Try setting the --base-directory flag to change which directory the node
is looking in, or use the --config-file flag to specify it explicitly.
""".trimIndent()
}
}
fun CliWrapperBase.initLogging(baseDirectory: Path) {

View File

@ -5,7 +5,7 @@ import net.corda.node.internal.NodeCliCommand
import net.corda.node.internal.NodeStartup
import net.corda.node.internal.RunAfterNodeInitialisation
class ClearNetworkCacheCli(startup: NodeStartup): NodeCliCommand("clear-network-cache", "Clears local copy of network map, on node startup it will be restored from server or file system.", startup) {
class ClearNetworkCacheCli(startup: NodeStartup): NodeCliCommand("clear-network-cache", "Clear local copy of network map, on node startup it will be restored from server or file system.", startup) {
override fun runProgram(): Int {
return startup.initialiseAndRun(cmdLineOptions, object: RunAfterNodeInitialisation {
override fun run(node: Node) = node.clearNetworkMapCache()

View File

@ -5,7 +5,7 @@ import net.corda.node.internal.NodeCliCommand
import net.corda.node.internal.NodeStartup
import net.corda.node.internal.RunAfterNodeInitialisation
class GenerateNodeInfoCli(startup: NodeStartup): NodeCliCommand("generate-node-info", "Performs the node start-up tasks necessary to generate the nodeInfo file, saves it to disk, then exits.", startup) {
class GenerateNodeInfoCli(startup: NodeStartup): NodeCliCommand("generate-node-info", "Perform the node start-up tasks necessary to generate the nodeInfo file, save it to disk, then exit.", startup) {
override fun runProgram(): Int {
return startup.initialiseAndRun(cmdLineOptions, object : RunAfterNodeInitialisation {
override fun run(node: Node) {

View File

@ -13,7 +13,7 @@ import net.corda.node.utilities.saveToTrustStore
import java.io.Console
import kotlin.system.exitProcess
class GenerateRpcSslCertsCli(startup: NodeStartup): NodeCliCommand("generate-rpc-ssl-settings", "Generates the SSL key and trust stores for a secure RPC connection.", startup) {
class GenerateRpcSslCertsCli(startup: NodeStartup): NodeCliCommand("generate-rpc-ssl-settings", "Generate the SSL key and trust stores for a secure RPC connection.", startup) {
override fun runProgram(): Int {
return startup.initialiseAndRun(cmdLineOptions, GenerateRpcSslCerts())
}

View File

@ -17,7 +17,7 @@ import picocli.CommandLine.Option
import java.io.File
import java.nio.file.Path
class InitialRegistrationCli(val startup: NodeStartup): CliWrapperBase("initial-registration", "Starts initial node registration with Corda network to obtain certificate from the permissioning server.") {
class InitialRegistrationCli(val startup: NodeStartup): CliWrapperBase("initial-registration", "Start initial node registration with Corda network to obtain certificate from the permissioning server.") {
@Option(names = ["-t", "--network-root-truststore"], description = ["Network root trust store obtained from network operator."])
var networkRootTrustStorePathParameter: Path? = null

View File

@ -0,0 +1,84 @@
package net.corda.node.internal.subcommands
import com.typesafe.config.Config
import com.typesafe.config.ConfigException
import com.typesafe.config.ConfigRenderOptions
import net.corda.cliutils.CliWrapperBase
import net.corda.cliutils.ExitCodes
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
import net.corda.core.utilities.loggerFor
import net.corda.node.SharedNodeCmdLineOptions
import net.corda.node.internal.initLogging
import net.corda.node.services.config.NodeConfiguration
import picocli.CommandLine.*
import java.nio.file.Path
internal class ValidateConfigurationCli : CliWrapperBase("validate-configuration", "Validate the configuration without starting the node.") {
internal companion object {
private val logger = loggerFor<ValidateConfigurationCli>()
internal fun logConfigurationErrors(errors: Iterable<Exception>, configFile: Path) {
errors.forEach { error ->
when (error) {
is ConfigException.IO -> logger.error(configFileNotFoundMessage(configFile))
else -> logger.error("Error while parsing node configuration.", error)
}
}
}
private fun configFileNotFoundMessage(configFile: Path): String {
return """
Unable to load the node config file from '$configFile'.
Try setting the --base-directory flag to change which directory the node
is looking in, or use the --config-file flag to specify it explicitly.
""".trimIndent()
}
}
@Mixin
private val cmdLineOptions = SharedNodeCmdLineOptions()
override fun initLogging() = initLogging(cmdLineOptions.baseDirectory)
override fun runProgram(): Int {
val configuration = cmdLineOptions.nodeConfiguration()
if (configuration.isInvalid) {
logConfigurationErrors(configuration.errors, cmdLineOptions.configFile)
return ExitCodes.FAILURE
}
return ExitCodes.SUCCESS
}
}
internal fun SharedNodeCmdLineOptions.nodeConfiguration(): Valid<NodeConfiguration> = NodeConfigurationParser.invoke(this)
private object NodeConfigurationParser : (SharedNodeCmdLineOptions) -> Valid<NodeConfiguration> {
private val logger = loggerFor<ValidateConfigurationCli>()
private val configRenderingOptions = ConfigRenderOptions.defaults().setComments(false).setOriginComments(false).setFormatted(true)
override fun invoke(cmds: SharedNodeCmdLineOptions): Valid<NodeConfiguration> {
return attempt(cmds::rawConfiguration).doIfValid(::log).attemptMap(cmds::parseConfiguration).mapValid(::validate)
}
internal fun log(config: Config) = logger.debug("Actual configuration:\n${config.root().render(configRenderingOptions)}")
private fun validate(configuration: NodeConfiguration): Valid<NodeConfiguration> {
return Validated.withResult(configuration, configuration.validate().asSequence().map { error -> IllegalArgumentException(error) }.toSet())
}
private fun <VALUE, MAPPED> Valid<VALUE>.attemptMap(convert: (VALUE) -> MAPPED): Valid<MAPPED> = mapValid { value -> attempt { convert.invoke(value) } }
private fun <VALUE> attempt(action: () -> VALUE): Valid<VALUE> {
return try {
valid(action.invoke())
} catch (exception: Exception) {
return invalid(exception)
}
}
}
private typealias Valid<TARGET> = Validated<TARGET, Exception>

View File

@ -14,10 +14,14 @@ import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.flows.StateMachineRunId
import net.corda.core.identity.Party
import net.corda.core.internal.extractFile
import net.corda.core.internal.uncheckedCast
import net.corda.core.messaging.*
import net.corda.core.node.services.Vault
import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.AttachmentQueryCriteria
import net.corda.core.node.services.vault.ColumnPredicate
import net.corda.core.node.services.vault.EqualityComparisonOperator
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
@ -52,6 +56,7 @@ import org.junit.Before
import org.junit.Test
import rx.Observable
import java.io.ByteArrayOutputStream
import java.util.jar.JarInputStream
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
@ -285,6 +290,51 @@ class CordaRPCOpsImplTest {
}
}
@Test
fun `can upload attachment with metadata`() {
withPermissions(invokeRpc(CordaRPCOps::uploadAttachmentWithMetadata), invokeRpc(CordaRPCOps::attachmentExists)) {
val inputJar = Thread.currentThread().contextClassLoader.getResourceAsStream(testJar)
val secureHash = rpc.uploadAttachmentWithMetadata(inputJar, "Iron Fist", "Season 2")
assertTrue(rpc.attachmentExists(secureHash))
}
}
@Test
fun `attachment uploaded with metadata has specified filename`() {
withPermissions(invokeRpc(CordaRPCOps::uploadAttachmentWithMetadata), invokeRpc(CordaRPCOps::queryAttachments)) {
val inputJar = Thread.currentThread().contextClassLoader.getResourceAsStream(testJar)
rpc.uploadAttachmentWithMetadata(inputJar, "The Punisher", "Season 1")
assertEquals(
rpc.queryAttachments(
AttachmentQueryCriteria.AttachmentsQueryCriteria(
filenameCondition = ColumnPredicate.EqualityComparison(
EqualityComparisonOperator.EQUAL,
"Season 1"
)
), null
).size, 1
)
}
}
@Test
fun `attachment uploaded with metadata has specified uploader`() {
withPermissions(invokeRpc(CordaRPCOps::uploadAttachmentWithMetadata), invokeRpc(CordaRPCOps::queryAttachments)) {
val inputJar = Thread.currentThread().contextClassLoader.getResourceAsStream(testJar)
rpc.uploadAttachmentWithMetadata(inputJar, "Daredevil", "Season 3")
assertEquals(
rpc.queryAttachments(
AttachmentQueryCriteria.AttachmentsQueryCriteria(
uploaderCondition = ColumnPredicate.EqualityComparison(
EqualityComparisonOperator.EQUAL,
"Daredevil"
)
), null
).size, 1
)
}
}
@Test
fun `attempt to start non-RPC flow`() {
withPermissions(startFlow<NonRPCFlow>()) {

View File

@ -34,7 +34,7 @@ To create a new release version in JIRA, you can run the following command:
$ ./test-manager create-version <PRODUCT> <VERSION> <CANDIDATE>
```
Note that `<CANDIDATE>` is optional. This command will create new versions in the following JIRA projects: `CORDA`, `ENT`, `ENM`, `CID` and `R3T`.
Note that `<CANDIDATE>` is optional and can either be a short integer representing a release candidate, or an eight digit date (on the format YYYYMMDD) for a release snapshot. This command will create new versions in the following JIRA projects: `CORDA`, `ENT`, `ENM`, `CID` and `R3T`.
## Create Release Tests

View File

@ -74,6 +74,7 @@ class Jira:
if dry_run:
return Issue(self, fields=fields)
try:
fields['labels'] = filter(lambda x: x is not None, fields['labels'])
issue = self.jira.create_issue(fields)
return Issue(self, issue=issue)
except JIRAError as error:

View File

@ -75,15 +75,23 @@ def list_test_cases(args):
print()
# }}}
# {{{ format_candidate() - Format a candidate number
def format_candidate(candidate):
if candidate > 100:
return '({})'.format(candidate)
else:
return 'RC{:02d}'.format(candidate)
# }}}
# {{{ show_status() - Show the status of all test runs for a specific release or release candidate
def show_status(args):
user, password = login('jira', args.user)
if not user or not password: sys.exit(1)
jira = Jira().login(user, password)
version = '{} {}'.format(product_map[args.PRODUCT], args.VERSION).replace('.0', '')
candidate = '{} RC{:02d}'.format(version, args.CANDIDATE) if args.CANDIDATE else version
candidate = '{} {}'.format(version, format_candidate(args.CANDIDATE)) if args.CANDIDATE else version
if args.CANDIDATE:
print(u'Status of test runs for {} version {} release candidate {}:'.format(yellow(product_map[args.PRODUCT]), yellow(args.VERSION), yellow('RC{:02d}'.format(args.CANDIDATE))))
print(u'Status of test runs for {} version {} release candidate {}:'.format(yellow(product_map[args.PRODUCT]), yellow(args.VERSION), yellow(format_candidate(args.CANDIDATE))))
else:
print(u'Status of test runs for {} version {}:'.format(yellow(product_map[args.PRODUCT]), yellow(args.VERSION)))
if args.verbose:
@ -141,7 +149,7 @@ def create_version(args):
if not user or not password: sys.exit(1)
jira = Jira().login(user, password)
version = '{} {}'.format(product_map[args.PRODUCT], args.VERSION).replace('.0', '')
version = '{} RC{:02d}'.format(version, args.CANDIDATE) if args.CANDIDATE else version
version = '{} {}'.format(version, format_candidate(args.CANDIDATE)) if args.CANDIDATE else version
confirm(u'Create new version {}?'.format(yellow(version)), auto_yes=args.yes or args.dry_run)
print()
if not args.dry_run:
@ -229,8 +237,8 @@ def create_release_candidate(args):
jira = Jira().login(user, password)
version = '{} {}'.format(product_map[args.PRODUCT], args.VERSION).replace('.0', '')
CANDIDATE = args.CANDIDATE[0]
candidate = '{} RC{:02d}'.format(version, CANDIDATE)
confirm(u'Create test run tickets for {} version {} release candidate {}?'.format(yellow(product_map[args.PRODUCT]), yellow(args.VERSION), yellow('RC{:02d}'.format(CANDIDATE))), auto_yes=args.yes or args.dry_run)
candidate = '{} {}'.format(version, format_candidate(CANDIDATE))
confirm(u'Create test run tickets for {} version {} release candidate {}?'.format(yellow(product_map[args.PRODUCT]), yellow(args.VERSION), yellow(format_candidate(CANDIDATE))), auto_yes=args.yes or args.dry_run)
if args.verbose:
print(faint('[{}]'.format(QUERY_LIST_TEST_INSTANCES.format(args.PRODUCT, version))))
print()
@ -247,7 +255,7 @@ def create_release_candidate(args):
continue
print()
has_tests = True
print(u' - Creating test run ticket for release candidate {} ...'.format(yellow('RC{:02d}'.format(CANDIDATE))))
print(u' - Creating test run ticket for release candidate {} ...'.format(yellow(format_candidate(CANDIDATE))))
if args.verbose:
print(faint(u' [{}]'.format(QUERY_LIST_TEST_RUN_FOR_TICKET.format(args.PRODUCT, candidate, issue.key))))
has_test_instance_for_version = len(list(jira.search(QUERY_LIST_TEST_RUN_FOR_TICKET.format(args.PRODUCT, candidate, issue.key))))
@ -291,7 +299,7 @@ def main():
nargs = '?'
else:
nargs = 1
command.add('CANDIDATE', help='the number of the release candidate, e.g., 1 for RC01', type=int, nargs=nargs)
command.add('CANDIDATE', help='the number of the release candidate, e.g., 1 for RC01, or an 8 digit date in the format YYYYMMDD for snapshot releases', type=int, nargs=nargs)
with program.command('list-tests', 'list test cases applicable to the provided specification', list_test_cases) as command:
mixin_product(command)

View File

@ -85,6 +85,14 @@ include 'tools:notary-healthcheck:client'
include 'notary:jpa'
include 'notary:mysql'
// Common libraries - start
include 'common-validation'
project(":common-validation").projectDir = new File("$settingsDir/common/validation")
include 'common-configuration-parsing'
project(":common-configuration-parsing").projectDir = new File("$settingsDir/common/configuration-parsing")
// Common libraries - end
apply from: 'buildCacheSettings.gradle'
if (JavaVersion.current() == JavaVersion.VERSION_1_8) {
@ -94,3 +102,4 @@ if (JavaVersion.current() == JavaVersion.VERSION_1_8) {
include 'core-deterministic:testing:verifier'
include 'serialization-deterministic'
}

View File

@ -821,6 +821,7 @@ class DriverDSLImpl(
"visualvm.display.name" to "corda-${config.corda.myLegalName}"
)
debugPort?.let {
systemProperties += "log4j2.level" to "debug"
systemProperties += "log4j2.debug" to "true"
}