Merge pull request #1589 from corda/dominic-merge-22-11-2018

OS merge Dominic 2018-11-22
This commit is contained in:
Shams Asari 2018-11-23 16:10:01 +00:00 committed by GitHub
commit 8eb89a3bbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
150 changed files with 4038 additions and 4573 deletions

View File

@ -4041,7 +4041,7 @@ public static final class net.corda.core.node.services.vault.QueryCriteria$Fungi
@Nullable
public final java.util.List<net.corda.core.identity.AbstractParty> getOwner()
@Nullable
public final java.util.List<net.corda.core.identity.AbstractParty> getParticipants()
public java.util.List<net.corda.core.identity.AbstractParty> getParticipants()
@Nullable
public final net.corda.core.node.services.vault.ColumnPredicate<Long> getQuantity()
@NotNull
@ -4078,7 +4078,7 @@ public static final class net.corda.core.node.services.vault.QueryCriteria$Linea
@Nullable
public final java.util.List<String> getExternalId()
@Nullable
public final java.util.List<net.corda.core.identity.AbstractParty> getParticipants()
public java.util.List<net.corda.core.identity.AbstractParty> getParticipants()
@NotNull
public net.corda.core.node.services.Vault$StateStatus getStatus()
@Nullable
@ -4305,16 +4305,16 @@ public abstract class net.corda.core.node.services.vault.SortAttribute extends j
##
@CordaSerializable
public static final class net.corda.core.node.services.vault.SortAttribute$Custom extends net.corda.core.node.services.vault.SortAttribute
public <init>(Class<? extends net.corda.core.schemas.PersistentState>, String)
public <init>(Class<? extends net.corda.core.schemas.StatePersistable>, String)
@NotNull
public final Class<? extends net.corda.core.schemas.PersistentState> component1()
public final Class<? extends net.corda.core.schemas.StatePersistable> component1()
@NotNull
public final String component2()
@NotNull
public final net.corda.core.node.services.vault.SortAttribute$Custom copy(Class<? extends net.corda.core.schemas.PersistentState>, String)
public final net.corda.core.node.services.vault.SortAttribute$Custom copy(Class<? extends net.corda.core.schemas.StatePersistable>, String)
public boolean equals(Object)
@NotNull
public final Class<? extends net.corda.core.schemas.PersistentState> getEntityStateClass()
public final Class<? extends net.corda.core.schemas.StatePersistable> getEntityStateClass()
@NotNull
public final String getEntityStateColumnName()
public int hashCode()

358
.idea/compiler.xml generated
View File

@ -1,358 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<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" />
<module name="capsule-crr-submission_test" target="1.8" />
<module name="capsule-hsm-cert-generator_main" target="1.8" />
<module name="capsule-hsm-cert-generator_test" target="1.8" />
<module name="capsule-hsm-crl-generator_main" target="1.8" />
<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-configuration-parsing_main" target="1.8" />
<module name="common-configuration-parsing_test" target="1.8" />
<module name="common-validation_main" target="1.8" />
<module name="common-validation_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" />
<module name="corda-project-tools_main" target="1.8" />
<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="ha-utilities_main" target="1.8" />
<module name="ha-utilities_test" target="1.8" />
<module name="health-survey_main" target="1.8" />
<module name="health-survey_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="mysql_main" target="1.8" />
<module name="mysql_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="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" />
<module name="testing-smoke-test-utils_main" target="1.8" />
<module name="testing-smoke-test-utils_test" target="1.8" />
<module name="testing-test-common_main" target="1.8" />
<module name="testing-test-common_test" target="1.8" />
<module name="testing-test-utils_main" target="1.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" />
<module name="verifier_main" target="1.8" />
<module name="verifier_test" target="1.8" />
<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>

View File

@ -79,6 +79,7 @@ buildscript {
ext.jcabi_manifests_version = '1.1'
ext.picocli_version = '3.8.0'
ext.commons_lang_version = '2.6'
ext.commons_io_version = '2.6'
// Name of the IntelliJ SDK created for the deterministic Java rt.jar.
// ext.deterministic_idea_sdk = '1.8 (Deterministic)'

View File

@ -3,7 +3,7 @@
package net.corda.client.jackson.internal
import com.fasterxml.jackson.annotation.*
import com.fasterxml.jackson.annotation.JsonCreator.Mode.DISABLED
import com.fasterxml.jackson.annotation.JsonCreator.Mode.*
import com.fasterxml.jackson.annotation.JsonInclude.Include
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParseException
@ -38,10 +38,8 @@ import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.parseAsHex
import net.corda.core.utilities.toHexString
import net.corda.serialization.internal.AllWhitelist
import net.corda.serialization.internal.amqp.SerializerFactoryBuilder
import net.corda.serialization.internal.amqp.constructorForDeserialization
import net.corda.serialization.internal.amqp.hasCordaSerializable
import net.corda.serialization.internal.amqp.propertiesForSerialization
import net.corda.serialization.internal.amqp.*
import net.corda.serialization.internal.model.LocalTypeInformation
import java.math.BigDecimal
import java.security.PublicKey
import java.security.cert.CertPath
@ -95,10 +93,11 @@ private class CordaSerializableBeanSerializerModifier : BeanSerializerModifier()
beanProperties: MutableList<BeanPropertyWriter>): MutableList<BeanPropertyWriter> {
val beanClass = beanDesc.beanClass
if (hasCordaSerializable(beanClass) && beanClass.kotlinObjectInstance == null) {
val ctor = constructorForDeserialization(beanClass)
val amqpProperties = propertiesForSerialization(ctor, beanClass, serializerFactory)
.serializationOrder
.mapNotNull { if (it.isCalculated) null else it.serializer.name }
val typeInformation = serializerFactory.getTypeInformation(beanClass)
val properties = typeInformation.propertiesOrEmptyMap
val amqpProperties = properties.mapNotNull { (name, property) ->
if (property.isCalculated) null else name
}
val propertyRenames = beanDesc.findProperties().associateBy({ it.name }, { it.internalName })
(amqpProperties - propertyRenames.values).let {
check(it.isEmpty()) { "Jackson didn't provide serialisers for $it" }

View File

@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
import net.corda.core.DoNotImplement
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TimeWindow
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature
import net.corda.core.identity.Party
import net.corda.core.internal.FetchDataFlow
@ -77,7 +78,7 @@ class NotaryFlow {
@Suspendable
protected fun notarise(notaryParty: Party): UntrustworthyData<NotarisationResponse> {
val session = initiateFlow(notaryParty)
val requestSignature = NotarisationRequest(stx.inputs, stx.id).generateSignature(serviceHub)
val requestSignature = generateRequestSignature()
return if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) {
sendAndReceiveValidating(session, requestSignature)
} else {
@ -121,5 +122,15 @@ class NotaryFlow {
return otherSideSession.sendAndReceiveWithRetry(payload)
}
}
/**
* Ensure that transaction ID instances are not referenced in the serialized form in case several input states are outputs of the
* same transaction.
*/
private fun generateRequestSignature(): NotarisationRequestSignature {
// TODO: This is not required any more once our AMQP serialization supports turning off object referencing.
val notarisationRequest = NotarisationRequest(stx.inputs.map { it.copy(txhash = SecureHash.parse(it.txhash.toString())) }, stx.id)
return notarisationRequest.generateSignature(serviceHub)
}
}
}
}

View File

@ -46,7 +46,10 @@ enum class CertRole(val validParents: NonEmptySet<CertRole?>, val isIdentity: Bo
LEGAL_IDENTITY(NonEmptySet.of(DOORMAN_CA, NODE_CA), true, true),
/** Confidential (limited visibility) identity of a legal entity. */
CONFIDENTIAL_LEGAL_IDENTITY(NonEmptySet.of(LEGAL_IDENTITY), true, false);
CONFIDENTIAL_LEGAL_IDENTITY(NonEmptySet.of(LEGAL_IDENTITY), true, false),
/** Signing certificate for Network Parameters. */
NETWORK_PARAMETERS(NonEmptySet.of(null), false, false);
companion object {
private val values by lazy(LazyThreadSafetyMode.NONE, CertRole::values)

View File

@ -516,4 +516,15 @@ fun <K, V> createSimpleCache(maxSize: Int, onEject: (MutableMap.MutableEntry<K,
}
}
fun <K,V> MutableMap<K,V>.toSynchronised(): MutableMap<K,V> = Collections.synchronizedMap(this)
fun <K, V> MutableMap<K, V>.toSynchronised(): MutableMap<K, V> = Collections.synchronizedMap(this)
private fun isPackageValid(packageName: String): Boolean = packageName.isNotEmpty() && !packageName.endsWith(".") && packageName.split(".").all { token ->
Character.isJavaIdentifierStart(token[0]) && token.toCharArray().drop(1).all { Character.isJavaIdentifierPart(it) }
}
/**
* Check if a string is a legal Java package name.
*/
fun requirePackageValid(name: String) {
require(isPackageValid(name)) { "Invalid Java package name: `$name`." }
}

View File

@ -63,4 +63,4 @@ fun validateTimeWindow(currentTime: Instant, timeWindow: TimeWindow?): NotaryErr
return if (timeWindow != null && currentTime !in timeWindow) {
NotaryError.TimeWindowInvalid(currentTime, timeWindow)
} else null
}
}

View File

@ -4,6 +4,7 @@ import net.corda.core.CordaRuntimeException
import net.corda.core.KeepForDJVM
import net.corda.core.crypto.toStringShort
import net.corda.core.identity.Party
import net.corda.core.internal.requirePackageValid
import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
@ -47,7 +48,7 @@ data class NetworkParameters(
@AutoAcceptable val epoch: Int,
@AutoAcceptable val whitelistedContractImplementations: Map<String, List<AttachmentId>>,
val eventHorizon: Duration,
@AutoAcceptable val packageOwnership: Map<JavaPackageName, PublicKey>
@AutoAcceptable val packageOwnership: Map<String, PublicKey>
) {
// DOCEND 1
@DeprecatedConstructorForDeserialization(1)
@ -92,9 +93,27 @@ data class NetworkParameters(
companion object {
private val memberPropertyPartition = NetworkParameters::class.declaredMemberProperties.asSequence()
.partition { it.isAutoAcceptable() }
private val autoAcceptableNamesAndGetters = memberPropertyPartition.first.associateBy({it.name}, {it.javaGetter})
private val autoAcceptableNamesAndGetters = memberPropertyPartition.first.associateBy({ it.name }, { it.javaGetter })
private val nonAutoAcceptableGetters = memberPropertyPartition.second.map { it.javaGetter }
val autoAcceptablePropertyNames = autoAcceptableNamesAndGetters.keys
/**
* Returns true if the [fullClassName] is in a subpackage of [packageName].
* E.g.: "com.megacorp" owns "com.megacorp.tokens.MegaToken"
*
* Note: The ownership check is ignoring case to prevent people from just releasing a jar with: "com.megaCorp.megatoken" and pretend they are MegaCorp.
* By making the check case insensitive, the node will require that the jar is signed by MegaCorp, so the attack fails.
*/
private fun owns(packageName: String, fullClassName: String) = fullClassName.startsWith("$packageName.", ignoreCase = true)
// Make sure that packages don't overlap so that ownership is clear.
private fun noOverlap(packages: Collection<String>) = packages.all { currentPackage ->
packages.none { otherPackage -> otherPackage != currentPackage && otherPackage.startsWith("${currentPackage}.") }
}
private fun KProperty1<out NetworkParameters, Any?>.isAutoAcceptable(): Boolean {
return this.findAnnotation<AutoAcceptable>() != null
}
}
init {
@ -104,6 +123,7 @@ data class NetworkParameters(
require(maxMessageSize > 0) { "maxMessageSize must be at least 1" }
require(maxTransactionSize > 0) { "maxTransactionSize must be at least 1" }
require(!eventHorizon.isNegative) { "eventHorizon must be positive value" }
packageOwnership.keys.forEach(::requirePackageValid)
require(noOverlap(packageOwnership.keys)) { "multiple packages added to the packageOwnership overlap." }
}
@ -165,7 +185,7 @@ data class NetworkParameters(
/**
* Returns the public key of the package owner of the [contractClassName], or null if not owned.
*/
fun getOwnerOf(contractClassName: String): PublicKey? = this.packageOwnership.filterKeys { it.owns(contractClassName) }.values.singleOrNull()
fun getOwnerOf(contractClassName: String): PublicKey? = this.packageOwnership.filterKeys { packageName -> owns(packageName, contractClassName) }.values.singleOrNull()
/**
* Returns true if the only properties changed in [newNetworkParameters] are [AutoAcceptable] and not
@ -173,7 +193,7 @@ data class NetworkParameters(
*/
fun canAutoAccept(newNetworkParameters: NetworkParameters, excludedParameterNames: Set<String>): Boolean {
return nonAutoAcceptableGetters.none { valueChanged(newNetworkParameters, it) } &&
autoAcceptableNamesAndGetters.none { excludedParameterNames.contains(it.key) && valueChanged(newNetworkParameters, it.value) }
autoAcceptableNamesAndGetters.none { excludedParameterNames.contains(it.key) && valueChanged(newNetworkParameters, it.value) }
}
private fun valueChanged(newNetworkParameters: NetworkParameters, getter: Method?): Boolean {
@ -197,38 +217,3 @@ data class NotaryInfo(val identity: Party, val validating: Boolean)
* version.
*/
class ZoneVersionTooLowException(message: String) : CordaRuntimeException(message)
/**
* A wrapper for a legal java package. Used by the network parameters to store package ownership.
*/
@CordaSerializable
data class JavaPackageName(val name: String) {
init {
require(isPackageValid(name)) { "Invalid Java package name: $name" }
}
/**
* Returns true if the [fullClassName] is in a subpackage of the current package.
* E.g.: "com.megacorp" owns "com.megacorp.tokens.MegaToken"
*
* Note: The ownership check is ignoring case to prevent people from just releasing a jar with: "com.megaCorp.megatoken" and pretend they are MegaCorp.
* By making the check case insensitive, the node will require that the jar is signed by MegaCorp, so the attack fails.
*/
fun owns(fullClassName: String) = fullClassName.startsWith("$name.", ignoreCase = true)
override fun toString() = name
}
// Check if a string is a legal Java package name.
private fun isPackageValid(packageName: String): Boolean = packageName.isNotEmpty() && !packageName.endsWith(".") && packageName.split(".").all { token ->
Character.isJavaIdentifierStart(token[0]) && token.toCharArray().drop(1).all { Character.isJavaIdentifierPart(it) }
}
// Make sure that packages don't overlap so that ownership is clear.
private fun noOverlap(packages: Collection<JavaPackageName>) = packages.all { currentPackage ->
packages.none { otherPackage -> otherPackage != currentPackage && otherPackage.name.startsWith("${currentPackage.name}.") }
}
private fun KProperty1<out NetworkParameters, Any?>.isAutoAcceptable(): Boolean {
return this.findAnnotation<AutoAcceptable>() != null
}

View File

@ -8,7 +8,7 @@ import net.corda.core.contracts.StateRef
import net.corda.core.contracts.UniqueIdentifier
import net.corda.core.identity.AbstractParty
import net.corda.core.node.services.Vault
import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.StatePersistable
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.OpaqueBytes
import java.time.Instant
@ -76,6 +76,7 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
open val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL
open val constraintTypes: Set<Vault.ConstraintInfo.Type> = emptySet()
open val constraints: Set<Vault.ConstraintInfo> = emptySet()
open val participants: List<AbstractParty>? = null
abstract val contractStateTypes: Set<Class<out ContractState>>?
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
return parser.parseCriteria(this)
@ -94,7 +95,8 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
val timeCondition: TimeCondition? = null,
override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL,
override val constraintTypes: Set<Vault.ConstraintInfo.Type> = emptySet(),
override val constraints: Set<Vault.ConstraintInfo> = emptySet()
override val constraints: Set<Vault.ConstraintInfo> = emptySet(),
override val participants: List<AbstractParty>? = null
) : CommonQueryCriteria() {
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
super.visit(parser)
@ -124,7 +126,7 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
* LinearStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultLinearState]
*/
data class LinearStateQueryCriteria @JvmOverloads constructor(
val participants: List<AbstractParty>? = null,
override val participants: List<AbstractParty>? = null,
val uuid: List<UUID>? = null,
val externalId: List<String>? = null,
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
@ -172,7 +174,7 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
* FungibleStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultFungibleStates]
*/
data class FungibleStateQueryCriteria(
val participants: List<AbstractParty>? = null,
override val participants: List<AbstractParty>? = null,
val quantity: ColumnPredicate<Long>? = null,
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
override val contractStateTypes: Set<Class<out ContractState>>? = null,
@ -188,7 +190,7 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
* FungibleStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultFungibleStates]
*/
data class FungibleAssetQueryCriteria @JvmOverloads constructor(
val participants: List<AbstractParty>? = null,
override val participants: List<AbstractParty>? = null,
val owner: List<AbstractParty>? = null,
val quantity: ColumnPredicate<Long>? = null,
val issuer: List<AbstractParty>? = null,
@ -231,7 +233,7 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
* Params
* [expression] refers to a (composable) type safe [CriteriaExpression]
*/
data class VaultCustomQueryCriteria<L : PersistentState> @JvmOverloads constructor(
data class VaultCustomQueryCriteria<L : StatePersistable> @JvmOverloads constructor(
val expression: CriteriaExpression<L, Boolean>,
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
override val contractStateTypes: Set<Class<out ContractState>>? = null,
@ -299,7 +301,7 @@ interface IQueryCriteriaParser : BaseQueryCriteriaParser<QueryCriteria, IQueryCr
fun parseCriteria(criteria: QueryCriteria.CommonQueryCriteria): Collection<Predicate>
fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection<Predicate>
fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection<Predicate>
fun <L : PersistentState> parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria<L>): Collection<Predicate>
fun <L : StatePersistable> parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria<L>): Collection<Predicate>
fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria): Collection<Predicate>
}

View File

@ -10,6 +10,7 @@ import net.corda.core.node.services.vault.ColumnPredicate.*
import net.corda.core.node.services.vault.EqualityComparisonOperator.*
import net.corda.core.node.services.vault.LikenessOperator.*
import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.StatePersistable
import net.corda.core.serialization.CordaSerializable
import java.lang.reflect.Field
import kotlin.jvm.internal.CallableReference
@ -234,7 +235,7 @@ sealed class SortAttribute {
* [entityStateColumnName] should reference an entity attribute name as defined by the associated mapped schema
* (for example, [CashSchemaV1.PersistentCashState::currency.name])
*/
data class Custom(val entityStateClass: Class<out PersistentState>,
data class Custom(val entityStateClass: Class<out StatePersistable>,
val entityStateColumnName: String) : SortAttribute()
}

View File

@ -212,7 +212,7 @@ open class TransactionBuilder @JvmOverloads constructor(
val refStateContractAttachments: List<AttachmentId> = referenceStateGroups
.filterNot { it.key in allContracts }
.map { refStateEntry ->
selectAttachmentThatSatisfiesConstraints(true, refStateEntry.key, refStateEntry.value.map { it.constraint }, services)
selectAttachmentThatSatisfiesConstraints(true, refStateEntry.key, refStateEntry.value, services)
}
// For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment.
@ -287,7 +287,7 @@ open class TransactionBuilder @JvmOverloads constructor(
fun selectAttachment() = selectAttachmentThatSatisfiesConstraints(
false,
contractClassName,
inputsAndOutputs.map { it.constraint }.toSet().filterNot { it in automaticConstraints },
inputsAndOutputs.filterNot { it.constraint in automaticConstraints },
services)
// This will contain the hash of the JAR that will be used by this Transaction.
@ -417,10 +417,11 @@ open class TransactionBuilder @JvmOverloads constructor(
* TODO - When the SignatureConstraint and contract version logic is in, this will need to query the attachments table and find the latest one that satisfies all constraints.
* TODO - select a version of the contract that is no older than the one from the previous transactions.
*/
private fun selectAttachmentThatSatisfiesConstraints(isReference: Boolean, contractClassName: String, constraints: List<AttachmentConstraint>, services: ServicesForResolution): AttachmentId {
private fun selectAttachmentThatSatisfiesConstraints(isReference: Boolean, contractClassName: String, states: List<TransactionState<ContractState>>, services: ServicesForResolution): AttachmentId {
val constraints = states.map { it.constraint }
require(constraints.none { it in automaticConstraints })
require(isReference || constraints.none { it is HashAttachmentConstraint })
return services.cordappProvider.getContractAttachmentID(contractClassName)!!
return services.cordappProvider.getContractAttachmentID(contractClassName) ?: throw MissingContractAttachments(states)
}
private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters) = contractClassName in networkParameters.whitelistedContractImplementations.keys

View File

@ -6,11 +6,7 @@ import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.node.JavaPackageName
import net.corda.core.transactions.LedgerTransaction
import net.corda.finance.POUNDS
import net.corda.finance.`issued by`
import net.corda.finance.contracts.asset.Cash
import net.corda.node.services.api.IdentityServiceInternal
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.DUMMY_NOTARY_NAME
@ -48,7 +44,7 @@ class PackageOwnershipVerificationTests {
doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY)
},
networkParameters = testNetworkParameters()
.copy(packageOwnership = mapOf(JavaPackageName("net.corda.core.contracts") to OWNER_KEY_PAIR.public))
.copy(packageOwnership = mapOf("net.corda.core.contracts" to OWNER_KEY_PAIR.public))
)
@Test

64
docker/build.gradle Normal file
View File

@ -0,0 +1,64 @@
evaluationDependsOn(":node:capsule")
buildscript {
repositories {
mavenLocal()
mavenCentral()
jcenter()
}
dependencies {
classpath 'com.bmuschko:gradle-docker-plugin:3.4.4'
}
}
import com.bmuschko.gradle.docker.DockerRemoteApiPlugin
import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
apply plugin: 'kotlin'
apply plugin: DockerRemoteApiPlugin
apply plugin: 'application'
// We need to set mainClassName before applying the shadow plugin.
mainClassName = 'net.corda.core.ConfigExporterMain'
apply plugin: 'com.github.johnrengelman.shadow'
dependencies{
compile project(':node')
}
shadowJar {
baseName = 'config-exporter'
classifier = null
version = null
zip64 true
}
task buildDockerFolder(dependsOn: [":node:capsule:buildCordaJAR", shadowJar]) {
doLast {
def cordaJar = project(":node:capsule").buildCordaJAR.archivePath
project.copy {
into new File(project.buildDir, "docker-temp")
from "src/bash/run-corda.sh"
from cordaJar
from shadowJar.archivePath
from "src/config/starting-node.conf"
from "src/bash/generate-config.sh"
from "src/docker/Dockerfile"
rename(cordaJar.name, "corda.jar")
rename(shadowJar.archivePath.name, "config-exporter.jar")
}
}
}
task buildOfficialDockerImage(type: DockerBuildImage, dependsOn: [buildDockerFolder]) {
final String runTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
//if we are a snapshot, append a timestamp
//if we are a release, append RELEASE
final String suffix = project.version.toString().toLowerCase().contains("snapshot") ? runTime : "RELEASE"
inputDir = new File(project.buildDir, "docker-temp")
tags = ["corda/corda-${project.version.toString().toLowerCase()}:${suffix}", "corda/corda-${project.version.toString().toLowerCase()}:latest"]
}

View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
docker run -ti \
-e MY_PUBLIC_ADDRESS="corda-node.example.com" \
-e ONE_TIME_DOWNLOAD_KEY="bbcb189e-9e4f-4b27-96db-134e8f592785" \
-e LOCALITY="London" -e COUNTRY="GB" \
-v $(pwd)/docker/config:/etc/corda \
-v $(pwd)/docker/certificates:/opt/corda/certificates \
corda/corda-4.0-snapshot:latest config-generator --testnet
docker run -ti \
--memory=2048m \
--cpus=2 \
-v $(pwd)/docker/config:/etc/corda \
-v $(pwd)/docker/certificates:/opt/corda/certificates \
-v $(pwd)/docker/persistence:/opt/corda/persistence \
-v $(pwd)/docker/logs:/opt/corda/logs \
-v $(pwd)/samples/bank-of-corda-demo/build/nodes/BankOfCorda/cordapps:/opt/corda/cordapps \
corda/corda-4.0-snapshot:latest

View File

@ -0,0 +1,27 @@
#!/usr/bin/env bash
##in this example the doorman will be running on the host machine on port 8080
##so the container must be launched with "host" networking
docker run -ti --net="host" \
-e MY_LEGAL_NAME="O=EXAMPLE,L=Berlin,C=DE" \
-e MY_PUBLIC_ADDRESS="corda.example-hoster.com" \
-e NETWORKMAP_URL="https://map.corda.example.com" \
-e DOORMAN_URL="https://doorman.corda.example.com" \
-e NETWORK_TRUST_PASSWORD="trustPass" \
-e MY_EMAIL_ADDRESS="cordauser@r3.com" \
-v $(pwd)/docker/config:/etc/corda \
-v $(pwd)/docker/certificates:/opt/corda/certificates \
corda/corda-4.0-snapshot:latest config-generator --generic
##set memory to 2gb max, and 2cores max
docker run -ti \
--memory=2048m \
--cpus=2 \
-v $(pwd)/docker/config:/etc/corda \
-v $(pwd)/docker/certificates:/opt/corda/certificates \
-v $(pwd)/docker/persistence:/opt/corda/persistence \
-v $(pwd)/docker/logs:/opt/corda/logs \
-v $(pwd)/samples/bank-of-corda-demo/build/nodes/BankOfCorda/cordapps:/opt/corda/cordapps \
-p 10200:10200 \
-p 10201:10201 \
corda/corda-4.0-snapshot:latest

View File

@ -0,0 +1,124 @@
#!/usr/bin/env bash
die() {
printf '%s\n' "$1" >&2
exit 1
}
show_help(){
echo "usage: generate-config <--testnet>|<--generic>"
echo -e "\t --testnet is used to generate config and certificates for joining TestNet"
echo -e "\t --generic is used to generate config and certificates for joining an existing Corda Compatibility Zone"
}
function generateTestnetConfig() {
RPC_PASSWORD=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) \
DB_PASSWORD=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) \
MY_PUBLIC_ADDRESS=${MY_PUBLIC_ADDRESS} \
MY_P2P_PORT=${MY_P2P_PORT} \
MY_RPC_PORT=${MY_RPC_PORT} \
MY_RPC_ADMIN_PORT=${MY_RPC_ADMIN_PORT} \
NETWORKMAP_URL='https://map.testnet.corda.network' \
DOORMAN_URL='https://doorman.testnet.corda.network' \
java -jar config-exporter.jar "TEST-NET-COMBINE" "node.conf" "/opt/corda/starting-node.conf" "${CONFIG_FOLDER}/node.conf"
}
function generateGenericCZConfig(){
: ${NETWORKMAP_URL:? '$NETWORKMAP_URL, the Compatibility Zone to join must be set as environment variable'}
: ${DOORMAN_URL:? '$DOORMAN_URL, the Doorman to use when joining must be set as environment variable'}
: ${MY_LEGAL_NAME:? '$MY_LEGAL_NAME, the X500 name to use when joining must be set as environment variable'}
: ${MY_EMAIL_ADDRESS:? '$MY_EMAIL_ADDRESS, the email to use when joining must be set as an environment variable'}
: ${NETWORK_TRUST_PASSWORD=:? '$NETWORK_TRUST_PASSWORD, the password to the network store to use when joining must be set as environment variable'}
if [[ ! -f ${CERTIFICATES_FOLDER}/${TRUST_STORE_NAME} ]]; then
die "Network Trust Root file not found"
fi
RPC_PASSWORD=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) \
DB_PASSWORD=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) \
MY_PUBLIC_ADDRESS=${MY_PUBLIC_ADDRESS} \
MY_P2P_PORT=${MY_P2P_PORT} \
MY_RPC_PORT=${MY_RPC_PORT} \
MY_RPC_ADMIN_PORT=${MY_RPC_ADMIN_PORT} \
java -jar config-exporter.jar "GENERIC-CZ" "/opt/corda/starting-node.conf" "${CONFIG_FOLDER}/node.conf"
java -Djava.security.egd=file:/dev/./urandom -Dcapsule.jvm.args="${JVM_ARGS}" -jar /opt/corda/bin/corda.jar \
initial-registration \
--base-directory=/opt/corda \
--config-file=/etc/corda/node.conf \
--network-root-truststore-password=${NETWORK_TRUST_PASSWORD} \
--network-root-truststore=${CERTIFICATES_FOLDER}/${TRUST_STORE_NAME}
}
function downloadTestnetCerts() {
if [[ ! -f ${CERTIFICATES_FOLDER}/certs.zip ]]; then
: ${ONE_TIME_DOWNLOAD_KEY:? '$ONE_TIME_DOWNLOAD_KEY must be set as environment variable'}
: ${LOCALITY:? '$LOCALITY (the locality used when registering for Testnet) must be set as environment variable'}
: ${COUNTRY:? '$COUNTRY (the country used when registering for Testnet) must be set as environment variable'}
curl -L -d "{\"x500Name\":{\"locality\":\"${LOCALITY}\", \"country\":\"${COUNTRY}\"}, \"configType\": \"INSTALLSCRIPT\", \"include\": { \"systemdServices\": false, \"cordapps\": false, \"cordaJar\": false, \"cordaWebserverJar\": false, \"scripts\": false} }" \
-H 'Content-Type: application/json' \
-X POST "https://testnet.corda.network/api/user/node/generate/one-time-key/redeem/$ONE_TIME_DOWNLOAD_KEY" \
-o "${CERTIFICATES_FOLDER}/certs.zip"
fi
rm -rf ${CERTIFICATES_FOLDER}/*.jks
unzip ${CERTIFICATES_FOLDER}/certs.zip
}
GENERATE_TEST_NET=0
GENERATE_GENERIC=0
while :; do
case $1 in
-h|-\?|--help)
show_help # Display a usage synopsis.
exit
;;
-t|--testnet)
if [[ ${GENERATE_GENERIC} = 0 ]]; then
GENERATE_TEST_NET=1
else
die 'ERROR: cannot generate config for multiple networks'
fi
;;
-g|--generic)
if [[ ${GENERATE_TEST_NET} = 0 ]]; then
GENERATE_GENERIC=1
else
die 'ERROR: cannot generate config for multiple networks'
fi
;;
--) # End of all options.
shift
break
;;
-?*)
printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2
;;
*) # Default case: No more options, so break out of the loop.
break
esac
shift
done
: ${TRUST_STORE_NAME="network-root-truststore.jks"}
: ${JVM_ARGS='-Xmx4g -Xms2g -XX:+UseG1GC'}
if [[ ${GENERATE_TEST_NET} == 1 ]]
then
: ${MY_PUBLIC_ADDRESS:? 'MY_PUBLIC_ADDRESS must be set as environment variable'}
downloadTestnetCerts
generateTestnetConfig
elif [[ ${GENERATE_GENERIC} == 1 ]]
then
: ${MY_PUBLIC_ADDRESS:? 'MY_PUBLIC_ADDRESS must be set as environment variable'}
generateGenericCZConfig
else
show_help
die "No Valid Configuration requested"
fi

10
docker/src/bash/run-corda.sh Executable file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash
: ${JVM_ARGS='-XX:+UseG1GC'}
JVM_ARGS="-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap "${JVM_ARGS}
if [[ ${JVM_ARGS} == *"Xmx"* ]]; then
echo "WARNING: the use of the -Xmx flag is not recommended within docker containers. Use the --memory option passed to the container to limit heap size"
fi
java -Djava.security.egd=file:/dev/./urandom -Dcapsule.jvm.args="${JVM_ARGS}" -jar /opt/corda/bin/corda.jar --base-directory=/opt/corda --config-file=/etc/corda/node.conf

View File

@ -0,0 +1,39 @@
myLegalName=${MY_LEGAL_NAME}
p2pAddress=${MY_PUBLIC_ADDRESS}":"${MY_P2P_PORT}
rpcSettings {
address="0.0.0.0:"${MY_RPC_PORT}
adminAddress="0.0.0.0:"${MY_RPC_ADMIN_PORT}
}
security {
authService {
dataSource {
type=INMEMORY
users=[
{
password=${RPC_PASSWORD}
permissions=[
ALL
]
user=rpcUser
}
]
}
}
}
networkServices : {
doormanURL = ${DOORMAN_URL}
networkMapURL = ${NETWORKMAP_URL}
}
detectPublicIp = false
dataSourceProperties {
dataSource {
password=${DB_PASSWORD}
url="jdbc:h2:file:/opt/corda/persistence/persistence;DB_CLOSE_ON_EXIT=FALSE;WRITE_DELAY=0;LOCK_TIMEOUT=10000"
user="sa"
}
dataSourceClassName="org.h2.jdbcx.JdbcDataSource"
}
emailAddress = ${MY_EMAIL_ADDRESS}

View File

@ -0,0 +1,70 @@
FROM azul/zulu-openjdk:8u192
RUN apt-get update && apt-get -y upgrade && apt-get -y install bash curl unzip
# Create dirs
RUN mkdir -p /opt/corda/cordapps
RUN mkdir -p /opt/corda/persistence
RUN mkdir -p /opt/corda/certificates
RUN mkdir -p /opt/corda/drivers
RUN mkdir -p /opt/corda/logs
RUN mkdir -p /opt/corda/bin
RUN mkdir -p /opt/corda/additional-node-infos
RUN mkdir -p /etc/corda
# Create corda user
RUN addgroup corda && \
useradd corda -g corda -m -d /opt/corda
WORKDIR /opt/corda
ENV CORDAPPS_FOLDER="/opt/corda/cordapps"
ENV PERSISTENCE_FOLDER="/opt/corda/persistence"
ENV CERTIFICATES_FOLDER="/opt/corda/certificates"
ENV DRIVERS_FOLDER="/opt/corda/drivers"
ENV CONFIG_FOLDER="/etc/corda"
ENV MY_P2P_PORT=10200
ENV MY_RPC_PORT=10201
ENV MY_RPC_ADMIN_PORT=10202
RUN chown -R corda:corda /opt/corda
RUN chown -R corda:corda /etc/corda
##CORDAPPS FOLDER
VOLUME ["/opt/corda/cordapps"]
##PERSISTENCE FOLDER
VOLUME ["/opt/corda/persistence"]
##CERTS FOLDER
VOLUME ["/opt/corda/certificates"]
##OPTIONAL JDBC DRIVERS FOLDER
VOLUME ["/opt/corda/drivers"]
##LOG FOLDER
VOLUME ["/opt/corda/logs"]
##ADDITIONAL NODE INFOS FOLDER
VOLUME ["/opt/corda/additional-node-infos"]
##CONFIG LOCATION
VOLUME ["/etc/corda"]
##CORDA JAR
ADD --chown=corda:corda corda.jar /opt/corda/bin/corda.jar
##CONFIG MANIPULATOR JAR
ADD --chown=corda:corda config-exporter.jar /opt/corda/config-exporter.jar
##CONFIG GENERATOR SHELL SCRIPT
ADD --chown=corda:corda generate-config.sh /opt/corda/bin/config-generator
##CORDA RUN SCRIPT
ADD --chown=corda:corda run-corda.sh /opt/corda/bin/run-corda
##BASE CONFIG FOR GENERATOR
ADD --chown=corda:corda starting-node.conf /opt/corda/starting-node.conf
##SET EXECUTABLE PERMISSIONS
RUN chmod +x /opt/corda/bin/config-generator
RUN chmod +x /opt/corda/bin/run-corda
ENV PATH=$PATH:/opt/corda/bin
EXPOSE $MY_P2P_PORT
EXPOSE $MY_RPC_PORT
USER "corda"
CMD ["run-corda"]

View File

@ -0,0 +1,84 @@
@file:JvmName("ConfigExporterMain")
package net.corda.core
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions
import com.typesafe.config.ConfigValueFactory
import net.corda.common.configuration.parsing.internal.Configuration
import net.corda.common.validation.internal.Validated
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.parseAsNodeConfiguration
import net.corda.nodeapi.internal.config.toConfig
import net.corda.nodeapi.internal.config.toConfigValue
import java.io.File
class ConfigExporter {
fun combineTestNetWithOurConfig(testNetConf: String, ourConf: String, outputFile: String) {
var ourParsedConfig = ConfigFactory.parseFile(File(ourConf))
val testNetParsedConfig = ConfigFactory.parseFile(File(testNetConf))
ourParsedConfig = ourParsedConfig.withValue("keyStorePassword", testNetParsedConfig.getValue("keyStorePassword"))
ourParsedConfig = ourParsedConfig.withValue("myLegalName", testNetParsedConfig.getValue("myLegalName"))
ourParsedConfig = ourParsedConfig.withValue("trustStorePassword", testNetParsedConfig.getValue("trustStorePassword"))
ourParsedConfig = ourParsedConfig.withValue("emailAddress", testNetParsedConfig.getValue("emailAddress"))
File(outputFile).writer().use { fileWriter ->
val finalConfig = ourParsedConfig.parseAsNodeConfigWithFallback().value().toConfig()
var configToWrite = ConfigFactory.empty()
ourParsedConfig.entrySet().sortedBy { it.key }.forEach { configEntry ->
//use all keys present in "ourConfig" but get values from "finalConfig"
val keyWithoutQuotes = configEntry.key.replace("\"", "")
println("creating config key: $keyWithoutQuotes with value: ${finalConfig.getValue(keyWithoutQuotes)}")
configToWrite = configToWrite.withValue(keyWithoutQuotes, finalConfig.getValue(keyWithoutQuotes))
}
fileWriter.write(configToWrite.root().render(ConfigRenderOptions.concise().setFormatted(true).setJson(false)))
}
}
fun buildGenericCZConfig(ourConf: String, outputFile: String){
val ourParsedConfig = ConfigFactory.parseFile(File(ourConf))
File(outputFile).writer().use { fileWriter ->
val finalConfig = ourParsedConfig.parseAsNodeConfigWithFallback().value().toConfig()
var configToWrite = ConfigFactory.empty()
ourParsedConfig.entrySet().sortedBy { it.key }.forEach { configEntry ->
//use all keys present in "ourConfig" but get values from "finalConfig"
val keyWithoutQuotes = configEntry.key.replace("\"", "")
println("creating config key: $keyWithoutQuotes with value: ${finalConfig.getValue(keyWithoutQuotes)}")
configToWrite = configToWrite.withValue(keyWithoutQuotes, finalConfig.getValue(keyWithoutQuotes))
}
fileWriter.write(configToWrite.root().render(ConfigRenderOptions.concise().setFormatted(true).setJson(false)))
}
}
}
fun Config.parseAsNodeConfigWithFallback(): Validated<NodeConfiguration, Configuration.Validation.Error> {
val referenceConfig = ConfigFactory.parseResources("reference.conf")
val nodeConfig = this
.withValue("baseDirectory", ConfigValueFactory.fromAnyRef("/opt/corda"))
.withFallback(referenceConfig)
.resolve()
return nodeConfig.parseAsNodeConfiguration()
}
fun main(args: Array<String>) {
val configExporter = ConfigExporter()
val command = args[0]
when (command) {
"TEST-NET-COMBINE" -> {
val testNetConf = args[1]
val ourConf = args[2]
val outputFile = args[3]
configExporter.combineTestNetWithOurConfig(testNetConf, ourConf, outputFile)
}
"GENERIC-CZ" -> {
val ourConf = args[1]
val outputFile = args[2]
configExporter.buildGenericCZConfig(ourConf, outputFile)
}
else -> {
throw IllegalArgumentException("Unknown command: $command")
}
}
}

View File

@ -164,7 +164,7 @@ useful if off-ledger data must be maintained in conjunction with on-ledger state
as a custom schema. See Samples below.
The code snippet below defines a ``PersistentFoo`` type inside ``FooSchemaV1``. Note that ``PersistentFoo`` is added to
a list of mapped types which is passed to ``MappedSChema``. This is exactly how state schemas are defined, except that
a list of mapped types which is passed to ``MappedSchema``. This is exactly how state schemas are defined, except that
the entity in this case should not subclass ``PersistentState`` (as it is not a state object). See examples:
.. container:: codeset
@ -173,7 +173,6 @@ the entity in this case should not subclass ``PersistentState`` (as it is not a
public class FooSchema {}
@CordaSerializable
public class FooSchemaV1 extends MappedSchema {
FooSchemaV1() {
super(FooSchema.class, 1, ImmutableList.of(PersistentFoo.class));
@ -208,9 +207,8 @@ Instances of ``PersistentFoo`` can be persisted inside a flow as follows:
.. sourcecode:: java
PersistentFoo foo = new PersistentFoo(new UniqueIdentifier().getId().toString(), "Bar");
node.getServices().withEntityManager(entityManager -> {
serviceHub.withEntityManager(entityManager -> {
entityManager.persist(foo);
entityManager.flush();
return null;
});

View File

@ -1,3 +1,9 @@
.. highlight:: kotlin
.. raw:: html
<script type="text/javascript" src="_static/jquery.js"></script>
<script type="text/javascript" src="_static/codesets.js"></script>
API: Vault Query
================
@ -569,4 +575,78 @@ The Corda Tutorials provide examples satisfying these additional Use Cases:
.. _JPQL: http://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#hql
.. _JPA: https://docs.spring.io/spring-data/jpa/docs/current/reference/html
Mapping owning keys to external IDs
-----------------------------------
When creating new public keys via the ``KeyManagementService``, it is possible to create an association between the newly created public
key and an external ID. This, in effect, allows CorDapp developers to group state ownership/participation keys by an account ID.
.. note:: This only works with freshly generated public keys and *not* the node's legal identity key. If you require that the freshly
generated keys be for the node's identity then use ``PersistentKeyManagementService.freshKeyAndCert`` instead of ``freshKey``.
Currently, the generation of keys for other identities is not supported.
The code snippet below show how keys can be associated with an external ID by using the exposed JPA functionality:
.. container:: codeset
.. sourcecode:: java
public AnonymousParty freshKeyForExternalId(UUID externalId, ServiceHub services) {
// Create a fresh key pair and return the public key.
AnonymousParty anonymousParty = freshKey();
// Associate the fresh key to an external ID.
services.withEntityManager(entityManager -> {
PersistentKeyManagementService.PublicKeyHashToExternalId mapping = PersistentKeyManagementService.PublicKeyHashToExternalId(externalId, anonymousParty.owningKey);
entityManager.persist(mapping);
return null;
});
return anonymousParty;
}
.. sourcecode:: kotlin
fun freshKeyForExternalId(externalId: UUID, services: ServiceHub): AnonymousParty {
// Create a fresh key pair and return the public key.
val anonymousParty = freshKey()
// Associate the fresh key to an external ID.
services.withEntityManager {
val mapping = PersistentKeyManagementService.PublicKeyHashToExternalId(externalId, anonymousParty.owningKey)
persist(mapping)
}
return anonymousParty
}
As can be seen in the code snippet above, the ``PublicKeyHashToExternalId`` entity has been added to ``PersistentKeyManagementService``,
which allows you to associate your public keys with external IDs. So far, so good.
.. note:: Here, it is worth noting that we must map **owning keys** to external IDs, as opposed to **state objects**. This is because it
might be the case that a ``LinearState`` is owned by two public keys generated by the same node.
The intuition here is that when these public keys are used to own or participate in a state object, it is trivial to then associate those
states with a particular external ID. Behind the scenes, when states are persisted to the vault, the owning keys for each state are
persisted to a ``PersistentParty`` table. The ``PersistentParty`` table can be joined with the ``PublicKeyHashToExternalId`` table to create
a view which maps each state to one or more external IDs. The entity relationship diagram below helps to explain how this works.
.. image:: resources/state-to-external-id.png
When performing a vault query, it is now possible to query for states by external ID using a custom query criteria.
.. container:: codeset
.. sourcecode:: java
UUID id = someExternalId;
FieldInfo externalIdField = getField("externalId", VaultSchemaV1.StateToExternalId.class);
CriteriaExpression externalId = Builder.equal(externalIdField, id);
QueryCriteria query = new VaultCustomQueryCriteria(externalId);
Vault.Page<StateType> results = vaultService.queryBy(StateType.class, query);
.. sourcecode:: kotlin
val id: UUID = someExternalId
val externalId = builder { VaultSchemaV1.StateToExternalId::externalId.equal(id) }
val queryCriteria = QueryCriteria.VaultCustomQueryCriteria(externalId)
val results = vaultService.queryBy<StateType>(queryCriteria).states
The ``VaultCustomQueryCriteria`` can also be combined with other query criteria, like custom schemas, for instance. See the vault query API
examples above for how to combine ``QueryCriteria``.

View File

@ -15,6 +15,7 @@ CorDapps
upgrade-notes
upgrading-cordapps
secure-coding-guidelines
flow-overriding
corda-api
flow-cookbook
cheat-sheet

View File

@ -4,8 +4,8 @@
<script type="text/javascript" src="_static/jquery.js"></script>
<script type="text/javascript" src="_static/codesets.js"></script>
How to extend the state machine
===============================
Extending the state machine
===========================
This article explains how to extend the state machine code that underlies flow execution. It is intended for Corda
contributors.

View File

@ -0,0 +1,71 @@
Contributing philosophy
=======================
.. contents::
Mission
-------
Corda is an open source project with the aim of developing an enterprise-grade distributed ledger platform for business across a variety of
industries. Corda was designed and developed to apply the concepts of blockchain and smart contract technologies to the requirements of
modern business transactions. It is unique in its aim to build a platform for businesses to transact freely with any counter-party while
retaining strict privacy. Corda provides an implementation of this vision in a code base which others are free to build on, contribute to
or innovate around. The mission of Corda is further detailed in the `Corda introductory white paper`_.
The project is supported and maintained by the `R3 Alliance <https://www.r3.com>`_, or R3 for short, which consists of over two hundred firms
working together to build and maintain this open source enterprise-grade blockchain platform.
Community Locations
-------------------
The Corda maintainers, developers and extended community make active use of the following channels:
* The `Corda Slack team <http://slack.corda.net/>`_ for general community discussion, and in particular:
* The ``#contributing`` channel for discussions around contributing
* The ``#design`` channel for discussions around the platform's design
* The `corda-dev mailing list <https://groups.io/g/corda-dev>`_ for discussion regarding Corda's design and roadmap
* The `GitHub issues board <https://github.com/corda/corda/issues>`_ for reporting platform bugs and potential enhancements
* The `Stack Overflow corda tag <https://stackoverflow.com/questions/tagged/corda>`_ for specific technical questions
Project Leadership and Maintainers
----------------------------------
The leader of this project is currently `Mike Hearn <https://github.com/mikehearn>`_, who is also the Lead Platform Engineer at R3. The
project leader appoints the project's Community Maintainers, who are responsible for merging community contributions into the code base and
acting as points of contact.
In addition to the project leader and community maintainer(s), developers employed by R3 who have passed our technical interview process
have commit privileges to the repo. All R3 contributions undergo peer review, which is documented in public in GitHub, before they can be
merged; they are held to the same standard as all other contributions. The community is encouraged both to observe and participate in this
`review process <https://github.com/corda/corda/pulls>`_.
.. _community-maintainers:
Community maintainers
^^^^^^^^^^^^^^^^^^^^^
Current community maintainers:
* `Joel Dudley <https://github.com/joeldudleyr3>`_ - Contact via the `Corda Slack team <http://slack.corda.net/>`_, either in the
``#community`` channel or via direct message using the handle ``@joel``
We anticipate additional maintainers joining the project in the future from across the community.
Existing Contributors
---------------------
Over two hundred individuals have contributed to the development of Corda. You can find a full list of contributors in the
`CONTRIBUTORS.md list <https://github.com/corda/corda/blob/master/CONTRIBUTORS.md>`_.
Transparency and Conflict Policy
--------------------------------
The project is supported and maintained by the `R3 Alliance <https://www.r3.com>`_, which consists of over two hundred firms working together
to build and maintain this open source enterprise-grade blockchain platform. We develop in the open and publish our
`Jira <https://r3-cev.atlassian.net/projects/CORDA/summary>`_ to give everyone visibility. R3 also maintains and distributes a commercial
distribution of Corda. Our vision is that distributions of Corda be compatible and interoperable, and our contribution and code review
guidelines are designed in part to enable this.
As the R3 Alliance is maintainer of the project and also develops a commercial distribution of Corda, what happens if a member of the
community contributes a feature which the R3 team have implemented only in their commercial product? How is this apparent conflict managed?
Our approach is simple: if the contribution meets the standards for the project (see above), then the existence of a competing commercial
implementation will not be used as a reason to reject it. In other words, it is our policy that should a community feature be contributed
which meets the criteria above, we will accept it or work with the contributor to merge/reconcile it with the commercial feature.
.. _`Corda introductory white paper`: _static/corda-platform-whitepaper.pdf

View File

@ -330,9 +330,14 @@ The available config fields are listed below.
The option takes effect only in production mode and defaults to Corda development keys (``["56CA54E803CB87C8472EBD3FBC6A2F1876E814CEEBF74860BD46997F40729367",
"83088052AF16700457AE2C978A7D8AC38DD6A7C713539D00B897CD03A5E5D31D"]``), in development mode any key is allowed to sign Cordpapp JARs.
:autoAcceptNetworkParameterChanges: This flag toggles auto accepting of network parameter changes and is enabled by default. If a network operator issues a network parameter change which modifies
only auto-acceptable options and this behaviour is enabled then the changes will be accepted without any manual intervention from the node operator. See
:doc:`network-map` for more information on the update process and current auto-acceptable parameters. Set to ``false`` to disable.
:networkParameterAcceptanceSettings: Optional settings for managing the network parameter auto-acceptance behaviour. If not provided then the defined defaults below are used.
:autoAcceptEnabled: This flag toggles auto accepting of network parameter changes. If a network operator issues a network parameter change which modifies only
auto-acceptable options and this behaviour is enabled then the changes will be accepted without any manual intervention from the node operator. See
:doc:`network-map` for more information on the update process and current auto-acceptable parameters. Set to ``false`` to disable. Defaults to true.
:excludedAutoAcceptableParameters: List of auto-acceptable parameter names to explicitly exclude from auto-accepting. Allows a node operator to control the behaviour at a
more granular level. Defaults to an empty list.
Examples
--------

View File

@ -6,6 +6,7 @@ Nodes
node-structure
generating-a-node
docker-image
running-a-node
deploying-a-node
corda-configuration-file

View File

@ -0,0 +1,163 @@
Official Corda Docker Image
===========================
Running a Node connected to a Compatibility Zone in Docker
----------------------------------------------------------
.. note:: Requirements: A valid node.conf and a valid set of certificates - (signed by the CZ)
In this example, the certificates are stored at ``/home/user/cordaBase/certificates``, the node configuration is in ``/home/user/cordaBase/config/node.conf`` and the CorDapps to run are in ``/home/TeamCityOutput/cordapps``
.. code-block:: shell
docker run -ti \
--memory=2048m \
--cpus=2 \
-v /home/user/cordaBase/config:/etc/corda \
-v /home/user/cordaBase/certificates:/opt/corda/certificates \
-v /home/user/cordaBase/persistence:/opt/corda/persistence \
-v /home/user/cordaBase/logs:/opt/corda/logs \
-v /home/TeamCityOutput/cordapps:/opt/corda/cordapps \
-p 10200:10200 \
-p 10201:10201 \
corda/corda-4.0-snapshot:latest
As the node runs within a container, several mount points are required
1. CorDapps - CorDapps must be mounted at location ``/opt/corda/cordapps``
2. Certificates - certificates must be mounted at location ``/opt/corda/certificates``
3. Config - the node config must be mounted at location ``/etc/corda/node.config``
4. Logging - all log files will be written to location ``/opt/corda/logs``
If using the H2 database
5. Persistence - the folder to hold the H2 database files must be mounted at location ``/opt/corda/persistence``
Running a Node connected to a Bootstrapped Network
--------------------------------------------------
.. note:: Requirements: A valid node.conf, a valid set of certificates, and an existing network-parameters file
In this example, we have previously generated a network-parameters file using the bootstrapper tool, which is stored at ``/home/user/sharedFolder/network-parameters``
.. code-block:: shell
docker run -ti \
--memory=2048m \
--cpus=2 \
-v /home/user/cordaBase/config:/etc/corda \
-v /home/user/cordaBase/certificates:/opt/corda/certificates \
-v /home/user/cordaBase/persistence:/opt/corda/persistence \
-v /home/user/cordaBase/logs:/opt/corda/logs \
-v /home/TeamCityOutput/cordapps:/opt/corda/cordapps \
-v /home/user/sharedFolder/node-infos:/opt/corda/additional-node-infos \
-v /home/user/sharedFolder/network-parameters:/opt/corda/network-parameters \
-p 10200:10200 \
-p 10201:10201 \
corda/corda-4.0-snapshot:latest
There is a new mount ``/home/user/sharedFolder/node-infos:/opt/corda/additional-node-infos`` which is used to hold the ``nodeInfo`` of all the nodes within the network.
As the node within the container starts up, it will place it's own nodeInfo into this directory. This will allow other nodes also using this folder to see this new node.
Generating Configs and Certificates
===================================
It is possible to utilize the image to automatically generate a sensible minimal configuration for joining an existing Corda network.
Joining TestNet
---------------
.. note:: Requirements: A valid registration for TestNet and a one-time code for joining TestNet.
.. code-block:: shell
docker run -ti \
-e MY_PUBLIC_ADDRESS="corda-node.example.com" \
-e ONE_TIME_DOWNLOAD_KEY="bbcb189e-9e4f-4b27-96db-134e8f592785" \
-e LOCALITY="London" -e COUNTRY="GB" \
-v /home/user/docker/config:/etc/corda \
-v /home/user/docker/certificates:/opt/corda/certificates \
corda/corda-4.0-snapshot:latest config-generator --testnet
``$MY_PUBLIC_ADDRESS`` will be the public address that this node will be advertised on.
``$ONE_TIME_DOWNLOAD_KEY`` is the one-time code provided for joining TestNet.
``$LOCALITY`` and ``$COUNTRY`` must be set to the values provided when joining TestNet.
When the container has finished executing ``config-generator`` the following will be true
1. A skeleton, but sensible minimum node.conf is present in ``/home/user/docker/config``
2. A set of certificates signed by TestNet in ``/home/user/docker/certificates``
It is now possible to start the node using the generated config and certificates
.. code-block:: shell
docker run -ti \
--memory=2048m \
--cpus=2 \
-v /home/user/docker/config:/etc/corda \
-v /home/user/docker/certificates:/opt/corda/certificates \
-v /home/user/docker/persistence:/opt/corda/persistence \
-v /home/user/docker/logs:/opt/corda/logs \
-v /home/user/corda/samples/bank-of-corda-demo/build/nodes/BankOfCorda/cordapps:/opt/corda/cordapps \
-p 10200:10200 \
-p 10201:10201 \
corda/corda-4.0-snapshot:latest
Joining An Existing Compatibility Zone
--------------------------------------
.. note:: Requirements: A Compatibility Zone, the Zone Trust Root and authorisation to join said Zone.
It is possible to use the image to automate the process of joining an existing Zone as detailed `here <joining-a-compatibility-zone.html#connecting-to-a-compatibility-zone>`__
The first step is to obtain the Zone Trust Root, and place it within a directory. In the below example, the Trust Root is stored at ``/home/user/docker/certificates/network-root-truststore.jks``.
It is possible to configure the name of the Trust Root file by setting the ``TRUST_STORE_NAME`` environment variable in the container.
.. code-block:: shell
docker run -ti --net="host" \
-e MY_LEGAL_NAME="O=EXAMPLE,L=Berlin,C=DE" \
-e MY_PUBLIC_ADDRESS="corda.example-hoster.com" \
-e NETWORKMAP_URL="https://map.corda.example.com" \
-e DOORMAN_URL="https://doorman.corda.example.com" \
-e NETWORK_TRUST_PASSWORD="trustPass" \
-e MY_EMAIL_ADDRESS="cordauser@r3.com" \
-v /home/user/docker/config:/etc/corda \
-v /home/user/docker/certificates:/opt/corda/certificates \
corda/corda-4.0-snapshot:latest config-generator --generic
Several environment variables must also be passed to the container to allow it to register:
1. ``MY_LEGAL_NAME`` - The X500 to use when generating the config. This must be the same as registered with the Zone.
2. ``MY_PUBLIC_ADDRESS`` - The public address to advertise the node on.
3. ``NETWORKMAP_URL`` - The address of the Zone's network map service (this should be provided to you by the Zone).
4. ``DOORMAN_URL`` - The address of the Zone's doorman service (this should be provided to you by the Zone).
5. ``NETWORK_TRUST_PASSWORD`` - The password to the Zone Trust Root (this should be provided to you by the Zone).
6. ``MY_EMAIL_ADDRESS`` - The email address to use when generating the config. This must be the same as registered with the Zone.
There are some optional variables which allow customisation of the generated config:
1. ``MY_P2P_PORT`` - The port to advertise the node on (defaults to 10200). If changed, ensure the container is launched with the correct published ports.
2. ``MY_RPC_PORT`` - The port to open for RPC connections to the node (defaults to 10201). If changed, ensure the container is launched with the correct published ports.
Once the container has finished performing the initial registration, the node can be started as normal
.. code-block:: shell
docker run -ti \
--memory=2048m \
--cpus=2 \
-v /home/user/docker/config:/etc/corda \
-v /home/user/docker/certificates:/opt/corda/certificates \
-v /home/user/docker/persistence:/opt/corda/persistence \
-v /home/user/docker/logs:/opt/corda/logs \
-v /home/user/corda/samples/bank-of-corda-demo/build/nodes/BankOfCorda/cordapps:/opt/corda/cordapps \
-p 10200:10200 \
-p 10201:10201 \
corda/corda-4.0-snapshot:latest

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View File

@ -74,7 +74,7 @@ couple of resources.
.. code:: bash
echo "issuableCurrencies : [ USD ]" > /opt/corda/cordapps/config/corda-finance-<VERSION>-corda.conf
echo "issuableCurrencies = [ USD ]" > /opt/corda/cordapps/config/corda-finance-<VERSION>-corda.conf
#. Restart the Corda node:

View File

@ -1,16 +1,22 @@
package net.corda.finance.compat
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.serialization.SerializedBytes
import net.corda.core.transactions.SignedTransaction
import net.corda.finance.contracts.asset.Cash
import net.corda.serialization.internal.AllWhitelist
import net.corda.serialization.internal.amqp.DeserializationInput
import net.corda.serialization.internal.amqp.Schema
import net.corda.serialization.internal.amqp.SerializationOutput
import net.corda.serialization.internal.amqp.SerializerFactoryBuilder
import net.corda.serialization.internal.amqp.custom.PublicKeySerializer
import net.corda.testing.core.SerializationEnvironmentRule
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.test.fail
// TODO: If this type of testing gets momentum, we can create a mini-framework that rides through list of files
// and performs necessary validation on all of them.
@ -20,19 +26,63 @@ class CompatibilityTest {
@JvmField
val testSerialization = SerializationEnvironmentRule()
val serializerFactory = SerializerFactoryBuilder.build(AllWhitelist, ClassLoader.getSystemClassLoader()).apply {
register(PublicKeySerializer)
}
@Test
fun issueCashTansactionReadTest() {
val inputStream = javaClass.classLoader.getResourceAsStream("compatibilityData/v3/node_transaction.dat")
assertNotNull(inputStream)
val inByteArray: ByteArray = inputStream.readBytes()
val transaction = inByteArray.deserialize<SignedTransaction>(context = SerializationDefaults.STORAGE_CONTEXT)
val input = DeserializationInput(serializerFactory)
val (transaction, envelope) = input.deserializeAndReturnEnvelope(
SerializedBytes(inByteArray),
SignedTransaction::class.java,
SerializationDefaults.STORAGE_CONTEXT)
assertNotNull(transaction)
val commands = transaction.tx.commands
assertEquals(1, commands.size)
assertTrue(commands.first().value is Cash.Commands.Issue)
// Serialize back and check that representation is byte-to-byte identical to what it was originally.
val serializedForm = transaction.serialize(context = SerializationDefaults.STORAGE_CONTEXT)
assertTrue(inByteArray.contentEquals(serializedForm.bytes))
val output = SerializationOutput(serializerFactory)
val (serializedBytes, schema) = output.serializeAndReturnSchema(transaction, SerializationDefaults.STORAGE_CONTEXT)
assertSchemasMatch(envelope.schema, schema)
assertTrue(inByteArray.contentEquals(serializedBytes.bytes))
}
private fun assertSchemasMatch(original: Schema, reserialized: Schema) {
if (original.toString() == reserialized.toString()) return
original.types.forEach { originalType ->
val reserializedType = reserialized.types.firstOrNull { it.name == originalType.name } ?:
fail("""Schema mismatch between original and re-serialized data. Could not find reserialized schema matching:
$originalType
""")
if (originalType.toString() != reserializedType.toString())
fail("""Schema mismatch between original and re-serialized data. Expected:
$originalType
but was:
$reserializedType
""")
}
reserialized.types.forEach { reserializedType ->
if (original.types.none { it.name == reserializedType.name })
fail("""Schema mismatch between original and re-serialized data. Could not find original schema matching:
$reserializedType
""")
}
}
}

View File

@ -467,6 +467,12 @@ enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurpo
KeyPurposeId.anyExtendedKeyUsage,
isCA = false,
role = CertRole.CONFIDENTIAL_LEGAL_IDENTITY
),
NETWORK_PARAMETERS(
KeyUsage(KeyUsage.digitalSignature),
isCA = false,
role = CertRole.NETWORK_PARAMETERS
)
}

View File

@ -9,7 +9,6 @@ import net.corda.core.identity.Party
import net.corda.core.internal.*
import net.corda.core.internal.concurrent.fork
import net.corda.core.internal.concurrent.transpose
import net.corda.core.node.JavaPackageName
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NodeInfo
import net.corda.core.node.NotaryInfo
@ -190,7 +189,7 @@ internal constructor(private val initSerEnv: Boolean,
}
/** Entry point for the tool */
fun bootstrap(directory: Path, copyCordapps: Boolean, minimumPlatformVersion: Int, packageOwnership: Map<JavaPackageName, PublicKey?> = emptyMap()) {
fun bootstrap(directory: Path, copyCordapps: Boolean, minimumPlatformVersion: Int, packageOwnership: Map<String, PublicKey?> = emptyMap()) {
require(minimumPlatformVersion <= PLATFORM_VERSION) { "Minimum platform version cannot be greater than $PLATFORM_VERSION" }
// Don't accidently include the bootstrapper jar as a CorDapp!
val bootstrapperJar = javaClass.location.toPath()
@ -206,7 +205,7 @@ internal constructor(private val initSerEnv: Boolean,
copyCordapps: Boolean,
fromCordform: Boolean,
minimumPlatformVersion: Int = PLATFORM_VERSION,
packageOwnership: Map<JavaPackageName, PublicKey?> = emptyMap()
packageOwnership: Map<String, PublicKey?> = emptyMap()
) {
directory.createDirectories()
println("Bootstrapping local test network in $directory")
@ -358,7 +357,7 @@ internal constructor(private val initSerEnv: Boolean,
when (netParamsFilesGrouped.size) {
0 -> return null
1 -> return netParamsFilesGrouped.keys.first().deserialize().verifiedNetworkMapCert(DEV_ROOT_CA.certificate)
1 -> return netParamsFilesGrouped.keys.first().deserialize().verifiedNetworkParametersCert(DEV_ROOT_CA.certificate)
}
val msg = StringBuilder("Differing sets of network parameters were found. Make sure all the nodes have the same " +
@ -368,7 +367,7 @@ internal constructor(private val initSerEnv: Boolean,
netParamsFiles.map { it.parent.fileName }.joinTo(msg, ", ")
msg.append(":\n")
val netParamsString = try {
bytes.deserialize().verifiedNetworkMapCert(DEV_ROOT_CA.certificate).toString()
bytes.deserialize().verifiedNetworkParametersCert(DEV_ROOT_CA.certificate).toString()
} catch (e: Exception) {
"Invalid network parameters file: $e"
}
@ -385,7 +384,7 @@ internal constructor(private val initSerEnv: Boolean,
existingNetParams: NetworkParameters?,
nodeDirs: List<Path>,
minimumPlatformVersion: Int,
packageOwnership: Map<JavaPackageName, PublicKey?>
packageOwnership: Map<String, PublicKey?>
): NetworkParameters {
// TODO Add config for maxMessageSize and maxTransactionSize
val netParams = if (existingNetParams != null) {

View File

@ -54,9 +54,9 @@ data class ParametersUpdate(
val updateDeadline: Instant
)
/** Verify that a Network Map certificate path and its [CertRole] is correct. */
fun <T : Any> SignedDataWithCert<T>.verifiedNetworkMapCert(rootCert: X509Certificate): T {
require(CertRole.extract(sig.by) == CertRole.NETWORK_MAP) { "Incorrect cert role: ${CertRole.extract(sig.by)}" }
/** Verify that a certificate path and its [CertRole] is correct. */
fun <T : Any> SignedDataWithCert<T>.verifiedCertWithRole(rootCert: X509Certificate, vararg certRoles: CertRole): T {
require(CertRole.extract(sig.by) in certRoles) { "Incorrect cert role: ${CertRole.extract(sig.by)}" }
val path = if (sig.parentCertsChain.isEmpty()) {
listOf(sig.by, rootCert)
} else {
@ -65,3 +65,15 @@ fun <T : Any> SignedDataWithCert<T>.verifiedNetworkMapCert(rootCert: X509Certifi
X509Utilities.validateCertificateChain(rootCert, path)
return verified()
}
/** Verify that a Network Map certificate path and its [CertRole] is correct. */
fun <T : Any> SignedDataWithCert<T>.verifiedNetworkMapCert(rootCert: X509Certificate): T {
return verifiedCertWithRole(rootCert, CertRole.NETWORK_MAP)
}
/** Verify that a Network Parameters certificate path and its [CertRole] is correct. */
fun <T : Any> SignedDataWithCert<T>.verifiedNetworkParametersCert(rootCert: X509Certificate): T {
// for backwards compatibility we allow network parameters to be signed with
// the networkmap cert, but going forwards we also accept the specific netparams cert as well
return verifiedCertWithRole(rootCert, CertRole.NETWORK_PARAMETERS, CertRole.NETWORK_MAP)
}

View File

@ -5,7 +5,6 @@ import net.corda.core.crypto.secureRandomBytes
import net.corda.core.crypto.sha256
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.*
import net.corda.core.node.JavaPackageName
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.serialize
@ -214,8 +213,8 @@ class NetworkBootstrapperTest {
private val ALICE = TestIdentity(ALICE_NAME, 70)
private val BOB = TestIdentity(BOB_NAME, 80)
private val alicePackageName = JavaPackageName("com.example.alice")
private val bobPackageName = JavaPackageName("com.example.bob")
private val alicePackageName = "com.example.alice"
private val bobPackageName = "com.example.bob"
@Test
fun `register new package namespace in existing network`() {
@ -238,7 +237,7 @@ class NetworkBootstrapperTest {
@Test
fun `attempt to register overlapping namespaces in existing network`() {
createNodeConfFile("alice", aliceConfig)
val greedyNamespace = JavaPackageName("com.example")
val greedyNamespace = "com.example"
bootstrap(packageOwnership = mapOf(Pair(greedyNamespace, ALICE.publicKey)))
assertContainsPackageOwner("alice", mapOf(Pair(greedyNamespace, ALICE.publicKey)))
// register overlapping package name
@ -293,7 +292,7 @@ class NetworkBootstrapperTest {
return bytes
}
private fun bootstrap(copyCordapps: Boolean = true, packageOwnership : Map<JavaPackageName, PublicKey?> = emptyMap()) {
private fun bootstrap(copyCordapps: Boolean = true, packageOwnership : Map<String, PublicKey?> = emptyMap()) {
providedCordaJar = (rootDir / "corda.jar").let { if (it.exists()) it.readAll() else null }
bootstrapper.bootstrap(rootDir, copyCordapps, PLATFORM_VERSION, packageOwnership)
}
@ -322,7 +321,7 @@ class NetworkBootstrapperTest {
}
private val Path.networkParameters: NetworkParameters get() {
return (this / NETWORK_PARAMS_FILE_NAME).readObject<SignedNetworkParameters>().verifiedNetworkMapCert(DEV_ROOT_CA.certificate)
return (this / NETWORK_PARAMS_FILE_NAME).readObject<SignedNetworkParameters>().verifiedNetworkParametersCert(DEV_ROOT_CA.certificate)
}
private val Path.nodeInfoFile: Path get() {
@ -363,7 +362,7 @@ class NetworkBootstrapperTest {
}
}
private fun assertContainsPackageOwner(nodeDirName: String, packageOwners: Map<JavaPackageName, PublicKey>) {
private fun assertContainsPackageOwner(nodeDirName: String, packageOwners: Map<String, PublicKey>) {
val networkParams = (rootDir / nodeDirName).networkParameters
assertThat(networkParams.packageOwnership).isEqualTo(packageOwners)
}

View File

@ -3,6 +3,7 @@ package net.corda.node
import com.typesafe.config.Config
import com.typesafe.config.ConfigException
import com.typesafe.config.ConfigFactory
import net.corda.cliutils.CommonCliConstants.BASE_DIR
import net.corda.common.configuration.parsing.internal.Configuration
import net.corda.common.validation.internal.Validated
import net.corda.common.validation.internal.Validated.Companion.invalid
@ -23,7 +24,7 @@ open class SharedNodeCmdLineOptions {
private val logger by lazy { loggerFor<SharedNodeCmdLineOptions>() }
}
@Option(
names = ["-b", "--base-directory"],
names = ["-b", BASE_DIR],
description = ["The node working directory where all the files are kept."]
)
var baseDirectory: Path = Paths.get(".").toAbsolutePath().normalize()

View File

@ -869,11 +869,14 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
private fun obtainIdentity(): Pair<PartyAndCertificate, KeyPair> {
val legalIdentityPrivateKeyAlias = "$NODE_IDENTITY_ALIAS_PREFIX-private-key"
if (!cryptoService.containsKey(legalIdentityPrivateKeyAlias)) {
var signingCertificateStore = configuration.signingCertificateStore.get()
if (!cryptoService.containsKey(legalIdentityPrivateKeyAlias) && !signingCertificateStore.contains(legalIdentityPrivateKeyAlias)) {
log.info("$legalIdentityPrivateKeyAlias not found in key store, generating fresh key!")
storeLegalIdentity(legalIdentityPrivateKeyAlias)
createAndStoreLegalIdentity(legalIdentityPrivateKeyAlias)
signingCertificateStore = configuration.signingCertificateStore.get() // We need to resync after [createAndStoreLegalIdentity].
} else {
checkAliasMismatch(legalIdentityPrivateKeyAlias, signingCertificateStore)
}
val signingCertificateStore = configuration.signingCertificateStore.get()
val x509Cert = signingCertificateStore.query { getCertificate(legalIdentityPrivateKeyAlias) }
// TODO: Use configuration to indicate composite key should be used instead of public key for the identity.
@ -891,22 +894,36 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
return getPartyAndCertificatePlusAliasKeyPair(certificates, legalIdentityPrivateKeyAlias)
}
// Check if a key alias exists only in one of the cryptoService and certSigningStore.
private fun checkAliasMismatch(alias: String, certificateStore: CertificateStore) {
if (cryptoService.containsKey(alias) != certificateStore.contains(alias)) {
val keyExistsIn: String = if (cryptoService.containsKey(alias)) "CryptoService" else "signingCertificateStore"
throw IllegalStateException("CryptoService and signingCertificateStore are not aligned, the entry for key-alias: $alias is only found in $keyExistsIn")
}
}
/** Loads pre-generated notary service cluster identity. */
private fun loadNotaryClusterIdentity(serviceLegalName: CordaX500Name): Pair<PartyAndCertificate, KeyPair> {
val privateKeyAlias = "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key"
val compositeKeyAlias = "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-composite-key"
val signingCertificateStore = configuration.signingCertificateStore.get()
val privateKeyAliasCertChain = try {
signingCertificateStore.query { getCertificateChain(privateKeyAlias) }
} catch (e: Exception) {
throw IllegalStateException("Certificate-chain for $privateKeyAlias cannot be found", e)
}
// A composite key is only required for BFT notaries.
val certificates = if (cryptoService.containsKey(compositeKeyAlias)) {
val certificates = if (cryptoService.containsKey(compositeKeyAlias) && signingCertificateStore.contains(compositeKeyAlias)) {
val certificate = signingCertificateStore[compositeKeyAlias]
// We have to create the certificate chain for the composite key manually, this is because we don't have a keystore
// provider that understand compositeKey-privateKey combo. The cert chain is created using the composite key certificate +
// the tail of the private key certificates, as they are both signed by the same certificate chain.
listOf(certificate) + signingCertificateStore.query { getCertificateChain(privateKeyAlias) }.drop(1)
listOf(certificate) + privateKeyAliasCertChain.drop(1)
} else {
// We assume the notary is CFT, and each cluster member shares the same notary key pair.
signingCertificateStore.query { getCertificateChain(privateKeyAlias) }
checkAliasMismatch(compositeKeyAlias, signingCertificateStore)
// If [compositeKeyAlias] does not exist, we assume the notary is CFT, and each cluster member shares the same notary key pair.
privateKeyAliasCertChain
}
val subject = CordaX500Name.build(certificates.first().subjectX500Principal)
@ -924,12 +941,12 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
return Pair(PartyAndCertificate(certPath), keyPair)
}
private fun storeLegalIdentity(alias: String): PartyAndCertificate {
private fun createAndStoreLegalIdentity(alias: String): PartyAndCertificate {
val legalIdentityPublicKey = generateKeyPair(alias)
val signingCertificateStore = configuration.signingCertificateStore.get()
val nodeCaCertPath = signingCertificateStore.value.getCertificateChain(X509Utilities.CORDA_CLIENT_CA)
val nodeCaCert = nodeCaCertPath[0] // This should be the same with signingCertificateStore[alias]
val nodeCaCert = nodeCaCertPath[0] // This should be the same with signingCertificateStore[alias].
val identityCert = X509Utilities.createCertificate(
CertificateType.LEGAL_IDENTITY,

View File

@ -6,10 +6,7 @@ import net.corda.core.node.NetworkParameters
import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger
import net.corda.node.services.network.NetworkMapClient
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME
import net.corda.nodeapi.internal.network.SignedNetworkParameters
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
import net.corda.nodeapi.internal.network.*
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.security.cert.X509Certificate
@ -93,7 +90,9 @@ class NetworkParametersReader(private val trustRoot: X509Certificate,
// By passing in just the SignedNetworkParameters object, this class guarantees that the networkParameters property
// could have only been derived from it.
class NetworkParametersAndSigned(val signed: SignedNetworkParameters, trustRoot: X509Certificate) {
val networkParameters: NetworkParameters = signed.verifiedNetworkMapCert(trustRoot)
// for backwards compatibility we allow netparams to be signed with the networkmap cert,
// but going forwards we also accept the distinct netparams cert as well
val networkParameters: NetworkParameters = signed.verifiedNetworkParametersCert(trustRoot)
operator fun component1() = networkParameters
operator fun component2() = signed
}

View File

@ -274,6 +274,9 @@ data class NodeConfigurationImpl(
if (cryptoServiceName == null && cryptoServiceConf != null) {
errors += "'cryptoServiceName' is mandatory when 'cryptoServiceConf' is specified"
}
if (notary != null && !(cryptoServiceName == null || cryptoServiceName == SupportedCryptoServices.BC_SIMPLE)) {
errors += "Notary node with a non supported 'cryptoServiceName' has been detected"
}
return errors
}

View File

@ -14,10 +14,8 @@ import org.bouncycastle.operator.ContentSigner
import java.security.KeyPair
import java.security.PrivateKey
import java.security.PublicKey
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Id
import javax.persistence.Lob
import java.util.*
import javax.persistence.*
/**
* A persistent re-implementation of [E2ETestKeyManagementService] to support node re-start.
@ -29,7 +27,7 @@ import javax.persistence.Lob
class PersistentKeyManagementService(cacheFactory: NamedCacheFactory, val identityService: PersistentIdentityService,
private val database: CordaPersistence) : SingletonSerializeAsToken(), KeyManagementServiceInternal {
@Entity
@javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}our_key_pairs")
@Table(name = "${NODE_DATABASE_PREFIX}our_key_pairs")
class PersistentKey(
@Id
@Column(name = "public_key_hash", length = MAX_HASH_HEX_SIZE, nullable = false)
@ -46,6 +44,24 @@ class PersistentKeyManagementService(cacheFactory: NamedCacheFactory, val identi
: this(publicKey.toStringShort(), publicKey.encoded, privateKey.encoded)
}
@Entity
@Table(name = "pk_hash_to_ext_id_map", indexes = [Index(name = "pk_hash_to_xid_idx", columnList = "public_key_hash")])
class PublicKeyHashToExternalId(
@Id
@GeneratedValue
@Column(name = "id", unique = true, nullable = false)
var key: Long? = null,
@Column(name = "external_id", nullable = false)
var externalId: UUID,
@Column(name = "public_key_hash", nullable = false)
var publicKeyHash: String
) {
constructor(accountId: UUID, publicKey: PublicKey)
: this(null, accountId, publicKey.toStringShort())
}
private companion object {
fun createKeyMap(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap<PublicKey, PrivateKey, PersistentKey, String> {
return AppendOnlyPersistentMap(

View File

@ -208,7 +208,7 @@ The node will shutdown now.""")
return
}
val newSignedNetParams = networkMapClient.getNetworkParameters(update.newParametersHash)
val newNetParams = newSignedNetParams.verifiedNetworkMapCert(trustRoot)
val newNetParams = newSignedNetParams.verifiedNetworkParametersCert(trustRoot)
logger.info("Downloaded new network parameters: $newNetParams from the update: $update")
newNetworkParameters = Pair(update, newSignedNetParams)
val updateInfo = ParametersUpdateInfo(
@ -233,7 +233,7 @@ The node will shutdown now.""")
// Add persisting of newest parameters from update.
val (update, signedNewNetParams) = requireNotNull(newNetworkParameters) { "Couldn't find parameters update for the hash: $parametersHash" }
// We should check that we sign the right data structure hash.
val newNetParams = signedNewNetParams.verifiedNetworkMapCert(trustRoot)
val newNetParams = signedNewNetParams.verifiedNetworkParametersCert(trustRoot)
val newParametersHash = signedNewNetParams.raw.hash
if (parametersHash == newParametersHash) {
// The latest parameters have priority.

View File

@ -44,8 +44,8 @@ class NodeSchemaService(private val extraSchemas: Set<MappedSchema> = emptySet()
PersistentIdentityService.PersistentIdentity::class.java,
PersistentIdentityService.PersistentIdentityNames::class.java,
ContractUpgradeServiceImpl.DBContractUpgrade::class.java,
RunOnceService.MutualExclusion::class.java
)){
RunOnceService.MutualExclusion::class.java,
PersistentKeyManagementService.PublicKeyHashToExternalId::class.java)) {
override val migrationResource = "node-core.changelog-master"
}
@ -82,17 +82,11 @@ class NodeSchemaService(private val extraSchemas: Set<MappedSchema> = emptySet()
// Because schema is always one supported by the state, just delegate.
override fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState {
if ((schema === VaultSchemaV1) && (state is LinearState))
return VaultSchemaV1.VaultLinearStates(state.linearId, state.participants)
return VaultSchemaV1.VaultLinearStates(state.linearId)
if ((schema === VaultSchemaV1) && (state is FungibleAsset<*>))
return VaultSchemaV1.VaultFungibleStates(state.owner, state.amount.quantity, state.amount.token.issuer.party, state.amount.token.issuer.reference, state.participants)
return VaultSchemaV1.VaultFungibleStates(state.owner, state.amount.quantity, state.amount.token.issuer.party, state.amount.token.issuer.reference)
if ((schema === VaultSchemaV1) && (state is FungibleState<*>))
return VaultSchemaV1.VaultFungibleStates(
participants = state.participants.toMutableSet(),
owner = null,
quantity = state.amount.quantity,
issuer = null,
issuerRef = null
)
return VaultSchemaV1.VaultFungibleStates(owner = null, quantity = state.amount.quantity, issuer = null, issuerRef = null)
return (state as QueryableState).generateMappedObject(schema)
}

View File

@ -17,6 +17,7 @@ import net.corda.core.node.services.vault.NullOperator.NOT_NULL
import net.corda.core.node.services.vault.QueryCriteria.CommonQueryCriteria
import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.PersistentStateRef
import net.corda.core.schemas.StatePersistable
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.trace
@ -222,7 +223,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
}
// incrementally build list of root entities (for later use in Sort parsing)
private val rootEntities = mutableMapOf<Class<out PersistentState>, Root<*>>(Pair(VaultSchemaV1.VaultStates::class.java, vaultStates))
private val rootEntities = mutableMapOf<Class<out StatePersistable>, Root<*>>(Pair(VaultSchemaV1.VaultStates::class.java, vaultStates))
private val aggregateExpressions = mutableListOf<Expression<*>>()
private val commonPredicates = mutableMapOf<Pair<String, Operator>, Predicate>() // schema attribute Name, operator -> predicate
private val constraintPredicates = mutableSetOf<Predicate>()
@ -412,13 +413,25 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
predicateSet.add(criteriaBuilder.and(vaultFungibleStates.get<ByteArray>("issuerRef").`in`(issuerRefs)))
}
// participants
// Participants.
criteria.participants?.let {
val participants = criteria.participants as List<AbstractParty>
val joinLinearStateToParty = vaultFungibleStates.joinSet<VaultSchemaV1.VaultFungibleStates, AbstractParty>("participants")
predicateSet.add(criteriaBuilder.and(joinLinearStateToParty.`in`(participants)))
criteriaQuery.distinct(true)
val participants = criteria.participants!!
// Get the persistent party entity.
val persistentPartyEntity = VaultSchemaV1.PersistentParty::class.java
val entityRoot = rootEntities.getOrElse(persistentPartyEntity) {
val entityRoot = criteriaQuery.from(persistentPartyEntity)
rootEntities[persistentPartyEntity] = entityRoot
entityRoot
}
// Add the join and participants predicates.
val statePartyJoin = criteriaBuilder.equal(vaultStates.get<VaultSchemaV1.VaultStates>("stateRef"), entityRoot.get<VaultSchemaV1.PersistentParty>("stateRef"))
val participantsPredicate = criteriaBuilder.and(entityRoot.get<VaultSchemaV1.PersistentParty>("x500Name").`in`(participants))
predicateSet.add(statePartyJoin)
predicateSet.add(participantsPredicate)
}
return predicateSet
}
@ -452,17 +465,29 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
predicateSet.add(criteriaBuilder.and(vaultLinearStates.get<String>("externalId").`in`(externalIds)))
}
// deal participants
// Participants.
criteria.participants?.let {
val participants = criteria.participants as List<AbstractParty>
val joinLinearStateToParty = vaultLinearStates.joinSet<VaultSchemaV1.VaultLinearStates, AbstractParty>("participants")
predicateSet.add(criteriaBuilder.and(joinLinearStateToParty.`in`(participants)))
criteriaQuery.distinct(true)
val participants = criteria.participants!!
// Get the persistent party entity.
val persistentPartyEntity = VaultSchemaV1.PersistentParty::class.java
val entityRoot = rootEntities.getOrElse(persistentPartyEntity) {
val entityRoot = criteriaQuery.from(persistentPartyEntity)
rootEntities[persistentPartyEntity] = entityRoot
entityRoot
}
// Add the join and participants predicates.
val statePartyJoin = criteriaBuilder.equal(vaultStates.get<VaultSchemaV1.VaultStates>("stateRef"), entityRoot.get<VaultSchemaV1.PersistentParty>("stateRef"))
val participantsPredicate = criteriaBuilder.and(entityRoot.get<VaultSchemaV1.PersistentParty>("x500Name").`in`(participants))
predicateSet.add(statePartyJoin)
predicateSet.add(participantsPredicate)
}
return predicateSet
}
override fun <L : PersistentState> parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria<L>): Collection<Predicate> {
override fun <L : StatePersistable> parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria<L>): Collection<Predicate> {
log.trace { "Parsing VaultCustomQueryCriteria: $criteria" }
val predicateSet = mutableSetOf<Predicate>()

View File

@ -149,8 +149,15 @@ class NodeVaultService(
//
// Adding a new column in the "VaultStates" table was considered the best approach.
val keys = stateOnly.participants.map { it.owningKey }
val persistentStateRef = PersistentStateRef(stateAndRef.key)
val isRelevant = isRelevant(stateOnly, keyManagementService.filterMyKeys(keys).toSet())
val constraintInfo = Vault.ConstraintInfo(stateAndRef.value.state.constraint)
// Save a row for each party in the state_party table.
// TODO: Perhaps these can be stored in a batch?
stateOnly.participants.forEach { participant ->
val persistentParty = VaultSchemaV1.PersistentParty(persistentStateRef, participant)
session.save(persistentParty)
}
val stateToAdd = VaultSchemaV1.VaultStates(
notary = stateAndRef.value.state.notary,
contractStateClassName = stateAndRef.value.state.data.javaClass.name,
@ -162,7 +169,7 @@ class NodeVaultService(
constraintType = constraintInfo.type(),
constraintData = constraintInfo.data()
)
stateToAdd.stateRef = PersistentStateRef(stateAndRef.key)
stateToAdd.stateRef = persistentStateRef
session.save(stateToAdd)
}
if (consumedStateRefs.isNotEmpty()) {

View File

@ -3,14 +3,18 @@ package net.corda.node.services.vault
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.MAX_ISSUER_REF_SIZE
import net.corda.core.contracts.UniqueIdentifier
import net.corda.core.crypto.toStringShort
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.node.services.MAX_CONSTRAINT_DATA_SIZE
import net.corda.core.node.services.Vault
import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.PersistentStateRef
import net.corda.core.schemas.StatePersistable
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.OpaqueBytes
import org.hibernate.annotations.Immutable
import org.hibernate.annotations.Type
import java.time.Instant
import java.util.*
@ -25,8 +29,18 @@ object VaultSchema
* First version of the Vault ORM schema
*/
@CordaSerializable
object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, version = 1,
mappedTypes = listOf(VaultStates::class.java, VaultLinearStates::class.java, VaultFungibleStates::class.java, VaultTxnNote::class.java)) {
object VaultSchemaV1 : MappedSchema(
schemaFamily = VaultSchema.javaClass,
version = 1,
mappedTypes = listOf(
VaultStates::class.java,
VaultLinearStates::class.java,
VaultFungibleStates::class.java,
VaultTxnNote::class.java,
PersistentParty::class.java,
StateToExternalId::class.java
)
) {
override val migrationResource = "vault-schema.changelog-master"
@ -84,16 +98,6 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio
class VaultLinearStates(
/** [ContractState] attributes */
/** X500Name of participant parties **/
@ElementCollection
@CollectionTable(name = "vault_linear_states_parts",
joinColumns = [(JoinColumn(name = "output_index", referencedColumnName = "output_index")), (JoinColumn(name = "transaction_id", referencedColumnName = "transaction_id"))],
foreignKey = ForeignKey(name = "FK__lin_stat_parts__lin_stat"))
@Column(name = "participants")
var participants: MutableSet<AbstractParty?>? = null,
// Reason for not using Set is described here:
// https://stackoverflow.com/questions/44213074/kotlin-collection-has-neither-generic-type-or-onetomany-targetentity
/**
* Represents a [LinearState] [UniqueIdentifier]
*/
@ -104,25 +108,12 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio
@Type(type = "uuid-char")
var uuid: UUID
) : PersistentState() {
constructor(uid: UniqueIdentifier, _participants: List<AbstractParty>) :
this(externalId = uid.externalId,
uuid = uid.id,
participants = _participants.toMutableSet())
constructor(uid: UniqueIdentifier) : this(externalId = uid.externalId, uuid = uid.id)
}
@Entity
@Table(name = "vault_fungible_states")
class VaultFungibleStates(
/** [ContractState] attributes */
/** X500Name of participant parties **/
@ElementCollection
@CollectionTable(name = "vault_fungible_states_parts",
joinColumns = [(JoinColumn(name = "output_index", referencedColumnName = "output_index")), (JoinColumn(name = "transaction_id", referencedColumnName = "transaction_id"))],
foreignKey = ForeignKey(name = "FK__fung_st_parts__fung_st"))
@Column(name = "participants", nullable = true)
var participants: MutableSet<AbstractParty>? = null,
/** [OwnableState] attributes */
/** X500Name of owner party **/
@ -149,12 +140,8 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio
@Type(type = "corda-wrapper-binary")
var issuerRef: ByteArray?
) : PersistentState() {
constructor(_owner: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: OpaqueBytes, _participants: List<AbstractParty>) :
this(owner = _owner,
quantity = _quantity,
issuer = _issuerParty,
issuerRef = _issuerRef.bytes,
participants = _participants.toMutableSet())
constructor(_owner: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: OpaqueBytes) :
this(owner = _owner, quantity = _quantity, issuer = _issuerParty, issuerRef = _issuerRef.bytes)
}
@Entity
@ -173,4 +160,47 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio
) {
constructor(txId: String, note: String) : this(0, txId, note)
}
}
@Entity
@Table(name = "state_party", indexes = [Index(name = "state_party_idx", columnList = "public_key_hash")])
class PersistentParty(
@Id
@GeneratedValue
@Column(name = "id", unique = true, nullable = false)
var id: Long? = null,
// Foreign key.
@Column(name = "state_ref")
var stateRef: PersistentStateRef,
@Column(name = "public_key_hash", nullable = false)
var publicKeyHash: String,
@Column(name = "x500_name", nullable = true)
var x500Name: AbstractParty? = null
) : StatePersistable {
constructor(stateRef: PersistentStateRef, abstractParty: AbstractParty)
: this(null, stateRef, abstractParty.owningKey.toStringShort(), abstractParty)
}
@Entity
@Immutable
@Table(name = "v_pkey_hash_ex_id_map")
class StateToExternalId(
@Id
@GeneratedValue
@Column(name = "id", unique = true, nullable = false)
var id: Long? = null,
// Foreign key.
@Column(name = "state_ref")
var stateRef: PersistentStateRef,
@Column(name = "public_key_hash")
var publicKeyHash: String,
@Column(name = "external_id")
var externalId: UUID
) : StatePersistable
}

View File

@ -11,5 +11,6 @@
<include file="migration/vault-schema.changelog-v6.xml"/>
<include file="migration/vault-schema.changelog-pkey-swap.xml"/>
<include file="migration/vault-schema.changelog-v7.xml"/>
<include file="migration/vault-schema.changelog-v8.xml"/>
</databaseChangeLog>

View File

@ -0,0 +1,36 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
<changeSet author="R3.Corda" id="create-external-id-to-state-party-view">
<createTable tableName="state_party">
<column name="output_index" type="INT"/>
<column name="transaction_id" type="NVARCHAR(64)"/>
<column name="id" type="INT"/>
<column name="public_key_hash" type="NVARCHAR(255)"/>
<column name="x500_name" type="NVARCHAR(255)"/>
</createTable>
<createIndex indexName="state_pk_hash_idx" tableName="state_party">
<column name="public_key_hash"/>
</createIndex>
<createTable tableName="pk_hash_to_ext_id_map">
<column name="id" type="INT"/>
<column name="external_id" type="NVARCHAR(255)"/>
<column name="public_key_hash" type="NVARCHAR(255)"/>
</createTable>
<createIndex indexName="pk_hash_to_xid_idx" tableName="pk_hash_to_ext_id_map">
<column name="public_key_hash"/>
</createIndex>
<createView viewName="v_pkey_hash_ex_id_map">
select
state_party.id,
state_party.public_key_hash,
state_party.transaction_id,
state_party.output_index,
pk_hash_to_ext_id_map.external_id
from state_party
join pk_hash_to_ext_id_map
on state_party.public_key_hash = pk_hash_to_ext_id_map.public_key_hash
</createView>
</changeSet>
</databaseChangeLog>

View File

@ -1,7 +1,6 @@
package net.corda.node.internal
import net.corda.core.crypto.generateKeyPair
import net.corda.core.node.JavaPackageName
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NotaryInfo
import net.corda.core.node.services.AttachmentId
@ -29,6 +28,7 @@ import org.junit.After
import org.junit.Test
import java.nio.file.Path
import java.time.Instant
import kotlin.test.assertEquals
import kotlin.test.assertFails
class NetworkParametersTest {
@ -91,9 +91,7 @@ class NetworkParametersTest {
1,
emptyMap(),
Int.MAX_VALUE.days,
mapOf(
JavaPackageName("com.!example.stuff") to key2
)
mapOf("com.!example.stuff" to key2)
)
}.withMessageContaining("Invalid Java package name")
@ -107,13 +105,13 @@ class NetworkParametersTest {
emptyMap(),
Int.MAX_VALUE.days,
mapOf(
JavaPackageName("com.example") to key1,
JavaPackageName("com.example.stuff") to key2
"com.example" to key1,
"com.example.stuff" to key2
)
)
}.withMessage("multiple packages added to the packageOwnership overlap.")
NetworkParameters(1,
val params = NetworkParameters(1,
emptyList(),
2001,
2000,
@ -122,21 +120,22 @@ class NetworkParametersTest {
emptyMap(),
Int.MAX_VALUE.days,
mapOf(
JavaPackageName("com.example") to key1,
JavaPackageName("com.examplestuff") to key2
"com.example" to key1,
"com.examplestuff" to key2
)
)
assert(JavaPackageName("com.example").owns("com.example.something.MyClass"))
assert(!JavaPackageName("com.example").owns("com.examplesomething.MyClass"))
assert(!JavaPackageName("com.exam").owns("com.example.something.MyClass"))
assertEquals(params.getOwnerOf("com.example.something.MyClass"), key1)
assertEquals(params.getOwnerOf("com.examplesomething.MyClass"), null)
assertEquals(params.getOwnerOf("com.examplestuff.something.MyClass"), key2)
assertEquals(params.getOwnerOf("com.exam.something.MyClass"), null)
}
@Test
fun `auto acceptance checks are correct`() {
val packageOwnership = mapOf(
JavaPackageName("com.example1") to generateKeyPair().public,
JavaPackageName("com.example2") to generateKeyPair().public)
"com.example1" to generateKeyPair().public,
"com.example2" to generateKeyPair().public)
val whitelistedContractImplementations = mapOf(
"example1" to listOf(AttachmentId.randomSHA256()),
"example2" to listOf(AttachmentId.randomSHA256()))

View File

@ -1,8 +1,7 @@
package net.corda.node.internal
import net.corda.cliutils.CommonCliConstants.BASE_DIR
import net.corda.core.internal.div
import net.corda.node.InitialRegistrationCmdLineOptions
import net.corda.node.internal.subcommands.InitialRegistrationCli
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
import org.assertj.core.api.Assertions.assertThat
import org.junit.BeforeClass
@ -44,7 +43,7 @@ class NodeStartupTest {
@Test
fun `--base-directory`() {
CommandLine.populateCommand(startup, "--base-directory", (workingDirectory / "another-base-dir").toString())
CommandLine.populateCommand(startup, BASE_DIR, (workingDirectory / "another-base-dir").toString())
assertThat(startup.cmdLineOptions.baseDirectory).isEqualTo(workingDirectory / "another-base-dir")
assertThat(startup.cmdLineOptions.configFile).isEqualTo(workingDirectory / "another-base-dir" / "node.conf")
assertThat(startup.cmdLineOptions.networkRootTrustStorePathParameter).isEqualTo(null)

View File

@ -22,10 +22,7 @@ import net.corda.core.internal.NODE_INFO_DIRECTORY
import net.corda.nodeapi.internal.NodeInfoAndSigned
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
import net.corda.nodeapi.internal.network.SignedNetworkParameters
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
import net.corda.nodeapi.internal.network.*
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.*
import net.corda.testing.internal.DEV_ROOT_CA
@ -240,7 +237,7 @@ class NetworkMapUpdaterTest {
assert(!updateFile.exists()) { "network parameters should not be auto accepted" }
updater.acceptNewNetworkParameters(newHash) { it.serialize().sign(ourKeyPair) }
val signedNetworkParams = updateFile.readObject<SignedNetworkParameters>()
val paramsFromFile = signedNetworkParams.verifiedNetworkMapCert(DEV_ROOT_CA.certificate)
val paramsFromFile = signedNetworkParams.verifiedNetworkParametersCert(DEV_ROOT_CA.certificate)
assertEquals(newParameters, paramsFromFile)
assertEquals(newHash, server.latestParametersAccepted(ourKeyPair.public))
}
@ -258,7 +255,7 @@ class NetworkMapUpdaterTest {
val newHash = newParameters.serialize().hash
val updateFile = baseDir / NETWORK_PARAMS_UPDATE_FILE_NAME
val signedNetworkParams = updateFile.readObject<SignedNetworkParameters>()
val paramsFromFile = signedNetworkParams.verifiedNetworkMapCert(DEV_ROOT_CA.certificate)
val paramsFromFile = signedNetworkParams.verifiedNetworkParametersCert(DEV_ROOT_CA.certificate)
assertEquals(newParameters, paramsFromFile)
assertEquals(newHash, server.latestParametersAccepted(ourKeyPair.public))
}

View File

@ -2,10 +2,7 @@ package net.corda.node.services.network
import com.google.common.jimfs.Configuration
import com.google.common.jimfs.Jimfs
import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.internal.exists
import net.corda.core.internal.readObject
import net.corda.core.internal.*
import net.corda.core.serialization.deserialize
import net.corda.core.utilities.days
import net.corda.core.utilities.seconds
@ -64,7 +61,7 @@ class NetworkParametersReaderTest {
// Parameters from update should be moved to `network-parameters` file.
val parametersFromFile = (baseDirectory / NETWORK_PARAMS_FILE_NAME)
.readObject<SignedNetworkParameters>()
.verifiedNetworkMapCert(DEV_ROOT_CA.certificate)
.verifiedNetworkParametersCert(DEV_ROOT_CA.certificate)
assertEquals(server.networkParameters, parametersFromFile)
}

View File

@ -0,0 +1,310 @@
package net.corda.node.services.transactions
import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.*
import net.corda.core.flows.*
import net.corda.core.identity.Party
import net.corda.core.internal.notary.generateSignature
import net.corda.core.messaging.MessageRecipients
import net.corda.core.node.ServiceHub
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds
import net.corda.node.services.messaging.Message
import net.corda.node.services.statemachine.InitialSessionMessage
import net.corda.testing.contracts.DummyContract
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.dummyCommand
import net.corda.testing.core.singleIdentity
import net.corda.testing.node.MockNetworkNotarySpec
import net.corda.testing.node.TestClock
import net.corda.testing.node.internal.*
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.time.Duration
import java.time.Instant
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class NonValidatingNotaryServiceTests {
private lateinit var mockNet: InternalMockNetwork
private lateinit var notaryNode: TestStartedNode
private lateinit var aliceNode: TestStartedNode
private lateinit var notary: Party
private lateinit var alice: Party
@Before
fun setup() {
mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts"),
notarySpecs = listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME, false)))
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
notaryNode = mockNet.defaultNotaryNode
notary = mockNet.defaultNotaryIdentity
alice = aliceNode.info.singleIdentity()
}
@After
fun cleanUp() {
mockNet.stopNodes()
}
@Test
fun `should sign a unique transaction with a valid time-window`() {
val stx = run {
val input = issueState(aliceNode.services, alice)
val tx = TransactionBuilder(notary)
.addInputState(input)
.addCommand(dummyCommand(alice.owningKey))
.setTimeWindow(Instant.now(), 30.seconds)
aliceNode.services.signInitialTransaction(tx)
}
val future = runNotaryClient(stx)
val signatures = future.getOrThrow()
signatures.forEach { it.verify(stx.id) }
}
@Test
fun `should sign a unique transaction without a time-window`() {
val stx = run {
val inputStates = issueStates(aliceNode.services, alice)
val tx = TransactionBuilder(notary)
.addInputState(inputStates[0])
.addInputState(inputStates[1])
.addCommand(dummyCommand(alice.owningKey))
aliceNode.services.signInitialTransaction(tx)
}
val future = runNotaryClient(stx)
val signatures = future.getOrThrow()
signatures.forEach { it.verify(stx.id) }
}
@Test
fun `should re-sign a transaction with an expired time-window`() {
val stx = run {
val inputState = issueState(aliceNode.services, alice)
val tx = TransactionBuilder(notary)
.addInputState(inputState)
.addCommand(dummyCommand(alice.owningKey))
.setTimeWindow(Instant.now(), 30.seconds)
aliceNode.services.signInitialTransaction(tx)
}
val sig1 = runNotaryClient(stx).getOrThrow().single()
assertEquals(sig1.by, notary.owningKey)
assertTrue(sig1.isValid(stx.id))
mockNet.nodes.forEach {
val nodeClock = (it.started!!.services.clock as TestClock)
nodeClock.advanceBy(Duration.ofDays(1))
}
val sig2 = runNotaryClient(stx).getOrThrow().single()
assertEquals(sig2.by, notary.owningKey)
}
@Test
fun `should report error for transaction with an invalid time-window`() {
val stx = run {
val inputState = issueState(aliceNode.services, alice)
val tx = TransactionBuilder(notary)
.addInputState(inputState)
.addCommand(dummyCommand(alice.owningKey))
.setTimeWindow(Instant.now().plusSeconds(3600), 30.seconds)
aliceNode.services.signInitialTransaction(tx)
}
val future = runNotaryClient(stx)
val ex = assertFailsWith(NotaryException::class) { future.getOrThrow() }
assertThat(ex.error).isInstanceOf(NotaryError.TimeWindowInvalid::class.java)
}
@Test
fun `notarise issue tx with time-window`() {
val stx = run {
val tx = DummyContract.generateInitial(Random().nextInt(), notary, alice.ref(0))
.setTimeWindow(Instant.now(), 30.seconds)
aliceNode.services.signInitialTransaction(tx)
}
val sig = runNotaryClient(stx).getOrThrow().single()
assertEquals(sig.by, notary.owningKey)
}
@Test
fun `should sign identical transaction multiple times (notarisation is idempotent)`() {
val stx = run {
val inputState = issueState(aliceNode.services, alice)
val tx = TransactionBuilder(notary)
.addInputState(inputState)
.addCommand(dummyCommand(alice.owningKey))
aliceNode.services.signInitialTransaction(tx)
}
val firstAttempt = NotaryFlow.Client(stx)
val secondAttempt = NotaryFlow.Client(stx)
val f1 = aliceNode.services.startFlow(firstAttempt).resultFuture
val f2 = aliceNode.services.startFlow(secondAttempt).resultFuture
mockNet.runNetwork()
// Note that the notary will only return identical signatures when using deterministic signature
// schemes (e.g. EdDSA) and when deterministic metadata is attached (no timestamps or nonces).
// We only really care that both signatures are over the same transaction and by the same notary.
val sig1 = f1.getOrThrow().single()
assertEquals(sig1.by, notary.owningKey)
assertTrue(sig1.isValid(stx.id))
val sig2 = f2.getOrThrow().single()
assertEquals(sig2.by, notary.owningKey)
assertTrue(sig2.isValid(stx.id))
}
@Test
fun `should report conflict when inputs are reused across transactions`() {
val firstState = issueState(aliceNode.services, alice)
val secondState = issueState(aliceNode.services, alice)
fun spendState(state: StateAndRef<*>): SignedTransaction {
val stx = run {
val tx = TransactionBuilder(notary)
.addInputState(state)
.addCommand(dummyCommand(alice.owningKey))
aliceNode.services.signInitialTransaction(tx)
}
aliceNode.services.startFlow(NotaryFlow.Client(stx))
mockNet.runNetwork()
return stx
}
val firstSpendTx = spendState(firstState)
val secondSpendTx = spendState(secondState)
val doubleSpendTx = run {
val tx = TransactionBuilder(notary)
.addInputState(issueState(aliceNode.services, alice))
.addInputState(firstState)
.addInputState(secondState)
.addCommand(dummyCommand(alice.owningKey))
aliceNode.services.signInitialTransaction(tx)
}
val doubleSpend = NotaryFlow.Client(doubleSpendTx) // Double spend the inputState in a second transaction.
val future = aliceNode.services.startFlow(doubleSpend)
mockNet.runNetwork()
val ex = assertFailsWith(NotaryException::class) { future.resultFuture.getOrThrow() }
val notaryError = ex.error as NotaryError.Conflict
assertEquals(notaryError.txId, doubleSpendTx.id)
with(notaryError) {
assertEquals(consumedStates.size, 2)
assertEquals(consumedStates[firstState.ref]!!.hashOfTransactionId, firstSpendTx.id.sha256())
assertEquals(consumedStates[secondState.ref]!!.hashOfTransactionId, secondSpendTx.id.sha256())
}
}
@Test
fun `should reject when notarisation request not signed by the requesting party`() {
runNotarisationAndInterceptClientPayload { originalPayload ->
val transaction = originalPayload.coreTransaction
val randomKeyPair = Crypto.generateKeyPair()
val bytesToSign = NotarisationRequest(transaction.inputs, transaction.id).serialize().bytes
val modifiedSignature = NotarisationRequestSignature(randomKeyPair.sign(bytesToSign), aliceNode.services.myInfo.platformVersion)
originalPayload.copy(requestSignature = modifiedSignature)
}
}
@Test
fun `should reject when incorrect notarisation request signed - inputs don't match`() {
runNotarisationAndInterceptClientPayload { originalPayload ->
val transaction = originalPayload.coreTransaction
val wrongInputs = listOf(StateRef(SecureHash.randomSHA256(), 0))
val request = NotarisationRequest(wrongInputs, transaction.id)
val modifiedSignature = request.generateSignature(aliceNode.services)
originalPayload.copy(requestSignature = modifiedSignature)
}
}
@Test
fun `should reject when incorrect notarisation request signed - transaction id doesn't match`() {
runNotarisationAndInterceptClientPayload { originalPayload ->
val transaction = originalPayload.coreTransaction
val wrongTransactionId = SecureHash.randomSHA256()
val request = NotarisationRequest(transaction.inputs, wrongTransactionId)
val modifiedSignature = request.generateSignature(aliceNode.services)
originalPayload.copy(requestSignature = modifiedSignature)
}
}
@Test
fun `should reject a transaction with too many inputs`() {
NotaryServiceTests.notariseWithTooManyInputs(aliceNode, alice, notary, mockNet)
}
private fun runNotarisationAndInterceptClientPayload(payloadModifier: (NotarisationPayload) -> NotarisationPayload) {
aliceNode.setMessagingServiceSpy(object : MessagingServiceSpy() {
override fun send(message: Message, target: MessageRecipients, sequenceKey: Any) {
val messageData = message.data.deserialize<Any>() as? InitialSessionMessage
val payload = messageData?.firstPayload!!.deserialize()
if (payload is NotarisationPayload) {
val alteredPayload = payloadModifier(payload)
val alteredMessageData = messageData.copy(firstPayload = alteredPayload.serialize())
val alteredMessage = InMemoryMessage(message.topic, OpaqueBytes(alteredMessageData.serialize().bytes), message.uniqueMessageId)
messagingService.send(alteredMessage, target)
} else {
messagingService.send(message, target)
}
}
})
val stx = run {
val inputState = issueState(aliceNode.services, alice)
val tx = TransactionBuilder(notary)
.addInputState(inputState)
.addCommand(dummyCommand(alice.owningKey))
aliceNode.services.signInitialTransaction(tx)
}
val future = runNotaryClient(stx)
val ex = assertFailsWith(NotaryException::class) { future.getOrThrow() }
assertThat(ex.error).isInstanceOf(NotaryError.RequestSignatureInvalid::class.java)
}
private fun runNotaryClient(stx: SignedTransaction): CordaFuture<List<TransactionSignature>> {
val flow = NotaryFlow.Client(stx)
val future = aliceNode.services.startFlow(flow).resultFuture
mockNet.runNetwork()
return future
}
private fun issueState(serviceHub: ServiceHub, identity: Party): StateAndRef<*> {
val tx = DummyContract.generateInitial(Random().nextInt(), notary, identity.ref(0))
val signedByNode = serviceHub.signInitialTransaction(tx)
val stx = notaryNode.services.addSignature(signedByNode, notary.owningKey)
serviceHub.recordTransactions(stx)
return StateAndRef(stx.coreTransaction.outputs.first(), StateRef(stx.id, 0))
}
private fun issueStates(serviceHub: ServiceHub, identity: Party): List<StateAndRef<*>> {
val tx = DummyContract.generateInitial(Random().nextInt(), notary, identity.ref(0))
val signedByNode = serviceHub.signInitialTransaction(tx)
val stx = notaryNode.services.addSignature(signedByNode, notary.owningKey)
serviceHub.recordTransactions(stx)
return listOf(StateAndRef(stx.coreTransaction.outputs[0], StateRef(stx.id, 0)),
StateAndRef(stx.coreTransaction.outputs[1], StateRef(stx.id, 1)))
}
}

View File

@ -99,9 +99,10 @@ class ValidatingNotaryServiceTests {
@Test
fun `should sign a unique transaction with a valid time-window`() {
val stx = run {
val inputState = issueState(aliceNode.services, alice)
val inputStates = issueStates(aliceNode.services, alice)
val tx = TransactionBuilder(notary)
.addInputState(inputState)
.addInputState(inputStates[0])
.addInputState(inputStates[1])
.addCommand(dummyCommand(alice.owningKey))
.setTimeWindow(Instant.now(), 30.seconds)
aliceNode.services.signInitialTransaction(tx)
@ -334,4 +335,13 @@ class ValidatingNotaryServiceTests {
serviceHub.recordTransactions(stx)
return StateAndRef(stx.coreTransaction.outputs.first(), StateRef(stx.id, 0))
}
private fun issueStates(serviceHub: ServiceHub, identity: Party): List<StateAndRef<*>> {
val tx = DummyContract.generateInitial(Random().nextInt(), notary, identity.ref(0))
val signedByNode = serviceHub.signInitialTransaction(tx)
val stx = notaryNode.services.addSignature(signedByNode, notary.owningKey)
serviceHub.recordTransactions(stx)
return listOf(StateAndRef(stx.coreTransaction.outputs[0], StateRef(stx.id, 0)),
StateAndRef(stx.coreTransaction.outputs[1], StateRef(stx.id, 1)))
}
}

View File

@ -0,0 +1,131 @@
package net.corda.node.services.vault
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.node.services.vault.builder
import net.corda.core.transactions.TransactionBuilder
import net.corda.node.services.api.IdentityServiceInternal
import net.corda.node.services.keys.PersistentKeyManagementService
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyState
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockServices
import org.junit.Rule
import org.junit.Test
import java.util.*
import kotlin.test.assertEquals
class ExternalIdMappingTest {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val cordapps = listOf(
"net.corda.node.services.persistence",
"net.corda.testing.contracts"
)
private val myself = TestIdentity(CordaX500Name("Me", "London", "GB"))
private val notary = TestIdentity(CordaX500Name("NotaryService", "London", "GB"), 1337L)
private val databaseAndServices = MockServices.makeTestDatabaseAndMockServices(
cordappPackages = cordapps,
identityService = rigorousMock<IdentityServiceInternal>().also {
doReturn(notary.party).whenever(it).partyFromKey(notary.publicKey)
doReturn(notary.party).whenever(it).wellKnownPartyFromAnonymous(notary.party)
doReturn(notary.party).whenever(it).wellKnownPartyFromX500Name(notary.name)
},
initialIdentity = myself,
networkParameters = testNetworkParameters(minimumPlatformVersion = 4)
)
private val services: MockServices = databaseAndServices.second
private val database: CordaPersistence = databaseAndServices.first
private fun freshKeyForExternalId(externalId: UUID): AnonymousParty {
val anonymousParty = freshKey()
database.transaction {
services.withEntityManager {
val mapping = PersistentKeyManagementService.PublicKeyHashToExternalId(externalId, anonymousParty.owningKey)
persist(mapping)
flush()
}
}
return anonymousParty
}
private fun freshKey(): AnonymousParty {
val key = services.keyManagementService.freshKey()
val anonymousParty = AnonymousParty(key)
// Add behaviour to the mock identity management service for dealing with the new key.
// It won't be able to resolve it as it's just an anonymous key that is not linked to an identity.
services.identityService.also { doReturn(null).whenever(it).wellKnownPartyFromAnonymous(anonymousParty) }
return anonymousParty
}
private fun createDummyState(participants: List<AbstractParty>): DummyState {
val tx = TransactionBuilder(notary = notary.party).apply {
addOutputState(DummyState(1, participants), DummyContract.PROGRAM_ID)
addCommand(DummyContract.Commands.Create(), participants.map { it.owningKey })
}
val stx = services.signInitialTransaction(tx)
database.transaction { services.recordTransactions(stx) }
return stx.tx.outputsOfType<DummyState>().single()
}
@Test
fun `Two states can be mapped to a single externalId`() {
val vaultService = services.vaultService
// Create new external ID and two keys mapped to it.
val id = UUID.randomUUID()
val keyOne = freshKeyForExternalId(id)
val keyTwo = freshKeyForExternalId(id)
// Create states with a public key assigned to the new external ID.
val dummyStateOne = createDummyState(listOf(keyOne))
val dummyStateTwo = createDummyState(listOf(keyTwo))
// This query should return two states!
val result = database.transaction {
val externalId = builder { VaultSchemaV1.StateToExternalId::externalId.`in`(listOf(id)) }
val queryCriteria = QueryCriteria.VaultCustomQueryCriteria(externalId)
vaultService.queryBy<DummyState>(queryCriteria).states
}
assertEquals(setOf(dummyStateOne, dummyStateTwo), result.map { it.state.data }.toSet())
// This query should return two states!
val resultTwo = database.transaction {
val externalId = builder { VaultSchemaV1.StateToExternalId::externalId.equal(id) }
val queryCriteria = QueryCriteria.VaultCustomQueryCriteria(externalId)
vaultService.queryBy<DummyState>(queryCriteria).states
}
assertEquals(setOf(dummyStateOne, dummyStateTwo), resultTwo.map { it.state.data }.toSet())
}
@Test
fun `One state can be mapped to multiple externalIds`() {
val vaultService = services.vaultService
// Create new external ID.
val idOne = UUID.randomUUID()
val keyOne = freshKeyForExternalId(idOne)
val idTwo = UUID.randomUUID()
val keyTwo = freshKeyForExternalId(idTwo)
// Create state with a public key assigned to the new external ID.
val dummyState = createDummyState(listOf(keyOne, keyTwo))
// This query should return one state!
val result = database.transaction {
val externalId = builder { VaultSchemaV1.StateToExternalId::externalId.`in`(listOf(idOne, idTwo)) }
val queryCriteria = QueryCriteria.VaultCustomQueryCriteria(externalId)
vaultService.queryBy<DummyState>(queryCriteria).states
}
assertEquals(dummyState, result.single().state.data)
}
}

View File

@ -8,8 +8,6 @@ import kotlin.test.assertTrue
class AddressUtilsTests {
@Test
fun `correctly determines if the provided address is public`() {
val hostName = InetAddress.getLocalHost()
assertFalse { AddressUtils.isPublic(hostName) }
assertFalse { AddressUtils.isPublic("localhost") }
assertFalse { AddressUtils.isPublic("127.0.0.1") }
assertFalse { AddressUtils.isPublic("::1") }
@ -28,4 +26,4 @@ class AddressUtilsTests {
assertTrue { AddressUtils.isPublic("corda.net") }
assertTrue { AddressUtils.isPublic("2607:f298:5:110f::eef:8729") }
}
}
}

View File

@ -16,8 +16,8 @@ fun createSerializerFactoryFactory(): SerializerFactoryFactory = DeterministicSe
private class DeterministicSerializerFactoryFactory : SerializerFactoryFactory {
override fun make(context: SerializationContext) =
SerializerFactoryBuilder.build(
whitelist = context.whitelist,
classCarpenter = DummyClassCarpenter(context.whitelist, context.deserializationClassLoader))
whitelist = context.whitelist,
classCarpenter = DummyClassCarpenter(context.whitelist, context.deserializationClassLoader))
}
private class DummyClassCarpenter(

View File

@ -12,7 +12,7 @@ import java.lang.reflect.Type
* [ByteArray] is automatically marshalled to/from the Proton-J wrapper, [Binary].
*/
class AMQPPrimitiveSerializer(clazz: Class<*>) : AMQPSerializer<Any> {
override val typeDescriptor = Symbol.valueOf(SerializerFactory.primitiveTypeName(clazz)!!)!!
override val typeDescriptor = Symbol.valueOf(AMQPTypeIdentifiers.primitiveTypeName(clazz))!!
override val type: Type = clazz
// NOOP since this is a primitive type.

View File

@ -3,6 +3,7 @@ package net.corda.serialization.internal.amqp
import net.corda.serialization.internal.model.*
import java.io.NotSerializableException
import java.util.*
import kotlin.collections.LinkedHashMap
/**
* Interprets AMQP [Schema] information to obtain [RemoteTypeInformation], caching by [TypeDescriptor].
@ -35,9 +36,17 @@ class AMQPRemoteTypeModel {
val interpretationState = InterpretationState(notationLookup, enumTransformsLookup, cache, emptySet())
return byTypeDescriptor.mapValues { (typeDescriptor, typeNotation) ->
val result = byTypeDescriptor.mapValues { (typeDescriptor, typeNotation) ->
cache.getOrPut(typeDescriptor) { interpretationState.run { typeNotation.name.typeIdentifier.interpretIdentifier() } }
}
val typesByIdentifier = result.values.associateBy { it.typeIdentifier }
result.values.forEach { typeInformation ->
if (typeInformation is RemoteTypeInformation.Cycle) {
typeInformation.follow = typesByIdentifier[typeInformation.typeIdentifier] ?:
throw NotSerializableException("Cannot resolve cyclic reference to ${typeInformation.typeIdentifier}")
}
}
return result
}
data class InterpretationState(val notationLookup: Map<TypeIdentifier, TypeNotation>,
@ -45,9 +54,6 @@ class AMQPRemoteTypeModel {
val cache: MutableMap<TypeDescriptor, RemoteTypeInformation>,
val seen: Set<TypeIdentifier>) {
private inline fun <T> forgetSeen(block: InterpretationState.() -> T): T =
withSeen(emptySet(), block)
private inline fun <T> withSeen(typeIdentifier: TypeIdentifier, block: InterpretationState.() -> T): T =
withSeen(seen + typeIdentifier, block)
@ -62,7 +68,7 @@ class AMQPRemoteTypeModel {
* know we have hit a cycle and respond accordingly.
*/
fun TypeIdentifier.interpretIdentifier(): RemoteTypeInformation =
if (this in seen) RemoteTypeInformation.Cycle(this) { forgetSeen { interpretIdentifier() } }
if (this in seen) RemoteTypeInformation.Cycle(this)
else withSeen(this) {
val identifier = this@interpretIdentifier
notationLookup[identifier]?.interpretNotation(identifier) ?: interpretNoNotation()
@ -85,7 +91,7 @@ class AMQPRemoteTypeModel {
* [RemoteTypeInformation].
*/
private fun CompositeType.interpretComposite(identifier: TypeIdentifier): RemoteTypeInformation {
val properties = fields.asSequence().map { it.interpret() }.toMap()
val properties = fields.asSequence().sortedBy { it.name }.map { it.interpret() }.toMap(LinkedHashMap())
val typeParameters = identifier.interpretTypeParameters()
val interfaceIdentifiers = provides.map { name -> name.typeIdentifier }
val isInterface = identifier in interfaceIdentifiers
@ -175,6 +181,11 @@ class AMQPRemoteTypeModel {
}
}
fun LocalTypeInformation.getEnumTransforms(factory: LocalSerializerFactory): EnumTransforms {
val transformsSchema = TransformsSchema.get(typeIdentifier.name, factory)
return interpretTransformSet(transformsSchema)
}
private fun interpretTransformSet(transformSet: EnumMap<TransformTypes, MutableList<Transform>>): EnumTransforms {
val defaultTransforms = transformSet[TransformTypes.EnumDefault]?.toList() ?: emptyList()
val defaults = defaultTransforms.associate { transform -> (transform as EnumDefaultSchemaTransform).new to transform.old }
@ -185,7 +196,7 @@ private fun interpretTransformSet(transformSet: EnumMap<TransformTypes, MutableL
}
private val TypeNotation.typeDescriptor: String get() = descriptor.name?.toString() ?:
throw NotSerializableException("Type notation has no type descriptor: $this")
throw NotSerializableException("Type notation has no type descriptor: $this")
private val String.typeIdentifier get(): TypeIdentifier = AMQPTypeIdentifierParser.parse(this)

View File

@ -2,6 +2,7 @@ package net.corda.serialization.internal.amqp
import net.corda.serialization.internal.model.TypeIdentifier
import org.apache.qpid.proton.amqp.*
import java.io.NotSerializableException
import java.lang.reflect.Type
import java.util.*
@ -9,8 +10,9 @@ object AMQPTypeIdentifiers {
fun isPrimitive(type: Type): Boolean = isPrimitive(TypeIdentifier.forGenericType(type))
fun isPrimitive(typeIdentifier: TypeIdentifier) = typeIdentifier in primitiveTypeNamesByName
fun primitiveTypeName(type: Type): String? =
primitiveTypeNamesByName[TypeIdentifier.forGenericType(type)]
fun primitiveTypeName(type: Type): String =
primitiveTypeNamesByName[TypeIdentifier.forGenericType(type)] ?:
throw NotSerializableException("Primitive type name requested for non-primitive type $type")
private val primitiveTypeNamesByName = sequenceOf(
Character::class to "char",

View File

@ -14,9 +14,9 @@ import java.lang.reflect.Type
* Serialization / deserialization of arrays.
*/
@KeepForDJVM
open class ArraySerializer(override val type: Type, factory: SerializerFactory) : AMQPSerializer<Any> {
open class ArraySerializer(override val type: Type, factory: LocalSerializerFactory) : AMQPSerializer<Any> {
companion object {
fun make(type: Type, factory: SerializerFactory) : AMQPSerializer<Any> {
fun make(type: Type, factory: LocalSerializerFactory) : AMQPSerializer<Any> {
contextLogger().debug { "Making array serializer, typename=${type.typeName}" }
return when (type) {
Array<Char>::class.java -> CharArraySerializer(factory)
@ -41,8 +41,8 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory)
// Special case handler for primitive byte arrays. This is needed because we can silently
// coerce a byte[] to our own binary type. Normally, if the component type was itself an
// array we'd keep walking down the chain but for byte[] stop here and use binary instead
val typeName = if (SerializerFactory.isPrimitive(type.componentType())) {
SerializerFactory.nameForType(type.componentType())
val typeName = if (AMQPTypeIdentifiers.isPrimitive(type.componentType())) {
AMQPTypeIdentifiers.nameForType(type.componentType())
} else {
calcTypeName(type.componentType(), debugOffset + 4)
}
@ -55,7 +55,7 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory)
}
override val typeDescriptor: Symbol by lazy {
Symbol.valueOf("$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")
factory.createDescriptor(type)
}
internal val elementType: Type by lazy { type.componentType() }
@ -103,7 +103,7 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory)
// Boxed Character arrays required a specialisation to handle the type conversion properly when populating
// the array since Kotlin won't allow an implicit cast from Int (as they're stored as 16bit ints) to Char
class CharArraySerializer(factory: SerializerFactory) : ArraySerializer(Array<Char>::class.java, factory) {
class CharArraySerializer(factory: LocalSerializerFactory) : ArraySerializer(Array<Char>::class.java, factory) {
override fun <T> List<T>.toArrayOfType(type: Type): Any {
val elementType = type.asClass()
val list = this
@ -114,11 +114,11 @@ class CharArraySerializer(factory: SerializerFactory) : ArraySerializer(Array<Ch
}
// Specialisation of [ArraySerializer] that handles arrays of unboxed java primitive types
abstract class PrimArraySerializer(type: Type, factory: SerializerFactory) : ArraySerializer(type, factory) {
abstract class PrimArraySerializer(type: Type, factory: LocalSerializerFactory) : ArraySerializer(type, factory) {
companion object {
// We don't need to handle the unboxed byte type as that is coercible to a byte array, but
// the other 7 primitive types we do
private val primTypes: Map<Type, (SerializerFactory) -> PrimArraySerializer> = mapOf(
private val primTypes: Map<Type, (LocalSerializerFactory) -> PrimArraySerializer> = mapOf(
IntArray::class.java to { f -> PrimIntArraySerializer(f) },
CharArray::class.java to { f -> PrimCharArraySerializer(f) },
BooleanArray::class.java to { f -> PrimBooleanArraySerializer(f) },
@ -129,7 +129,7 @@ abstract class PrimArraySerializer(type: Type, factory: SerializerFactory) : Arr
// ByteArray::class.java <-> NOT NEEDED HERE (see comment above)
)
fun make(type: Type, factory: SerializerFactory) = primTypes[type]!!(factory)
fun make(type: Type, factory: LocalSerializerFactory) = primTypes[type]!!(factory)
}
fun localWriteObject(data: Data, func: () -> Unit) {
@ -137,7 +137,7 @@ abstract class PrimArraySerializer(type: Type, factory: SerializerFactory) : Arr
}
}
class PrimIntArraySerializer(factory: SerializerFactory) : PrimArraySerializer(IntArray::class.java, factory) {
class PrimIntArraySerializer(factory: LocalSerializerFactory) : PrimArraySerializer(IntArray::class.java, factory) {
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
context: SerializationContext, debugIndent: Int
) {
@ -147,7 +147,7 @@ class PrimIntArraySerializer(factory: SerializerFactory) : PrimArraySerializer(I
}
}
class PrimCharArraySerializer(factory: SerializerFactory) : PrimArraySerializer(CharArray::class.java, factory) {
class PrimCharArraySerializer(factory: LocalSerializerFactory) : PrimArraySerializer(CharArray::class.java, factory) {
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
context: SerializationContext, debugIndent: Int
) {
@ -168,7 +168,7 @@ class PrimCharArraySerializer(factory: SerializerFactory) : PrimArraySerializer(
}
}
class PrimBooleanArraySerializer(factory: SerializerFactory) : PrimArraySerializer(BooleanArray::class.java, factory) {
class PrimBooleanArraySerializer(factory: LocalSerializerFactory) : PrimArraySerializer(BooleanArray::class.java, factory) {
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
context: SerializationContext, debugIndent: Int
) {
@ -178,7 +178,7 @@ class PrimBooleanArraySerializer(factory: SerializerFactory) : PrimArraySerializ
}
}
class PrimDoubleArraySerializer(factory: SerializerFactory) :
class PrimDoubleArraySerializer(factory: LocalSerializerFactory) :
PrimArraySerializer(DoubleArray::class.java, factory) {
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
context: SerializationContext, debugIndent: Int
@ -189,7 +189,7 @@ class PrimDoubleArraySerializer(factory: SerializerFactory) :
}
}
class PrimFloatArraySerializer(factory: SerializerFactory) :
class PrimFloatArraySerializer(factory: LocalSerializerFactory) :
PrimArraySerializer(FloatArray::class.java, factory) {
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
context: SerializationContext, debugIndent: Int) {
@ -199,7 +199,7 @@ class PrimFloatArraySerializer(factory: SerializerFactory) :
}
}
class PrimShortArraySerializer(factory: SerializerFactory) :
class PrimShortArraySerializer(factory: LocalSerializerFactory) :
PrimArraySerializer(ShortArray::class.java, factory) {
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
context: SerializationContext, debugIndent: Int
@ -210,7 +210,7 @@ class PrimShortArraySerializer(factory: SerializerFactory) :
}
}
class PrimLongArraySerializer(factory: SerializerFactory) :
class PrimLongArraySerializer(factory: LocalSerializerFactory) :
PrimArraySerializer(LongArray::class.java, factory) {
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
context: SerializationContext, debugIndent: Int

View File

@ -1,11 +1,13 @@
package net.corda.serialization.internal.amqp
import net.corda.core.KeepForDJVM
import net.corda.core.internal.uncheckedCast
import net.corda.core.serialization.SerializationContext
import net.corda.core.utilities.NonEmptySet
import net.corda.serialization.internal.model.LocalTypeInformation
import net.corda.serialization.internal.model.TypeIdentifier
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import java.util.*
@ -15,11 +17,11 @@ import kotlin.collections.LinkedHashSet
* Serialization / deserialization of predefined set of supported [Collection] types covering mostly [List]s and [Set]s.
*/
@KeepForDJVM
class CollectionSerializer(private val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer<Any> {
override val type: Type = declaredType as? DeserializedParameterizedType
?: DeserializedParameterizedType.make(SerializerFactory.nameForType(declaredType))
class CollectionSerializer(private val declaredType: ParameterizedType, factory: LocalSerializerFactory) : AMQPSerializer<Any> {
override val type: Type = declaredType
override val typeDescriptor: Symbol by lazy {
Symbol.valueOf("$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")
factory.createDescriptor(type)
}
companion object {
@ -33,40 +35,60 @@ class CollectionSerializer(private val declaredType: ParameterizedType, factory:
NonEmptySet::class.java to { list -> NonEmptySet.copyOf(list) }
))
private val supportedTypeIdentifiers = supportedTypes.keys.asSequence().map { TypeIdentifier.forClass(it) }.toSet()
/**
* Replace erased collection types with parameterised types with wildcard type parameters, so that they are represented
* appropriately in the AMQP schema.
*/
fun resolveDeclared(declaredTypeInformation: LocalTypeInformation.ACollection): LocalTypeInformation.ACollection {
if (declaredTypeInformation.typeIdentifier.erased in supportedTypeIdentifiers)
return reparameterise(declaredTypeInformation)
throw NotSerializableException(
"Cannot derive collection type for declared type: " +
declaredTypeInformation.prettyPrint(false))
}
fun resolveActual(actualClass: Class<*>, declaredTypeInformation: LocalTypeInformation.ACollection): LocalTypeInformation.ACollection {
if (declaredTypeInformation.typeIdentifier.erased in supportedTypeIdentifiers)
return reparameterise(declaredTypeInformation)
val collectionClass = findMostSuitableCollectionType(actualClass)
val erasedInformation = LocalTypeInformation.ACollection(
collectionClass,
TypeIdentifier.forClass(collectionClass),
LocalTypeInformation.Unknown)
return when(declaredTypeInformation.typeIdentifier) {
is TypeIdentifier.Parameterised -> erasedInformation.withElementType(declaredTypeInformation.elementType)
else -> erasedInformation.withElementType(LocalTypeInformation.Unknown)
}
}
private fun reparameterise(typeInformation: LocalTypeInformation.ACollection): LocalTypeInformation.ACollection =
when(typeInformation.typeIdentifier) {
is TypeIdentifier.Parameterised -> typeInformation
is TypeIdentifier.Erased -> typeInformation.withElementType(LocalTypeInformation.Unknown)
else -> throw NotSerializableException(
"Unexpected type identifier ${typeInformation.typeIdentifier.prettyPrint(false)} " +
"for collection type ${typeInformation.prettyPrint(false)}")
}
private fun findMostSuitableCollectionType(actualClass: Class<*>): Class<out Collection<*>> =
supportedTypes.keys.findLast { it.isAssignableFrom(actualClass) }!!
private fun findConcreteType(clazz: Class<*>): (List<*>) -> Collection<*> {
return supportedTypes[clazz] ?: throw AMQPNotSerializableException(
clazz,
"Unsupported collection type $clazz.",
"Supported Collections are ${supportedTypes.keys.joinToString(",")}")
}
fun deriveParameterizedType(declaredType: Type, declaredClass: Class<*>, actualClass: Class<*>?): ParameterizedType {
if (supportedTypes.containsKey(declaredClass)) {
// Simple case - it is already known to be a collection.
return deriveParametrizedType(declaredType, uncheckedCast(declaredClass))
} else if (actualClass != null && Collection::class.java.isAssignableFrom(actualClass)) {
// Declared class is not collection, but [actualClass] is - represent it accordingly.
val collectionClass = findMostSuitableCollectionType(actualClass)
return deriveParametrizedType(declaredType, collectionClass)
}
throw AMQPNotSerializableException(
declaredType,
"Cannot derive collection type for declaredType: '$declaredType', " +
"declaredClass: '$declaredClass', actualClass: '$actualClass'")
}
private fun deriveParametrizedType(declaredType: Type, collectionClass: Class<out Collection<*>>): ParameterizedType =
(declaredType as? ParameterizedType)
?: DeserializedParameterizedType(collectionClass, arrayOf(SerializerFactory.AnyType))
private fun findMostSuitableCollectionType(actualClass: Class<*>): Class<out Collection<*>> =
supportedTypes.keys.findLast { it.isAssignableFrom(actualClass) }!!
}
private val concreteBuilder: (List<*>) -> Collection<*> = findConcreteType(declaredType.rawType as Class<*>)
private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "list", Descriptor(typeDescriptor), emptyList())
private val typeNotation: TypeNotation = RestrictedType(AMQPTypeIdentifiers.nameForType(declaredType), null, emptyList(), "list", Descriptor(typeDescriptor), emptyList())
private val outboundType = resolveTypeVariables(declaredType.actualTypeArguments[0], null)
private val inboundType = declaredType.actualTypeArguments[0]

View File

@ -0,0 +1,270 @@
package net.corda.serialization.internal.amqp
import net.corda.core.serialization.SerializationContext
import net.corda.serialization.internal.model.*
import org.apache.qpid.proton.amqp.Binary
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Method
import java.lang.reflect.Field
import java.lang.reflect.Type
/**
* A strategy for reading a property value during deserialization.
*/
interface PropertyReadStrategy {
companion object {
/**
* Select the correct strategy for reading properties, based on the property type.
*/
fun make(name: String, typeIdentifier: TypeIdentifier, type: Type): PropertyReadStrategy =
if (AMQPTypeIdentifiers.isPrimitive(typeIdentifier)) {
when (typeIdentifier) {
in characterTypes -> AMQPCharPropertyReadStrategy
else -> AMQPPropertyReadStrategy
}
} else {
DescribedTypeReadStrategy(name, typeIdentifier, type)
}
}
/**
* Use this strategy to read the value of a property during deserialization.
*/
fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any?
}
/**
* A strategy for writing a property value during serialisation.
*/
interface PropertyWriteStrategy {
companion object {
/**
* Select the correct strategy for writing properties, based on the property information.
*/
fun make(name: String, propertyInformation: LocalPropertyInformation, factory: LocalSerializerFactory): PropertyWriteStrategy {
val reader = PropertyReader.make(propertyInformation)
val type = propertyInformation.type
return if (AMQPTypeIdentifiers.isPrimitive(type.typeIdentifier)) {
when (type.typeIdentifier) {
in characterTypes -> AMQPCharPropertyWriteStategy(reader)
else -> AMQPPropertyWriteStrategy(reader)
}
} else {
DescribedTypeWriteStrategy(name, propertyInformation, reader) { factory.get(propertyInformation.type) }
}
}
}
/**
* Write any [TypeNotation] needed to the [SerializationOutput].
*/
fun writeClassInfo(output: SerializationOutput)
/**
* Write the property's value to the [SerializationOutput].
*/
fun writeProperty(obj: Any?, data: Data, output: SerializationOutput, context: SerializationContext, debugIndent: Int)
}
/**
* Combines strategies for reading and writing a given property's value during serialisation/deserialisation.
*/
interface PropertySerializer : PropertyReadStrategy, PropertyWriteStrategy {
/**
* The name of the property.
*/
val name: String
/**
* Whether the property is calculated.
*/
val isCalculated: Boolean
}
/**
* A [PropertySerializer] for a property of a [LocalTypeInformation.Composable] type.
*/
class ComposableTypePropertySerializer(
override val name: String,
override val isCalculated: Boolean,
private val readStrategy: PropertyReadStrategy,
private val writeStrategy: PropertyWriteStrategy) :
PropertySerializer,
PropertyReadStrategy by readStrategy,
PropertyWriteStrategy by writeStrategy {
companion object {
/**
* Make a [PropertySerializer] for the given [LocalPropertyInformation].
*
* @param name The name of the property.
* @param propertyInformation [LocalPropertyInformation] for the property.
* @param factory The [LocalSerializerFactory] to use when writing values for this property.
*/
fun make(name: String, propertyInformation: LocalPropertyInformation, factory: LocalSerializerFactory): PropertySerializer =
ComposableTypePropertySerializer(
name,
propertyInformation.isCalculated,
PropertyReadStrategy.make(name, propertyInformation.type.typeIdentifier, propertyInformation.type.observedType),
PropertyWriteStrategy.make(name, propertyInformation, factory))
/**
* Make a [PropertySerializer] for use in deserialization only, when deserializing a type that requires evolution.
*
* @param name The name of the property.
* @param isCalculated Whether the property is calculated.
* @param typeIdentifier The [TypeIdentifier] for the property type.
* @param type The local [Type] for the property type.
*/
fun makeForEvolution(name: String, isCalculated: Boolean, typeIdentifier: TypeIdentifier, type: Type): PropertySerializer =
ComposableTypePropertySerializer(
name,
isCalculated,
PropertyReadStrategy.make(name, typeIdentifier, type),
EvolutionPropertyWriteStrategy)
}
}
/**
* Obtains the value of a property from an instance of the type to which that property belongs, either by calling a getter method
* or by reading the value of a private backing field.
*/
sealed class PropertyReader {
companion object {
/**
* Make a [PropertyReader] based on the provided [LocalPropertyInformation].
*/
fun make(propertyInformation: LocalPropertyInformation) = when(propertyInformation) {
is LocalPropertyInformation.GetterSetterProperty -> GetterReader(propertyInformation.observedGetter)
is LocalPropertyInformation.ConstructorPairedProperty -> GetterReader(propertyInformation.observedGetter)
is LocalPropertyInformation.ReadOnlyProperty -> GetterReader(propertyInformation.observedGetter)
is LocalPropertyInformation.CalculatedProperty -> GetterReader(propertyInformation.observedGetter)
is LocalPropertyInformation.PrivateConstructorPairedProperty -> FieldReader(propertyInformation.observedField)
}
}
/**
* Get the value of the property from the supplied instance, or null if the instance is itself null.
*/
abstract fun read(obj: Any?): Any?
/**
* Reads a property using a getter [Method].
*/
class GetterReader(private val getter: Method): PropertyReader() {
init {
getter.isAccessible = true
}
override fun read(obj: Any?): Any? = if (obj == null) null else getter.invoke(obj)
}
/**
* Reads a property using a backing [Field].
*/
class FieldReader(private val field: Field): PropertyReader() {
init {
field.isAccessible = true
}
override fun read(obj: Any?): Any? = if (obj == null) null else field.get(obj)
}
}
private val characterTypes = setOf(
TypeIdentifier.forClass(Char::class.javaObjectType),
TypeIdentifier.forClass(Char::class.javaPrimitiveType!!)
)
object EvolutionPropertyWriteStrategy : PropertyWriteStrategy {
override fun writeClassInfo(output: SerializationOutput) =
throw UnsupportedOperationException("Evolution serializers cannot write values")
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput, context: SerializationContext, debugIndent: Int) =
throw UnsupportedOperationException("Evolution serializers cannot write values")
}
/**
* Read a type that comes with its own [TypeDescriptor], by calling back into [RemoteSerializerFactory] to obtain a suitable
* serializer for that descriptor.
*/
class DescribedTypeReadStrategy(name: String,
typeIdentifier: TypeIdentifier,
private val type: Type): PropertyReadStrategy {
private val nameForDebug = "$name(${typeIdentifier.prettyPrint(false)})"
override fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any? =
ifThrowsAppend({ nameForDebug }) {
input.readObjectOrNull(obj, schemas, type, context)
}
}
/**
* Writes a property value into [SerializationOutput], together with a schema information describing it.
*/
class DescribedTypeWriteStrategy(private val name: String,
private val propertyInformation: LocalPropertyInformation,
private val reader: PropertyReader,
private val serializerProvider: () -> AMQPSerializer<Any>) : PropertyWriteStrategy {
// Lazy to avoid getting into infinite loops when there are cycles.
private val serializer by lazy { serializerProvider() }
private val nameForDebug get() = "$name(${propertyInformation.type.typeIdentifier.prettyPrint(false)})"
override fun writeClassInfo(output: SerializationOutput) {
if (propertyInformation.type !is LocalTypeInformation.Top) {
serializer.writeClassInfo(output)
}
}
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput, context: SerializationContext,
debugIndent: Int) = ifThrowsAppend({ nameForDebug }) {
val propertyValue = reader.read(obj)
output.writeObjectOrNull(propertyValue, data, propertyInformation.type.observedType, context, debugIndent)
}
}
object AMQPPropertyReadStrategy : PropertyReadStrategy {
override fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any? =
if (obj is Binary) obj.array else obj
}
class AMQPPropertyWriteStrategy(private val reader: PropertyReader) : PropertyWriteStrategy {
override fun writeClassInfo(output: SerializationOutput) {}
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput,
context: SerializationContext, debugIndent: Int
) {
val value = reader.read(obj)
// ByteArrays have to be wrapped in an AMQP Binary wrapper.
if (value is ByteArray) {
data.putObject(Binary(value))
} else {
data.putObject(value)
}
}
}
object AMQPCharPropertyReadStrategy : PropertyReadStrategy {
override fun readProperty(obj: Any?, schemas: SerializationSchemas,
input: DeserializationInput, context: SerializationContext
): Any? {
return if (obj == null) null else (obj as Short).toChar()
}
}
class AMQPCharPropertyWriteStategy(private val reader: PropertyReader) : PropertyWriteStrategy {
override fun writeClassInfo(output: SerializationOutput) {}
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput,
context: SerializationContext, debugIndent: Int
) {
val input = reader.read(obj)
if (input != null) data.putShort((input as Char).toShort()) else data.putNull()
}
}

View File

@ -4,7 +4,6 @@ import com.google.common.reflect.TypeToken
import net.corda.core.internal.uncheckedCast
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationCustomSerializer
import net.corda.serialization.internal.amqp.SerializerFactory.Companion.nameForType
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type
@ -63,9 +62,11 @@ class CorDappCustomSerializer(
override val type = types[CORDAPP_TYPE]
val proxyType = types[PROXY_TYPE]
override val typeDescriptor: Symbol = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${nameForType(type)}")
override val typeDescriptor: Symbol = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${AMQPTypeIdentifiers.nameForType(type)}")
val descriptor: Descriptor = Descriptor(typeDescriptor)
private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer(proxyType, factory) }
private val proxySerializer: ObjectSerializer by lazy {
ObjectSerializer.make(factory.getTypeInformation(proxyType), factory)
}
override fun writeClassInfo(output: SerializationOutput) {}
@ -77,8 +78,8 @@ class CorDappCustomSerializer(
data.withDescribed(descriptor) {
data.withList {
proxySerializer.propertySerializers.serializationOrder.forEach {
it.serializer.writeProperty(proxy, this, output, context)
(proxySerializer as ObjectSerializer).propertySerializers.forEach { (_, serializer) ->
serializer.writeProperty(proxy, this, output, context, debugIndent)
}
}
}

View File

@ -3,7 +3,7 @@ package net.corda.serialization.internal.amqp
import net.corda.core.KeepForDJVM
import net.corda.core.internal.uncheckedCast
import net.corda.core.serialization.SerializationContext
import net.corda.serialization.internal.amqp.SerializerFactory.Companion.nameForType
import net.corda.serialization.internal.model.FingerprintWriter
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type
@ -67,13 +67,13 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.clazz
override val type: Type get() = clazz
override val typeDescriptor: Symbol by lazy {
Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForDescriptors(superClassSerializer.typeDescriptor.toString(), nameForType(clazz))}")
Symbol.valueOf("$DESCRIPTOR_DOMAIN:${FingerprintWriter(false).write(arrayOf(superClassSerializer.typeDescriptor.toString(), AMQPTypeIdentifiers.nameForType(clazz)).joinToString()).fingerprint}")
}
private val typeNotation: TypeNotation = RestrictedType(
SerializerFactory.nameForType(clazz),
AMQPTypeIdentifiers.nameForType(clazz),
null,
emptyList(),
SerializerFactory.nameForType(superClassSerializer.type),
AMQPTypeIdentifiers.nameForType(superClassSerializer.type),
Descriptor(typeDescriptor),
emptyList())
@ -102,7 +102,7 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
*/
abstract class CustomSerializerImp<T : Any>(protected val clazz: Class<T>, protected val withInheritance: Boolean) : CustomSerializer<T>() {
override val type: Type get() = clazz
override val typeDescriptor: Symbol = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${nameForType(clazz)}")
override val typeDescriptor: Symbol = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${AMQPTypeIdentifiers.nameForType(clazz)}")
override fun writeClassInfo(output: SerializationOutput) {}
override val descriptor: Descriptor = Descriptor(typeDescriptor)
override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz
@ -127,19 +127,19 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
*/
abstract class Proxy<T : Any, P : Any>(clazz: Class<T>,
protected val proxyClass: Class<P>,
protected val factory: SerializerFactory,
protected val factory: LocalSerializerFactory,
withInheritance: Boolean = true) : CustomSerializerImp<T>(clazz, withInheritance) {
override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz
private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer(proxyClass, factory) }
private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer.make(factory.getTypeInformation(proxyClass), factory) }
override val schemaForDocumentation: Schema by lazy {
val typeNotations = mutableSetOf<TypeNotation>(
CompositeType(
nameForType(type),
AMQPTypeIdentifiers.nameForType(type),
null,
emptyList(),
descriptor, (proxySerializer.typeNotation as CompositeType).fields))
descriptor, proxySerializer.fields))
for (additional in additionalSerializers) {
typeNotations.addAll(additional.schemaForDocumentation.types)
}
@ -158,8 +158,8 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
) {
val proxy = toProxy(obj)
data.withList {
proxySerializer.propertySerializers.serializationOrder.forEach {
it.serializer.writeProperty(proxy, this, output, context)
proxySerializer.propertySerializers.forEach { (_, serializer) ->
serializer.writeProperty(proxy, this, output, context, 0)
}
}
}
@ -191,8 +191,8 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
: CustomSerializerImp<T>(clazz, withInheritance) {
override val schemaForDocumentation = Schema(
listOf(RestrictedType(nameForType(type), "", listOf(nameForType(type)),
SerializerFactory.primitiveTypeName(String::class.java)!!,
listOf(RestrictedType(AMQPTypeIdentifiers.nameForType(type), "", listOf(AMQPTypeIdentifiers.nameForType(type)),
AMQPTypeIdentifiers.primitiveTypeName(String::class.java),
descriptor, emptyList())))
override fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput,

View File

@ -25,5 +25,5 @@ class DefaultDescriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistr
}
override fun getOrBuild(descriptor: String, builder: () -> AMQPSerializer<Any>) =
get(descriptor) ?: builder().also { newSerializer -> this[descriptor] = newSerializer }
registry.getOrPut(descriptor) { builder() }
}

View File

@ -8,6 +8,7 @@ import net.corda.core.serialization.SerializedBytes
import net.corda.core.utilities.ByteSequence
import net.corda.core.utilities.loggerFor
import net.corda.serialization.internal.*
import net.corda.serialization.internal.model.TypeIdentifier
import org.apache.qpid.proton.amqp.Binary
import org.apache.qpid.proton.amqp.DescribedType
import org.apache.qpid.proton.amqp.UnsignedInteger
@ -168,8 +169,8 @@ class DeserializationInput constructor(
val objectRead = when (obj) {
is DescribedType -> {
// Look up serializer in factory by descriptor
val serializer = serializerFactory.get(obj.descriptor, schemas)
if (SerializerFactory.AnyType != type && serializer.type != type && with(serializer.type) {
val serializer = serializerFactory.get(obj.descriptor.toString(), schemas)
if (type != TypeIdentifier.UnknownType.getLocalType() && serializer.type != type && with(serializer.type) {
!isSubClassOf(type) && !materiallyEquivalentTo(type)
}
) {

View File

@ -1,18 +0,0 @@
package net.corda.serialization.internal.amqp
import java.lang.reflect.GenericArrayType
import java.lang.reflect.Type
import java.util.*
/**
* Implementation of [GenericArrayType] that we can actually construct.
*/
class DeserializedGenericArrayType(private val componentType: Type) : GenericArrayType {
override fun getGenericComponentType(): Type = componentType
override fun getTypeName(): String = "${componentType.typeName}[]"
override fun toString(): String = typeName
override fun hashCode(): Int = Objects.hashCode(componentType)
override fun equals(other: Any?): Boolean {
return other is GenericArrayType && (componentType == other.genericComponentType)
}
}

View File

@ -1,174 +0,0 @@
package net.corda.serialization.internal.amqp
import com.google.common.primitives.Primitives
import net.corda.core.KeepForDJVM
import java.io.NotSerializableException
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import java.lang.reflect.TypeVariable
import java.util.*
/**
* Implementation of [ParameterizedType] that we can actually construct, and a parser from the string representation
* of the JDK implementation which we use as the textual format in the AMQP schema.
*/
@KeepForDJVM
class DeserializedParameterizedType(
private val rawType: Class<*>,
private val params: Array<out Type>,
private val ownerType: Type? = null
) : ParameterizedType {
init {
if (params.isEmpty()) {
throw AMQPNotSerializableException(rawType, "Must be at least one parameter type in a ParameterizedType")
}
if (params.size != rawType.typeParameters.size) {
throw AMQPNotSerializableException(
rawType,
"Expected ${rawType.typeParameters.size} for ${rawType.name} but found ${params.size}")
}
}
private fun boundedType(type: TypeVariable<out Class<out Any>>): Boolean {
return !(type.bounds.size == 1 && type.bounds[0] == Object::class.java)
}
private val _typeName: String = makeTypeName()
private fun makeTypeName(): String {
val paramsJoined = params.joinToString(", ") { it.typeName }
return "${rawType.name}<$paramsJoined>"
}
companion object {
// Maximum depth/nesting of generics before we suspect some DoS attempt.
const val MAX_DEPTH: Int = 32
fun make(name: String, cl: ClassLoader = DeserializedParameterizedType::class.java.classLoader): Type {
val paramTypes = ArrayList<Type>()
val pos = parseTypeList("$name>", paramTypes, cl)
if (pos <= name.length) {
throw AMQPNoTypeNotSerializableException(
"Malformed string form of ParameterizedType. Unexpected '>' at character position $pos of $name.")
}
if (paramTypes.size != 1) {
throw AMQPNoTypeNotSerializableException("Expected only one type, but got $paramTypes")
}
return paramTypes[0]
}
private fun parseTypeList(params: String, types: MutableList<Type>, cl: ClassLoader, depth: Int = 0): Int {
var pos = 0
var typeStart = 0
var needAType = true
var skippingWhitespace = false
while (pos < params.length) {
if (params[pos] == '<') {
val typeEnd = pos++
val paramTypes = ArrayList<Type>()
pos = parseTypeParams(params, pos, paramTypes, cl, depth + 1)
types += makeParameterizedType(params.substring(typeStart, typeEnd).trim(), paramTypes, cl)
typeStart = pos
needAType = false
} else if (params[pos] == ',') {
val typeEnd = pos++
val typeName = params.substring(typeStart, typeEnd).trim()
if (!typeName.isEmpty()) {
types += makeType(typeName, cl)
} else if (needAType) {
throw AMQPNoTypeNotSerializableException("Expected a type, not ','")
}
typeStart = pos
needAType = true
} else if (params[pos] == '>') {
val typeEnd = pos++
val typeName = params.substring(typeStart, typeEnd).trim()
if (!typeName.isEmpty()) {
types += makeType(typeName, cl)
} else if (needAType) {
throw AMQPNoTypeNotSerializableException("Expected a type, not '>'")
}
return pos
} else {
// Skip forwards, checking character types
if (pos == typeStart) {
skippingWhitespace = false
if (params[pos].isWhitespace()) {
typeStart = ++pos
} else if (!needAType) {
throw AMQPNoTypeNotSerializableException("Not expecting a type")
} else if (params[pos] == '?') {
pos++
} else if (!params[pos].isJavaIdentifierStart()) {
throw AMQPNoTypeNotSerializableException("Invalid character at start of type: ${params[pos]}")
} else {
pos++
}
} else {
if (params[pos].isWhitespace()) {
pos++
skippingWhitespace = true
} else if (!skippingWhitespace && (params[pos] == '.' || params[pos].isJavaIdentifierPart())) {
pos++
} else {
throw AMQPNoTypeNotSerializableException(
"Invalid character ${params[pos]} in middle of type $params at idx $pos")
}
}
}
}
throw AMQPNoTypeNotSerializableException("Missing close generics '>'")
}
private fun makeType(typeName: String, cl: ClassLoader): Type {
// Not generic
return if (typeName == "?") SerializerFactory.AnyType else {
Primitives.wrap(SerializerFactory.primitiveType(typeName) ?: Class.forName(typeName, false, cl))
}
}
private fun makeParameterizedType(rawTypeName: String, args: MutableList<Type>, cl: ClassLoader): Type {
return DeserializedParameterizedType(makeType(rawTypeName, cl) as Class<*>, args.toTypedArray(), null)
}
private fun parseTypeParams(
params: String,
startPos: Int,
paramTypes: MutableList<Type>,
cl: ClassLoader,
depth: Int
): Int {
if (depth == MAX_DEPTH) {
throw AMQPNoTypeNotSerializableException("Maximum depth of nested generics reached: $depth")
}
return startPos + parseTypeList(params.substring(startPos), paramTypes, cl, depth)
}
}
override fun getRawType(): Type = rawType
override fun getOwnerType(): Type? = ownerType
override fun getActualTypeArguments(): Array<out Type> = params
override fun getTypeName(): String = _typeName
override fun toString(): String = _typeName
override fun hashCode(): Int {
return Arrays.hashCode(this.actualTypeArguments) xor Objects.hashCode(this.ownerType) xor Objects.hashCode(this.rawType)
}
override fun equals(other: Any?): Boolean {
return if (other is ParameterizedType) {
if (this === other) {
true
} else {
this.ownerType == other.ownerType && this.rawType == other.rawType && Arrays.equals(this.actualTypeArguments, other.actualTypeArguments)
}
} else {
false
}
}
}

View File

@ -2,6 +2,7 @@ package net.corda.serialization.internal.amqp
import net.corda.core.internal.uncheckedCast
import net.corda.core.serialization.SerializationContext
import net.corda.serialization.internal.model.LocalTypeInformation
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException
@ -37,100 +38,20 @@ import java.util.*
*/
class EnumEvolutionSerializer(
override val type: Type,
factory: SerializerFactory,
factory: LocalSerializerFactory,
private val conversions: Map<String, String>,
private val ordinals: Map<String, Int>) : AMQPSerializer<Any> {
override val typeDescriptor = Symbol.valueOf(
"$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")!!
companion object {
private fun MutableMap<String, String>.mapInPlace(f: (String) -> String) {
val i = iterator()
while (i.hasNext()) {
val curr = i.next()
curr.setValue(f(curr.value))
}
}
/**
* Builds an Enum Evolver serializer.
*
* @param old The description of the enum as it existed at the time of serialisation taken from the
* received AMQP header
* @param new The Serializer object we built based on the current state of the enum class on our classpath
* @param factory the [SerializerFactory] that is building this serialization object.
* @param schemas the transforms attached to the class in the AMQP header, i.e. the transforms
* known at serialization time
*/
fun make(old: RestrictedType,
new: AMQPSerializer<Any>,
factory: SerializerFactory,
schemas: SerializationSchemas): AMQPSerializer<Any> {
val wireTransforms = schemas.transforms.types[old.name]
?: EnumMap<TransformTypes, MutableList<Transform>>(TransformTypes::class.java)
val localTransforms = TransformsSchema.get(old.name, factory)
// remember, the longer the list the newer we're assuming the transform set it as we assume
// evolution annotations are never removed, only added to
val transforms = if (wireTransforms.size > localTransforms.size) wireTransforms else localTransforms
// if either of these isn't of the cast type then something has gone terribly wrong
// elsewhere in the code
val defaultRules: List<EnumDefaultSchemaTransform>? = uncheckedCast(transforms[TransformTypes.EnumDefault])
val renameRules: List<RenameSchemaTransform>? = uncheckedCast(transforms[TransformTypes.Rename])
// What values exist on the enum as it exists on the class path
val localValues = new.type.asClass().enumConstants.map { it.toString() }
val conversions: MutableMap<String, String> = localValues
.union(defaultRules?.map { it.new }?.toSet() ?: emptySet())
.union(renameRules?.map { it.to } ?: emptySet())
.associateBy({ it }, { it })
.toMutableMap()
val rules: MutableMap<String, String> = mutableMapOf()
rules.putAll(defaultRules?.associateBy({ it.new }, { it.old }) ?: emptyMap())
val renameRulesMap = renameRules?.associateBy({ it.to }, { it.from }) ?: emptyMap()
rules.putAll(renameRulesMap)
// take out set of all possible constants and build a map from those to the
// existing constants applying the rename and defaulting rules as defined
// in the schema
while (conversions.filterNot { it.value in localValues }.isNotEmpty()) {
conversions.mapInPlace { rules[it] ?: it }
}
// you'd think this was overkill to get access to the ordinal values for each constant but it's actually
// rather tricky when you don't have access to the actual type, so this is a nice way to be able
// to precompute and pass to the actual object
val ordinals = localValues.mapIndexed { i, s -> Pair(s, i) }.toMap()
// create a mapping between the ordinal value and the name as it was serialised converted
// to the name as it exists. We want to test any new constants have been added to the end
// of the enum class
val serialisedOrds = ((schemas.schema.types.find { it.name == old.name } as RestrictedType).choices
.associateBy({ it.value.toInt() }, { conversions[it.name] }))
if (ordinals.filterNot { serialisedOrds[it.value] == it.key }.isNotEmpty()) {
throw AMQPNotSerializableException(
new.type,
"Constants have been reordered, additions must be appended to the end")
}
return EnumEvolutionSerializer(new.type, factory, conversions, ordinals)
}
}
override val typeDescriptor = factory.createDescriptor(type)
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
context: SerializationContext
): Any {
val enumName = (obj as List<*>)[0] as String
if (enumName !in conversions) {
throw AMQPNotSerializableException(type, "No rule to evolve enum constant $type::$enumName")
}
val converted = conversions[enumName] ?: throw AMQPNotSerializableException(type, "No rule to evolve enum constant $type::$enumName")
val ordinal = ordinals[converted] ?: throw AMQPNotSerializableException(type, "Ordinal not found for enum value $type::$converted")
return type.asClass().enumConstants[ordinals[conversions[enumName]]!!]
return type.asClass().enumConstants[ordinal]
}
override fun writeClassInfo(output: SerializationOutput) {

View File

@ -1,24 +1,21 @@
package net.corda.serialization.internal.amqp
import net.corda.core.serialization.SerializationContext
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException
import java.lang.reflect.Type
/**
* Our definition of an enum with the AMQP spec is a list (of two items, a string and an int) that is
* a restricted type with a number of choices associated with it
*/
class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: SerializerFactory) : AMQPSerializer<Any> {
class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: LocalSerializerFactory) : AMQPSerializer<Any> {
override val type: Type = declaredType
private val typeNotation: TypeNotation
override val typeDescriptor = Symbol.valueOf(
"$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")!!
override val typeDescriptor = factory.createDescriptor(type)
init {
typeNotation = RestrictedType(
SerializerFactory.nameForType(declaredType),
AMQPTypeIdentifiers.nameForType(declaredType),
null, emptyList(), "list", Descriptor(typeDescriptor),
declaredClass.enumConstants.zip(IntRange(0, declaredClass.enumConstants.size)).map {
Choice(it.first.toString(), it.second.toString())

View File

@ -1,312 +0,0 @@
package net.corda.serialization.internal.amqp
import net.corda.core.KeepForDJVM
import net.corda.core.internal.isConcreteClass
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import net.corda.core.serialization.SerializationContext
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.core.utilities.loggerFor
import net.corda.serialization.internal.carpenter.getTypeAsClass
import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException
import java.lang.reflect.Type
import kotlin.reflect.KFunction
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.jvm.javaType
import kotlin.reflect.jvm.jvmErasure
/**
* Serializer for deserializing objects whose definition has changed since they
* were serialised.
*
* @property oldReaders A linked map representing the properties of the object as they were serialized. Note
* this may contain properties that are no longer needed by the class. These *must* be read however to ensure
* any refferenced objects in the object stream are captured properly
* @property kotlinConstructor
* @property constructorArgs used to hold the properties as sent to the object's constructor. Passed in as a
* pre populated array as properties not present on the old constructor must be initialised in the factory
*/
abstract class EvolutionSerializer(
clazz: Type,
factory: SerializerFactory,
protected val oldReaders: Map<String, OldParam>,
override val kotlinConstructor: KFunction<Any>
) : ObjectSerializer(clazz, factory) {
// explicitly set as empty to indicate it's unused by this type of serializer
override val propertySerializers = PropertySerializersEvolution()
/**
* Represents a parameter as would be passed to the constructor of the class as it was
* when it was serialised and NOT how that class appears now
*
* @param resultsIndex index into the constructor argument list where the read property
* should be placed
* @param property object to read the actual property value
*/
@KeepForDJVM
data class OldParam(var resultsIndex: Int, val property: PropertySerializer) {
fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput,
new: Array<Any?>, context: SerializationContext
) = property.readProperty(obj, schemas, input, context).apply {
if (resultsIndex >= 0) {
new[resultsIndex] = this
}
}
override fun toString(): String {
return "resultsIndex = $resultsIndex property = ${property.name}"
}
}
companion object {
val logger = contextLogger()
/**
* Unlike the generic deserialization case where we need to locate the primary constructor
* for the object (or our best guess) in the case of an object whose structure has changed
* since serialisation we need to attempt to locate a constructor that we can use. For example,
* its parameters match the serialised members and it will initialise any newly added
* elements.
*
* TODO: Type evolution
* TODO: rename annotation
*/
private fun getEvolverConstructor(type: Type, oldArgs: Map<String, OldParam>): KFunction<Any>? {
val clazz: Class<*> = type.asClass()
if (!clazz.isConcreteClass) return null
val oldArgumentSet = oldArgs.map { Pair(it.key as String?, it.value.property.resolvedType.asClass()) }
var maxConstructorVersion = Integer.MIN_VALUE
var constructor: KFunction<Any>? = null
clazz.kotlin.constructors.forEach {
val version = it.findAnnotation<DeprecatedConstructorForDeserialization>()?.version ?: Integer.MIN_VALUE
if (version > maxConstructorVersion &&
oldArgumentSet.containsAll(it.parameters.map { v -> Pair(v.name, v.type.javaType.asClass()) })
) {
constructor = it
maxConstructorVersion = version
with(logger) {
info("Select annotated constructor version=$version nparams=${it.parameters.size}")
debug{" params=${it.parameters}"}
}
} else if (version != Integer.MIN_VALUE){
with(logger) {
info("Ignore annotated constructor version=$version nparams=${it.parameters.size}")
debug{" params=${it.parameters}"}
}
}
}
// if we didn't get an exact match revert to existing behaviour, if the new parameters
// are not mandatory (i.e. nullable) things are fine
return constructor ?: run {
logger.info("Failed to find annotated historic constructor")
constructorForDeserialization(type)
}
}
private fun makeWithConstructor(
new: ObjectSerializer,
factory: SerializerFactory,
constructor: KFunction<Any>,
readersAsSerialized: Map<String, OldParam>): AMQPSerializer<Any> {
// Java doesn't care about nullability unless it's a primitive in which
// case it can't be referenced. Unfortunately whilst Kotlin does apply
// Nullability annotations we cannot use them here as they aren't
// retained at runtime so we cannot rely on the absence of
// any particular NonNullable annotation type to indicate cross
// compiler nullability
val isKotlin = (new.type.javaClass.declaredAnnotations.any {
it.annotationClass.qualifiedName == "kotlin.Metadata"
})
constructor.parameters.withIndex().forEach {
if ((readersAsSerialized[it.value.name!!] ?.apply { this.resultsIndex = it.index }) == null) {
// If there is no value in the byte stream to map to the parameter of the constructor
// this is ok IFF it's a Kotlin class and the parameter is non nullable OR
// its a Java class and the parameter is anything but an unboxed primitive.
// Otherwise we throw the error and leave
if ((isKotlin && !it.value.type.isMarkedNullable)
|| (!isKotlin && isJavaPrimitive(it.value.type.jvmErasure.java))
) {
throw AMQPNotSerializableException(
new.type,
"New parameter \"${it.value.name}\" is mandatory, should be nullable for evolution " +
"to work, isKotlinClass=$isKotlin type=${it.value.type}")
}
}
}
return EvolutionSerializerViaConstructor(new.type, factory, readersAsSerialized, constructor)
}
private fun makeWithSetters(
new: ObjectSerializer,
factory: SerializerFactory,
constructor: KFunction<Any>,
readersAsSerialized: Map<String, OldParam>,
classProperties: Map<String, PropertyDescriptor>): AMQPSerializer<Any> {
val setters = propertiesForSerializationFromSetters(classProperties,
new.type,
factory).associateBy({ it.serializer.name }, { it })
return EvolutionSerializerViaSetters(new.type, factory, readersAsSerialized, constructor, setters)
}
/**
* Build a serialization object for deserialization only of objects serialised
* as different versions of a class.
*
* @param old is an object holding the schema that represents the object
* as it was serialised and the type descriptor of that type
* @param new is the Serializer built for the Class as it exists now, not
* how it was serialised and persisted.
* @param factory the [SerializerFactory] associated with the serialization
* context this serializer is being built for
*/
fun make(old: CompositeType,
new: ObjectSerializer,
factory: SerializerFactory
): AMQPSerializer<Any> {
// The order in which the properties were serialised is important and must be preserved
val readersAsSerialized = LinkedHashMap<String, OldParam>()
old.fields.forEach {
readersAsSerialized[it.name] = try {
OldParam(-1, PropertySerializer.make(it.name, EvolutionPropertyReader(),
it.getTypeAsClass(factory.classloader), factory))
} catch (e: ClassNotFoundException) {
throw AMQPNotSerializableException(new.type, e.message ?: "")
}
}
// cope with the situation where a generic interface was serialised as a type, in such cases
// return the synthesised object which is, given the absence of a constructor, a no op
val constructor = getEvolverConstructor(new.type, readersAsSerialized) ?: return new
val classProperties = new.type.asClass().propertyDescriptors()
return if (classProperties.isNotEmpty() && constructor.parameters.isEmpty()) {
makeWithSetters(new, factory, constructor, readersAsSerialized, classProperties)
} else {
makeWithConstructor(new, factory, constructor, readersAsSerialized)
}
}
}
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
context: SerializationContext, debugIndent: Int
) {
throw UnsupportedOperationException("It should be impossible to write an evolution serializer")
}
}
class EvolutionSerializerViaConstructor(
clazz: Type,
factory: SerializerFactory,
oldReaders: Map<String, EvolutionSerializer.OldParam>,
kotlinConstructor: KFunction<Any>) : EvolutionSerializer(clazz, factory, oldReaders, kotlinConstructor) {
/**
* Unlike a normal [readObject] call where we simply apply the parameter deserialisers
* to the object list of values we need to map that list, which is ordered per the
* constructor of the original state of the object, we need to map the new parameter order
* of the current constructor onto that list inserting nulls where new parameters are
* encountered.
*
* TODO: Object references
*/
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
context: SerializationContext
): Any {
if (obj !is List<*>) throw NotSerializableException("Body of described type is unexpected $obj")
val constructorArgs : Array<Any?> = arrayOfNulls<Any?>(kotlinConstructor.parameters.size)
// *must* read all the parameters in the order they were serialized
oldReaders.values.zip(obj).map { it.first.readProperty(it.second, schemas, input, constructorArgs, context) }
return javaConstructor?.newInstance(*(constructorArgs)) ?: throw NotSerializableException(
"Attempt to deserialize an interface: $clazz. Serialized form is invalid.")
}
}
/**
* Specific instance of an [EvolutionSerializer] where the properties of the object are set via calling
* named setter functions on the instantiated object.
*/
class EvolutionSerializerViaSetters(
clazz: Type,
factory: SerializerFactory,
oldReaders: Map<String, EvolutionSerializer.OldParam>,
kotlinConstructor: KFunction<Any>,
private val setters: Map<String, PropertyAccessor>) : EvolutionSerializer(clazz, factory, oldReaders, kotlinConstructor) {
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
context: SerializationContext
): Any {
if (obj !is List<*>) throw NotSerializableException("Body of described type is unexpected $obj")
val instance: Any = javaConstructor?.newInstance() ?: throw NotSerializableException(
"Failed to instantiate instance of object $clazz")
// *must* read all the parameters in the order they were serialized
oldReaders.values.zip(obj).forEach {
// if that property still exists on the new object then set it
it.first.property.readProperty(it.second, schemas, input, context).apply {
setters[it.first.property.name]?.set(instance, this)
}
}
return instance
}
}
/**
* Instances of this type are injected into a [SerializerFactory] at creation time to dictate the
* behaviour of evolution within that factory. Under normal circumstances this will simply
* be an object that returns an [EvolutionSerializer]. Of course, any implementation that
* extends this class can be written to invoke whatever behaviour is desired.
*/
interface EvolutionSerializerProvider {
fun getEvolutionSerializer(
factory: SerializerFactory,
typeNotation: TypeNotation,
newSerializer: AMQPSerializer<Any>,
schemas: SerializationSchemas): AMQPSerializer<Any>
}
/**
* The normal use case for generating an [EvolutionSerializer]'s based on the differences
* between the received schema and the class as it exists now on the class path,
*/
@KeepForDJVM
object DefaultEvolutionSerializerProvider : EvolutionSerializerProvider {
override fun getEvolutionSerializer(factory: SerializerFactory,
typeNotation: TypeNotation,
newSerializer: AMQPSerializer<Any>,
schemas: SerializationSchemas): AMQPSerializer<Any> {
return factory.registerByDescriptor(typeNotation.descriptor.name!!) {
when (typeNotation) {
is CompositeType -> EvolutionSerializer.make(typeNotation, newSerializer as ObjectSerializer, factory)
is RestrictedType -> {
// The fingerprint of a generic collection can be changed through bug fixes to the
// fingerprinting function making it appear as if the class has altered whereas it hasn't.
// Given we don't support the evolution of these generic containers, if it appears
// one has been changed, simply return the original serializer and associate it with
// both the new and old fingerprint
if (newSerializer is CollectionSerializer || newSerializer is MapSerializer) {
newSerializer
} else if (newSerializer is EnumSerializer){
EnumEvolutionSerializer.make(typeNotation, newSerializer, factory, schemas)
}
else {
loggerFor<SerializerFactory>().error("typeNotation=${typeNotation.name} Need to evolve unsupported type")
throw NotSerializableException ("${typeNotation.name} cannot be evolved")
}
}
}
}
}
}

View File

@ -0,0 +1,170 @@
package net.corda.serialization.internal.amqp
import net.corda.serialization.internal.model.*
import java.io.NotSerializableException
/**
* A factory that knows how to create serialisers when there is a mismatch between the remote and local type schemas.
*/
interface EvolutionSerializerFactory {
/**
* Compare the given [RemoteTypeInformation] and [LocalTypeInformation], and construct (if needed) an evolution
* serialiser that can take properties serialised in the remote schema and construct an object conformant to the local schema.
*
* Will return null if no evolution is necessary, because the schemas are compatible.
*/
fun getEvolutionSerializer(
remote: RemoteTypeInformation,
local: LocalTypeInformation): AMQPSerializer<Any>?
}
class EvolutionSerializationException(remoteTypeInformation: RemoteTypeInformation, reason: String)
: NotSerializableException(
"""
Cannot construct evolution serializer for remote type ${remoteTypeInformation.prettyPrint(false)}
$reason
""".trimIndent()
)
class DefaultEvolutionSerializerFactory(
private val localSerializerFactory: LocalSerializerFactory,
private val classLoader: ClassLoader,
private val mustPreserveDataWhenEvolving: Boolean): EvolutionSerializerFactory {
override fun getEvolutionSerializer(remote: RemoteTypeInformation,
local: LocalTypeInformation): AMQPSerializer<Any>? =
when(remote) {
is RemoteTypeInformation.Composable ->
if (local is LocalTypeInformation.Composable) remote.getEvolutionSerializer(local)
else null
is RemoteTypeInformation.AnEnum ->
if (local is LocalTypeInformation.AnEnum) remote.getEvolutionSerializer(local)
else null
else -> null
}
private fun RemoteTypeInformation.Composable.getEvolutionSerializer(
localTypeInformation: LocalTypeInformation.Composable): AMQPSerializer<Any>? {
// The no-op case: although the fingerprints don't match for some reason, we have compatible signatures.
// This might happen because of inconsistent type erasure, changes to the behaviour of the fingerprinter,
// or changes to the type itself - such as adding an interface - that do not change its serialisation/deserialisation
// signature.
if (propertyNamesMatch(localTypeInformation)) {
// Make sure types are assignment-compatible, and return the local serializer for the type.
validateCompatibility(localTypeInformation)
return null
}
// Failing that, we have to create an evolution serializer.
val bestMatchEvolutionConstructor = findEvolverConstructor(localTypeInformation.evolutionConstructors, properties)
val constructorForEvolution = bestMatchEvolutionConstructor?.constructor ?: localTypeInformation.constructor
val evolverProperties = bestMatchEvolutionConstructor?.properties ?: localTypeInformation.properties
validateEvolvability(evolverProperties)
return buildComposableEvolutionSerializer(localTypeInformation, constructorForEvolution, evolverProperties)
}
private fun RemoteTypeInformation.Composable.propertyNamesMatch(localTypeInformation: LocalTypeInformation.Composable): Boolean =
properties.keys == localTypeInformation.properties.keys
private fun RemoteTypeInformation.Composable.validateCompatibility(localTypeInformation: LocalTypeInformation.Composable) {
properties.asSequence().zip(localTypeInformation.properties.values.asSequence()).forEach { (remote, localProperty) ->
val (name, remoteProperty) = remote
val localClass = localProperty.type.observedType.asClass()
val remoteClass = remoteProperty.type.typeIdentifier.getLocalType(classLoader).asClass()
if (!localClass.isAssignableFrom(remoteClass)) {
throw EvolutionSerializationException(this,
"Local type $localClass of property $name is not assignable from remote type $remoteClass")
}
}
}
// Find the evolution constructor with the highest version number whose parameters are all assignable from the
// provided property types.
private fun findEvolverConstructor(constructors: List<EvolutionConstructorInformation>,
properties: Map<String, RemotePropertyInformation>): EvolutionConstructorInformation? {
val propertyTypes = properties.mapValues { (_, info) -> info.type.typeIdentifier.getLocalType(classLoader).asClass() }
// Evolver constructors are listed in ascending version order, so we just want the last that matches.
return constructors.lastOrNull { (_, evolverProperties) ->
// We have a match if all mandatory evolver properties have a type-compatible property in the remote type.
evolverProperties.all { (name, evolverProperty) ->
val propertyType = propertyTypes[name]
if (propertyType == null) !evolverProperty.isMandatory
else evolverProperty.type.observedType.asClass().isAssignableFrom(propertyType)
}
}
}
private fun RemoteTypeInformation.Composable.validateEvolvability(localProperties: Map<PropertyName, LocalPropertyInformation>) {
val remotePropertyNames = properties.keys
val localPropertyNames = localProperties.keys
val deletedProperties = remotePropertyNames - localPropertyNames
val newProperties = localPropertyNames - remotePropertyNames
// Here is where we can exercise a veto on evolutions that remove properties.
if (deletedProperties.isNotEmpty() && mustPreserveDataWhenEvolving)
throw EvolutionSerializationException(this,
"Property ${deletedProperties.first()} of remote ContractState type is not present in local type, " +
"and context is configured to prevent forwards-compatible deserialization.")
// Check mandatory-ness of constructor-set properties.
newProperties.forEach { propertyName ->
if (localProperties[propertyName]!!.mustBeProvided) throw EvolutionSerializationException(
this,
"Mandatory property $propertyName of local type is not present in remote type - " +
"did someone remove a property from the schema without considering old clients?")
}
}
private val LocalPropertyInformation.mustBeProvided: Boolean get() = when(this) {
is LocalPropertyInformation.ConstructorPairedProperty -> isMandatory
is LocalPropertyInformation.PrivateConstructorPairedProperty -> isMandatory
else -> false
}
private fun RemoteTypeInformation.AnEnum.getEvolutionSerializer(
localTypeInformation: LocalTypeInformation.AnEnum): AMQPSerializer<Any>? {
if (members == localTypeInformation.members) return null
val remoteTransforms = transforms
val localTransforms = localTypeInformation.getEnumTransforms(localSerializerFactory)
val transforms = if (remoteTransforms.size > localTransforms.size) remoteTransforms else localTransforms
val localOrdinals = localTypeInformation.members.asSequence().mapIndexed { ord, member -> member to ord }.toMap()
val remoteOrdinals = members.asSequence().mapIndexed { ord, member -> member to ord }.toMap()
val rules = transforms.defaults + transforms.renames
// We just trust our transformation rules not to contain cycles here.
tailrec fun findLocal(remote: String): String =
if (remote in localOrdinals) remote
else findLocal(rules[remote] ?: throw EvolutionSerializationException(
this,
"Cannot resolve local enum member $remote to a member of ${localOrdinals.keys} using rules $rules"
))
val conversions = members.associate { it to findLocal(it) }
val convertedOrdinals = remoteOrdinals.asSequence().map { (member, ord) -> ord to conversions[member]!! }.toMap()
if (localOrdinals.any { (name, ordinal) -> convertedOrdinals[ordinal] != name })
throw EvolutionSerializationException(
this,
"Constants have been reordered, additions must be appended to the end")
return EnumEvolutionSerializer(localTypeInformation.observedType, localSerializerFactory, conversions, localOrdinals)
}
private fun RemoteTypeInformation.Composable.buildComposableEvolutionSerializer(
localTypeInformation: LocalTypeInformation.Composable,
constructor: LocalConstructorInformation,
properties: Map<String, LocalPropertyInformation>): AMQPSerializer<Any> =
EvolutionObjectSerializer.make(
localTypeInformation,
this,
constructor,
properties,
classLoader)
}

View File

@ -1,202 +0,0 @@
package net.corda.serialization.internal.amqp
import com.google.common.hash.Hasher
import com.google.common.hash.Hashing
import net.corda.core.KeepForDJVM
import net.corda.core.internal.isConcreteClass
import net.corda.core.internal.kotlinObjectInstance
import net.corda.core.utilities.toBase64
import net.corda.serialization.internal.amqp.SerializerFactory.Companion.isPrimitive
import java.lang.reflect.*
import java.util.*
/**
* Should be implemented by classes which wish to provide pluggable fingerprinting on types for a [SerializerFactory]
*/
@KeepForDJVM
interface FingerPrinter {
/**
* Return a unique identifier for a type, usually this will take into account the constituent elements
* of said type such that any modification to any sub element wll generate a different fingerprint
*/
fun fingerprint(type: Type): String
}
/**
* Implementation of the finger printing mechanism used by default
*/
@KeepForDJVM
class SerializerFingerPrinter(val factory: SerializerFactory) : FingerPrinter {
/**
* The method generates a fingerprint for a given JVM [Type] that should be unique to the schema representation.
* Thus it only takes into account properties and types and only supports the same object graph subset as the overall
* serialization code.
*
* The idea being that even for two classes that share the same name but differ in a minor way, the fingerprint will be
* different.
*/
override fun fingerprint(type: Type): String = FingerPrintingState(factory).fingerprint(type)
}
// Representation of the current state of fingerprinting
internal class FingerPrintingState(private val factory: SerializerFactory) {
companion object {
private const val ARRAY_HASH: String = "Array = true"
private const val ENUM_HASH: String = "Enum = true"
private const val ALREADY_SEEN_HASH: String = "Already seen = true"
private const val NULLABLE_HASH: String = "Nullable = true"
private const val NOT_NULLABLE_HASH: String = "Nullable = false"
private const val ANY_TYPE_HASH: String = "Any type = true"
}
private val typesSeen: MutableSet<Type> = mutableSetOf()
private var currentContext: Type? = null
private var hasher: Hasher = newDefaultHasher()
// Fingerprint the type recursively, and return the encoded fingerprint written into the hasher.
fun fingerprint(type: Type) = fingerprintType(type).hasher.fingerprint
// This method concatenates various elements of the types recursively as unencoded strings into the hasher,
// effectively creating a unique string for a type which we then hash in the calling function above.
private fun fingerprintType(type: Type): FingerPrintingState = apply {
// Don't go round in circles.
if (hasSeen(type)) append(ALREADY_SEEN_HASH)
else ifThrowsAppend(
{ type.typeName },
{
typesSeen.add(type)
currentContext = type
fingerprintNewType(type)
})
}
// For a type we haven't seen before, determine the correct path depending on the type of type it is.
private fun fingerprintNewType(type: Type) = when (type) {
is ParameterizedType -> fingerprintParameterizedType(type)
// Previously, we drew a distinction between TypeVariable, WildcardType, and AnyType, changing
// the signature of the fingerprinted object. This, however, doesn't work as it breaks bi-
// directional fingerprints. That is, fingerprinting a concrete instance of a generic
// type (Example<Int>), creates a different fingerprint from the generic type itself (Example<T>)
//
// On serialization Example<Int> is treated as Example<T>, a TypeVariable
// On deserialisation it is seen as Example<?>, A WildcardType *and* a TypeVariable
// Note: AnyType is a special case of WildcardType used in other parts of the
// serializer so both cases need to be dealt with here
//
// If we treat these types as fundamentally different and alter the fingerprint we will
// end up breaking into the evolver when we shouldn't or, worse, evoking the carpenter.
is SerializerFactory.AnyType,
is WildcardType,
is TypeVariable<*> -> append("?$ANY_TYPE_HASH")
is Class<*> -> fingerprintClass(type)
is GenericArrayType -> fingerprintType(type.genericComponentType).append(ARRAY_HASH)
else -> throw AMQPNotSerializableException(type, "Don't know how to hash")
}
private fun fingerprintClass(type: Class<*>) = when {
type.isArray -> fingerprintType(type.componentType).append(ARRAY_HASH)
type.isPrimitiveOrCollection -> append(type.name)
type.isEnum -> fingerprintEnum(type)
else -> fingerprintWithCustomSerializerOrElse(type, type) {
if (type.kotlinObjectInstance != null) append(type.name)
else fingerprintObject(type)
}
}
private fun fingerprintParameterizedType(type: ParameterizedType) {
// Hash the rawType + params
type.asClass().let { clazz ->
if (clazz.isCollectionOrMap) append(clazz.name)
else fingerprintWithCustomSerializerOrElse(clazz, type) {
fingerprintObject(type)
}
}
// ...and concatenate the type data for each parameter type.
type.actualTypeArguments.forEach { paramType ->
fingerprintType(paramType)
}
}
private fun fingerprintObject(type: Type) {
// Hash the class + properties + interfaces
append(type.asClass().name)
orderedPropertiesForSerialization(type).forEach { prop ->
fingerprintType(prop.serializer.resolvedType)
fingerprintPropSerialiser(prop)
}
interfacesForSerialization(type, factory).forEach { iface ->
fingerprintType(iface)
}
}
// ensures any change to the enum (adding constants) will trigger the need for evolution
private fun fingerprintEnum(type: Class<*>) {
append(type.enumConstants.joinToString())
append(type.name)
append(ENUM_HASH)
}
private fun fingerprintPropSerialiser(prop: PropertyAccessor) {
append(prop.serializer.name)
append(if (prop.serializer.mandatory) NOT_NULLABLE_HASH
else NULLABLE_HASH)
}
// Write the given character sequence into the hasher.
private fun append(chars: CharSequence) {
hasher = hasher.putUnencodedChars(chars)
}
// Give any custom serializers loaded into the factory the chance to supply their own type-descriptors
private fun fingerprintWithCustomSerializerOrElse(
clazz: Class<*>,
declaredType: Type,
defaultAction: () -> Unit)
: Unit = factory.findCustomSerializer(clazz, declaredType)?.let {
append(it.typeDescriptor)
} ?: defaultAction()
// Test whether we are in a state in which we have already seen the given type.
//
// We don't include Example<?> and Example<T> where type is ? or T in this otherwise we
// generate different fingerprints for class Outer<T>(val a: Inner<T>) when serialising
// and deserializing (assuming deserialization is occurring in a factory that didn't
// serialise the object in the first place (and thus the cache lookup fails). This is also
// true of Any, where we need Example<A, B> and Example<?, ?> to have the same fingerprint
private fun hasSeen(type: Type) = (type in typesSeen)
&& (type !== SerializerFactory.AnyType)
&& (type !is TypeVariable<*>)
&& (type !is WildcardType)
private fun orderedPropertiesForSerialization(type: Type): List<PropertyAccessor> {
return propertiesForSerialization(
if (type.asClass().isConcreteClass) constructorForDeserialization(type) else null,
currentContext ?: type,
factory).serializationOrder
}
}
// region Utility functions
// Create a new instance of the [Hasher] used for fingerprinting by the default [SerializerFingerPrinter]
private fun newDefaultHasher() = Hashing.murmur3_128().newHasher()
// We obtain a fingerprint from a [Hasher] by taking the Base 64 encoding of its hash bytes
private val Hasher.fingerprint get() = hash().asBytes().toBase64()
internal fun fingerprintForDescriptors(vararg typeDescriptors: String): String =
newDefaultHasher().putUnencodedChars(typeDescriptors.joinToString()).fingerprint
private val Class<*>.isCollectionOrMap get() =
(Collection::class.java.isAssignableFrom(this) || Map::class.java.isAssignableFrom(this))
&& !EnumSet::class.java.isAssignableFrom(this)
private val Class<*>.isPrimitiveOrCollection get() =
isPrimitive(this) || isCollectionOrMap
// endregion

View File

@ -0,0 +1,226 @@
package net.corda.serialization.internal.amqp
import net.corda.core.internal.kotlinObjectInstance
import net.corda.core.serialization.ClassWhitelist
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.core.utilities.trace
import net.corda.serialization.internal.model.*
import org.apache.qpid.proton.amqp.Symbol
import java.io.NotSerializableException
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import java.lang.reflect.WildcardType
import java.util.*
import javax.annotation.concurrent.ThreadSafe
/**
* A factory that handles the serialisation and deserialisation of [Type]s visible from a given [ClassLoader].
*
* Unlike the [RemoteSerializerFactory], which deals with types for which we have [Schema] information and serialised data,
* the [LocalSerializerFactory] deals with types for which we have a Java [Type] (and perhaps some in-memory data, from which
* we can discover the actual [Class] we are working with.
*/
interface LocalSerializerFactory {
/**
* The [ClassWhitelist] used by this factory. Classes must be whitelisted for serialization, because they are expected
* to be written in a secure manner.
*/
val whitelist: ClassWhitelist
/**
* The [ClassLoader] used by this factory.
*/
val classloader: ClassLoader
/**
* Obtain an [AMQPSerializer] for an object of actual type [actualClass], and declared type [declaredType].
*/
fun get(actualClass: Class<*>, declaredType: Type): AMQPSerializer<Any>
/**
* Obtain an [AMQPSerializer] for the [declaredType].
*/
fun get(declaredType: Type): AMQPSerializer<Any> = get(getTypeInformation(declaredType))
/**
* Obtain an [AMQPSerializer] for the type having the given [typeInformation].
*/
fun get(typeInformation: LocalTypeInformation): AMQPSerializer<Any>
/**
* Obtain [LocalTypeInformation] for the given [Type].
*/
fun getTypeInformation(type: Type): LocalTypeInformation
/**
* Use the [FingerPrinter] to create a type descriptor for the given [type].
*/
fun createDescriptor(type: Type): Symbol = createDescriptor(getTypeInformation(type))
/**
* Use the [FingerPrinter] to create a type descriptor for the given [typeInformation].
*/
fun createDescriptor(typeInformation: LocalTypeInformation): Symbol
/**
* Obtain or register [Transform]s for the given class [name].
*
* Eventually this information should be moved into the [LocalTypeInformation] for the type.
*/
fun getOrBuildTransform(name: String, builder: () -> EnumMap<TransformTypes, MutableList<Transform>>):
EnumMap<TransformTypes, MutableList<Transform>>
}
/**
* A [LocalSerializerFactory] equipped with a [LocalTypeModel] and a [FingerPrinter] to help it build fingerprint-based descriptors
* and serializers for local types.
*/
@ThreadSafe
class DefaultLocalSerializerFactory(
override val whitelist: ClassWhitelist,
private val typeModel: LocalTypeModel,
private val fingerPrinter: FingerPrinter,
override val classloader: ClassLoader,
private val descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry,
private val customSerializerRegistry: CustomSerializerRegistry,
private val onlyCustomSerializers: Boolean)
: LocalSerializerFactory {
companion object {
val logger = contextLogger()
}
private val transformsCache: MutableMap<String, EnumMap<TransformTypes, MutableList<Transform>>> = DefaultCacheProvider.createCache()
private val serializersByType: MutableMap<TypeIdentifier, AMQPSerializer<Any>> = DefaultCacheProvider.createCache()
override fun createDescriptor(typeInformation: LocalTypeInformation): Symbol =
Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerPrinter.fingerprint(typeInformation)}")
override fun getTypeInformation(type: Type): LocalTypeInformation = typeModel.inspect(type)
override fun getOrBuildTransform(name: String, builder: () -> EnumMap<TransformTypes, MutableList<Transform>>):
EnumMap<TransformTypes, MutableList<Transform>> =
transformsCache.computeIfAbsent(name) { _ -> builder() }
override fun get(typeInformation: LocalTypeInformation): AMQPSerializer<Any> =
get(typeInformation.observedType, typeInformation)
private fun make(typeInformation: LocalTypeInformation, build: () -> AMQPSerializer<Any>) =
make(typeInformation.typeIdentifier, build)
private fun make(typeIdentifier: TypeIdentifier, build: () -> AMQPSerializer<Any>) =
serializersByType.computeIfAbsent(typeIdentifier) { _ -> build() }
private fun get(declaredType: Type, localTypeInformation: LocalTypeInformation): AMQPSerializer<Any> {
val declaredClass = declaredType.asClass()
// can be useful to enable but will be *extremely* chatty if you do
logger.trace { "Get Serializer for $declaredClass ${declaredType.typeName}" }
return when(localTypeInformation) {
is LocalTypeInformation.ACollection -> makeDeclaredCollection(localTypeInformation)
is LocalTypeInformation.AMap -> makeDeclaredMap(localTypeInformation)
is LocalTypeInformation.AnEnum -> makeDeclaredEnum(localTypeInformation, declaredType, declaredClass)
else -> makeClassSerializer(declaredClass, declaredType, declaredType, localTypeInformation)
}.also { serializer -> descriptorBasedSerializerRegistry[serializer.typeDescriptor.toString()] = serializer }
}
private fun makeDeclaredEnum(localTypeInformation: LocalTypeInformation, declaredType: Type, declaredClass: Class<*>): AMQPSerializer<Any> =
make(localTypeInformation) {
whitelist.requireWhitelisted(declaredType)
EnumSerializer(declaredType, declaredClass, this)
}
private fun makeActualEnum(localTypeInformation: LocalTypeInformation, declaredType: Type, declaredClass: Class<*>): AMQPSerializer<Any> =
make(localTypeInformation) {
whitelist.requireWhitelisted(declaredType)
EnumSerializer(declaredType, declaredClass, this)
}
private fun makeDeclaredCollection(localTypeInformation: LocalTypeInformation.ACollection): AMQPSerializer<Any> {
val resolved = CollectionSerializer.resolveDeclared(localTypeInformation)
return make(resolved) {
CollectionSerializer(resolved.typeIdentifier.getLocalType(classloader) as ParameterizedType, this)
}
}
private fun makeDeclaredMap(localTypeInformation: LocalTypeInformation.AMap): AMQPSerializer<Any> {
val resolved = MapSerializer.resolveDeclared(localTypeInformation)
return make(resolved) {
MapSerializer(resolved.typeIdentifier.getLocalType(classloader) as ParameterizedType, this)
}
}
override fun get(actualClass: Class<*>, declaredType: Type): AMQPSerializer<Any> {
// can be useful to enable but will be *extremely* chatty if you do
logger.trace { "Get Serializer for $actualClass ${declaredType.typeName}" }
val declaredClass = declaredType.asClass()
val actualType: Type = inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType
val declaredTypeInformation = typeModel.inspect(declaredType)
val actualTypeInformation = typeModel.inspect(actualType)
return when(actualTypeInformation) {
is LocalTypeInformation.ACollection -> makeActualCollection(actualClass,declaredTypeInformation as? LocalTypeInformation.ACollection ?: actualTypeInformation)
is LocalTypeInformation.AMap -> makeActualMap(declaredType, actualClass,declaredTypeInformation as? LocalTypeInformation.AMap ?: actualTypeInformation)
is LocalTypeInformation.AnEnum -> makeActualEnum(actualTypeInformation, actualType, actualClass)
else -> makeClassSerializer(actualClass, actualType, declaredType, actualTypeInformation)
}.also { serializer -> descriptorBasedSerializerRegistry[serializer.typeDescriptor.toString()] = serializer }
}
private fun makeActualMap(declaredType: Type, actualClass: Class<*>, typeInformation: LocalTypeInformation.AMap): AMQPSerializer<Any> {
declaredType.asClass().checkSupportedMapType()
val resolved = MapSerializer.resolveActual(actualClass, typeInformation)
return make(resolved) {
MapSerializer(resolved.typeIdentifier.getLocalType(classloader) as ParameterizedType, this)
}
}
private fun makeActualCollection(actualClass: Class<*>, typeInformation: LocalTypeInformation.ACollection): AMQPSerializer<Any> {
val resolved = CollectionSerializer.resolveActual(actualClass, typeInformation)
return serializersByType.computeIfAbsent(resolved.typeIdentifier) {
CollectionSerializer(resolved.typeIdentifier.getLocalType(classloader) as ParameterizedType, this)
}
}
private fun makeClassSerializer(
clazz: Class<*>,
type: Type,
declaredType: Type,
typeInformation: LocalTypeInformation
): AMQPSerializer<Any> = make(typeInformation) {
logger.debug { "class=${clazz.simpleName}, type=$type is a composite type" }
when {
clazz.isSynthetic -> // Explicitly ban synthetic classes, we have no way of recreating them when deserializing. This also
// captures Lambda expressions and other anonymous functions
throw AMQPNotSerializableException(
type,
"Serializer does not support synthetic classes")
AMQPTypeIdentifiers.isPrimitive(typeInformation.typeIdentifier) -> AMQPPrimitiveSerializer(clazz)
else -> customSerializerRegistry.findCustomSerializer(clazz, declaredType) ?:
makeNonCustomSerializer(type, typeInformation, clazz)
}
}
private fun makeNonCustomSerializer(type: Type, typeInformation: LocalTypeInformation, clazz: Class<*>): AMQPSerializer<Any> = when {
onlyCustomSerializers -> throw AMQPNotSerializableException(type, "Only allowing custom serializers")
type.isArray() ->
if (clazz.componentType.isPrimitive) PrimArraySerializer.make(type, this)
else {
ArraySerializer.make(type, this)
}
else -> {
val singleton = clazz.kotlinObjectInstance
if (singleton != null) {
whitelist.requireWhitelisted(clazz)
SingletonSerializer(clazz, singleton, this)
} else {
whitelist.requireWhitelisted(type)
ObjectSerializer.make(typeInformation, this)
}
}
}
}

View File

@ -4,6 +4,8 @@ import net.corda.core.KeepForDJVM
import net.corda.core.StubOutForDJVM
import net.corda.core.internal.uncheckedCast
import net.corda.core.serialization.SerializationContext
import net.corda.serialization.internal.model.LocalTypeInformation
import net.corda.serialization.internal.model.TypeIdentifier
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException
@ -18,11 +20,10 @@ private typealias MapCreationFunction = (Map<*, *>) -> Map<*, *>
* Serialization / deserialization of certain supported [Map] types.
*/
@KeepForDJVM
class MapSerializer(private val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer<Any> {
override val type: Type = (declaredType as? DeserializedParameterizedType)
?: DeserializedParameterizedType.make(SerializerFactory.nameForType(declaredType), factory.classloader)
override val typeDescriptor: Symbol = Symbol.valueOf(
"$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")
class MapSerializer(private val declaredType: ParameterizedType, factory: LocalSerializerFactory) : AMQPSerializer<Any> {
override val type: Type = declaredType
override val typeDescriptor: Symbol = factory.createDescriptor(type)
companion object {
// NB: Order matters in this map, the most specific classes should be listed at the end
@ -39,29 +40,43 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial
}
))
private val supportedTypeIdentifiers = supportedTypes.keys.asSequence()
.map { TypeIdentifier.forGenericType(it) }.toSet()
private fun findConcreteType(clazz: Class<*>): MapCreationFunction {
return supportedTypes[clazz] ?: throw AMQPNotSerializableException(clazz, "Unsupported map type $clazz.")
}
fun deriveParameterizedType(declaredType: Type, declaredClass: Class<*>, actualClass: Class<*>?): ParameterizedType {
declaredClass.checkSupportedMapType()
if (supportedTypes.containsKey(declaredClass)) {
// Simple case - it is already known to be a map.
return deriveParametrizedType(declaredType, uncheckedCast(declaredClass))
} else if (actualClass != null && Map::class.java.isAssignableFrom(actualClass)) {
// Declared class is not map, but [actualClass] is - represent it accordingly.
val mapClass = findMostSuitableMapType(actualClass)
return deriveParametrizedType(declaredType, mapClass)
}
fun resolveDeclared(declaredTypeInformation: LocalTypeInformation.AMap): LocalTypeInformation.AMap {
declaredTypeInformation.observedType.asClass().checkSupportedMapType()
if (supportedTypeIdentifiers.contains(declaredTypeInformation.typeIdentifier.erased))
return if (!declaredTypeInformation.isErased) declaredTypeInformation
else declaredTypeInformation.withParameters(LocalTypeInformation.Unknown, LocalTypeInformation.Unknown)
throw AMQPNotSerializableException(declaredType,
"Cannot derive map type for declaredType=\"$declaredType\", declaredClass=\"$declaredClass\", actualClass=\"$actualClass\"")
throw NotSerializableException("Cannot derive map type for declared type " +
declaredTypeInformation.prettyPrint(false))
}
private fun deriveParametrizedType(declaredType: Type, collectionClass: Class<out Map<*, *>>): ParameterizedType =
(declaredType as? ParameterizedType)
?: DeserializedParameterizedType(collectionClass, arrayOf(SerializerFactory.AnyType, SerializerFactory.AnyType))
fun resolveActual(actualClass: Class<*>, declaredTypeInformation: LocalTypeInformation.AMap): LocalTypeInformation.AMap {
declaredTypeInformation.observedType.asClass().checkSupportedMapType()
if (supportedTypeIdentifiers.contains(declaredTypeInformation.typeIdentifier.erased)) {
return if (!declaredTypeInformation.isErased) declaredTypeInformation
else declaredTypeInformation.withParameters(LocalTypeInformation.Unknown, LocalTypeInformation.Unknown)
}
val mapClass = findMostSuitableMapType(actualClass)
val erasedInformation = LocalTypeInformation.AMap(
mapClass,
TypeIdentifier.forClass(mapClass),
LocalTypeInformation.Unknown, LocalTypeInformation.Unknown)
return when(declaredTypeInformation.typeIdentifier) {
is TypeIdentifier.Parameterised -> erasedInformation.withParameters(
declaredTypeInformation.keyType,
declaredTypeInformation.valueType)
else -> erasedInformation.withParameters(LocalTypeInformation.Unknown, LocalTypeInformation.Unknown)
}
}
private fun findMostSuitableMapType(actualClass: Class<*>): Class<out Map<*, *>> =
MapSerializer.supportedTypes.keys.findLast { it.isAssignableFrom(actualClass) }!!
@ -69,7 +84,7 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial
private val concreteBuilder: MapCreationFunction = findConcreteType(declaredType.rawType as Class<*>)
private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "map", Descriptor(typeDescriptor), emptyList())
private val typeNotation: TypeNotation = RestrictedType(AMQPTypeIdentifiers.nameForType(declaredType), null, emptyList(), "map", Descriptor(typeDescriptor), emptyList())
private val inboundKeyType = declaredType.actualTypeArguments[0]
private val outboundKeyType = resolveTypeVariables(inboundKeyType, null)
@ -108,7 +123,6 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
context: SerializationContext
): Any = ifThrowsAppend({ declaredType.typeName }) {
// TODO: General generics question. Do we need to validate that entries in Maps and Collections match the generic type? Is it a security hole?
val entries: Iterable<Pair<Any?, Any?>> = (obj as Map<*, *>).map { readEntry(schemas, input, it, context) }
concreteBuilder(entries.toMap())
}

View File

@ -0,0 +1,112 @@
package net.corda.serialization.internal.amqp
import net.corda.serialization.internal.model.*
import java.io.NotSerializableException
interface ObjectBuilder {
companion object {
fun makeProvider(typeInformation: LocalTypeInformation.Composable): () -> ObjectBuilder =
makeProvider(typeInformation.typeIdentifier, typeInformation.constructor, typeInformation.properties)
fun makeProvider(typeIdentifier: TypeIdentifier, constructor: LocalConstructorInformation, properties: Map<String, LocalPropertyInformation>): () -> ObjectBuilder {
val nonCalculatedProperties = properties.asSequence()
.filterNot { (name, property) -> property.isCalculated }
.sortedBy { (name, _) -> name }
.map { (_, property) -> property }
.toList()
val propertyIndices = nonCalculatedProperties.mapNotNull {
when(it) {
is LocalPropertyInformation.ConstructorPairedProperty -> it.constructorSlot.parameterIndex
is LocalPropertyInformation.PrivateConstructorPairedProperty -> it.constructorSlot.parameterIndex
else -> null
}
}.toIntArray()
if (propertyIndices.isNotEmpty()) {
if (propertyIndices.size != nonCalculatedProperties.size) {
throw NotSerializableException(
"Some but not all properties of ${typeIdentifier.prettyPrint(false)} " +
"are constructor-based")
}
return { ConstructorBasedObjectBuilder(constructor, propertyIndices) }
}
val getterSetter = nonCalculatedProperties.filterIsInstance<LocalPropertyInformation.GetterSetterProperty>()
return { SetterBasedObjectBuilder(constructor, getterSetter) }
}
}
fun initialize()
fun populate(slot: Int, value: Any?)
fun build(): Any
}
class SetterBasedObjectBuilder(
val constructor: LocalConstructorInformation,
val properties: List<LocalPropertyInformation.GetterSetterProperty>): ObjectBuilder {
private lateinit var target: Any
override fun initialize() {
target = constructor.observedMethod.call()
}
override fun populate(slot: Int, value: Any?) {
properties[slot].observedSetter.invoke(target, value)
}
override fun build(): Any = target
}
class ConstructorBasedObjectBuilder(
val constructor: LocalConstructorInformation,
val parameterIndices: IntArray): ObjectBuilder {
private val params = arrayOfNulls<Any>(parameterIndices.size)
override fun initialize() {}
override fun populate(slot: Int, value: Any?) {
if (slot >= parameterIndices.size) {
assert(false)
}
val parameterIndex = parameterIndices[slot]
if (parameterIndex >= params.size) {
assert(false)
}
params[parameterIndex] = value
}
override fun build(): Any = constructor.observedMethod.call(*params)
}
class EvolutionObjectBuilder(private val localBuilder: ObjectBuilder, val slotAssignments: IntArray): ObjectBuilder {
companion object {
fun makeProvider(typeIdentifier: TypeIdentifier, constructor: LocalConstructorInformation, localProperties: Map<String, LocalPropertyInformation>, providedProperties: List<String>): () -> ObjectBuilder {
val localBuilderProvider = ObjectBuilder.makeProvider(typeIdentifier, constructor, localProperties)
val localPropertyIndices = localProperties.asSequence()
.filter { (_, property) -> !property.isCalculated }
.mapIndexed { slot, (name, _) -> name to slot }
.toMap()
val reroutedIndices = providedProperties.map { propertyName -> localPropertyIndices[propertyName] ?: -1 }
.toIntArray()
return { EvolutionObjectBuilder(localBuilderProvider(), reroutedIndices) }
}
}
override fun initialize() {
localBuilder.initialize()
}
override fun populate(slot: Int, value: Any?) {
val slotAssignment = slotAssignments[slot]
if (slotAssignment != -1) localBuilder.populate(slotAssignment, value)
}
override fun build(): Any = localBuilder.build()
}

View File

@ -1,185 +1,204 @@
package net.corda.serialization.internal.amqp
import net.corda.core.internal.isConcreteClass
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.trace
import net.corda.serialization.internal.amqp.SerializerFactory.Companion.nameForType
import net.corda.serialization.internal.model.*
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException
import java.lang.reflect.Constructor
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Type
import kotlin.reflect.jvm.javaConstructor
/**
* Responsible for serializing and deserializing a regular object instance via a series of properties
* (matched with a constructor).
*/
open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerializer<Any> {
override val type: Type get() = clazz
open val kotlinConstructor = if (clazz.asClass().isConcreteClass) constructorForDeserialization(clazz) else null
val javaConstructor by lazy { kotlinConstructor?.javaConstructor }
interface ObjectSerializer : AMQPSerializer<Any> {
val propertySerializers: Map<String, PropertySerializer>
val fields: List<Field>
companion object {
private val logger = contextLogger()
fun make(typeInformation: LocalTypeInformation, factory: LocalSerializerFactory): ObjectSerializer {
val typeDescriptor = factory.createDescriptor(typeInformation)
val typeNotation = TypeNotationGenerator.getTypeNotation(typeInformation, typeDescriptor)
return when (typeInformation) {
is LocalTypeInformation.Composable ->
makeForComposable(typeInformation, typeNotation, typeDescriptor, factory)
is LocalTypeInformation.AnInterface,
is LocalTypeInformation.Abstract ->
makeForAbstract(typeNotation, typeInformation, typeDescriptor, factory)
else -> throw NotSerializableException("Cannot build object serializer for $typeInformation")
}
}
private fun makeForAbstract(typeNotation: CompositeType,
typeInformation: LocalTypeInformation,
typeDescriptor: Symbol,
factory: LocalSerializerFactory): AbstractObjectSerializer {
val propertySerializers = makePropertySerializers(typeInformation.propertiesOrEmptyMap, factory)
val writer = ComposableObjectWriter(typeNotation, typeInformation.interfacesOrEmptyList, propertySerializers)
return AbstractObjectSerializer(typeInformation.observedType, typeDescriptor, propertySerializers,
typeNotation.fields, writer)
}
private fun makeForComposable(typeInformation: LocalTypeInformation.Composable,
typeNotation: CompositeType,
typeDescriptor: Symbol,
factory: LocalSerializerFactory): ComposableObjectSerializer {
val propertySerializers = makePropertySerializers(typeInformation.properties, factory)
val reader = ComposableObjectReader(
typeInformation.typeIdentifier,
propertySerializers,
ObjectBuilder.makeProvider(typeInformation))
val writer = ComposableObjectWriter(
typeNotation,
typeInformation.interfaces,
propertySerializers)
return ComposableObjectSerializer(
typeInformation.observedType,
typeDescriptor,
propertySerializers,
typeNotation.fields,
reader,
writer)
}
private fun makePropertySerializers(properties: Map<PropertyName, LocalPropertyInformation>,
factory: LocalSerializerFactory): Map<String, PropertySerializer> =
properties.mapValues { (name, property) ->
ComposableTypePropertySerializer.make(name, property, factory)
}
}
}
open val propertySerializers: PropertySerializers by lazy {
propertiesForSerialization(kotlinConstructor, clazz, factory)
}
class ComposableObjectSerializer(
override val type: Type,
override val typeDescriptor: Symbol,
override val propertySerializers: Map<PropertyName, PropertySerializer>,
override val fields: List<Field>,
private val reader: ComposableObjectReader,
private val writer: ComposableObjectWriter): ObjectSerializer {
private val typeName = nameForType(clazz)
override fun writeClassInfo(output: SerializationOutput) = writer.writeClassInfo(output)
override val typeDescriptor: Symbol = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext, debugIndent: Int) =
writer.writeObject(obj, data, type, output, context, debugIndent)
// We restrict to only those annotated or whitelisted
private val interfaces = interfacesForSerialization(clazz, factory)
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any =
reader.readObject(obj, schemas, input, context)
}
internal open val typeNotation: TypeNotation by lazy {
CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor), generateFields())
}
override fun writeClassInfo(output: SerializationOutput) {
class ComposableObjectWriter(
private val typeNotation: TypeNotation,
private val interfaces: List<LocalTypeInformation>,
private val propertySerializers: Map<PropertyName, PropertySerializer>
) {
fun writeClassInfo(output: SerializationOutput) {
if (output.writeTypeNotations(typeNotation)) {
for (iface in interfaces) {
output.requireSerializer(iface)
output.requireSerializer(iface.observedType)
}
propertySerializers.serializationOrder.forEach { property ->
property.serializer.writeClassInfo(output)
propertySerializers.values.forEach { serializer ->
serializer.writeClassInfo(output)
}
}
}
override fun writeObject(
obj: Any,
data: Data,
type: Type,
output: SerializationOutput,
context: SerializationContext,
debugIndent: Int) = ifThrowsAppend({ clazz.typeName }
) {
if (propertySerializers.deserializableSize != javaConstructor?.parameterCount &&
javaConstructor?.parameterCount ?: 0 > 0
) {
throw AMQPNotSerializableException(type, "Serialization constructor for class $type expects "
+ "${javaConstructor?.parameterCount} parameters but we have ${propertySerializers.size} "
+ "properties to serialize.")
}
// Write described
fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext, debugIndent: Int) {
data.withDescribed(typeNotation.descriptor) {
// Write list
withList {
propertySerializers.serializationOrder.forEach { property ->
property.serializer.writeProperty(obj, this, output, context, debugIndent + 1)
propertySerializers.values.forEach { propertySerializer ->
propertySerializer.writeProperty(obj, this, output, context, debugIndent + 1)
}
}
}
}
}
override fun readObject(
obj: Any,
schemas: SerializationSchemas,
input: DeserializationInput,
context: SerializationContext): Any = ifThrowsAppend({ clazz.typeName }) {
if (obj is List<*>) {
if (obj.size != propertySerializers.size) {
throw AMQPNotSerializableException(type, "${obj.size} objects to deserialize, but " +
"${propertySerializers.size} properties in described type $typeName")
}
class ComposableObjectReader(
val typeIdentifier: TypeIdentifier,
private val propertySerializers: Map<PropertyName, PropertySerializer>,
private val objectBuilderProvider: () -> ObjectBuilder
) {
return if (propertySerializers.byConstructor) {
readObjectBuildViaConstructor(obj, schemas, input, context)
} else {
readObjectBuildViaSetters(obj, schemas, input, context)
}
} else {
throw AMQPNotSerializableException(type, "Body of described type is unexpected $obj")
}
}
private fun readObjectBuildViaConstructor(
obj: List<*>,
schemas: SerializationSchemas,
input: DeserializationInput,
context: SerializationContext): Any = ifThrowsAppend({ clazz.typeName }) {
logger.trace { "Calling construction based construction for ${clazz.typeName}" }
return construct(propertySerializers.serializationOrder
.zip(obj)
.mapNotNull { (accessor, obj) ->
// Ensure values get read out of input no matter what
val value = accessor.serializer.readProperty(obj, schemas, input, context)
when(accessor) {
is PropertyAccessorConstructor -> accessor.initialPosition to value
is CalculatedPropertyAccessor -> null
else -> throw UnsupportedOperationException(
"${accessor::class.simpleName} accessor not supported " +
"for constructor-based object building")
}
fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any =
ifThrowsAppend({ typeIdentifier.prettyPrint(false) }) {
if (obj !is List<*>) throw NotSerializableException("Body of described type is unexpected $obj")
if (obj.size < propertySerializers.size) {
throw NotSerializableException("${obj.size} objects to deserialize, but " +
"${propertySerializers.size} properties in described type ${typeIdentifier.prettyPrint(false)}")
}
.sortedWith(compareBy { it.first })
.map { it.second })
}
private fun readObjectBuildViaSetters(
obj: List<*>,
schemas: SerializationSchemas,
input: DeserializationInput,
context: SerializationContext): Any = ifThrowsAppend({ clazz.typeName }) {
logger.trace { "Calling setter based construction for ${clazz.typeName}" }
val builder = objectBuilderProvider()
builder.initialize()
obj.asSequence().zip(propertySerializers.values.asSequence())
// Read _all_ properties from the stream
.map { (item, property) -> property to property.readProperty(item, schemas, input, context) }
// Throw away any calculated properties
.filter { (property, _) -> !property.isCalculated }
// Write the rest into the builder
.forEachIndexed { slot, (_, propertyValue) -> builder.populate(slot, propertyValue) }
return builder.build()
}
}
val instance: Any = javaConstructor?.newInstanceUnwrapped() ?: throw AMQPNotSerializableException(
type,
"Failed to instantiate instance of object $clazz")
class AbstractObjectSerializer(
override val type: Type,
override val typeDescriptor: Symbol,
override val propertySerializers: Map<PropertyName, PropertySerializer>,
override val fields: List<Field>,
private val writer: ComposableObjectWriter): ObjectSerializer {
override fun writeClassInfo(output: SerializationOutput) =
writer.writeClassInfo(output)
// read the properties out of the serialised form, since we're invoking the setters the order we
// do it in doesn't matter
val propertiesFromBlob = obj
.zip(propertySerializers.serializationOrder)
.map { it.second.serializer.readProperty(it.first, schemas, input, context) }
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext, debugIndent: Int) =
writer.writeObject(obj, data, type, output, context, debugIndent)
// one by one take a property and invoke the setter on the class
propertySerializers.serializationOrder.zip(propertiesFromBlob).forEach {
it.first.set(instance, it.second)
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any =
throw UnsupportedOperationException("Cannot deserialize abstract type ${type.typeName}")
}
class EvolutionObjectSerializer(
override val type: Type,
override val typeDescriptor: Symbol,
override val propertySerializers: Map<PropertyName, PropertySerializer>,
private val reader: ComposableObjectReader): ObjectSerializer {
companion object {
fun make(localTypeInformation: LocalTypeInformation.Composable, remoteTypeInformation: RemoteTypeInformation.Composable, constructor: LocalConstructorInformation,
properties: Map<String, LocalPropertyInformation>, classLoader: ClassLoader): EvolutionObjectSerializer {
val propertySerializers = makePropertySerializers(properties, remoteTypeInformation.properties, classLoader)
val reader = ComposableObjectReader(
localTypeInformation.typeIdentifier,
propertySerializers,
EvolutionObjectBuilder.makeProvider(localTypeInformation.typeIdentifier, constructor, properties, remoteTypeInformation.properties.keys.sorted()))
return EvolutionObjectSerializer(
localTypeInformation.observedType,
Symbol.valueOf(remoteTypeInformation.typeDescriptor),
propertySerializers,
reader)
}
return instance
private fun makePropertySerializers(localProperties: Map<String, LocalPropertyInformation>,
remoteProperties: Map<String, RemotePropertyInformation>,
classLoader: ClassLoader): Map<String, PropertySerializer> =
remoteProperties.mapValues { (name, property) ->
val localProperty = localProperties[name]
val isCalculated = localProperty?.isCalculated ?: false
val type = localProperty?.type?.observedType ?: property.type.typeIdentifier.getLocalType(classLoader)
ComposableTypePropertySerializer.makeForEvolution(name, isCalculated, property.type.typeIdentifier, type)
}
}
private fun generateFields(): List<Field> {
return propertySerializers.serializationOrder.map {
Field(it.serializer.name, it.serializer.type, it.serializer.requires, it.serializer.default, null, it.serializer.mandatory, false)
}
}
override val fields: List<Field> get() = emptyList()
private fun generateProvides(): List<String> = interfaces.map { nameForType(it) }
override fun writeClassInfo(output: SerializationOutput) =
throw UnsupportedOperationException("Evolved types cannot be written")
fun construct(properties: List<Any?>): Any {
logger.trace { "Calling constructor: '$javaConstructor' with properties '$properties'" }
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext, debugIndent: Int) =
throw UnsupportedOperationException("Evolved types cannot be written")
if (properties.size != javaConstructor?.parameterCount) {
throw AMQPNotSerializableException(type, "Serialization constructor for class $type expects "
+ "${javaConstructor?.parameterCount} parameters but we have ${properties.size} "
+ "serialized properties.")
}
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any =
reader.readObject(obj, schemas, input, context)
return javaConstructor?.newInstanceUnwrapped(*properties.toTypedArray())
?: throw AMQPNotSerializableException(
type,
"Attempt to deserialize an interface: $clazz. Serialized form is invalid.")
}
private fun <T> Constructor<T>.newInstanceUnwrapped(vararg args: Any?): T {
try {
return newInstance(*args)
} catch (e: InvocationTargetException) {
throw e.cause!!
}
}
}

View File

@ -1,146 +0,0 @@
package net.corda.serialization.internal.amqp
import net.corda.core.KeepForDJVM
import net.corda.core.serialization.SerializationContext
import org.apache.qpid.proton.amqp.Binary
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type
/**
* Base class for serialization of a property of an object.
*/
sealed class PropertySerializer(val name: String, val propertyReader: PropertyReader, val resolvedType: Type) {
abstract fun writeClassInfo(output: SerializationOutput)
abstract fun writeProperty(obj: Any?, data: Data, output: SerializationOutput, context: SerializationContext, debugIndent: Int = 0)
abstract fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any?
val type: String = generateType()
val requires: List<String> = generateRequires()
val default: String? = generateDefault()
val mandatory: Boolean = generateMandatory()
private val isInterface: Boolean get() = resolvedType.asClass().isInterface
private val isJVMPrimitive: Boolean get() = resolvedType.asClass().isPrimitive
private fun generateType(): String {
return if (isInterface || resolvedType == Any::class.java) "*" else SerializerFactory.nameForType(resolvedType)
}
private fun generateRequires(): List<String> {
return if (isInterface) listOf(SerializerFactory.nameForType(resolvedType)) else emptyList()
}
private fun generateDefault(): String? =
if (isJVMPrimitive) {
when (resolvedType) {
java.lang.Boolean.TYPE -> "false"
java.lang.Character.TYPE -> "&#0"
else -> "0"
}
} else {
null
}
private fun generateMandatory(): Boolean {
return isJVMPrimitive || !(propertyReader.isNullable())
}
companion object {
fun make(name: String, readMethod: PropertyReader, resolvedType: Type, factory: SerializerFactory): PropertySerializer {
return if (SerializerFactory.isPrimitive(resolvedType)) {
when (resolvedType) {
Char::class.java, Character::class.java -> AMQPCharPropertySerializer(name, readMethod)
else -> AMQPPrimitivePropertySerializer(name, readMethod, resolvedType)
}
} else {
DescribedTypePropertySerializer(name, readMethod, resolvedType) { factory.get(null, resolvedType) }
}
}
}
/**
* A property serializer for a complex type (another object).
*/
@KeepForDJVM
class DescribedTypePropertySerializer(
name: String,
readMethod: PropertyReader,
resolvedType: Type,
private val lazyTypeSerializer: () -> AMQPSerializer<*>) : PropertySerializer(name, readMethod, resolvedType) {
// This is lazy so we don't get an infinite loop when a method returns an instance of the class.
private val typeSerializer: AMQPSerializer<*> by lazy { lazyTypeSerializer() }
override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({ nameForDebug }) {
if (resolvedType != Any::class.java) {
typeSerializer.writeClassInfo(output)
}
}
override fun readProperty(
obj: Any?,
schemas: SerializationSchemas,
input: DeserializationInput,
context: SerializationContext): Any? = ifThrowsAppend({ nameForDebug }) {
input.readObjectOrNull(obj, schemas, resolvedType, context)
}
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput,
context: SerializationContext, debugIndent: Int) = ifThrowsAppend({ nameForDebug }
) {
output.writeObjectOrNull(propertyReader.read(obj), data, resolvedType, context, debugIndent)
}
private val nameForDebug = "$name(${resolvedType.typeName})"
}
/**
* A property serializer for most AMQP primitive type (Int, String, etc).
*/
class AMQPPrimitivePropertySerializer(
name: String,
readMethod: PropertyReader,
resolvedType: Type) : PropertySerializer(name, readMethod, resolvedType) {
override fun writeClassInfo(output: SerializationOutput) {}
override fun readProperty(obj: Any?, schemas: SerializationSchemas,
input: DeserializationInput, context: SerializationContext
): Any? {
return if (obj is Binary) obj.array else obj
}
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput,
context: SerializationContext, debugIndent: Int
) {
val value = propertyReader.read(obj)
if (value is ByteArray) {
data.putObject(Binary(value))
} else {
data.putObject(value)
}
}
}
/**
* A property serializer for the AMQP char type, needed as a specialisation as the underlying
* value of the character is stored in numeric UTF-16 form and on deserialization requires explicit
* casting back to a char otherwise it's treated as an Integer and a TypeMismatch occurs
*/
class AMQPCharPropertySerializer(name: String, readMethod: PropertyReader) :
PropertySerializer(name, readMethod, Character::class.java) {
override fun writeClassInfo(output: SerializationOutput) {}
override fun readProperty(obj: Any?, schemas: SerializationSchemas,
input: DeserializationInput, context: SerializationContext
): Any? {
return if (obj == null) null else (obj as Short).toChar()
}
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput,
context: SerializationContext, debugIndent: Int
) {
val input = propertyReader.read(obj)
if (input != null) data.putShort((input as Char).toShort()) else data.putNull()
}
}
}

View File

@ -1,243 +0,0 @@
package net.corda.serialization.internal.amqp
import net.corda.core.KeepForDJVM
import net.corda.core.serialization.SerializableCalculatedProperty
import net.corda.core.utilities.loggerFor
import java.io.NotSerializableException
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.lang.reflect.Type
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaGetter
import kotlin.reflect.jvm.kotlinProperty
abstract class PropertyReader {
abstract fun read(obj: Any?): Any?
abstract fun isNullable(): Boolean
}
/**
* Accessor for those properties of a class that have defined getter functions.
*/
@KeepForDJVM
class PublicPropertyReader(private val readMethod: Method) : PropertyReader() {
init {
readMethod.isAccessible = true
}
private fun Method.returnsNullable(): Boolean {
try {
val returnTypeString = this.declaringClass.kotlin.memberProperties.firstOrNull {
it.javaGetter == this
}?.returnType?.toString() ?: "?"
return returnTypeString.endsWith('?') || returnTypeString.endsWith('!')
} catch (e: kotlin.reflect.jvm.internal.KotlinReflectionInternalError) {
// This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue
// is: https://youtrack.jetbrains.com/issue/KT-13077
// TODO: Revisit this when Kotlin issue is fixed.
// So this used to report as an error, but given we serialise exceptions all the time it
// provides for very scary log files so move this to trace level
loggerFor<PropertySerializer>().let { logger ->
logger.trace("Using kotlin introspection on internal type ${this.declaringClass}")
logger.trace("Unexpected internal Kotlin error", e)
}
return true
}
}
override fun read(obj: Any?): Any? {
return readMethod.invoke(obj)
}
override fun isNullable(): Boolean = readMethod.returnsNullable()
val genericReturnType get() = readMethod.genericReturnType
}
/**
* Accessor for those properties of a class that do not have defined getter functions. In which case
* we used reflection to remove the unreadable status from that property whilst it's accessed.
*/
@KeepForDJVM
class PrivatePropertyReader(val field: Field, parentType: Type) : PropertyReader() {
init {
loggerFor<PropertySerializer>().warn("Create property Serializer for private property '${field.name}' not "
+ "exposed by a getter on class '$parentType'\n"
+ "\tNOTE: This behaviour will be deprecated at some point in the future and a getter required")
}
override fun read(obj: Any?): Any? {
field.isAccessible = true
val rtn = field.get(obj)
field.isAccessible = false
return rtn
}
override fun isNullable() = try {
field.kotlinProperty?.returnType?.isMarkedNullable ?: false
} catch (e: kotlin.reflect.jvm.internal.KotlinReflectionInternalError) {
// This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue
// is: https://youtrack.jetbrains.com/issue/KT-13077
// TODO: Revisit this when Kotlin issue is fixed.
// So this used to report as an error, but given we serialise exceptions all the time it
// provides for very scary log files so move this to trace level
loggerFor<PropertySerializer>().let { logger ->
logger.trace("Using kotlin introspection on internal type $field")
logger.trace("Unexpected internal Kotlin error", e)
}
true
}
}
/**
* Special instance of a [PropertyReader] for use only by [EvolutionSerializer]s to make
* it explicit that no properties are ever actually read from an object as the evolution
* serializer should only be accessing the already serialized form.
*/
class EvolutionPropertyReader : PropertyReader() {
override fun read(obj: Any?): Any? {
throw UnsupportedOperationException("It should be impossible for an evolution serializer to "
+ "be reading from an object")
}
override fun isNullable() = true
}
/**
* Represents a generic interface to a serializable property of an object.
*
* @property initialPosition where in the constructor used for serialization the property occurs.
* @property serializer a [PropertySerializer] wrapping access to the property. This will either be a
* method invocation on the getter or, if not publicly accessible, reflection based by temporally
* making the property accessible.
*/
abstract class PropertyAccessor(
open val serializer: PropertySerializer) {
companion object : Comparator<PropertyAccessor> {
override fun compare(p0: PropertyAccessor?, p1: PropertyAccessor?): Int {
return p0?.serializer?.name?.compareTo(p1?.serializer?.name ?: "") ?: 0
}
}
open val isCalculated get() = false
/**
* Override to control how the property is set on the object.
*/
abstract fun set(instance: Any, obj: Any?)
override fun toString(): String {
return serializer.name
}
}
/**
* Implementation of [PropertyAccessor] representing a property of an object that
* is serialized and deserialized via JavaBean getter and setter style methods.
*/
class PropertyAccessorGetterSetter(
getter: PropertySerializer,
private val setter: Method) : PropertyAccessor(getter) {
init {
/**
* Play nicely with Java interop, public methods aren't marked as accessible
*/
setter.isAccessible = true
}
/**
* Invokes the setter on the underlying object passing in the serialized value.
*/
override fun set(instance: Any, obj: Any?) {
setter.invoke(instance, *listOf(obj).toTypedArray())
}
}
/**
* Implementation of [PropertyAccessor] representing a property of an object that
* is serialized via a JavaBean getter but deserialized using the constructor
* of the object the property belongs to.
*/
class PropertyAccessorConstructor(
val initialPosition: Int,
override val serializer: PropertySerializer) : PropertyAccessor(serializer) {
/**
* Because the property should be being set on the object through the constructor any
* calls to the explicit setter should be an error.
*/
override fun set(instance: Any, obj: Any?) {
NotSerializableException("Attempting to access a setter on an object being instantiated " +
"via its constructor.")
}
override fun toString(): String =
"${serializer.name}($initialPosition)"
}
/**
* Implementation of [PropertyAccessor] representing a calculated property of an object that is serialized
* so that it can be used by the class carpenter, but ignored on deserialisation as there is no setter or
* constructor parameter to receive its value.
*
* This will only be created for calculated properties that are accessible via no-argument methods annotated
* with [SerializableCalculatedProperty].
*/
class CalculatedPropertyAccessor(override val serializer: PropertySerializer): PropertyAccessor(serializer) {
override val isCalculated: Boolean
get() = true
override fun set(instance: Any, obj: Any?) = Unit // do nothing, as it's a calculated value
}
/**
* Represents a collection of [PropertyAccessor]s that represent the serialized form
* of an object.
*
* @property serializationOrder a list of [PropertyAccessor]. For deterministic serialization
* should be sorted.
* @property size how many properties are being serialized.
* @property byConstructor are the properties of the class represented by this set of properties populated
* on deserialization via the object's constructor or the corresponding setter functions. Should be
* overridden and set appropriately by child types.
*/
abstract class PropertySerializers(
val serializationOrder: List<PropertyAccessor>) {
companion object {
fun make(serializationOrder: List<PropertyAccessor>) =
when (serializationOrder.find { !it.isCalculated }) {
is PropertyAccessorConstructor -> PropertySerializersConstructor(serializationOrder)
is PropertyAccessorGetterSetter -> PropertySerializersSetter(serializationOrder)
null -> PropertySerializersNoProperties()
else -> {
throw AMQPNoTypeNotSerializableException("Unknown Property Accessor type, cannot create set")
}
}
}
val size get() = serializationOrder.size
abstract val byConstructor: Boolean
val deserializableSize = serializationOrder.count { !it.isCalculated }
}
class PropertySerializersNoProperties : PropertySerializers(emptyList()) {
override val byConstructor get() = true
}
class PropertySerializersConstructor(
serializationOrder: List<PropertyAccessor>) : PropertySerializers(serializationOrder) {
override val byConstructor get() = true
}
class PropertySerializersSetter(
serializationOrder: List<PropertyAccessor>) : PropertySerializers(serializationOrder) {
override val byConstructor get() = false
}
class PropertySerializersEvolution : PropertySerializers(emptyList()) {
override val byConstructor get() = false
}

View File

@ -0,0 +1,141 @@
package net.corda.serialization.internal.amqp
import net.corda.core.utilities.contextLogger
import net.corda.serialization.internal.model.*
import org.hibernate.type.descriptor.java.ByteTypeDescriptor
import java.io.NotSerializableException
/**
* A factory that knows how to create serializers to deserialize values sent to us by remote parties.
*/
interface RemoteSerializerFactory {
/**
* Lookup and manufacture a serializer for the given AMQP type descriptor, assuming we also have the necessary types
* contained in the provided [Schema].
*
* @param typeDescriptor The type descriptor for the type to obtain a serializer for.
* @param schema The schemas sent along with the serialized data.
*/
@Throws(NotSerializableException::class)
fun get(typeDescriptor: TypeDescriptor, schema: SerializationSchemas): AMQPSerializer<Any>
}
/**
* Represents the reflection of some [RemoteTypeInformation] by some [LocalTypeInformation], which we use to make
* decisions about evolution.
*/
data class RemoteAndLocalTypeInformation(
val remoteTypeInformation: RemoteTypeInformation,
val localTypeInformation: LocalTypeInformation)
/**
* A [RemoteSerializerFactory] which uses an [AMQPRemoteTypeModel] to interpret AMQP [Schema]s into [RemoteTypeInformation],
* reflects this into [LocalTypeInformation] using a [LocalTypeModel] and a [TypeLoader], and compares the two in order to
* decide whether to return the serializer provided by the [LocalSerializerFactory] or to construct a special evolution serializer
* using the [EvolutionSerializerFactory].
*
* Its decisions are recorded by registering the chosen serialisers against their type descriptors
* in the [DescriptorBasedSerializerRegistry].
*
* @param evolutionSerializerFactory The [EvolutionSerializerFactory] to use to create evolution serializers, when necessary.
* @param descriptorBasedSerializerRegistry The registry to use to store serializers by [TypeDescriptor].
* @param remoteTypeModel The [AMQPRemoteTypeModel] to use to interpret AMPQ [Schema] information into [RemoteTypeInformation].
* @param localTypeModel The [LocalTypeModel] to use to obtain [LocalTypeInformation] for reflected [Type]s.
* @param typeLoader The [TypeLoader] to use to load local [Type]s reflecting [RemoteTypeInformation].
* @param localSerializerFactory The [LocalSerializerFactory] to use to obtain serializers for non-evolved types.
*/
class DefaultRemoteSerializerFactory(
private val evolutionSerializerFactory: EvolutionSerializerFactory,
private val descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry,
private val remoteTypeModel: AMQPRemoteTypeModel,
private val localTypeModel: LocalTypeModel,
private val typeLoader: TypeLoader,
private val localSerializerFactory: LocalSerializerFactory)
: RemoteSerializerFactory {
companion object {
private val logger = contextLogger()
}
override fun get(typeDescriptor: TypeDescriptor, schema: SerializationSchemas): AMQPSerializer<Any> =
// If we have seen this descriptor before, we assume we have seen everything in this schema before.
descriptorBasedSerializerRegistry.getOrBuild(typeDescriptor) {
logger.trace("get Serializer descriptor=$typeDescriptor")
// Interpret all of the types in the schema into RemoteTypeInformation, and reflect that into LocalTypeInformation.
val remoteTypeInformationMap = remoteTypeModel.interpret(schema)
val reflected = reflect(remoteTypeInformationMap)
// Get, and record in the registry, serializers for all of the types contained in the schema.
// This will save us having to re-interpret the entire schema on re-entry when deserialising individual property values.
val serializers = reflected.mapValues { (descriptor, remoteLocalPair) ->
descriptorBasedSerializerRegistry.getOrBuild(descriptor) {
getUncached(remoteLocalPair.remoteTypeInformation, remoteLocalPair.localTypeInformation)
}
}
// Return the specific serializer the caller asked for.
serializers[typeDescriptor] ?: throw NotSerializableException(
"Could not find type matching descriptor $typeDescriptor.")
}
private fun getUncached(remoteTypeInformation: RemoteTypeInformation, localTypeInformation: LocalTypeInformation): AMQPSerializer<Any> {
val remoteDescriptor = remoteTypeInformation.typeDescriptor
// Obtain a serializer and descriptor for the local type.
val localSerializer = localSerializerFactory.get(localTypeInformation)
val localDescriptor = localSerializer.typeDescriptor.toString()
return when {
// If descriptors match, we can return the local serializer straight away.
localDescriptor == remoteDescriptor -> localSerializer
// Can we deserialise without evolution, e.g. going from List<Foo> to List<*>?
remoteTypeInformation.isDeserialisableWithoutEvolutionTo(localTypeInformation) -> localSerializer
// Are the remote/local types evolvable? If so, ask the evolution serializer factory for a serializer, returning
// the local serializer if it returns null (i.e. no evolution required).
remoteTypeInformation.isEvolvableTo(localTypeInformation) ->
evolutionSerializerFactory.getEvolutionSerializer(remoteTypeInformation, localTypeInformation)
?: localSerializer
// Descriptors don't match, and something is probably broken, but we let the framework do what it can with the local
// serialiser (BlobInspectorTest uniquely breaks if we throw an exception here, and passes if we just warn and continue).
else -> {
logger.warn("""
Mismatch between type descriptors, but remote type is not evolvable to local type.
Remote type (descriptor: $remoteDescriptor)
${remoteTypeInformation.prettyPrint(false)}
Local type (descriptor $localDescriptor):
${localTypeInformation.prettyPrint(false)}
""")
localSerializer
}
}
}
private fun reflect(remoteInformation: Map<TypeDescriptor, RemoteTypeInformation>):
Map<TypeDescriptor, RemoteAndLocalTypeInformation> {
val localInformationByIdentifier = typeLoader.load(remoteInformation.values).mapValues { (_, type) ->
localTypeModel.inspect(type)
}
return remoteInformation.mapValues { (_, remoteInformation) ->
RemoteAndLocalTypeInformation(remoteInformation, localInformationByIdentifier[remoteInformation.typeIdentifier]!!)
}
}
private fun RemoteTypeInformation.isEvolvableTo(localTypeInformation: LocalTypeInformation): Boolean = when(this) {
is RemoteTypeInformation.Composable -> localTypeInformation is LocalTypeInformation.Composable
is RemoteTypeInformation.AnEnum -> localTypeInformation is LocalTypeInformation.AnEnum
else -> false
}
private fun RemoteTypeInformation.isDeserialisableWithoutEvolutionTo(localTypeInformation: LocalTypeInformation) =
this is RemoteTypeInformation.Parameterised &&
(localTypeInformation is LocalTypeInformation.ACollection ||
localTypeInformation is LocalTypeInformation.AMap)
}

View File

@ -2,243 +2,10 @@ package net.corda.serialization.internal.amqp
import com.google.common.primitives.Primitives
import com.google.common.reflect.TypeToken
import net.corda.core.internal.isConcreteClass
import net.corda.core.serialization.*
import net.corda.serialization.internal.model.TypeIdentifier
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.*
import java.lang.reflect.Field
import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaConstructor
import kotlin.reflect.jvm.javaType
/**
* Code for finding the constructor we will use for deserialization.
*
* If any constructor is uniquely annotated with [@ConstructorForDeserialization], then that constructor is chosen.
* An error is reported if more than one constructor is annotated.
*
* Otherwise, if there is a Kotlin primary constructor, it selects that, and if not it selects either the unique
* constructor or, if there are two and one is the default no-argument constructor, the non-default constructor.
*/
fun constructorForDeserialization(type: Type): KFunction<Any> {
val clazz = type.asClass().apply {
if (!isConcreteClass) throw AMQPNotSerializableException(type,
"Cannot find deserialisation constructor for non-concrete class $this")
}
val kotlinCtors = clazz.kotlin.constructors
val annotatedCtors = kotlinCtors.filter { it.findAnnotation<ConstructorForDeserialization>() != null }
if (annotatedCtors.size > 1) throw AMQPNotSerializableException(
type,
"More than one constructor for $clazz is annotated with @ConstructorForDeserialization.")
val defaultCtor = kotlinCtors.firstOrNull { it.parameters.isEmpty() }
val nonDefaultCtors = kotlinCtors.filter { it != defaultCtor }
val preferredCandidate = annotatedCtors.firstOrNull() ?:
clazz.kotlin.primaryConstructor ?:
when(nonDefaultCtors.size) {
1 -> nonDefaultCtors.first()
0 -> defaultCtor ?: throw AMQPNotSerializableException(type, "No constructor found for $clazz.")
else -> throw AMQPNotSerializableException(type, "No unique non-default constructor found for $clazz.")
}
return preferredCandidate.apply { isAccessible = true }
}
/**
* Identifies the properties to be used during serialization by attempting to find those that match the parameters
* to the deserialization constructor, if the class is concrete. If it is abstract, or an interface, then use all
* the properties.
*
* Note, you will need any Java classes to be compiled with the `-parameters` option to ensure constructor parameters
* have names accessible via reflection.
*/
fun <T : Any> propertiesForSerialization(
kotlinConstructor: KFunction<T>?,
type: Type,
factory: SerializerFactory): PropertySerializers = PropertySerializers.make(
getValueProperties(kotlinConstructor, type, factory)
.addCalculatedProperties(factory, type)
.sortedWith(PropertyAccessor))
fun <T : Any> getValueProperties(kotlinConstructor: KFunction<T>?, type: Type, factory: SerializerFactory)
: List<PropertyAccessor> =
if (kotlinConstructor != null) {
propertiesForSerializationFromConstructor(kotlinConstructor, type, factory)
} else {
propertiesForSerializationFromAbstract(type.asClass(), type, factory)
}
private fun List<PropertyAccessor>.addCalculatedProperties(factory: SerializerFactory, type: Type)
: List<PropertyAccessor> {
val nonCalculated = map { it.serializer.name }.toSet()
return this + type.asClass().calculatedPropertyDescriptors().mapNotNull { (name, descriptor) ->
if (name in nonCalculated) null else {
val calculatedPropertyMethod = descriptor.getter
?: throw IllegalStateException("Property $name is not a calculated property")
CalculatedPropertyAccessor(PropertySerializer.make(
name,
PublicPropertyReader(calculatedPropertyMethod),
calculatedPropertyMethod.genericReturnType,
factory))
}
}
}
/**
* From a constructor, determine which properties of a class are to be serialized.
*
* @param kotlinConstructor The constructor to be used to instantiate instances of the class
* @param type The class's [Type]
* @param factory The factory generating the serializer wrapping this function.
*/
internal fun <T : Any> propertiesForSerializationFromConstructor(
kotlinConstructor: KFunction<T>,
type: Type,
factory: SerializerFactory): List<PropertyAccessor> {
val clazz = (kotlinConstructor.returnType.classifier as KClass<*>).javaObjectType
val classProperties = clazz.propertyDescriptors()
// Annoyingly there isn't a better way to ascertain that the constructor for the class
// has a synthetic parameter inserted to capture the reference to the outer class. You'd
// think you could inspect the parameter and check the isSynthetic flag but that is always
// false so given the naming convention is specified by the standard we can just check for
// this
kotlinConstructor.javaConstructor?.apply {
if (parameterCount > 0 && parameters[0].name == "this$0") throw SyntheticParameterException(type)
}
if (classProperties.isNotEmpty() && kotlinConstructor.parameters.isEmpty()) {
return propertiesForSerializationFromSetters(classProperties, type, factory)
}
return kotlinConstructor.parameters.withIndex().map { param ->
toPropertyAccessorConstructor(param.index, param.value, classProperties, type, clazz, factory)
}
}
private fun toPropertyAccessorConstructor(index: Int, param: KParameter, classProperties: Map<String, PropertyDescriptor>, type: Type, clazz: Class<out Any>, factory: SerializerFactory): PropertyAccessorConstructor {
// name cannot be null, if it is then this is a synthetic field and we will have bailed
// out prior to this
val name = param.name!!
// We will already have disambiguated getA for property A or a but we still need to cope
// with the case we don't know the case of A when the parameter doesn't match a property
// but has a getter
val matchingProperty = classProperties[name] ?: classProperties[name.capitalize()]
?: throw AMQPNotSerializableException(type,
"Constructor parameter - \"$name\" - doesn't refer to a property of \"$clazz\"")
// If the property has a getter we'll use that to retrieve it's value from the instance, if it doesn't
// *for *now* we switch to a reflection based method
val propertyReader = matchingProperty.getter?.let { getter ->
getPublicPropertyReader(getter, type, param, name, clazz)
} ?: matchingProperty.field?.let { field ->
getPrivatePropertyReader(field, type)
} ?: throw AMQPNotSerializableException(type,
"No property matching constructor parameter named - \"$name\" - " +
"of \"${param}\". If using Java, check that you have the -parameters option specified " +
"in the Java compiler. Alternately, provide a proxy serializer " +
"(SerializationCustomSerializer) if recompiling isn't an option")
return PropertyAccessorConstructor(
index,
PropertySerializer.make(name, propertyReader.first, propertyReader.second, factory))
}
/**
* If we determine a class has a constructor that takes no parameters then check for pairs of getters / setters
* and use those
*/
fun propertiesForSerializationFromSetters(
properties: Map<String, PropertyDescriptor>,
type: Type,
factory: SerializerFactory): List<PropertyAccessor> =
properties.asSequence().map { entry ->
val (name, property) = entry
val getter = property.getter
val setter = property.setter
if (getter == null || setter == null) return@map null
PropertyAccessorGetterSetter(
PropertySerializer.make(
name,
PublicPropertyReader(getter),
resolveTypeVariables(getter.genericReturnType, type),
factory),
setter)
}.filterNotNull().toList()
private fun getPrivatePropertyReader(field: Field, type: Type) =
PrivatePropertyReader(field, type) to resolveTypeVariables(field.genericType, type)
private fun getPublicPropertyReader(getter: Method, type: Type, param: KParameter, name: String, clazz: Class<out Any>): Pair<PublicPropertyReader, Type> {
val returnType = resolveTypeVariables(getter.genericReturnType, type)
val paramToken = TypeToken.of(param.type.javaType)
val rawParamType = TypeToken.of(paramToken.rawType)
if (!(paramToken.isSupertypeOf(returnType)
|| paramToken.isSupertypeOf(getter.genericReturnType)
// cope with the case where the constructor parameter is a generic type (T etc) but we
// can discover it's raw type. When bounded this wil be the bounding type, unbounded
// generics this will be object
|| rawParamType.isSupertypeOf(returnType)
|| rawParamType.isSupertypeOf(getter.genericReturnType))) {
throw AMQPNotSerializableException(
type,
"Property - \"$name\" - has type \"$returnType\" on \"$clazz\" " +
"but differs from constructor parameter type \"${param.type.javaType}\"")
}
return PublicPropertyReader(getter) to returnType
}
private fun propertiesForSerializationFromAbstract(
clazz: Class<*>,
type: Type,
factory: SerializerFactory): List<PropertyAccessor> =
clazz.propertyDescriptors().asSequence().withIndex().mapNotNull { (index, entry) ->
val (name, property) = entry
if (property.getter == null || property.field == null) return@mapNotNull null
val getter = property.getter
val returnType = resolveTypeVariables(getter.genericReturnType, type)
PropertyAccessorConstructor(
index,
PropertySerializer.make(name, PublicPropertyReader(getter), returnType, factory))
}.toList()
internal fun interfacesForSerialization(type: Type, serializerFactory: SerializerFactory): List<Type> =
exploreType(type, serializerFactory).toList()
private fun exploreType(type: Type, serializerFactory: SerializerFactory, interfaces: MutableSet<Type> = LinkedHashSet()): MutableSet<Type> {
val clazz = type.asClass()
if (clazz.isInterface) {
// Ignore classes we've already seen, and stop exploring once we reach a branch that has no `CordaSerializable`
// annotation or whitelisting.
if (clazz in interfaces || serializerFactory.whitelist.isNotWhitelisted(clazz)) return interfaces
else interfaces += type
}
(clazz.genericInterfaces.asSequence() + clazz.genericSuperclass)
.filterNotNull()
.forEach { exploreType(resolveTypeVariables(it, type), serializerFactory, interfaces) }
return interfaces
}
/**
* Extension helper for writing described objects.
@ -283,7 +50,7 @@ fun resolveTypeVariables(actualType: Type, contextType: Type?): Type {
return if (resolvedType is TypeVariable<*>) {
val bounds = resolvedType.bounds
return if (bounds.isEmpty()) {
SerializerFactory.AnyType
TypeIdentifier.UnknownType.getLocalType()
} else if (bounds.size == 1) {
resolveTypeVariables(bounds[0], contextType)
} else throw AMQPNotSerializableException(
@ -309,8 +76,9 @@ internal fun Type.asClass(): Class<*> {
internal fun Type.asArray(): Type? {
return when(this) {
is Class<*> -> this.arrayClass()
is ParameterizedType -> DeserializedGenericArrayType(this)
is Class<*>,
is ParameterizedType -> TypeIdentifier.ArrayOf(TypeIdentifier.forGenericType(this))
.getLocalType(this::class.java.classLoader ?: TypeIdentifier::class.java.classLoader)
else -> null
}
}
@ -324,9 +92,10 @@ internal fun Type.componentType(): Type {
return (this as? Class<*>)?.componentType ?: (this as GenericArrayType).genericComponentType
}
internal fun Class<*>.asParameterizedType(): ParameterizedType {
return DeserializedParameterizedType(this, this.typeParameters)
}
internal fun Class<*>.asParameterizedType(): ParameterizedType =
TypeIdentifier.Erased(this.name, this.typeParameters.size)
.toParameterized(this.typeParameters.map { TypeIdentifier.forGenericType(it) })
.getLocalType(classLoader ?: TypeIdentifier::class.java.classLoader) as ParameterizedType
internal fun Type.asParameterizedType(): ParameterizedType {
return when (this) {
@ -374,19 +143,4 @@ fun hasCordaSerializable(type: Class<*>): Boolean {
return type.isAnnotationPresent(CordaSerializable::class.java)
|| type.interfaces.any(::hasCordaSerializable)
|| (type.superclass != null && hasCordaSerializable(type.superclass))
}
fun isJavaPrimitive(type: Class<*>) = type in JavaPrimitiveTypes.primativeTypes
private object JavaPrimitiveTypes {
val primativeTypes = hashSetOf<Class<*>>(
Boolean::class.java,
Char::class.java,
Byte::class.java,
Short::class.java,
Int::class.java,
Long::class.java,
Float::class.java,
Double::class.java,
Void::class.java)
}
}

View File

@ -7,10 +7,12 @@ import net.corda.core.utilities.contextLogger
import net.corda.serialization.internal.CordaSerializationEncoding
import net.corda.serialization.internal.SectionId
import net.corda.serialization.internal.byteArrayOutput
import net.corda.serialization.internal.model.TypeIdentifier
import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException
import java.io.OutputStream
import java.lang.reflect.Type
import java.lang.reflect.WildcardType
import java.util.*
import kotlin.collections.LinkedHashSet
@ -28,7 +30,7 @@ data class BytesAndSchemas<T : Any>(
*/
@KeepForDJVM
open class SerializationOutput constructor(
internal val serializerFactory: SerializerFactory
internal val serializerFactory: LocalSerializerFactory
) {
companion object {
private val logger = contextLogger()
@ -118,7 +120,7 @@ open class SerializationOutput constructor(
if (obj == null) {
data.putNull()
} else {
writeObject(obj, data, if (type == SerializerFactory.AnyType) obj.javaClass else type, context, debugIndent)
writeObject(obj, data, if (type == TypeIdentifier.UnknownType.getLocalType()) obj.javaClass else type, context, debugIndent)
}
}
@ -148,8 +150,15 @@ open class SerializationOutput constructor(
}
internal open fun requireSerializer(type: Type) {
if (type != SerializerFactory.AnyType && type != Object::class.java) {
val serializer = serializerFactory.get(null, type)
if (type != Object::class.java && type.typeName != "?") {
val resolvedType = when(type) {
is WildcardType ->
if (type.upperBounds.size == 1) type.upperBounds[0]
else throw NotSerializableException("Cannot obtain upper bound for type $type")
else -> type
}
val serializer = serializerFactory.get(resolvedType)
if (serializer !in serializerHistory) {
serializerHistory.add(serializer)
serializer.writeClassInfo(this)

View File

@ -1,29 +1,11 @@
package net.corda.serialization.internal.amqp
import com.google.common.primitives.Primitives
import net.corda.core.KeepForDJVM
import net.corda.core.StubOutForDJVM
import net.corda.core.internal.kotlinObjectInstance
import net.corda.core.internal.uncheckedCast
import net.corda.core.serialization.ClassWhitelist
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.trace
import net.corda.serialization.internal.carpenter.*
import net.corda.serialization.internal.model.DefaultCacheProvider
import org.apache.qpid.proton.amqp.*
import java.io.NotSerializableException
import java.lang.reflect.*
import java.util.*
import javax.annotation.concurrent.ThreadSafe
@KeepForDJVM
data class SerializationSchemas(val schema: Schema, val transforms: TransformsSchema)
@KeepForDJVM
data class FactorySchemaAndDescriptor(val schemas: SerializationSchemas, val typeDescriptor: Any)
@KeepForDJVM
data class CustomSerializersCacheKey(val clazz: Class<*>, val declaredType: Type)
/**
* Factory of serializers designed to be shared across threads and invocations.
@ -34,426 +16,15 @@ data class CustomSerializersCacheKey(val clazz: Class<*>, val declaredType: Type
* @property onlyCustomSerializers used for testing, when set will cause the factory to throw a
* [NotSerializableException] if it cannot find a registered custom serializer for a given type
*/
// TODO: support for intern-ing of deserialized objects for some core types (e.g. PublicKey) for memory efficiency
// TODO: maybe support for caching of serialized form of some core types for performance
// TODO: profile for performance in general
// TODO: use guava caches etc so not unbounded
// TODO: allow definition of well known types that are left out of the schema.
// TODO: migrate some core types to unsigned integer descriptor
// TODO: document and alert to the fact that classes cannot default superclass/interface properties otherwise they are "erased" due to matching with constructor.
// TODO: type name prefixes for interfaces and abstract classes? Or use label?
// TODO: generic types should define restricted type alias with source of the wildcarded version, I think, if we're to generate classes from schema
// TODO: need to rethink matching of constructor to properties in relation to implementing interfaces and needing those properties etc.
// TODO: need to support super classes as well as interfaces with our current code base... what's involved? If we continue to ban, what is the impact?
@KeepForDJVM
@ThreadSafe
interface SerializerFactory {
val whitelist: ClassWhitelist
val classCarpenter: ClassCarpenter
val fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter
// Caches
val serializersByType: MutableMap<Type, AMQPSerializer<Any>>
val serializersByDescriptor: MutableMap<Any, AMQPSerializer<Any>>
val transformsCache: MutableMap<String, EnumMap<TransformTypes, MutableList<Transform>>>
val fingerPrinter: FingerPrinter
val classloader: ClassLoader
/**
* Look up, and manufacture if necessary, a serializer for the given type.
*
* @param actualClass Will be null if there isn't an actual object instance available (e.g. for
* restricted type processing).
*/
@Throws(NotSerializableException::class)
fun get(actualClass: Class<*>?, declaredType: Type): AMQPSerializer<Any>
interface SerializerFactory : LocalSerializerFactory, RemoteSerializerFactory, CustomSerializerRegistry
/**
* Lookup and manufacture a serializer for the given AMQP type descriptor, assuming we also have the necessary types
* contained in the [Schema].
*/
@Throws(NotSerializableException::class)
fun get(typeDescriptor: Any, schema: SerializationSchemas): AMQPSerializer<Any>
/**
* Register a custom serializer for any type that cannot be serialized or deserialized by the default serializer
* that expects to find getters and a constructor with a parameter for each property.
*/
fun register(customSerializer: CustomSerializer<out Any>)
fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer<Any>?
fun registerExternal(customSerializer: CorDappCustomSerializer)
fun registerByDescriptor(name: Symbol, serializerCreator: () -> AMQPSerializer<Any>): AMQPSerializer<Any>
object AnyType : WildcardType {
override fun getUpperBounds(): Array<Type> = arrayOf(Object::class.java)
override fun getLowerBounds(): Array<Type> = emptyArray()
override fun toString(): String = "?"
}
companion object {
fun isPrimitive(type: Type): Boolean = primitiveTypeName(type) != null
fun primitiveTypeName(type: Type): String? {
val clazz = type as? Class<*> ?: return null
return primitiveTypeNames[Primitives.unwrap(clazz)]
}
fun primitiveType(type: String): Class<*>? {
return namesOfPrimitiveTypes[type]
}
private val primitiveTypeNames: Map<Class<*>, String> = mapOf(
Character::class.java to "char",
Char::class.java to "char",
Boolean::class.java to "boolean",
Byte::class.java to "byte",
UnsignedByte::class.java to "ubyte",
Short::class.java to "short",
UnsignedShort::class.java to "ushort",
Int::class.java to "int",
UnsignedInteger::class.java to "uint",
Long::class.java to "long",
UnsignedLong::class.java to "ulong",
Float::class.java to "float",
Double::class.java to "double",
Decimal32::class.java to "decimal32",
Decimal64::class.java to "decimal64",
Decimal128::class.java to "decimal128",
Date::class.java to "timestamp",
UUID::class.java to "uuid",
ByteArray::class.java to "binary",
String::class.java to "string",
Symbol::class.java to "symbol")
private val namesOfPrimitiveTypes: Map<String, Class<*>> = primitiveTypeNames.map { it.value to it.key }.toMap()
fun nameForType(type: Type): String = when (type) {
is Class<*> -> {
primitiveTypeName(type) ?: if (type.isArray) {
"${nameForType(type.componentType)}${if (type.componentType.isPrimitive) "[p]" else "[]"}"
} else type.name
}
is ParameterizedType -> {
"${nameForType(type.rawType)}<${type.actualTypeArguments.joinToString { nameForType(it) }}>"
}
is GenericArrayType -> "${nameForType(type.genericComponentType)}[]"
is WildcardType -> "?"
is TypeVariable<*> -> "?"
else -> throw AMQPNotSerializableException(type, "Unable to render type $type to a string.")
}
}
}
open class DefaultSerializerFactory(
override val whitelist: ClassWhitelist,
override val classCarpenter: ClassCarpenter,
private val evolutionSerializerProvider: EvolutionSerializerProvider,
override val fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter,
private val onlyCustomSerializers: Boolean = false
) : SerializerFactory {
// Caches
override val serializersByType: MutableMap<Type, AMQPSerializer<Any>> = DefaultCacheProvider.createCache()
override val serializersByDescriptor: MutableMap<Any, AMQPSerializer<Any>> = DefaultCacheProvider.createCache()
private var customSerializers: List<SerializerFor> = emptyList()
private val customSerializersCache: MutableMap<CustomSerializersCacheKey, AMQPSerializer<Any>?> = DefaultCacheProvider.createCache()
override val transformsCache: MutableMap<String, EnumMap<TransformTypes, MutableList<Transform>>> = DefaultCacheProvider.createCache()
override val fingerPrinter by lazy { fingerPrinterConstructor(this) }
override val classloader: ClassLoader get() = classCarpenter.classloader
// Used to short circuit any computation for a given input, for performance.
private data class MemoType(val actualClass: Class<*>?, val declaredType: Type) : Type
/**
* Look up, and manufacture if necessary, a serializer for the given type.
*
* @param actualClass Will be null if there isn't an actual object instance available (e.g. for
* restricted type processing).
*/
@Throws(NotSerializableException::class)
override fun get(actualClass: Class<*>?, declaredType: Type): AMQPSerializer<Any> {
// can be useful to enable but will be *extremely* chatty if you do
logger.trace { "Get Serializer for $actualClass ${declaredType.typeName}" }
val ourType = MemoType(actualClass, declaredType)
// ConcurrentHashMap.get() is lock free, but computeIfAbsent is not, even if the key is in the map already.
return serializersByType[ourType] ?: run {
val declaredClass = declaredType.asClass()
val actualType: Type = if (actualClass == null) declaredType
else inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType
val serializer = when {
// Declared class may not be set to Collection, but actual class could be a collection.
// In this case use of CollectionSerializer is perfectly appropriate.
(Collection::class.java.isAssignableFrom(declaredClass) ||
(actualClass != null && Collection::class.java.isAssignableFrom(actualClass))) &&
!EnumSet::class.java.isAssignableFrom(actualClass ?: declaredClass) -> {
val declaredTypeAmended = CollectionSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass)
serializersByType.computeIfAbsent(declaredTypeAmended) {
CollectionSerializer(declaredTypeAmended, this)
}
}
// Declared class may not be set to Map, but actual class could be a map.
// In this case use of MapSerializer is perfectly appropriate.
(Map::class.java.isAssignableFrom(declaredClass) ||
(actualClass != null && Map::class.java.isAssignableFrom(actualClass))) -> {
val declaredTypeAmended = MapSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass)
serializersByType.computeIfAbsent(declaredTypeAmended) {
makeMapSerializer(declaredTypeAmended)
}
}
Enum::class.java.isAssignableFrom(actualClass ?: declaredClass) -> {
logger.trace {
"class=[${actualClass?.simpleName} | $declaredClass] is an enumeration " +
"declaredType=${declaredType.typeName} " +
"isEnum=${declaredType::class.java.isEnum}"
}
serializersByType.computeIfAbsent(actualClass ?: declaredClass) {
whitelist.requireWhitelisted(actualType)
EnumSerializer(actualType, actualClass ?: declaredClass, this)
}
}
else -> {
makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType)
}
}
serializersByDescriptor.putIfAbsent(serializer.typeDescriptor, serializer)
// Always store the short-circuit too, for performance.
serializersByType.putIfAbsent(ourType, serializer)
return serializer
}
}
/**
* Lookup and manufacture a serializer for the given AMQP type descriptor, assuming we also have the necessary types
* contained in the [Schema].
*/
@Throws(NotSerializableException::class)
override fun get(typeDescriptor: Any, schema: SerializationSchemas): AMQPSerializer<Any> {
return serializersByDescriptor[typeDescriptor] ?: {
logger.trace("get Serializer descriptor=${typeDescriptor}")
processSchema(FactorySchemaAndDescriptor(schema, typeDescriptor))
serializersByDescriptor[typeDescriptor] ?: throw NotSerializableException(
"Could not find type matching descriptor $typeDescriptor.")
}()
}
/**
* Register a custom serializer for any type that cannot be serialized or deserialized by the default serializer
* that expects to find getters and a constructor with a parameter for each property.
*/
override fun register(customSerializer: CustomSerializer<out Any>) {
logger.trace("action=\"Registering custom serializer\", class=\"${customSerializer.type}\"")
if (!serializersByDescriptor.containsKey(customSerializer.typeDescriptor)) {
customSerializers += customSerializer
serializersByDescriptor[customSerializer.typeDescriptor] = customSerializer
for (additional in customSerializer.additionalSerializers) {
register(additional)
}
}
}
override fun registerExternal(customSerializer: CorDappCustomSerializer) {
logger.trace("action=\"Registering external serializer\", class=\"${customSerializer.type}\"")
if (!serializersByDescriptor.containsKey(customSerializer.typeDescriptor)) {
customSerializers += customSerializer
serializersByDescriptor[customSerializer.typeDescriptor] = customSerializer
}
}
/**
* Iterate over an AMQP schema, for each type ascertain whether it's on ClassPath of [classloader] and,
* if not, use the [ClassCarpenter] to generate a class to use in its place.
*/
private fun processSchema(schemaAndDescriptor: FactorySchemaAndDescriptor, sentinel: Boolean = false) {
val requiringCarpentry = schemaAndDescriptor.schemas.schema.types.mapNotNull { typeNotation ->
try {
getOrRegisterSerializer(schemaAndDescriptor, typeNotation)
return@mapNotNull null
} catch (e: ClassNotFoundException) {
if (sentinel) {
logger.error("typeNotation=${typeNotation.name} error=\"after Carpentry attempt failed to load\"")
throw e
}
logger.trace { "typeNotation=\"${typeNotation.name}\" action=\"carpentry required\"" }
return@mapNotNull typeNotation
}
}.toList()
if (requiringCarpentry.isEmpty()) return
runCarpentry(schemaAndDescriptor, CarpenterMetaSchema.buildWith(classloader, requiringCarpentry))
}
private fun getOrRegisterSerializer(schemaAndDescriptor: FactorySchemaAndDescriptor, typeNotation: TypeNotation) {
logger.trace { "descriptor=${schemaAndDescriptor.typeDescriptor}, typeNotation=${typeNotation.name}" }
val serialiser = processSchemaEntry(typeNotation)
// if we just successfully built a serializer for the type but the type fingerprint
// doesn't match that of the serialised object then we may be dealing with different
// instance of the class, and such we need to build an EvolutionSerializer
if (serialiser.typeDescriptor == typeNotation.descriptor.name) return
logger.trace { "typeNotation=${typeNotation.name} action=\"requires Evolution\"" }
evolutionSerializerProvider.getEvolutionSerializer(this, typeNotation, serialiser, schemaAndDescriptor.schemas)
}
private fun processSchemaEntry(typeNotation: TypeNotation) = when (typeNotation) {
// java.lang.Class (whether a class or interface)
is CompositeType -> {
logger.trace("typeNotation=${typeNotation.name} amqpType=CompositeType")
processCompositeType(typeNotation)
}
// Collection / Map, possibly with generics
is RestrictedType -> {
logger.trace("typeNotation=${typeNotation.name} amqpType=RestrictedType")
processRestrictedType(typeNotation)
}
}
// TODO: class loader logic, and compare the schema.
private fun processRestrictedType(typeNotation: RestrictedType) =
get(null, typeForName(typeNotation.name, classloader))
private fun processCompositeType(typeNotation: CompositeType): AMQPSerializer<Any> {
// TODO: class loader logic, and compare the schema.
val type = typeForName(typeNotation.name, classloader)
return get(type.asClass(), type)
}
private fun typeForName(name: String, classloader: ClassLoader): Type = when {
name.endsWith("[]") -> {
val elementType = typeForName(name.substring(0, name.lastIndex - 1), classloader)
if (elementType is ParameterizedType || elementType is GenericArrayType) {
DeserializedGenericArrayType(elementType)
} else if (elementType is Class<*>) {
java.lang.reflect.Array.newInstance(elementType, 0).javaClass
} else {
throw AMQPNoTypeNotSerializableException("Not able to deserialize array type: $name")
}
}
name.endsWith("[p]") -> // There is no need to handle the ByteArray case as that type is coercible automatically
// to the binary type and is thus handled by the main serializer and doesn't need a
// special case for a primitive array of bytes
when (name) {
"int[p]" -> IntArray::class.java
"char[p]" -> CharArray::class.java
"boolean[p]" -> BooleanArray::class.java
"float[p]" -> FloatArray::class.java
"double[p]" -> DoubleArray::class.java
"short[p]" -> ShortArray::class.java
"long[p]" -> LongArray::class.java
else -> throw AMQPNoTypeNotSerializableException("Not able to deserialize array type: $name")
}
else -> DeserializedParameterizedType.make(name, classloader)
}
@StubOutForDJVM
private fun runCarpentry(schemaAndDescriptor: FactorySchemaAndDescriptor, metaSchema: CarpenterMetaSchema) {
val mc = MetaCarpenter(metaSchema, classCarpenter)
try {
mc.build()
} catch (e: MetaCarpenterException) {
// preserve the actual message locally
loggerFor<SerializerFactory>().apply {
error("${e.message} [hint: enable trace debugging for the stack trace]")
trace("", e)
}
// prevent carpenter exceptions escaping into the world, convert things into a nice
// NotSerializableException for when this escapes over the wire
NotSerializableException(e.name)
}
processSchema(schemaAndDescriptor, true)
}
private fun makeClassSerializer(
clazz: Class<*>,
type: Type,
declaredType: Type
): AMQPSerializer<Any> = serializersByType.computeIfAbsent(type) {
logger.debug { "class=${clazz.simpleName}, type=$type is a composite type" }
if (clazz.isSynthetic) {
// Explicitly ban synthetic classes, we have no way of recreating them when deserializing. This also
// captures Lambda expressions and other anonymous functions
throw AMQPNotSerializableException(
type,
"Serializer does not support synthetic classes")
} else if (SerializerFactory.isPrimitive(clazz)) {
AMQPPrimitiveSerializer(clazz)
} else {
findCustomSerializer(clazz, declaredType) ?: run {
if (onlyCustomSerializers) {
throw AMQPNotSerializableException(type, "Only allowing custom serializers")
}
if (type.isArray()) {
// Don't need to check the whitelist since each element will come back through the whitelisting process.
if (clazz.componentType.isPrimitive) PrimArraySerializer.make(type, this)
else ArraySerializer.make(type, this)
} else {
val singleton = clazz.kotlinObjectInstance
if (singleton != null) {
whitelist.requireWhitelisted(clazz)
SingletonSerializer(clazz, singleton, this)
} else {
whitelist.requireWhitelisted(type)
ObjectSerializer(type, this)
}
}
}
}
}
override fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer<Any>? {
return customSerializersCache.computeIfAbsent(CustomSerializersCacheKey(clazz, declaredType), ::doFindCustomSerializer)
}
private fun doFindCustomSerializer(key: CustomSerializersCacheKey): AMQPSerializer<Any>? {
val (clazz, declaredType) = key
// e.g. Imagine if we provided a Map serializer this way, then it won't work if the declared type is
// AbstractMap, only Map. Otherwise it needs to inject additional schema for a RestrictedType source of the
// super type. Could be done, but do we need it?
for (customSerializer in customSerializers) {
if (customSerializer.isSerializerFor(clazz)) {
val declaredSuperClass = declaredType.asClass().superclass
return if (declaredSuperClass == null
|| !customSerializer.isSerializerFor(declaredSuperClass)
|| !customSerializer.revealSubclassesInSchema
) {
logger.debug("action=\"Using custom serializer\", class=${clazz.typeName}, " +
"declaredType=${declaredType.typeName}")
@Suppress("UNCHECKED_CAST")
customSerializer as? AMQPSerializer<Any>
} else {
// Make a subclass serializer for the subclass and return that...
CustomSerializer.SubClass(clazz, uncheckedCast(customSerializer))
}
}
}
return null
}
private fun makeMapSerializer(declaredType: ParameterizedType): AMQPSerializer<Any> {
val rawType = declaredType.rawType as Class<*>
rawType.checkSupportedMapType()
return MapSerializer(declaredType, this)
}
override fun registerByDescriptor(name: Symbol, serializerCreator: () -> AMQPSerializer<Any>): AMQPSerializer<Any> =
serializersByDescriptor.computeIfAbsent(name) { _ -> serializerCreator() }
companion object {
private val logger = contextLogger()
}
}
class ComposedSerializerFactory(
private val localSerializerFactory: LocalSerializerFactory,
private val remoteSerializerFactory: RemoteSerializerFactory,
private val customSerializerRegistry: CachingCustomSerializerRegistry
) : SerializerFactory,
LocalSerializerFactory by localSerializerFactory,
RemoteSerializerFactory by remoteSerializerFactory,
CustomSerializerRegistry by customSerializerRegistry

View File

@ -5,49 +5,126 @@ import net.corda.core.KeepForDJVM
import net.corda.core.serialization.ClassWhitelist
import net.corda.serialization.internal.carpenter.ClassCarpenter
import net.corda.serialization.internal.carpenter.ClassCarpenterImpl
import net.corda.serialization.internal.model.*
import java.io.NotSerializableException
@KeepForDJVM
object SerializerFactoryBuilder {
@JvmStatic
@JvmOverloads
fun build(
whitelist: ClassWhitelist,
classCarpenter: ClassCarpenter,
evolutionSerializerProvider: EvolutionSerializerProvider = DefaultEvolutionSerializerProvider,
fingerPrinterProvider: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter,
onlyCustomSerializers: Boolean = false): SerializerFactory {
fun build(whitelist: ClassWhitelist, classCarpenter: ClassCarpenter): SerializerFactory {
return makeFactory(
whitelist,
classCarpenter,
evolutionSerializerProvider,
fingerPrinterProvider,
onlyCustomSerializers)
DefaultDescriptorBasedSerializerRegistry(),
true,
null,
false,
false)
}
@JvmStatic
@DeleteForDJVM
fun build(
whitelist: ClassWhitelist,
classCarpenter: ClassCarpenter,
descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry =
DefaultDescriptorBasedSerializerRegistry(),
allowEvolution: Boolean = true,
overrideFingerPrinter: FingerPrinter? = null,
onlyCustomSerializers: Boolean = false,
mustPreserveDataWhenEvolving: Boolean = false): SerializerFactory {
return makeFactory(
whitelist,
classCarpenter,
descriptorBasedSerializerRegistry,
allowEvolution,
overrideFingerPrinter,
onlyCustomSerializers,
mustPreserveDataWhenEvolving)
}
@JvmStatic
@JvmOverloads
@DeleteForDJVM
fun build(
whitelist: ClassWhitelist,
carpenterClassLoader: ClassLoader,
lenientCarpenterEnabled: Boolean = false,
evolutionSerializerProvider: EvolutionSerializerProvider = DefaultEvolutionSerializerProvider,
fingerPrinterProvider: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter,
onlyCustomSerializers: Boolean = false): SerializerFactory {
descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry =
DefaultDescriptorBasedSerializerRegistry(),
allowEvolution: Boolean = true,
overrideFingerPrinter: FingerPrinter? = null,
onlyCustomSerializers: Boolean = false,
mustPreserveDataWhenEvolving: Boolean = false): SerializerFactory {
return makeFactory(
whitelist,
ClassCarpenterImpl(whitelist, carpenterClassLoader, lenientCarpenterEnabled),
evolutionSerializerProvider,
fingerPrinterProvider,
onlyCustomSerializers)
descriptorBasedSerializerRegistry,
allowEvolution,
overrideFingerPrinter,
onlyCustomSerializers,
mustPreserveDataWhenEvolving)
}
private fun makeFactory(whitelist: ClassWhitelist,
classCarpenter: ClassCarpenter,
evolutionSerializerProvider: EvolutionSerializerProvider,
fingerPrinterProvider: (SerializerFactory) -> FingerPrinter,
onlyCustomSerializers: Boolean) =
DefaultSerializerFactory(whitelist, classCarpenter, evolutionSerializerProvider, fingerPrinterProvider,
onlyCustomSerializers)
descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry,
allowEvolution: Boolean,
overrideFingerPrinter: FingerPrinter?,
onlyCustomSerializers: Boolean,
mustPreserveDataWhenEvolving: Boolean): SerializerFactory {
val customSerializerRegistry = CachingCustomSerializerRegistry(descriptorBasedSerializerRegistry)
val localTypeModel = ConfigurableLocalTypeModel(
WhitelistBasedTypeModelConfiguration(
whitelist,
customSerializerRegistry))
val fingerPrinter = overrideFingerPrinter ?:
TypeModellingFingerPrinter(customSerializerRegistry)
val localSerializerFactory = DefaultLocalSerializerFactory(
whitelist,
localTypeModel,
fingerPrinter,
classCarpenter.classloader,
descriptorBasedSerializerRegistry,
customSerializerRegistry,
onlyCustomSerializers)
val typeLoader = ClassCarpentingTypeLoader(
SchemaBuildingRemoteTypeCarpenter(classCarpenter),
classCarpenter.classloader)
val evolutionSerializerFactory = if (allowEvolution) DefaultEvolutionSerializerFactory(
localSerializerFactory,
classCarpenter.classloader,
mustPreserveDataWhenEvolving
) else NoEvolutionSerializerFactory
val remoteSerializerFactory = DefaultRemoteSerializerFactory(
evolutionSerializerFactory,
descriptorBasedSerializerRegistry,
AMQPRemoteTypeModel(),
localTypeModel,
typeLoader,
localSerializerFactory)
return ComposedSerializerFactory(localSerializerFactory, remoteSerializerFactory, customSerializerRegistry)
}
}
object NoEvolutionSerializerFactory : EvolutionSerializerFactory {
override fun getEvolutionSerializer(remoteTypeInformation: RemoteTypeInformation, localTypeInformation: LocalTypeInformation): AMQPSerializer<Any> {
throw NotSerializableException("""
Evolution not permitted.
Remote:
${remoteTypeInformation.prettyPrint(false)}
Local:
${localTypeInformation.prettyPrint(false)}
""")
}
}

View File

@ -1,6 +1,7 @@
package net.corda.serialization.internal.amqp
import net.corda.core.serialization.SerializationContext
import net.corda.serialization.internal.model.LocalTypeInformation
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type
@ -10,13 +11,12 @@ import java.lang.reflect.Type
* absolutely nothing, or null as a described type) when we have a singleton within the node that we just
* want converting back to that singleton instance on the receiving JVM.
*/
class SingletonSerializer(override val type: Class<*>, val singleton: Any, factory: SerializerFactory) : AMQPSerializer<Any> {
override val typeDescriptor = Symbol.valueOf(
"$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")!!
class SingletonSerializer(override val type: Class<*>, val singleton: Any, factory: LocalSerializerFactory) : AMQPSerializer<Any> {
override val typeDescriptor = factory.createDescriptor(type)
private val interfaces = interfacesForSerialization(type, factory)
private val interfaces = (factory.getTypeInformation(type) as LocalTypeInformation.Singleton).interfaces
private fun generateProvides(): List<String> = interfaces.map { it.typeName }
private fun generateProvides(): List<String> = interfaces.map { it.typeIdentifier.name }
internal val typeNotation: TypeNotation = RestrictedType(type.typeName, "Singleton", generateProvides(), "boolean", Descriptor(typeDescriptor), emptyList())

View File

@ -7,6 +7,7 @@ import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.trace
import net.corda.serialization.internal.NotSerializableDetailedException
import net.corda.serialization.internal.NotSerializableWithReasonException
import net.corda.serialization.internal.model.DefaultCacheProvider
import org.apache.qpid.proton.amqp.DescribedType
import org.apache.qpid.proton.codec.DescribedTypeConstructor
import java.io.NotSerializableException
@ -207,7 +208,8 @@ data class TransformsSchema(val types: Map<String, EnumMap<TransformTypes, Mutab
* @param sf the [SerializerFactory] building this transform set. Needed as each can define it's own
* class loader and this dictates which classes we can and cannot see
*/
fun get(name: String, sf: SerializerFactory) = sf.transformsCache.computeIfAbsent(name) {
fun get(name: String, sf: LocalSerializerFactory) =
sf.getOrBuildTransform(name) {
val transforms = EnumMap<TransformTypes, MutableList<Transform>>(TransformTypes::class.java)
try {
val clazz = sf.classloader.loadClass(name)
@ -244,7 +246,7 @@ data class TransformsSchema(val types: Map<String, EnumMap<TransformTypes, Mutab
private fun getAndAdd(
type: String,
sf: SerializerFactory,
sf: LocalSerializerFactory,
map: MutableMap<String, EnumMap<TransformTypes, MutableList<Transform>>>) {
try {
get(type, sf).apply {
@ -268,7 +270,7 @@ data class TransformsSchema(val types: Map<String, EnumMap<TransformTypes, Mutab
* @param schema should be a [Schema] generated for a serialised data structure
* @param sf should be provided by the same serialization context that generated the schema
*/
fun build(schema: Schema, sf: SerializerFactory) = TransformsSchema(
fun build(schema: Schema, sf: LocalSerializerFactory) = TransformsSchema(
mutableMapOf<String, EnumMap<TransformTypes, MutableList<Transform>>>().apply {
schema.types.forEach { type -> getAndAdd(type.name, sf, this) }
})

View File

@ -0,0 +1,73 @@
package net.corda.serialization.internal.amqp
import net.corda.serialization.internal.model.LocalPropertyInformation
import net.corda.serialization.internal.model.LocalTypeInformation
import net.corda.serialization.internal.model.TypeIdentifier
import org.apache.qpid.proton.amqp.Symbol
import java.io.NotSerializableException
object TypeNotationGenerator {
fun getTypeNotation(typeInformation: LocalTypeInformation, typeDescriptor: Symbol) = when(typeInformation) {
is LocalTypeInformation.AnInterface -> typeInformation.getTypeNotation(typeDescriptor)
is LocalTypeInformation.Composable -> typeInformation.getTypeNotation(typeDescriptor)
is LocalTypeInformation.Abstract -> typeInformation.getTypeNotation(typeDescriptor)
else -> throw NotSerializableException("Cannot generate type notation for $typeInformation")
}
private val LocalTypeInformation.amqpTypeName get() = AMQPTypeIdentifiers.nameForType(typeIdentifier)
private fun LocalTypeInformation.AnInterface.getTypeNotation(typeDescriptor: Symbol): CompositeType =
makeCompositeType(
(sequenceOf(this) + interfaces.asSequence()).toList(),
properties,
typeDescriptor)
private fun LocalTypeInformation.Composable.getTypeNotation(typeDescriptor: Symbol): CompositeType =
makeCompositeType(interfaces, properties, typeDescriptor)
private fun LocalTypeInformation.Abstract.getTypeNotation(typeDescriptor: Symbol): CompositeType =
makeCompositeType(interfaces, properties, typeDescriptor)
private fun LocalTypeInformation.makeCompositeType(
interfaces: List<LocalTypeInformation>,
properties: Map<String, LocalPropertyInformation>,
typeDescriptor: Symbol): CompositeType {
val provides = interfaces.map { it.amqpTypeName }
val fields = properties.map { (name, property) ->
property.getField(name)
}
return CompositeType(
amqpTypeName,
null,
provides,
Descriptor(typeDescriptor),
fields)
}
private fun LocalPropertyInformation.getField(name: String): Field {
val (typeName, requires) = when(type) {
is LocalTypeInformation.AnInterface,
is LocalTypeInformation.ACollection,
is LocalTypeInformation.AMap -> "*" to listOf(type.amqpTypeName)
else -> type.amqpTypeName to emptyList()
}
val defaultValue: String? = defaultValues[type.typeIdentifier]
return Field(name, typeName, requires, defaultValue, null, isMandatory, false)
}
private val defaultValues = sequenceOf(
Boolean::class to "false",
Byte::class to "0",
Int::class to "0",
Char::class to "&#0",
Short::class to "0",
Long::class to "0",
Float::class to "0",
Double::class to "0").associate { (type, value) ->
TypeIdentifier.forClass(type.javaPrimitiveType!!) to value
}
}

View File

@ -7,7 +7,6 @@ import java.lang.reflect.*
* Try and infer concrete types for any generics type variables for the actual class encountered,
* based on the declared type.
*/
// TODO: test GenericArrayType
fun inferTypeVariables(actualClass: Class<*>,
declaredClass: Class<*>,
declaredType: Type): Type? = when (declaredType) {
@ -17,10 +16,7 @@ fun inferTypeVariables(actualClass: Class<*>,
inferTypeVariables(actualClass.componentType, declaredComponent.asClass(), declaredComponent)?.asArray()
}
// Nothing to infer, otherwise we'd have ParameterizedType
is Class<*> -> actualClass
is TypeVariable<*> -> actualClass
is WildcardType -> actualClass
else -> throw UnsupportedOperationException("Cannot infer type variables for type $declaredType")
else -> actualClass
}
/**
@ -32,12 +28,6 @@ private fun inferTypeVariables(actualClass: Class<*>, declaredClass: Class<*>, d
return null
}
if (!declaredClass.isAssignableFrom(actualClass)) {
throw AMQPNotSerializableException(
declaredType,
"Found object of type $actualClass in a property expecting $declaredType")
}
if (actualClass.typeParameters.isEmpty()) {
return actualClass
}
@ -55,7 +45,7 @@ private fun inferTypeVariables(actualClass: Class<*>, declaredClass: Class<*>, d
TypeResolver().where(chainEntry, newResolved)
}
// The end type is a special case as it is a Class, so we need to fake up a ParameterizedType for it to get the TypeResolver to do anything.
val endType = DeserializedParameterizedType(actualClass, actualClass.typeParameters)
val endType = actualClass.asParameterizedType()
return resolver.resolveType(endType)
}

View File

@ -5,6 +5,7 @@ import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.trace
import net.corda.serialization.internal.amqp.AMQPNotSerializableException
import net.corda.serialization.internal.amqp.CustomSerializer
import net.corda.serialization.internal.amqp.LocalSerializerFactory
import net.corda.serialization.internal.amqp.SerializerFactory
import net.corda.serialization.internal.amqp.custom.ClassSerializer.ClassProxy
@ -12,7 +13,7 @@ import net.corda.serialization.internal.amqp.custom.ClassSerializer.ClassProxy
* A serializer for [Class] that uses [ClassProxy] proxy object to write out
*/
class ClassSerializer(
factory: SerializerFactory
factory: LocalSerializerFactory
) : CustomSerializer.Proxy<Class<*>, ClassSerializer.ClassProxy>(
Class::class.java,
ClassProxy::class.java,

View File

@ -20,7 +20,7 @@ object InputStreamSerializer : CustomSerializer.Implements<InputStream>(InputStr
type.toString(),
"",
listOf(type.toString()),
SerializerFactory.primitiveTypeName(ByteArray::class.java)!!,
AMQPTypeIdentifiers.primitiveTypeName(ByteArray::class.java),
descriptor,
emptyList())))

View File

@ -8,11 +8,10 @@ import net.corda.serialization.internal.checkUseCase
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type
import java.security.PrivateKey
import java.util.*
object PrivateKeySerializer : CustomSerializer.Implements<PrivateKey>(PrivateKey::class.java) {
override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), SerializerFactory.primitiveTypeName(ByteArray::class.java)!!, descriptor, emptyList())))
override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), AMQPTypeIdentifiers.primitiveTypeName(ByteArray::class.java), descriptor, emptyList())))
override fun writeDescribedObject(obj: PrivateKey, data: Data, type: Type, output: SerializationOutput,
context: SerializationContext

View File

@ -11,7 +11,7 @@ import java.security.PublicKey
* A serializer that writes out a public key in X.509 format.
*/
object PublicKeySerializer : CustomSerializer.Implements<PublicKey>(PublicKey::class.java) {
override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), SerializerFactory.primitiveTypeName(ByteArray::class.java)!!, descriptor, emptyList())))
override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), AMQPTypeIdentifiers.primitiveTypeName(ByteArray::class.java), descriptor, emptyList())))
override fun writeDescribedObject(obj: PublicKey, data: Data, type: Type, output: SerializationOutput,
context: SerializationContext

View File

@ -6,10 +6,13 @@ import net.corda.core.KeepForDJVM
import net.corda.core.serialization.SerializationFactory
import net.corda.core.utilities.contextLogger
import net.corda.serialization.internal.amqp.*
import net.corda.serialization.internal.model.LocalConstructorInformation
import net.corda.serialization.internal.model.LocalPropertyInformation
import net.corda.serialization.internal.model.LocalTypeInformation
import java.io.NotSerializableException
@KeepForDJVM
class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<Throwable, ThrowableSerializer.ThrowableProxy>(Throwable::class.java, ThrowableProxy::class.java, factory) {
class ThrowableSerializer(factory: LocalSerializerFactory) : CustomSerializer.Proxy<Throwable, ThrowableSerializer.ThrowableProxy>(Throwable::class.java, ThrowableProxy::class.java, factory) {
companion object {
private val logger = contextLogger()
@ -19,15 +22,23 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<T
override val additionalSerializers: Iterable<CustomSerializer<out Any>> = listOf(StackTraceElementSerializer(factory))
private val LocalTypeInformation.constructor: LocalConstructorInformation get() = when(this) {
is LocalTypeInformation.NonComposable -> constructor ?:
throw NotSerializableException("$this has no deserialization constructor")
is LocalTypeInformation.Composable -> constructor
is LocalTypeInformation.Opaque -> expand.constructor
else -> throw NotSerializableException("$this has no deserialization constructor")
}
override fun toProxy(obj: Throwable): ThrowableProxy {
val extraProperties: MutableMap<String, Any?> = LinkedHashMap()
val message = if (obj is CordaThrowable) {
// Try and find a constructor
try {
val constructor = constructorForDeserialization(obj.javaClass)
propertiesForSerializationFromConstructor(constructor, obj.javaClass, factory).forEach { property ->
extraProperties[property.serializer.name] = property.serializer.propertyReader.read(obj)
}
val typeInformation = factory.getTypeInformation(obj.javaClass)
extraProperties.putAll(typeInformation.propertiesOrEmptyMap.mapValues { (_, property) ->
PropertyReader.make(property).read(obj)
})
} catch (e: NotSerializableException) {
logger.warn("Unexpected exception", e)
}
@ -52,8 +63,13 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<T
// If it is CordaException or CordaRuntimeException, we can seek any constructor and then set the properties
// Otherwise we just make a CordaRuntimeException
if (CordaThrowable::class.java.isAssignableFrom(clazz) && Throwable::class.java.isAssignableFrom(clazz)) {
val constructor = constructorForDeserialization(clazz)
val throwable = constructor.callBy(constructor.parameters.map { it to proxy.additionalProperties[it.name] }.toMap())
val typeInformation = factory.getTypeInformation(clazz)
val constructor = typeInformation.constructor
val params = constructor.parameters.map { parameter ->
proxy.additionalProperties[parameter.name] ?:
proxy.additionalProperties[parameter.name.capitalize()]
}
val throwable = constructor.observedMethod.call(*params.toTypedArray())
(throwable as CordaThrowable).apply {
if (this.javaClass.name != proxy.exceptionClass) this.originalExceptionClassName = proxy.exceptionClass
this.setMessage(proxy.message)
@ -85,7 +101,7 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<T
val additionalProperties: Map<String, Any?>)
}
class StackTraceElementSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<StackTraceElement, StackTraceElementSerializer.StackTraceElementProxy>(StackTraceElement::class.java, StackTraceElementProxy::class.java, factory) {
class StackTraceElementSerializer(factory: LocalSerializerFactory) : CustomSerializer.Proxy<StackTraceElement, StackTraceElementSerializer.StackTraceElementProxy>(StackTraceElement::class.java, StackTraceElementProxy::class.java, factory) {
override fun toProxy(obj: StackTraceElement): StackTraceElementProxy = StackTraceElementProxy(obj.className, obj.methodName, obj.fileName, obj.lineNumber)
override fun fromProxy(proxy: StackTraceElementProxy): StackTraceElement = StackTraceElement(proxy.declaringClass, proxy.methodName, proxy.fileName, proxy.lineNumber)

View File

@ -12,7 +12,7 @@ object X509CRLSerializer : CustomSerializer.Implements<X509CRL>(X509CRL::class.j
type.toString(),
"",
listOf(type.toString()),
SerializerFactory.primitiveTypeName(ByteArray::class.java)!!,
AMQPTypeIdentifiers.primitiveTypeName(ByteArray::class.java),
descriptor,
emptyList()
)))

View File

@ -12,7 +12,7 @@ object X509CertificateSerializer : CustomSerializer.Implements<X509Certificate>(
type.toString(),
"",
listOf(type.toString()),
SerializerFactory.primitiveTypeName(ByteArray::class.java)!!,
AMQPTypeIdentifiers.primitiveTypeName(ByteArray::class.java),
descriptor,
emptyList()
)))

View File

@ -1,154 +0,0 @@
@file:JvmName("AMQPSchemaExtensions")
package net.corda.serialization.internal.carpenter
import net.corda.core.DeleteForDJVM
import net.corda.core.serialization.SerializationContext
import net.corda.serialization.internal.amqp.CompositeType
import net.corda.serialization.internal.amqp.RestrictedType
import net.corda.serialization.internal.amqp.Field as AMQPField
import net.corda.serialization.internal.amqp.Schema as AMQPSchema
@DeleteForDJVM
fun AMQPSchema.carpenterSchema(classloader: ClassLoader): CarpenterMetaSchema {
val rtn = CarpenterMetaSchema.newInstance()
types.filterIsInstance<CompositeType>().forEach {
it.carpenterSchema(classloader, carpenterSchemas = rtn)
}
return rtn
}
/**
* if we can load the class then we MUST know about all of it's composite elements
*/
private fun CompositeType.validatePropertyTypes(classloader: ClassLoader) {
fields.forEach {
if (!it.validateType(classloader)) throw UncarpentableException(name, it.name, it.type)
}
}
fun AMQPField.typeAsString() = if (type == "*") requires[0] else type
/**
* based upon this AMQP schema either
* a) add the corresponding carpenter schema to the [carpenterSchemas] param
* b) add the class to the dependency tree in [carpenterSchemas] if it cannot be instantiated
* at this time
*
* @param classloader the class loader provided by the [SerializationContext]
* @param carpenterSchemas structure that holds the dependency tree and list of classes that
* need constructing
* @param force by default a schema is not added to [carpenterSchemas] if it already exists
* on the class path. For testing purposes schema generation can be forced
*/
@DeleteForDJVM
fun CompositeType.carpenterSchema(classloader: ClassLoader,
carpenterSchemas: CarpenterMetaSchema,
force: Boolean = false) {
if (classloader.exists(name)) {
validatePropertyTypes(classloader)
if (!force) return
}
val providesList = mutableListOf<Class<*>>()
var isInterface = false
var isCreatable = true
provides.forEach {
if (name == it) {
isInterface = true
return@forEach
}
try {
providesList.add(classloader.loadClass(it.stripGenerics()))
} catch (e: ClassNotFoundException) {
carpenterSchemas.addDepPair(this, name, it)
isCreatable = false
}
}
val m: MutableMap<String, Field> = mutableMapOf()
fields.forEach {
try {
m[it.name] = FieldFactory.newInstance(it.mandatory, it.name, it.getTypeAsClass(classloader))
} catch (e: ClassNotFoundException) {
carpenterSchemas.addDepPair(this, name, it.typeAsString())
isCreatable = false
}
}
if (isCreatable) {
carpenterSchemas.carpenterSchemas.add(CarpenterSchemaFactory.newInstance(
name = name,
fields = m,
interfaces = providesList,
isInterface = isInterface))
}
}
// This is potentially problematic as we're assuming the only type of restriction we will be
// carpenting for, an enum, but actually trying to split out RestrictedType into something
// more polymorphic is hard. Additionally, to conform to AMQP we're really serialising
// this as a list so...
@DeleteForDJVM
fun RestrictedType.carpenterSchema(carpenterSchemas: CarpenterMetaSchema) {
val m: MutableMap<String, Field> = mutableMapOf()
choices.forEach { m[it.name] = EnumField() }
carpenterSchemas.carpenterSchemas.add(EnumSchema(name = name, fields = m))
}
// map a pair of (typename, mandatory) to the corresponding class type
// where the mandatory AMQP flag maps to the types nullability
val typeStrToType: Map<Pair<String, Boolean>, Class<out Any?>> = mapOf(
Pair("int", true) to Int::class.javaPrimitiveType!!,
Pair("int", false) to Integer::class.javaObjectType,
Pair("short", true) to Short::class.javaPrimitiveType!!,
Pair("short", false) to Short::class.javaObjectType,
Pair("long", true) to Long::class.javaPrimitiveType!!,
Pair("long", false) to Long::class.javaObjectType,
Pair("char", true) to Char::class.javaPrimitiveType!!,
Pair("char", false) to java.lang.Character::class.java,
Pair("boolean", true) to Boolean::class.javaPrimitiveType!!,
Pair("boolean", false) to Boolean::class.javaObjectType,
Pair("double", true) to Double::class.javaPrimitiveType!!,
Pair("double", false) to Double::class.javaObjectType,
Pair("float", true) to Float::class.javaPrimitiveType!!,
Pair("float", false) to Float::class.javaObjectType,
Pair("byte", true) to Byte::class.javaPrimitiveType!!,
Pair("byte", false) to Byte::class.javaObjectType
)
fun String.stripGenerics(): String = if (this.endsWith('>')) {
this.substring(0, this.indexOf('<'))
} else this
fun AMQPField.getTypeAsClass(classloader: ClassLoader) = (typeStrToType[Pair(type, mandatory)] ?: when (type) {
"string" -> String::class.java
"binary" -> ByteArray::class.java
"*" -> if (requires.isEmpty()) Any::class.java else {
classloader.loadClass(requires[0].stripGenerics())
}
else -> classloader.loadClass(type.stripGenerics())
})!!
fun AMQPField.validateType(classloader: ClassLoader) =
when (type) {
"byte", "int", "string", "short", "long", "char", "boolean", "double", "float" -> true
"*" -> classloader.exists(requires[0])
else -> classloader.exists(type)
}
private fun ClassLoader.exists(clazz: String) = run {
try {
this.loadClass(clazz); true
} catch (e: ClassNotFoundException) {
false
}
}

View File

@ -23,14 +23,3 @@ class NullablePrimitiveException(val name: String, val field: Class<out Any>) :
class UncarpentableException(name: String, field: String, type: String) :
ClassCarpenterException("Class $name is loadable yet contains field $field of unknown type $type")
/**
* A meta exception used by the [MetaCarpenter] to wrap any exceptions generated during the build
* process and associate those with the current schema being processed. This makes for cleaner external
* error hand
*
* @property name The name of the schema, and thus the class being created, when the error was occured
* @property e The [ClassCarpenterException] this is wrapping
*/
class MetaCarpenterException(val name: String, val e: ClassCarpenterException) : CordaRuntimeException(
"Whilst processing class '$name' - ${e.message}")

View File

@ -1,127 +0,0 @@
package net.corda.serialization.internal.carpenter
import net.corda.core.DeleteForDJVM
import net.corda.core.KeepForDJVM
import net.corda.core.StubOutForDJVM
import net.corda.serialization.internal.amqp.CompositeType
import net.corda.serialization.internal.amqp.RestrictedType
import net.corda.serialization.internal.amqp.TypeNotation
/**
* Generated from an AMQP schema this class represents the classes unknown to the deserializer and that thusly
* require carpenting up in bytecode form. This is a multi step process as carpenting one object may be dependent
* upon the creation of others, this information is tracked in the dependency tree represented by
* [dependencies] and [dependsOn]. Creatable classes are stored in [carpenterSchemas].
*
* The state of this class after initial generation is expected to mutate as classes are built by the carpenter
* enabling the resolution of dependencies and thus new carpenter schemas added whilst those already
* carpented schemas are removed.
*
* @property carpenterSchemas The list of carpentable classes
* @property dependencies Maps a class to a list of classes that depend on it being built first
* @property dependsOn Maps a class to a list of classes it depends on being built before it
*
* Once a class is constructed we can quickly check for resolution by first looking at all of its dependents in the
* [dependencies] map. This will give us a list of classes that depended on that class being carpented. We can then
* in turn look up all of those classes in the [dependsOn] list, remove their dependency on the newly created class,
* and if that list is reduced to zero know we can now generate a [Schema] for them and carpent them up
*/
@KeepForDJVM
data class CarpenterMetaSchema(
val carpenterSchemas: MutableList<Schema>,
val dependencies: MutableMap<String, Pair<TypeNotation, MutableList<String>>>,
val dependsOn: MutableMap<String, MutableList<String>>) {
companion object CarpenterSchemaConstructor {
fun buildWith(classLoader: ClassLoader, types: List<TypeNotation>) =
newInstance().apply {
types.forEach { buildFor(it, classLoader) }
}
fun newInstance(): CarpenterMetaSchema {
return CarpenterMetaSchema(mutableListOf(), mutableMapOf(), mutableMapOf())
}
}
fun addDepPair(type: TypeNotation, dependant: String, dependee: String) {
dependsOn.computeIfAbsent(dependee, { mutableListOf() }).add(dependant)
dependencies.computeIfAbsent(dependant, { Pair(type, mutableListOf()) }).second.add(dependee)
}
val size
get() = carpenterSchemas.size
fun isEmpty() = carpenterSchemas.isEmpty()
fun isNotEmpty() = carpenterSchemas.isNotEmpty()
// We could make this an abstract method on TypeNotation but that
// would mean the amqp package being "more" infected with carpenter
// specific bits.
@StubOutForDJVM
fun buildFor(target: TypeNotation, cl: ClassLoader): Unit = when (target) {
is RestrictedType -> target.carpenterSchema(this)
is CompositeType -> target.carpenterSchema(cl, this, false)
}
}
/**
* Take a dependency tree of [CarpenterMetaSchema] and reduce it to zero by carpenting those classes that
* require it. As classes are carpented check for dependency resolution, if now free generate a [Schema] for
* that class and add it to the list of classes ([CarpenterMetaSchema.carpenterSchemas]) that require
* carpenting
*
* @property cc a reference to the actual class carpenter we're using to constuct classes
* @property objects a list of carpented classes loaded into the carpenters class loader
*/
@DeleteForDJVM
abstract class MetaCarpenterBase(val schemas: CarpenterMetaSchema, val cc: ClassCarpenter) {
val objects = mutableMapOf<String, Class<*>>()
fun step(newObject: Schema) {
objects[newObject.name] = cc.build(newObject)
// go over the list of everything that had a dependency on the newly
// carpented class existing and remove it from their dependency list, If that
// list is now empty we have no impediment to carpenting that class up
schemas.dependsOn.remove(newObject.name)?.forEach { dependent ->
require(newObject.name in schemas.dependencies[dependent]!!.second)
schemas.dependencies[dependent]?.second?.remove(newObject.name)
// we're out of blockers so we can now create the type
if (schemas.dependencies[dependent]?.second?.isEmpty() == true) {
(schemas.dependencies.remove(dependent)?.first as CompositeType).carpenterSchema(
classloader = cc.classloader,
carpenterSchemas = schemas)
}
}
}
abstract fun build()
val classloader: ClassLoader
get() = cc.classloader
}
@DeleteForDJVM
class MetaCarpenter(schemas: CarpenterMetaSchema, cc: ClassCarpenter) : MetaCarpenterBase(schemas, cc) {
override fun build() {
while (schemas.carpenterSchemas.isNotEmpty()) {
val newObject = schemas.carpenterSchemas.removeAt(0)
try {
step(newObject)
} catch (e: ClassCarpenterException) {
throw MetaCarpenterException(newObject.name, e)
}
}
}
}
@DeleteForDJVM
class TestMetaCarpenter(schemas: CarpenterMetaSchema, cc: ClassCarpenter) : MetaCarpenterBase(schemas, cc) {
override fun build() {
if (schemas.carpenterSchemas.isEmpty()) return
step(schemas.carpenterSchemas.removeAt(0))
}
}

View File

@ -74,7 +74,7 @@ fun EnumMap<SchemaFlags, Boolean>.simpleFieldAccess(): Boolean {
class ClassSchema(
name: String,
fields: Map<String, Field>,
superclass: Schema? = null,
superclass: Schema? = null, // always null for now, but retained because non-null superclass is supported by carpenter.
interfaces: List<Class<*>> = emptyList()
) : Schema(name, fields, superclass, interfaces, { newName, field -> field.name = newName }) {
override fun generateFields(cw: ClassWriter) {
@ -128,11 +128,10 @@ object CarpenterSchemaFactory {
fun newInstance(
name: String,
fields: Map<String, Field>,
superclass: Schema? = null,
interfaces: List<Class<*>> = emptyList(),
isInterface: Boolean = false
): Schema =
if (isInterface) InterfaceSchema(name, fields, superclass, interfaces)
else ClassSchema(name, fields, superclass, interfaces)
if (isInterface) InterfaceSchema(name, fields, null, interfaces)
else ClassSchema(name, fields, null, interfaces)
}

Some files were not shown because too many files have changed in this diff Show More