mirror of
https://github.com/corda/corda.git
synced 2025-03-15 08:41:04 +00:00
Merge pull request #1589 from corda/dominic-merge-22-11-2018
OS merge Dominic 2018-11-22
This commit is contained in:
commit
8eb89a3bbc
@ -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
358
.idea/compiler.xml
generated
@ -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>
|
@ -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)'
|
||||
|
@ -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" }
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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`." }
|
||||
}
|
||||
|
@ -63,4 +63,4 @@ fun validateTimeWindow(currentTime: Instant, timeWindow: TimeWindow?): NotaryErr
|
||||
return if (timeWindow != null && currentTime !in timeWindow) {
|
||||
NotaryError.TimeWindowInvalid(currentTime, timeWindow)
|
||||
} else null
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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>
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
64
docker/build.gradle
Normal 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"]
|
||||
}
|
19
docker/src/bash/example-generate-testnet.sh
Executable file
19
docker/src/bash/example-generate-testnet.sh
Executable 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
|
27
docker/src/bash/example-join-generic-cz.sh
Executable file
27
docker/src/bash/example-join-generic-cz.sh
Executable 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
|
124
docker/src/bash/generate-config.sh
Executable file
124
docker/src/bash/generate-config.sh
Executable 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
10
docker/src/bash/run-corda.sh
Executable 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
|
39
docker/src/config/starting-node.conf
Normal file
39
docker/src/config/starting-node.conf
Normal 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}
|
70
docker/src/docker/Dockerfile
Normal file
70
docker/src/docker/Dockerfile
Normal 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"]
|
84
docker/src/main/kotlin/net.corda.core/ConfigExporter.kt
Normal file
84
docker/src/main/kotlin/net.corda.core/ConfigExporter.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
});
|
||||
|
||||
|
@ -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``.
|
@ -15,6 +15,7 @@ CorDapps
|
||||
upgrade-notes
|
||||
upgrading-cordapps
|
||||
secure-coding-guidelines
|
||||
flow-overriding
|
||||
corda-api
|
||||
flow-cookbook
|
||||
cheat-sheet
|
||||
|
@ -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.
|
||||
|
71
docs/source/contributing-philosophy.rst
Normal file
71
docs/source/contributing-philosophy.rst
Normal 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
|
@ -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
|
||||
--------
|
||||
|
@ -6,6 +6,7 @@ Nodes
|
||||
|
||||
node-structure
|
||||
generating-a-node
|
||||
docker-image
|
||||
running-a-node
|
||||
deploying-a-node
|
||||
corda-configuration-file
|
||||
|
163
docs/source/docker-image.rst
Normal file
163
docs/source/docker-image.rst
Normal 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
|
||||
|
BIN
docs/source/resources/state-to-external-id.png
Normal file
BIN
docs/source/resources/state-to-external-id.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 98 KiB |
@ -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:
|
||||
|
||||
|
@ -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
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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>()
|
||||
|
@ -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()) {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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()))
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)))
|
||||
}
|
||||
}
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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() }
|
||||
}
|
@ -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)
|
||||
}
|
||||
) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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())
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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())
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
@ -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!!
|
||||
}
|
||||
}
|
||||
}
|
@ -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 -> "�"
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
@ -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)}
|
||||
""")
|
||||
}
|
||||
}
|
@ -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())
|
||||
|
||||
|
@ -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) }
|
||||
})
|
||||
|
@ -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 "�",
|
||||
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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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())))
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
)))
|
||||
|
@ -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()
|
||||
)))
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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}")
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user