From e3ccf6a79f1e183c3784e0e73e1823a1301561ab Mon Sep 17 00:00:00 2001 From: Stefano Franz Date: Tue, 20 Nov 2018 13:38:44 +0000 Subject: [PATCH 01/17] Add Docker image as output of build (#4223) --- docker/build.gradle | 64 +++++++ docker/src/bash/example-generate-testnet.sh | 19 ++ docker/src/bash/example-join-generic-cz.sh | 27 +++ docker/src/bash/generate-config.sh | 124 +++++++++++++ docker/src/bash/run-corda.sh | 10 ++ docker/src/config/starting-node.conf | 39 +++++ docker/src/docker/Dockerfile | 70 ++++++++ .../kotlin/net.corda.core/ConfigExporter.kt | 84 +++++++++ docs/source/building-a-cordapp-index.rst | 1 + docs/source/corda-nodes-index.rst | 1 + docs/source/docker-image.rst | 163 ++++++++++++++++++ settings.gradle | 1 + 12 files changed, 603 insertions(+) create mode 100644 docker/build.gradle create mode 100755 docker/src/bash/example-generate-testnet.sh create mode 100755 docker/src/bash/example-join-generic-cz.sh create mode 100755 docker/src/bash/generate-config.sh create mode 100755 docker/src/bash/run-corda.sh create mode 100644 docker/src/config/starting-node.conf create mode 100644 docker/src/docker/Dockerfile create mode 100644 docker/src/main/kotlin/net.corda.core/ConfigExporter.kt create mode 100644 docs/source/docker-image.rst diff --git a/docker/build.gradle b/docker/build.gradle new file mode 100644 index 0000000000..0bb9d2afe1 --- /dev/null +++ b/docker/build.gradle @@ -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"] +} \ No newline at end of file diff --git a/docker/src/bash/example-generate-testnet.sh b/docker/src/bash/example-generate-testnet.sh new file mode 100755 index 0000000000..069fdd8ff8 --- /dev/null +++ b/docker/src/bash/example-generate-testnet.sh @@ -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 \ No newline at end of file diff --git a/docker/src/bash/example-join-generic-cz.sh b/docker/src/bash/example-join-generic-cz.sh new file mode 100755 index 0000000000..3d42b788b3 --- /dev/null +++ b/docker/src/bash/example-join-generic-cz.sh @@ -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 \ No newline at end of file diff --git a/docker/src/bash/generate-config.sh b/docker/src/bash/generate-config.sh new file mode 100755 index 0000000000..dc71bf4b71 --- /dev/null +++ b/docker/src/bash/generate-config.sh @@ -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 + diff --git a/docker/src/bash/run-corda.sh b/docker/src/bash/run-corda.sh new file mode 100755 index 0000000000..c47545d9c5 --- /dev/null +++ b/docker/src/bash/run-corda.sh @@ -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 \ No newline at end of file diff --git a/docker/src/config/starting-node.conf b/docker/src/config/starting-node.conf new file mode 100644 index 0000000000..b78e38b53d --- /dev/null +++ b/docker/src/config/starting-node.conf @@ -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} diff --git a/docker/src/docker/Dockerfile b/docker/src/docker/Dockerfile new file mode 100644 index 0000000000..5340c68638 --- /dev/null +++ b/docker/src/docker/Dockerfile @@ -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"] diff --git a/docker/src/main/kotlin/net.corda.core/ConfigExporter.kt b/docker/src/main/kotlin/net.corda.core/ConfigExporter.kt new file mode 100644 index 0000000000..05b38531bc --- /dev/null +++ b/docker/src/main/kotlin/net.corda.core/ConfigExporter.kt @@ -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 { + 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) { + 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") + } + } +} \ No newline at end of file diff --git a/docs/source/building-a-cordapp-index.rst b/docs/source/building-a-cordapp-index.rst index 1586941e93..45f0c9d90e 100644 --- a/docs/source/building-a-cordapp-index.rst +++ b/docs/source/building-a-cordapp-index.rst @@ -15,6 +15,7 @@ CorDapps upgrade-notes upgrading-cordapps secure-coding-guidelines + flow-overriding corda-api flow-cookbook cheat-sheet diff --git a/docs/source/corda-nodes-index.rst b/docs/source/corda-nodes-index.rst index 23fce7574e..a1803d14ab 100644 --- a/docs/source/corda-nodes-index.rst +++ b/docs/source/corda-nodes-index.rst @@ -6,6 +6,7 @@ Nodes node-structure generating-a-node + docker-image running-a-node deploying-a-node corda-configuration-file diff --git a/docs/source/docker-image.rst b/docs/source/docker-image.rst new file mode 100644 index 0000000000..4b866a376f --- /dev/null +++ b/docs/source/docker-image.rst @@ -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 `__ + +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 + diff --git a/settings.gradle b/settings.gradle index 26a9cfde4d..dece86ebff 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,6 +16,7 @@ include 'client:jfx' include 'client:mock' include 'client:rpc' include 'djvm' +include 'docker' include 'djvm:cli' include 'webserver' include 'webserver:webcapsule' From 7e9ee197895fa404c456004dd5d663ad064a49bd Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Tue, 20 Nov 2018 13:52:10 +0000 Subject: [PATCH 02/17] [CORDA-2218] CryptoService signingCertificateStore alias match (#4227) --- .../net/corda/node/internal/AbstractNode.kt | 35 ++++++++++++++----- .../services/config/NodeConfigurationImpl.kt | 3 ++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 3414e53b48..e613b6dfb4 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -869,11 +869,14 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private fun obtainIdentity(): Pair { 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(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 { 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(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, diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt index d2c4745e1c..12a2b0fc4d 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt @@ -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 } From c2364f44318777552f2152e2c565673af6818d56 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Tue, 20 Nov 2018 14:01:39 +0000 Subject: [PATCH 03/17] [CORDA-2211]: Added error for older bash versions when attempting to install shell extensions. (#4263) --- build.gradle | 1 + tools/cliutils/build.gradle | 1 + .../net/corda/cliutils/CordaCliWrapper.kt | 4 ++ .../cliutils/InstallShellExtensionsParser.kt | 52 +++++++++++++++++-- 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index f24c896583..6ddd3f6ffd 100644 --- a/build.gradle +++ b/build.gradle @@ -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)' diff --git a/tools/cliutils/build.gradle b/tools/cliutils/build.gradle index 0f341432b7..d505751347 100644 --- a/tools/cliutils/build.gradle +++ b/tools/cliutils/build.gradle @@ -9,6 +9,7 @@ dependencies { compile project(":core") compile "info.picocli:picocli:$picocli_version" + compile "commons-io:commons-io:$commons_io_version" compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version" compile "org.slf4j:slf4j-api:$slf4j_version" compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt index 3d15a585f3..0da2f9e44e 100644 --- a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt @@ -184,6 +184,10 @@ abstract class CordaCliWrapper(alias: String, description: String) : CliWrapperB } fun printHelp() = cmd.usage(System.out) + + fun printlnErr(message: String) = System.err.println(message) + + fun printlnWarn(message: String) = System.err.println(message) } /** diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/InstallShellExtensionsParser.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/InstallShellExtensionsParser.kt index a9be0cbae0..17f5bbe876 100644 --- a/tools/cliutils/src/main/kotlin/net/corda/cliutils/InstallShellExtensionsParser.kt +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/InstallShellExtensionsParser.kt @@ -1,6 +1,8 @@ package net.corda.cliutils import net.corda.core.internal.* +import org.apache.commons.io.IOUtils +import org.apache.commons.lang.SystemUtils import picocli.CommandLine import picocli.CommandLine.Command import java.nio.file.Path @@ -9,6 +11,10 @@ import java.nio.file.StandardCopyOption import java.util.* private class ShellExtensionsGenerator(val parent: CordaCliWrapper) { + private companion object { + private const val minSupportedBashVersion = 4 + } + private class SettingsFile(val filePath: Path) { private val lines: MutableList by lazy { getFileLines() } var fileModified: Boolean = false @@ -80,10 +86,22 @@ private class ShellExtensionsGenerator(val parent: CordaCliWrapper) { autoCompleteFile.writeText(builder.toString()) } - fun installShellExtensions() { + fun installShellExtensions(): Int { // Get jar location and generate alias command val command = "alias ${parent.alias}='java -jar \"${jarLocation.toStringWithDeWindowsfication()}\"'" - generateAutoCompleteFile(parent.alias) + var generateAutoCompleteFile = true + if (SystemUtils.IS_OS_UNIX && installedShell() == ShellType.BASH) { + val semanticParts = declaredBashVersion().split(".") + semanticParts.firstOrNull()?.toIntOrNull()?.let { major -> + if (major < minSupportedBashVersion) { + parent.printlnWarn("Cannot install shell extension for bash major version earlier than $minSupportedBashVersion. Please upgrade your bash version. Aliases should still work.") + generateAutoCompleteFile = false + } + } + } + if (generateAutoCompleteFile) { + generateAutoCompleteFile(parent.alias) + } // Get bash settings file val bashSettingsFile = SettingsFile(userHome / ".bashrc") @@ -101,9 +119,34 @@ private class ShellExtensionsGenerator(val parent: CordaCliWrapper) { zshSettingsFile.addIfNotExists(completionFileCommand) zshSettingsFile.updateAndBackupIfNecessary() - println("Installation complete, ${parent.alias} is available in bash with autocompletion. ") + if (generateAutoCompleteFile) { + println("Installation complete, ${parent.alias} is available in bash with autocompletion.") + } else { + println("Installation complete, ${parent.alias} is available in bash, but autocompletion was not installed because of an old version of bash.") + } println("Type `${parent.alias} ` from the commandline.") println("Restart bash for this to take effect, or run `. ~/.bashrc` in bash or `. ~/.zshrc` in zsh to re-initialise your shell now") + return ExitCodes.SUCCESS + } + + private fun declaredBashVersion(): String = execCommand("bash -c 'echo \$BASH_VERSION'") + + private fun installedShell(): ShellType { + val path = execCommand("bash -c 'echo \$SHELL'") + return when { + path.endsWith("/zsh") -> ShellType.ZSH + path.endsWith("/bash") -> ShellType.BASH + else -> ShellType.OTHER + } + } + + private enum class ShellType { + ZSH, BASH, OTHER + } + + private fun execCommand(command: String): String { + val process = ProcessBuilder(command) + return IOUtils.toString(process.start().inputStream, Charsets.UTF_8) } fun checkForAutoCompleteUpdate() { @@ -127,8 +170,7 @@ private class ShellExtensionsGenerator(val parent: CordaCliWrapper) { class InstallShellExtensionsParser(private val cliWrapper: CordaCliWrapper) : CliWrapperBase("install-shell-extensions", "Install alias and autocompletion for bash and zsh") { private val generator = ShellExtensionsGenerator(cliWrapper) override fun runProgram(): Int { - generator.installShellExtensions() - return ExitCodes.SUCCESS + return generator.installShellExtensions() } fun updateShellExtensions() = generator.checkForAutoCompleteUpdate() From bda07561f74c550a7bae5ca22d6e9bdf55a6b20f Mon Sep 17 00:00:00 2001 From: Tudor Malene Date: Tue, 20 Nov 2018 14:19:32 +0000 Subject: [PATCH 04/17] ENT-2675 Improve error message (#4258) * ENT-2675 Improve error message * ENT-2675 Improve error message --- .../net/corda/core/transactions/TransactionBuilder.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 19cab5b63a..77c1453c44 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -212,7 +212,7 @@ open class TransactionBuilder @JvmOverloads constructor( val refStateContractAttachments: List = 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, services: ServicesForResolution): AttachmentId { + private fun selectAttachmentThatSatisfiesConstraints(isReference: Boolean, contractClassName: String, states: List>, 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 From 035656bd89c7b834daec67b0683ccd2a6cfd25ac Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Tue, 20 Nov 2018 17:15:48 +0000 Subject: [PATCH 05/17] ENT-2653: Introducing CommonCliConstants (#4265) --- node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt | 3 ++- .../test/kotlin/net/corda/node/internal/NodeStartupTest.kt | 5 ++--- .../kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt b/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt index 37293ddf51..5a9a0788ae 100644 --- a/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt +++ b/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt @@ -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() } } @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() diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt index 976c1fec79..e49e36d6a2 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt @@ -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) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 87c36dd2a6..98333f36d4 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -6,6 +6,7 @@ import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigValueFactory import net.corda.client.rpc.internal.createCordaRPCClientWithSslAndClassLoader +import net.corda.cliutils.CommonCliConstants.BASE_DIR import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.firstOf import net.corda.core.flows.FlowLogic @@ -889,7 +890,7 @@ class DriverDSLImpl( writeConfig(handle.baseDirectory, "web-server.conf", handle.toWebServerConfig()) return ProcessUtilities.startJavaProcess( className = className, // cannot directly get class for this, so just use string - arguments = listOf("--base-directory", handle.baseDirectory.toString()), + arguments = listOf(BASE_DIR, handle.baseDirectory.toString()), jdwpPort = debugPort, extraJvmArguments = listOf("-Dname=node-${handle.p2pAddress}-webserver") + inheritFromParentProcess().map { "-D${it.first}=${it.second}" }, From 3cc1c47cf06c4417a8403ec65a89bded1e2af0d9 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Tue, 20 Nov 2018 18:17:11 +0000 Subject: [PATCH 06/17] Combines all contributing info in one place. Clean-up. (#4266) --- .idea/compiler.xml | 358 ------------------ .../contributing-flow-state-machines.rst | 4 +- docs/source/contributing-philosophy.rst | 71 ++++ 3 files changed, 73 insertions(+), 360 deletions(-) delete mode 100644 .idea/compiler.xml create mode 100644 docs/source/contributing-philosophy.rst diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index d9e19c1d1d..0000000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,358 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/source/contributing-flow-state-machines.rst b/docs/source/contributing-flow-state-machines.rst index 5f88aace8a..bc81305365 100644 --- a/docs/source/contributing-flow-state-machines.rst +++ b/docs/source/contributing-flow-state-machines.rst @@ -4,8 +4,8 @@ -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. diff --git a/docs/source/contributing-philosophy.rst b/docs/source/contributing-philosophy.rst new file mode 100644 index 0000000000..7d04a93efd --- /dev/null +++ b/docs/source/contributing-philosophy.rst @@ -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 `_, 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 `_ 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 `_ for discussion regarding Corda's design and roadmap +* The `GitHub issues board `_ for reporting platform bugs and potential enhancements +* The `Stack Overflow corda tag `_ for specific technical questions + +Project Leadership and Maintainers +---------------------------------- +The leader of this project is currently `Mike Hearn `_, 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 `_. + +.. _community-maintainers: + +Community maintainers +^^^^^^^^^^^^^^^^^^^^^ +Current community maintainers: + +* `Joel Dudley `_ - Contact via the `Corda Slack team `_, 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 `_. + +Transparency and Conflict Policy +-------------------------------- +The project is supported and maintained by the `R3 Alliance `_, 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 `_ 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 From bc48f8112e78b95e21290b5c7604d6ed16afb515 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Wed, 21 Nov 2018 12:13:04 +0000 Subject: [PATCH 07/17] Address util tests: remove invalid test case (#4269) Local host address can also be public --- .../test/kotlin/net/corda/node/utilities/AddressUtilsTests.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/node/src/test/kotlin/net/corda/node/utilities/AddressUtilsTests.kt b/node/src/test/kotlin/net/corda/node/utilities/AddressUtilsTests.kt index 26ce708b4f..e009ca3d5c 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/AddressUtilsTests.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/AddressUtilsTests.kt @@ -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") } } -} \ No newline at end of file +} From 683e641a613ff2cd4de83dbcfe16dd7ea57905b3 Mon Sep 17 00:00:00 2001 From: Oliver Knowles Date: Wed, 21 Nov 2018 12:51:21 +0000 Subject: [PATCH 08/17] Fix node configuration doc for network parameter auto-acceptance settings (#4268) --- docs/source/corda-configuration-file.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 19487a73c8..80892ae2d7 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -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 -------- From 1c59d321eee668929767975eb12414d994ae4695 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Wed, 21 Nov 2018 12:54:58 +0000 Subject: [PATCH 09/17] Update testnet-explorer-corda.rst (#4270) --- docs/source/testnet-explorer.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/testnet-explorer.rst b/docs/source/testnet-explorer.rst index a709907aff..7c95c55faa 100644 --- a/docs/source/testnet-explorer.rst +++ b/docs/source/testnet-explorer.rst @@ -74,7 +74,7 @@ couple of resources. .. code:: bash - echo "issuableCurrencies : [ USD ]" > /opt/corda/cordapps/config/corda-finance--corda.conf + echo "issuableCurrencies = [ USD ]" > /opt/corda/cordapps/config/corda-finance--corda.conf #. Restart the Corda node: From a9dfb7828c8e0752678e3a2037d00bcc443c5035 Mon Sep 17 00:00:00 2001 From: Dominic Fox <40790090+distributedleetravis@users.noreply.github.com> Date: Wed, 21 Nov 2018 15:56:56 +0000 Subject: [PATCH 10/17] Fix unit test failure due to Windows end-of-line characters (#4271) --- .../internal/model/ClassCarpentingTypeLoaderTests.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/model/ClassCarpentingTypeLoaderTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/model/ClassCarpentingTypeLoaderTests.kt index a2a2cc280d..1e79409733 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/model/ClassCarpentingTypeLoaderTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/model/ClassCarpentingTypeLoaderTests.kt @@ -53,6 +53,8 @@ class ClassCarpentingTypeLoaderTests { val person = personType.make("Arthur Putey", 42, address, listOf(previousAddress)) val personJson = ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(person) + .replace("\r\n", "\n") + assertEquals(""" { "name" : "Arthur Putey", From 08ae58529a5c982bddc8d7c0e118eb1551d1446e Mon Sep 17 00:00:00 2001 From: Thomas Schroeter Date: Wed, 21 Nov 2018 16:50:32 +0000 Subject: [PATCH 11/17] [CORDA-2243] Notary client should sign over NotarisationRequest serialised with object referencing turned off (#4264) * Sign later to make sure the notary service constructs the same serialized bytes * Add non validating notary tests * Revert "Sign later to make sure the notary service constructs the same serialized bytes" This reverts commit cefba5c350622df44227e89feacb12a7f12e1f7e. * Dereference SecureHashes before creating the notarisation request signature * Update comment * Address comments * Undo accidental renaming --- .../kotlin/net/corda/core/flows/NotaryFlow.kt | 15 +- .../corda/core/internal/notary/NotaryUtils.kt | 2 +- .../NonValidatingNotaryServiceTests.kt | 310 ++++++++++++++++++ .../ValidatingNotaryServiceTests.kt | 14 +- .../corda/testing/contracts/DummyContract.kt | 5 +- 5 files changed, 339 insertions(+), 7 deletions(-) create mode 100644 node/src/test/kotlin/net/corda/node/services/transactions/NonValidatingNotaryServiceTests.kt diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt index 630ed496ef..86b58d12ad 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt @@ -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 { 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) + } } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryUtils.kt b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryUtils.kt index f4b80e4427..6eb6228404 100644 --- a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryUtils.kt @@ -63,4 +63,4 @@ fun validateTimeWindow(currentTime: Instant, timeWindow: TimeWindow?): NotaryErr return if (timeWindow != null && currentTime !in timeWindow) { NotaryError.TimeWindowInvalid(currentTime, timeWindow) } else null -} \ No newline at end of file +} diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NonValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NonValidatingNotaryServiceTests.kt new file mode 100644 index 0000000000..a8603d9a0b --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NonValidatingNotaryServiceTests.kt @@ -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() 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> { + 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> { + 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))) + } +} diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index a9b27b5b51..4fa58bbd3a 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -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> { + 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))) + } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt index fa38493fce..eaffc9d491 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt @@ -60,8 +60,9 @@ data class DummyContract(val blank: Any? = null) : Contract { fun generateInitial(magicNumber: Int, notary: Party, owner: PartyAndReference, vararg otherOwners: PartyAndReference): TransactionBuilder { val owners = listOf(owner) + otherOwners return if (owners.size == 1) { - val state = SingleOwnerState(magicNumber, owners.first().party) - TransactionBuilder(notary).withItems(StateAndContract(state, PROGRAM_ID), Command(Commands.Create(), owners.first().party.owningKey)) + val items = arrayOf(StateAndContract(SingleOwnerState(magicNumber, owners.first().party), PROGRAM_ID), Command(Commands.Create(), owners.first().party.owningKey), + StateAndContract(SingleOwnerState(magicNumber, owners.first().party), PROGRAM_ID), Command(Commands.Create(), owners.first().party.owningKey)) + TransactionBuilder(notary).withItems(*items) } else { val state = MultiOwnerState(magicNumber, owners.map { it.party }) TransactionBuilder(notary).withItems(StateAndContract(state, PROGRAM_ID), Command(Commands.Create(), owners.map { it.party.owningKey })) From d3b849736221a74bb5ae51da53ff7066f5b61691 Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Wed, 21 Nov 2018 18:36:30 +0000 Subject: [PATCH 12/17] CORDA-2150 testing infrastructure for non downgrade contract version rule - TestDSL creates Jar with manifest file (#4274) This is preliminary addition to test "Implementation-Version" entry from META-INF/MANIFEST.MF in TestDSL.kt. MockCordappProvider was creating JAR without manifest file. Added the manifest file with the obligatory (by JAR spec) attribute "Manifest-Version", other attributes can be added. --- .../src/main/kotlin/net/corda/testing/dsl/TestDSL.kt | 4 ++++ .../corda/testing/dsl/TransactionDSLInterpreter.kt | 11 ++++++++++- .../net/corda/testing/internal/InternalTestUtils.kt | 8 ++++++-- .../corda/testing/internal/MockCordappProvider.kt | 12 +++++++----- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index c98a026365..77a243c8dc 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -155,6 +155,10 @@ data class TestTransactionDSLInterpreter private constructor( attachment((services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage, attachmentId, signers)) } + override fun _attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List, jarManifestAttributes: Map){ + attachment((services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage, attachmentId, signers, jarManifestAttributes)) + } + } data class TestLedgerDSLInterpreter private constructor( diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt index e47a8037e5..e7ccdafb86 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt @@ -97,6 +97,15 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup { * @param attachmentId The attachment */ fun _attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List) + + /** + * Attaches an attachment containing the named contract to the transaction. + * @param contractClassName The contract class to attach. + * @param attachmentId The attachment. + * @param signers The signers. + * @param jarManifestAttributes The JAR manifest file attributes. + */ + fun _attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List, jarManifestAttributes: Map) } /** @@ -203,7 +212,7 @@ class TransactionDSL(interpreter: T, private */ fun attachment(contractClassName: ContractClassName) = _attachment(contractClassName) - fun attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List) = _attachment(contractClassName, attachmentId, signers) + fun attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List, jarManifestAttributes: Map = emptyMap()) = _attachment(contractClassName, attachmentId, signers, jarManifestAttributes) fun attachment(contractClassName: ContractClassName, attachmentId: AttachmentId) = _attachment(contractClassName, attachmentId, emptyList()) fun attachments(vararg contractClassNames: ContractClassName) = contractClassNames.forEach { attachment(it) } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt index 52ba72d528..a66e956bf8 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt @@ -13,6 +13,7 @@ import net.corda.core.internal.NamedCacheFactory import net.corda.core.node.NodeInfo import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.loggerFor +import net.corda.node.internal.cordapp.set import net.corda.node.internal.createCordaPersistence import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.internal.startHikariPool @@ -41,6 +42,7 @@ import java.util.* import java.util.jar.JarOutputStream import java.util.zip.ZipEntry import javax.security.auth.x500.X500Principal +import java.util.jar.Manifest @Suppress("unused") inline fun T.kryoSpecific(reason: String, function: () -> Unit) = if (!AMQP_ENABLED) { @@ -181,9 +183,11 @@ fun configureDatabase(hikariProperties: Properties, /** * Convenience method for creating a fake attachment containing a file with some content. */ -fun fakeAttachment(filePath: String, content: String): ByteArray { +fun fakeAttachment(filePath: String, content: String, manifestAttributes: Map = emptyMap()): ByteArray { val bs = ByteArrayOutputStream() - JarOutputStream(bs).use { js -> + val manifest = Manifest() + manifestAttributes.forEach{ manifest[it.key] = it.value} //adding manually instead of putAll, as it requires typed keys, not strings + JarOutputStream(bs, manifest).use { js -> js.putNextEntry(ZipEntry(filePath)) js.writer().apply { append(content); flush() } js.closeEntry() diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt index 877a53f934..63af2c3106 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt @@ -12,7 +12,7 @@ import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.testing.services.MockAttachmentStorage import java.nio.file.Paths import java.security.PublicKey -import java.util.* +import java.util.jar.Attributes class MockCordappProvider( cordappLoader: CordappLoader, @@ -22,7 +22,7 @@ class MockCordappProvider( private val cordappRegistry = mutableListOf>() - fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage, contractHash: AttachmentId? = null, signers: List = emptyList()): AttachmentId { + fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage, contractHash: AttachmentId? = null, signers: List = emptyList(), jarManifestAttributes: Map = emptyMap()): AttachmentId { val cordapp = CordappImpl( contractClassNames = listOf(contractClassName), initiatedFlows = emptyList(), @@ -37,8 +37,10 @@ class MockCordappProvider( info = CordappImpl.Info.UNKNOWN, allFlows = emptyList(), jarHash = SecureHash.allOnesHash) + val jarManifestAttributesWithObligatoryElement = jarManifestAttributes.toMutableMap() + jarManifestAttributesWithObligatoryElement.putIfAbsent(Attributes.Name.MANIFEST_VERSION.toString(), "1.0") if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) && it.second == contractHash }) { - cordappRegistry.add(Pair(cordapp, findOrImportAttachment(listOf(contractClassName), fakeAttachmentCached(contractClassName), attachments, contractHash, signers))) + cordappRegistry.add(Pair(cordapp, findOrImportAttachment(listOf(contractClassName), fakeAttachmentCached(contractClassName, jarManifestAttributesWithObligatoryElement), attachments, contractHash, signers))) } return cordappRegistry.findLast { contractClassName in it.first.contractClassNames }?.second!! } @@ -58,7 +60,7 @@ class MockCordappProvider( } private val attachmentsCache = mutableMapOf() - private fun fakeAttachmentCached(contractClass: String): ByteArray = attachmentsCache.computeIfAbsent(contractClass) { - fakeAttachment(contractClass, contractClass) + private fun fakeAttachmentCached(contractClass: String, manifestAttributes: Map = emptyMap()): ByteArray = attachmentsCache.computeIfAbsent(contractClass + manifestAttributes.toSortedMap()) { + fakeAttachment(contractClass, contractClass, manifestAttributes) } } From 0a6c3822e5d6ffbb17a7ab58a1102f2a6de30aa0 Mon Sep 17 00:00:00 2001 From: Tudor Malene Date: Wed, 21 Nov 2018 20:41:56 +0000 Subject: [PATCH 13/17] ENT-2763 - Change packageOwnership type (#4273) * ENT-2763 - Change packageOwnership type * ENT-2763 - Address code review comment. * ENT-2673 Address code review comments. * ENT-2673 Address code review comments. * ENT-2673 Fix test * ENT-2673 Address code review comments. --- .../net/corda/core/internal/InternalUtils.kt | 13 +++- .../net/corda/core/node/NetworkParameters.kt | 63 +++++++------------ .../PackageOwnershipVerificationTests.kt | 6 +- .../internal/network/NetworkBootstrapper.kt | 7 +-- .../network/NetworkBootstrapperTest.kt | 11 ++-- .../node/internal/NetworkParametersTest.kt | 27 ++++---- .../kotlin/net/corda/bootstrapper/Main.kt | 23 +++---- .../bootstrapper/PackageOwnerParsingTest.kt | 11 ++-- 8 files changed, 71 insertions(+), 90 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index 1a0a1d5143..56ccac1646 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -516,4 +516,15 @@ fun createSimpleCache(maxSize: Int, onEject: (MutableMap.MutableEntry MutableMap.toSynchronised(): MutableMap = Collections.synchronizedMap(this) \ No newline at end of file +fun MutableMap.toSynchronised(): MutableMap = 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`." } +} diff --git a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt index 9a71d84f90..d43311b07f 100644 --- a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt +++ b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt @@ -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>, val eventHorizon: Duration, - @AutoAcceptable val packageOwnership: Map + @AutoAcceptable val packageOwnership: Map ) { // 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) = packages.all { currentPackage -> + packages.none { otherPackage -> otherPackage != currentPackage && otherPackage.startsWith("${currentPackage}.") } + } + + private fun KProperty1.isAutoAcceptable(): Boolean { + return this.findAnnotation() != 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): 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) = packages.all { currentPackage -> - packages.none { otherPackage -> otherPackage != currentPackage && otherPackage.name.startsWith("${currentPackage.name}.") } -} - -private fun KProperty1.isAutoAcceptable(): Boolean { - return this.findAnnotation() != null -} \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/contracts/PackageOwnershipVerificationTests.kt b/core/src/test/kotlin/net/corda/core/contracts/PackageOwnershipVerificationTests.kt index ecbb8f8374..f2da9f77ea 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/PackageOwnershipVerificationTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/PackageOwnershipVerificationTests.kt @@ -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 diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index 36c567107c..a8dea65b33 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -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 = emptyMap()) { + fun bootstrap(directory: Path, copyCordapps: Boolean, minimumPlatformVersion: Int, packageOwnership: Map = 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 = emptyMap() + packageOwnership: Map = emptyMap() ) { directory.createDirectories() println("Bootstrapping local test network in $directory") @@ -385,7 +384,7 @@ internal constructor(private val initSerEnv: Boolean, existingNetParams: NetworkParameters?, nodeDirs: List, minimumPlatformVersion: Int, - packageOwnership: Map + packageOwnership: Map ): NetworkParameters { // TODO Add config for maxMessageSize and maxTransactionSize val netParams = if (existingNetParams != null) { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt index 0c2345db05..cd691b53f4 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt @@ -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 = emptyMap()) { + private fun bootstrap(copyCordapps: Boolean = true, packageOwnership : Map = emptyMap()) { providedCordaJar = (rootDir / "corda.jar").let { if (it.exists()) it.readAll() else null } bootstrapper.bootstrap(rootDir, copyCordapps, PLATFORM_VERSION, packageOwnership) } @@ -363,7 +362,7 @@ class NetworkBootstrapperTest { } } - private fun assertContainsPackageOwner(nodeDirName: String, packageOwners: Map) { + private fun assertContainsPackageOwner(nodeDirName: String, packageOwners: Map) { val networkParams = (rootDir / nodeDirName).networkParameters assertThat(networkParams.packageOwnership).isEqualTo(packageOwners) } diff --git a/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt b/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt index 4be3c41682..6546ab887c 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt @@ -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())) diff --git a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt index ed21e8cb26..0ec24440fd 100644 --- a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt +++ b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt @@ -3,7 +3,7 @@ package net.corda.bootstrapper import net.corda.cliutils.CordaCliWrapper import net.corda.cliutils.start import net.corda.core.internal.PLATFORM_VERSION -import net.corda.core.node.JavaPackageName +import net.corda.core.internal.requirePackageValid import net.corda.nodeapi.internal.crypto.loadKeyStore import net.corda.nodeapi.internal.network.NetworkBootstrapper import picocli.CommandLine @@ -47,13 +47,12 @@ class NetworkBootstrapperRunner : CordaCliWrapper("bootstrapper", "Bootstrap a l var registerPackageOwnership: List = mutableListOf() @Option(names = ["--unregister-package-owner"], - converter = [JavaPackageNameConverter::class], description = [ "Unregister owner of Java package namespace in the network-parameters.", "Format: [java-package-namespace]", " `java-package-namespace` is case insensitive and cannot be a sub-package of an existing registered namespace" ]) - var unregisterPackageOwnership: List = mutableListOf() + var unregisterPackageOwnership: List = mutableListOf() override fun runProgram(): Int { NetworkBootstrapper().bootstrap(dir.toAbsolutePath().normalize(), @@ -67,10 +66,10 @@ class NetworkBootstrapperRunner : CordaCliWrapper("bootstrapper", "Bootstrap a l } -data class PackageOwner(val javaPackageName: JavaPackageName, val publicKey: PublicKey) +data class PackageOwner(val javaPackageName: String, val publicKey: PublicKey) /** - * Converter from String to PackageOwner (JavaPackageName and PublicKey) + * Converter from String to PackageOwner (String and PublicKey) */ class PackageOwnerConverter : CommandLine.ITypeConverter { override fun convert(packageOwner: String): PackageOwner { @@ -78,8 +77,11 @@ class PackageOwnerConverter : CommandLine.ITypeConverter { val packageOwnerSpec = packageOwner.split(";") if (packageOwnerSpec.size < 4) throw IllegalArgumentException("Package owner must specify 4 elements separated by semi-colon: 'java-package-namespace;keyStorePath;keyStorePassword;alias'") + // java package name validation - val javaPackageName = JavaPackageName(packageOwnerSpec[0]) + val javaPackageName = packageOwnerSpec[0] + requirePackageValid(javaPackageName) + // cater for passwords that include the argument delimiter field val keyStorePassword = if (packageOwnerSpec.size > 4) @@ -105,12 +107,3 @@ class PackageOwnerConverter : CommandLine.ITypeConverter { else throw IllegalArgumentException("Must specify package owner argument: 'java-package-namespace;keyStorePath;keyStorePassword;alias'") } } - -/** - * Converter from String to JavaPackageName. - */ -class JavaPackageNameConverter : CommandLine.ITypeConverter { - override fun convert(packageName: String): JavaPackageName { - return JavaPackageName(packageName) - } -} diff --git a/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/PackageOwnerParsingTest.kt b/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/PackageOwnerParsingTest.kt index cbc9b53cd4..b0df16c391 100644 --- a/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/PackageOwnerParsingTest.kt +++ b/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/PackageOwnerParsingTest.kt @@ -2,7 +2,6 @@ package net.corda.bootstrapper import net.corda.core.internal.deleteRecursively import net.corda.core.internal.div -import net.corda.core.node.JavaPackageName import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.CHARLIE_NAME @@ -55,7 +54,7 @@ class PackageOwnerParsingTest { val aliceKeyStorePath = dirAlice / "_teststore" val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;$ALICE_PASS;$ALICE") commandLine.parse(*args) - assertThat(networkBootstrapper.registerPackageOwnership[0].javaPackageName).isEqualTo(JavaPackageName("com.example.stuff")) + assertThat(networkBootstrapper.registerPackageOwnership[0].javaPackageName).isEqualTo("com.example.stuff") } @Test @@ -141,7 +140,7 @@ class PackageOwnerParsingTest { "net.something4;$aliceKeyStorePath5;\"\"passw;rd\"\";${ALICE}5") packageOwnerSpecs.forEachIndexed { i, packageOwnerSpec -> commandLine.parse(*arrayOf("--register-package-owner", packageOwnerSpec)) - assertThat(networkBootstrapper.registerPackageOwnership[0].javaPackageName).isEqualTo(JavaPackageName("net.something$i")) + assertThat(networkBootstrapper.registerPackageOwnership[0].javaPackageName).isEqualTo("net.something$i") } } @@ -149,7 +148,7 @@ class PackageOwnerParsingTest { fun `parse unregister request with single mapping`() { val args = arrayOf("--unregister-package-owner", "com.example.stuff") commandLine.parse(*args) - assertThat(networkBootstrapper.unregisterPackageOwnership).contains(JavaPackageName("com.example.stuff")) + assertThat(networkBootstrapper.unregisterPackageOwnership).contains("com.example.stuff") } @Test @@ -158,8 +157,8 @@ class PackageOwnerParsingTest { val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;$ALICE_PASS;$ALICE", "--unregister-package-owner", "com.example.stuff2") commandLine.parse(*args) - assertThat(networkBootstrapper.registerPackageOwnership.map { it.javaPackageName }).contains(JavaPackageName("com.example.stuff")) - assertThat(networkBootstrapper.unregisterPackageOwnership).contains(JavaPackageName("com.example.stuff2")) + assertThat(networkBootstrapper.registerPackageOwnership.map { it.javaPackageName }).contains("com.example.stuff") + assertThat(networkBootstrapper.unregisterPackageOwnership).contains("com.example.stuff2") } } From 4cd77dca3a2ea3117fe0277992476012113cd502 Mon Sep 17 00:00:00 2001 From: James Brown <33660060+jamesbr3@users.noreply.github.com> Date: Thu, 22 Nov 2018 10:49:18 +0000 Subject: [PATCH 14/17] CORDA-2199 NetworkParameters certificate role (#4278) --- .../kotlin/net/corda/core/internal/CertRole.kt | 5 ++++- .../nodeapi/internal/crypto/X509Utilities.kt | 6 ++++++ .../internal/network/NetworkBootstrapper.kt | 4 ++-- .../nodeapi/internal/network/NetworkMap.kt | 18 +++++++++++++++--- .../network/NetworkBootstrapperTest.kt | 2 +- .../node/internal/NetworkParametersReader.kt | 9 ++++----- .../node/services/network/NetworkMapUpdater.kt | 4 ++-- .../services/network/NetworkMapUpdaterTest.kt | 9 +++------ .../network/NetworkParametersReaderTest.kt | 7 ++----- 9 files changed, 39 insertions(+), 25 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/internal/CertRole.kt b/core/src/main/kotlin/net/corda/core/internal/CertRole.kt index f2946d3c72..4a7d94832a 100644 --- a/core/src/main/kotlin/net/corda/core/internal/CertRole.kt +++ b/core/src/main/kotlin/net/corda/core/internal/CertRole.kt @@ -46,7 +46,10 @@ enum class CertRole(val validParents: NonEmptySet, 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) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt index e78e63716f..5fbb9fcb66 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt @@ -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 ) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index a8dea65b33..11f04dc97b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -357,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 " + @@ -367,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" } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt index ae164aad99..776de34159 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt @@ -54,9 +54,9 @@ data class ParametersUpdate( val updateDeadline: Instant ) -/** Verify that a Network Map certificate path and its [CertRole] is correct. */ -fun SignedDataWithCert.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 SignedDataWithCert.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 SignedDataWithCert.verifiedNetworkMapCert(rootCert: X509Certifi X509Utilities.validateCertificateChain(rootCert, path) return verified() } + +/** Verify that a Network Map certificate path and its [CertRole] is correct. */ +fun SignedDataWithCert.verifiedNetworkMapCert(rootCert: X509Certificate): T { + return verifiedCertWithRole(rootCert, CertRole.NETWORK_MAP) +} + +/** Verify that a Network Parameters certificate path and its [CertRole] is correct. */ +fun SignedDataWithCert.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) +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt index cd691b53f4..ac5e5b7104 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt @@ -321,7 +321,7 @@ class NetworkBootstrapperTest { } private val Path.networkParameters: NetworkParameters get() { - return (this / NETWORK_PARAMS_FILE_NAME).readObject().verifiedNetworkMapCert(DEV_ROOT_CA.certificate) + return (this / NETWORK_PARAMS_FILE_NAME).readObject().verifiedNetworkParametersCert(DEV_ROOT_CA.certificate) } private val Path.nodeInfoFile: Path get() { diff --git a/node/src/main/kotlin/net/corda/node/internal/NetworkParametersReader.kt b/node/src/main/kotlin/net/corda/node/internal/NetworkParametersReader.kt index 98d318f40c..5acfb7e237 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NetworkParametersReader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NetworkParametersReader.kt @@ -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 } diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt index 66355758c7..0c80be1589 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt @@ -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. diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt index b6451f3217..57c6ce8e57 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt @@ -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() - 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() - 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)) } diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt index 08fa5c3a85..4378ddc314 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt @@ -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() - .verifiedNetworkMapCert(DEV_ROOT_CA.certificate) + .verifiedNetworkParametersCert(DEV_ROOT_CA.certificate) assertEquals(server.networkParameters, parametersFromFile) } From d91b07907f425c86a4f7c214c320637781d79b54 Mon Sep 17 00:00:00 2001 From: Dominic Fox <40790090+distributedleetravis@users.noreply.github.com> Date: Thu, 22 Nov 2018 11:44:40 +0000 Subject: [PATCH 15/17] CORDA-2099 serialisation rewrite (#4257) * Type model first draft * Introduce TypeIdentifier * Attempting to retrofit fingerprinter with type model * Complete retrofitting typemodel to fingerprinter * Ensure component types are resolved correctly * Fixes and tests * Move resolveAgainst to TypeIdentifier * Remote type modelling and reflection * Convert TypeIdentifiers back into types * Translate AMQP type strings to type identifiers * Start replacing DeserializedParameterizedType * Start roundtripping types through AMQP serialization * Comments on type modelling fingerprinter * kdocs and interface reorganisation * Lots and lots of kdocs, bugfix for cyclic references * Separate SerializerFactory construction from concrete implementation * Fewer build methods * Method naming that doesn't fatally confuse determinisation * Extract SerializerFactory interface * Reset to master's version of compiler.xml * Un-ignore flickering test * Enums don't have superclasses * Break out custom serializer registry * Refactor to separate remote and local serializer factories * Shrink interfaces * Further interface narrowing * Fingerprinting local type model * LocalSerializerFactory uses LocalTypeInformation * Resolve wildcards to their upper bounds * Actually cache custom serializers * Fix various bugs * Remove print statements * There are no cycles in type identifiers * Drive class carpentry from RemoteTypeInformation * Refactor and comment * Comments * Comments and pretty-printer extraction * Format long methods with braces * Serialise composable types using LocalTypeInformation * Warnings if a type is non-composable * Rename lookup -> findOrBuild * Evolution serialisation (no enums yet) * Eliminate old ObjectSerializer and evolver * Enum evolution * Opacity claims types less greedily * Fix type notation and type erasure bug * Clean up unused code paths * Delete unused codepaths * Move whitelist based type model configuration to own file * Move opaque type list * Make all evolution serialisers in one go when schema received * Minor tweaks * Commenting and tidying * Comments * Rebase against master * Make flag for controlling evolution behaviour visible * propertiesOrEmptyMap * Restore error messages * Test for CORDA-4107 * PR fixes * Patch cycles in remote type information after creation * Fix line breaks in unit test on Windows * This time for sure * EvolutionSerializerFactoryTests * Fix some pretty-printing issues, and a carpenter bug * PR fixes * Clarify evolution constructor ordering * Remote TODO comment, which has been moved to a JIRA story --- .../client/jackson/internal/CordaModule.kt | 17 +- .../corda/finance/compat/CompatibilityTest.kt | 60 ++- .../internal/amqp/AMQPSerializerFactories.kt | 4 +- .../internal/amqp/AMQPPrimitiveSerializer.kt | 2 +- .../internal/amqp/AMQPRemoteTypeModel.kt | 25 +- .../internal/amqp/AMQPTypeIdentifiers.kt | 6 +- .../internal/amqp/ArraySerializer.kt | 32 +- .../internal/amqp/CollectionSerializer.kt | 80 ++-- .../amqp/ComposableTypePropertySerializer.kt | 270 +++++++++++ .../internal/amqp/CorDappCustomSerializer.kt | 11 +- .../internal/amqp/CustomSerializer.kt | 26 +- .../amqp/DescriptorBasedSerializerRegistry.kt | 2 +- .../internal/amqp/DeserializationInput.kt | 5 +- .../amqp/DeserializedGenericArrayType.kt | 18 - .../amqp/DeserializedParameterizedType.kt | 174 ------- .../internal/amqp/EnumEvolutionSerializer.kt | 91 +--- .../internal/amqp/EnumSerializer.kt | 9 +- .../internal/amqp/EvolutionSerializer.kt | 312 ------------ .../amqp/EvolutionSerializerFactory.kt | 170 +++++++ .../internal/amqp/FingerPrinter.kt | 202 -------- .../internal/amqp/LocalSerializerFactory.kt | 226 +++++++++ .../internal/amqp/MapSerializer.kt | 58 ++- .../internal/amqp/ObjectBuilder.kt | 112 +++++ .../internal/amqp/ObjectSerializer.kt | 297 ++++++------ .../internal/amqp/PropertySerializer.kt | 146 ------ .../internal/amqp/PropertySerializers.kt | 243 ---------- .../internal/amqp/RemoteSerializerFactory.kt | 141 ++++++ .../internal/amqp/SerializationHelper.kt | 266 +---------- .../internal/amqp/SerializationOutput.kt | 17 +- .../internal/amqp/SerializerFactory.kt | 447 +----------------- .../internal/amqp/SerializerFactoryBuilder.kt | 123 ++++- .../internal/amqp/SingletonSerializer.kt | 10 +- .../internal/amqp/TransformsSchema.kt | 8 +- .../internal/amqp/TypeNotationGenerator.kt | 73 +++ .../internal/amqp/TypeParameterUtils.kt | 14 +- .../internal/amqp/custom/ClassSerializer.kt | 3 +- .../amqp/custom/InputStreamSerializer.kt | 2 +- .../amqp/custom/PrivateKeySerializer.kt | 3 +- .../amqp/custom/PublicKeySerializer.kt | 2 +- .../amqp/custom/ThrowableSerializer.kt | 32 +- .../internal/amqp/custom/X509CRLSerializer.kt | 2 +- .../amqp/custom/X509CertificateSerializer.kt | 2 +- .../carpenter/AMQPSchemaExtensions.kt | 154 ------ .../internal/carpenter/Exceptions.kt | 11 - .../internal/carpenter/MetaCarpenter.kt | 127 ----- .../internal/carpenter/Schema.kt | 7 +- .../model/CarpentryDependencyGraph.kt | 3 +- .../internal/model/LocalTypeInformation.kt | 65 ++- .../model/LocalTypeInformationBuilder.kt | 22 +- .../internal/model/RemoteTypeCarpenter.kt | 14 +- .../internal/model/RemoteTypeInformation.kt | 14 +- .../model/TypeModellingFingerPrinter.kt | 233 +++++++++ .../amqp/JavaPrivatePropertyTests.java | 33 +- ...aCalculatedValuesToClassCarpenterTest.java | 45 +- .../internal/ListsSerializationTest.kt | 6 +- .../AbstractAMQPSerializationSchemeTest.kt | 3 +- .../internal/amqp/CorDappSerializerTests.kt | 13 +- .../DeserializeNeedingCarpentryOfEnumsTest.kt | 14 +- ...erializeNeedingCarpentrySimpleTypesTest.kt | 5 +- .../amqp/DeserializeSimpleTypesTests.kt | 34 +- .../DeserializedParameterizedTypeTests.kt | 105 ---- .../internal/amqp/EnumEvolvabilityTests.kt | 17 - .../amqp/EvolutionSerializerFactoryTests.kt | 55 +++ .../EvolutionSerializerProviderTesting.kt | 22 - .../internal/amqp/EvolvabilityTests.kt | 11 + .../internal/amqp/FingerPrinterTesting.kt | 26 +- .../internal/amqp/GenericsTests.kt | 21 - .../internal/amqp/PrivatePropertyTests.kt | 79 +--- .../internal/amqp/SerializationOutputTests.kt | 52 +- .../amqp/SerializationPropertyOrdering.kt | 28 +- .../internal/amqp/SerializationSchemaTests.kt | 85 +--- ...ticInitialisationOfSerializedObjectTest.kt | 4 +- .../internal/amqp/testutils/AMQPTestUtils.kt | 47 +- .../CalculatedValuesToClassCarpenterTests.kt | 101 ---- .../internal/carpenter/ClassCarpenterTest.kt | 6 +- .../carpenter/ClassCarpenterTestUtils.kt | 112 +++-- ...berCompositeSchemaToClassCarpenterTests.kt | 272 +++-------- .../InheritanceSchemaToClassCarpenterTests.kt | 406 +++------------- ...berCompositeSchemaToClassCarpenterTests.kt | 124 +++-- ...berCompositeSchemaToClassCarpenterTests.kt | 205 -------- .../model/ClassCarpentingTypeLoaderTests.kt | 2 +- .../internal/model/LocalTypeModelTests.kt | 6 +- .../internal/model/TypeIdentifierTests.kt | 21 + .../corda/tools/shell/InteractiveShellTest.kt | 2 +- 84 files changed, 2381 insertions(+), 3974 deletions(-) create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ComposableTypePropertySerializer.kt delete mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializedGenericArrayType.kt delete mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializedParameterizedType.kt delete mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerFactory.kt delete mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/amqp/LocalSerializerFactory.kt create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectBuilder.kt delete mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializer.kt delete mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializers.kt create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/amqp/RemoteSerializerFactory.kt create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TypeNotationGenerator.kt delete mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/AMQPSchemaExtensions.kt delete mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/MetaCarpenter.kt create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeModellingFingerPrinter.kt delete mode 100644 serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializedParameterizedTypeTests.kt create mode 100644 serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerFactoryTests.kt delete mode 100644 serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerProviderTesting.kt delete mode 100644 serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/CalculatedValuesToClassCarpenterTests.kt delete mode 100644 serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/SingleMemberCompositeSchemaToClassCarpenterTests.kt diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt index 2a2a11041d..388b9c3205 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt @@ -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): MutableList { 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" } diff --git a/finance/src/test/kotlin/net/corda/finance/compat/CompatibilityTest.kt b/finance/src/test/kotlin/net/corda/finance/compat/CompatibilityTest.kt index bf1dc2cb88..b329fd1a17 100644 --- a/finance/src/test/kotlin/net/corda/finance/compat/CompatibilityTest.kt +++ b/finance/src/test/kotlin/net/corda/finance/compat/CompatibilityTest.kt @@ -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(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 +""") + } } } \ No newline at end of file diff --git a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt index d0bb4b4798..2b1d66ab76 100644 --- a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt +++ b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt @@ -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( diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPPrimitiveSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPPrimitiveSerializer.kt index adccbe0bc7..5342f683a4 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPPrimitiveSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPPrimitiveSerializer.kt @@ -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 { - 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. diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPRemoteTypeModel.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPRemoteTypeModel.kt index f717f87f3b..6865ef4de4 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPRemoteTypeModel.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPRemoteTypeModel.kt @@ -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, @@ -45,9 +54,6 @@ class AMQPRemoteTypeModel { val cache: MutableMap, val seen: Set) { - private inline fun forgetSeen(block: InterpretationState.() -> T): T = - withSeen(emptySet(), block) - private inline fun 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>): 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 { +open class ArraySerializer(override val type: Type, factory: LocalSerializerFactory) : AMQPSerializer { companion object { - fun make(type: Type, factory: SerializerFactory) : AMQPSerializer { + fun make(type: Type, factory: LocalSerializerFactory) : AMQPSerializer { contextLogger().debug { "Making array serializer, typename=${type.typeName}" } return when (type) { Array::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::class.java, factory) { +class CharArraySerializer(factory: LocalSerializerFactory) : ArraySerializer(Array::class.java, factory) { override fun List.toArrayOfType(type: Type): Any { val elementType = type.asClass() val list = this @@ -114,11 +114,11 @@ class CharArraySerializer(factory: SerializerFactory) : ArraySerializer(Array PrimArraySerializer> = mapOf( + private val primTypes: Map 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 diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CollectionSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CollectionSerializer.kt index 7df6abd9ed..29953e840a 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CollectionSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CollectionSerializer.kt @@ -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 { - override val type: Type = declaredType as? DeserializedParameterizedType - ?: DeserializedParameterizedType.make(SerializerFactory.nameForType(declaredType)) +class CollectionSerializer(private val declaredType: ParameterizedType, factory: LocalSerializerFactory) : AMQPSerializer { + 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> = + 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>): ParameterizedType = - (declaredType as? ParameterizedType) - ?: DeserializedParameterizedType(collectionClass, arrayOf(SerializerFactory.AnyType)) - - private fun findMostSuitableCollectionType(actualClass: Class<*>): Class> = - 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] diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ComposableTypePropertySerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ComposableTypePropertySerializer.kt new file mode 100644 index 0000000000..aa9764fd1e --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ComposableTypePropertySerializer.kt @@ -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) : 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() + } +} \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CorDappCustomSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CorDappCustomSerializer.kt index f1b509c940..b614fc7ac7 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CorDappCustomSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CorDappCustomSerializer.kt @@ -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) } } } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt index cbd54f08c2..8e44e3ab56 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt @@ -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 : AMQPSerializer, 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 : AMQPSerializer, SerializerFor { */ abstract class CustomSerializerImp(protected val clazz: Class, protected val withInheritance: Boolean) : CustomSerializer() { 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 : AMQPSerializer, SerializerFor { */ abstract class Proxy(clazz: Class, protected val proxyClass: Class

, - protected val factory: SerializerFactory, + protected val factory: LocalSerializerFactory, withInheritance: Boolean = true) : CustomSerializerImp(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( 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 : AMQPSerializer, 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 : AMQPSerializer, SerializerFor { : CustomSerializerImp(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, diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DescriptorBasedSerializerRegistry.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DescriptorBasedSerializerRegistry.kt index 2a2d17127d..8adc48fbed 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DescriptorBasedSerializerRegistry.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DescriptorBasedSerializerRegistry.kt @@ -25,5 +25,5 @@ class DefaultDescriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistr } override fun getOrBuild(descriptor: String, builder: () -> AMQPSerializer) = - get(descriptor) ?: builder().also { newSerializer -> this[descriptor] = newSerializer } + registry.getOrPut(descriptor) { builder() } } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt index 7bc06c5a24..f5ed5dd924 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt @@ -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) } ) { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializedGenericArrayType.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializedGenericArrayType.kt deleted file mode 100644 index 364b5afa6e..0000000000 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializedGenericArrayType.kt +++ /dev/null @@ -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) - } -} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializedParameterizedType.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializedParameterizedType.kt deleted file mode 100644 index f6321e1dc3..0000000000 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializedParameterizedType.kt +++ /dev/null @@ -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, - 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>): 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() - 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, 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() - 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, cl: ClassLoader): Type { - return DeserializedParameterizedType(makeType(rawTypeName, cl) as Class<*>, args.toTypedArray(), null) - } - - private fun parseTypeParams( - params: String, - startPos: Int, - paramTypes: MutableList, - 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 = 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 - } - } -} \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt index 5e7010c71c..ec9ef9e678 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt @@ -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, private val ordinals: Map) : AMQPSerializer { - override val typeDescriptor = Symbol.valueOf( - "$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")!! - - companion object { - private fun MutableMap.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, - factory: SerializerFactory, - schemas: SerializationSchemas): AMQPSerializer { - val wireTransforms = schemas.transforms.types[old.name] - ?: EnumMap>(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? = uncheckedCast(transforms[TransformTypes.EnumDefault]) - val renameRules: List? = 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 = localValues - .union(defaultRules?.map { it.new }?.toSet() ?: emptySet()) - .union(renameRules?.map { it.to } ?: emptySet()) - .associateBy({ it }, { it }) - .toMutableMap() - - val rules: MutableMap = 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) { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumSerializer.kt index 1bb12190f2..da8b922649 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumSerializer.kt @@ -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 { +class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: LocalSerializerFactory) : AMQPSerializer { 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()) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt deleted file mode 100644 index 700a3b51bb..0000000000 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt +++ /dev/null @@ -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, - override val kotlinConstructor: KFunction -) : 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, 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): KFunction? { - 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? = null - - clazz.kotlin.constructors.forEach { - val version = it.findAnnotation()?.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, - readersAsSerialized: Map): AMQPSerializer { - - // 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, - readersAsSerialized: Map, - classProperties: Map): AMQPSerializer { - 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 { - // The order in which the properties were serialised is important and must be preserved - val readersAsSerialized = LinkedHashMap() - 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, - kotlinConstructor: KFunction) : 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 = arrayOfNulls(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, - kotlinConstructor: KFunction, - private val setters: Map) : 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, - schemas: SerializationSchemas): AMQPSerializer -} - -/** - * 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, - schemas: SerializationSchemas): AMQPSerializer { - 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().error("typeNotation=${typeNotation.name} Need to evolve unsupported type") - throw NotSerializableException ("${typeNotation.name} cannot be evolved") - } - } - } - } - } -} \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerFactory.kt new file mode 100644 index 0000000000..49cb02dba4 --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerFactory.kt @@ -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? +} + +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? = + 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? { + // 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, + properties: Map): 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) { + 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? { + 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): AMQPSerializer = + EvolutionObjectSerializer.make( + localTypeInformation, + this, + constructor, + properties, + classLoader) +} \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt deleted file mode 100644 index d03e3e6da1..0000000000 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt +++ /dev/null @@ -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 = 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), creates a different fingerprint from the generic type itself (Example) - // - // On serialization Example is treated as Example, 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 where type is ? or T in this otherwise we - // generate different fingerprints for class Outer(val a: Inner) 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 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 { - 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 diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/LocalSerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/LocalSerializerFactory.kt new file mode 100644 index 0000000000..9b1b530764 --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/LocalSerializerFactory.kt @@ -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 + + /** + * Obtain an [AMQPSerializer] for the [declaredType]. + */ + fun get(declaredType: Type): AMQPSerializer = get(getTypeInformation(declaredType)) + + /** + * Obtain an [AMQPSerializer] for the type having the given [typeInformation]. + */ + fun get(typeInformation: LocalTypeInformation): AMQPSerializer + + /** + * 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>): + EnumMap> +} + +/** + * 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>> = DefaultCacheProvider.createCache() + private val serializersByType: MutableMap> = 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>): + EnumMap> = + transformsCache.computeIfAbsent(name) { _ -> builder() } + + override fun get(typeInformation: LocalTypeInformation): AMQPSerializer = + get(typeInformation.observedType, typeInformation) + + private fun make(typeInformation: LocalTypeInformation, build: () -> AMQPSerializer) = + make(typeInformation.typeIdentifier, build) + + private fun make(typeIdentifier: TypeIdentifier, build: () -> AMQPSerializer) = + serializersByType.computeIfAbsent(typeIdentifier) { _ -> build() } + + private fun get(declaredType: Type, localTypeInformation: LocalTypeInformation): AMQPSerializer { + 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 = + make(localTypeInformation) { + whitelist.requireWhitelisted(declaredType) + EnumSerializer(declaredType, declaredClass, this) + } + + private fun makeActualEnum(localTypeInformation: LocalTypeInformation, declaredType: Type, declaredClass: Class<*>): AMQPSerializer = + make(localTypeInformation) { + whitelist.requireWhitelisted(declaredType) + EnumSerializer(declaredType, declaredClass, this) + } + + private fun makeDeclaredCollection(localTypeInformation: LocalTypeInformation.ACollection): AMQPSerializer { + val resolved = CollectionSerializer.resolveDeclared(localTypeInformation) + return make(resolved) { + CollectionSerializer(resolved.typeIdentifier.getLocalType(classloader) as ParameterizedType, this) + } + } + + private fun makeDeclaredMap(localTypeInformation: LocalTypeInformation.AMap): AMQPSerializer { + val resolved = MapSerializer.resolveDeclared(localTypeInformation) + return make(resolved) { + MapSerializer(resolved.typeIdentifier.getLocalType(classloader) as ParameterizedType, this) + } + } + + override fun get(actualClass: Class<*>, declaredType: Type): AMQPSerializer { + // 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 { + 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 { + 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 = 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 = 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) + } + } + } + +} \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/MapSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/MapSerializer.kt index 742c6a84ed..2e00e8d206 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/MapSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/MapSerializer.kt @@ -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 { - 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 { + 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>): 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> = 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> = (obj as Map<*, *>).map { readEntry(schemas, input, it, context) } concreteBuilder(entries.toMap()) } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectBuilder.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectBuilder.kt new file mode 100644 index 0000000000..2b32e5482b --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectBuilder.kt @@ -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): () -> 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() + return { SetterBasedObjectBuilder(constructor, getterSetter) } + } + } + + fun initialize() + fun populate(slot: Int, value: Any?) + fun build(): Any +} + +class SetterBasedObjectBuilder( + val constructor: LocalConstructorInformation, + val properties: List): 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(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, providedProperties: List): () -> 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() +} \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt index 9ae529b608..5d422e9156 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt @@ -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 { - 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 { + + val propertySerializers: Map + val fields: List 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, + factory: LocalSerializerFactory): Map = + 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, + override val fields: List, + 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, + private val propertySerializers: Map +) { + 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, + 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, + override val fields: List, + 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, + private val reader: ComposableObjectReader): ObjectSerializer { + + companion object { + fun make(localTypeInformation: LocalTypeInformation.Composable, remoteTypeInformation: RemoteTypeInformation.Composable, constructor: LocalConstructorInformation, + properties: Map, 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, + remoteProperties: Map, + classLoader: ClassLoader): Map = + 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 { - 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 get() = emptyList() - private fun generateProvides(): List = interfaces.map { nameForType(it) } + override fun writeClassInfo(output: SerializationOutput) = + throw UnsupportedOperationException("Evolved types cannot be written") - fun construct(properties: List): 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 Constructor.newInstanceUnwrapped(vararg args: Any?): T { - try { - return newInstance(*args) - } catch (e: InvocationTargetException) { - throw e.cause!! - } - } } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializer.kt deleted file mode 100644 index 3b3ee33478..0000000000 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializer.kt +++ /dev/null @@ -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 = 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 { - 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() - } - } -} - diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializers.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializers.kt deleted file mode 100644 index 517af0406a..0000000000 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializers.kt +++ /dev/null @@ -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().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().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().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 { - 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) { - companion object { - fun make(serializationOrder: List) = - 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) : PropertySerializers(serializationOrder) { - override val byConstructor get() = true -} - -class PropertySerializersSetter( - serializationOrder: List) : PropertySerializers(serializationOrder) { - override val byConstructor get() = false -} - -class PropertySerializersEvolution : PropertySerializers(emptyList()) { - override val byConstructor get() = false -} - - diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/RemoteSerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/RemoteSerializerFactory.kt new file mode 100644 index 0000000000..c92947651b --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/RemoteSerializerFactory.kt @@ -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 +} + +/** + * 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 = + // 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 { + 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 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): + Map { + 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) +} \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt index aed85b9825..3a7014615c 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt @@ -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 { - 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() != 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 propertiesForSerialization( - kotlinConstructor: KFunction?, - type: Type, - factory: SerializerFactory): PropertySerializers = PropertySerializers.make( - getValueProperties(kotlinConstructor, type, factory) - .addCalculatedProperties(factory, type) - .sortedWith(PropertyAccessor)) - -fun getValueProperties(kotlinConstructor: KFunction?, type: Type, factory: SerializerFactory) - : List = - if (kotlinConstructor != null) { - propertiesForSerializationFromConstructor(kotlinConstructor, type, factory) - } else { - propertiesForSerializationFromAbstract(type.asClass(), type, factory) - } - -private fun List.addCalculatedProperties(factory: SerializerFactory, type: Type) - : List { - 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 propertiesForSerializationFromConstructor( - kotlinConstructor: KFunction, - type: Type, - factory: SerializerFactory): List { - 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, type: Type, clazz: Class, 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, - type: Type, - factory: SerializerFactory): List = - 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): Pair { - 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 = - 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 = - exploreType(type, serializerFactory).toList() - -private fun exploreType(type: Type, serializerFactory: SerializerFactory, interfaces: MutableSet = LinkedHashSet()): MutableSet { - 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>( - 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) -} +} \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationOutput.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationOutput.kt index d24e5ea77b..1ba283203f 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationOutput.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationOutput.kt @@ -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( */ @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) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt index 8e2e1e1330..42d0f1dda9 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt @@ -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> - val serializersByDescriptor: MutableMap> - val transformsCache: MutableMap>> - 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 +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 - - /** - * 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) - - fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer? - fun registerExternal(customSerializer: CorDappCustomSerializer) - fun registerByDescriptor(name: Symbol, serializerCreator: () -> AMQPSerializer): AMQPSerializer - - object AnyType : WildcardType { - override fun getUpperBounds(): Array = arrayOf(Object::class.java) - - override fun getLowerBounds(): Array = 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, 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> = 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> = DefaultCacheProvider.createCache() - override val serializersByDescriptor: MutableMap> = DefaultCacheProvider.createCache() - private var customSerializers: List = emptyList() - private val customSerializersCache: MutableMap?> = DefaultCacheProvider.createCache() - override val transformsCache: MutableMap>> = 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 { - // 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 { - 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) { - 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 { - // 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().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 = 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? { - return customSerializersCache.computeIfAbsent(CustomSerializersCacheKey(clazz, declaredType), ::doFindCustomSerializer) - } - - private fun doFindCustomSerializer(key: CustomSerializersCacheKey): AMQPSerializer? { - 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 - } else { - // Make a subclass serializer for the subclass and return that... - CustomSerializer.SubClass(clazz, uncheckedCast(customSerializer)) - } - } - } - return null - } - - private fun makeMapSerializer(declaredType: ParameterizedType): AMQPSerializer { - val rawType = declaredType.rawType as Class<*> - rawType.checkSupportedMapType() - return MapSerializer(declaredType, this) - } - - override fun registerByDescriptor(name: Symbol, serializerCreator: () -> AMQPSerializer): AMQPSerializer = - serializersByDescriptor.computeIfAbsent(name) { _ -> serializerCreator() } - - companion object { - private val logger = contextLogger() - } - -} \ No newline at end of file +class ComposedSerializerFactory( + private val localSerializerFactory: LocalSerializerFactory, + private val remoteSerializerFactory: RemoteSerializerFactory, + private val customSerializerRegistry: CachingCustomSerializerRegistry +) : SerializerFactory, + LocalSerializerFactory by localSerializerFactory, + RemoteSerializerFactory by remoteSerializerFactory, + CustomSerializerRegistry by customSerializerRegistry \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt index 0d05d4e0de..665eda5c56 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt @@ -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 { + throw NotSerializableException(""" +Evolution not permitted. + +Remote: +${remoteTypeInformation.prettyPrint(false)} + +Local: +${localTypeInformation.prettyPrint(false)} + """) + } } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SingletonSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SingletonSerializer.kt index 584501a877..8328620504 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SingletonSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SingletonSerializer.kt @@ -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 { - override val typeDescriptor = Symbol.valueOf( - "$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")!! +class SingletonSerializer(override val type: Class<*>, val singleton: Any, factory: LocalSerializerFactory) : AMQPSerializer { + 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 = interfaces.map { it.typeName } + private fun generateProvides(): List = interfaces.map { it.typeIdentifier.name } internal val typeNotation: TypeNotation = RestrictedType(type.typeName, "Singleton", generateProvides(), "boolean", Descriptor(typeDescriptor), emptyList()) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformsSchema.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformsSchema.kt index 1409bc95c7..15afeb60b1 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformsSchema.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformsSchema.kt @@ -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>(TransformTypes::class.java) try { val clazz = sf.classloader.loadClass(name) @@ -244,7 +246,7 @@ data class TransformsSchema(val types: Map>>) { try { get(type, sf).apply { @@ -268,7 +270,7 @@ data class TransformsSchema(val types: Map>>().apply { schema.types.forEach { type -> getAndAdd(type.name, sf, this) } }) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TypeNotationGenerator.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TypeNotationGenerator.kt new file mode 100644 index 0000000000..802c1df68e --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TypeNotationGenerator.kt @@ -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, + properties: Map, + 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 + } +} \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TypeParameterUtils.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TypeParameterUtils.kt index 72720add79..f16f58b0ae 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TypeParameterUtils.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TypeParameterUtils.kt @@ -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) } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ClassSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ClassSerializer.kt index dd4cbd9f9f..8d60f06e0c 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ClassSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ClassSerializer.kt @@ -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, ClassSerializer.ClassProxy>( Class::class.java, ClassProxy::class.java, diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/InputStreamSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/InputStreamSerializer.kt index 1a7f5fce89..46d2ae80e3 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/InputStreamSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/InputStreamSerializer.kt @@ -20,7 +20,7 @@ object InputStreamSerializer : CustomSerializer.Implements(InputStr type.toString(), "", listOf(type.toString()), - SerializerFactory.primitiveTypeName(ByteArray::class.java)!!, + AMQPTypeIdentifiers.primitiveTypeName(ByteArray::class.java), descriptor, emptyList()))) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PrivateKeySerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PrivateKeySerializer.kt index 7bf9bbf344..118ca2312d 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PrivateKeySerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PrivateKeySerializer.kt @@ -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::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 diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PublicKeySerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PublicKeySerializer.kt index bf6025360d..ef1be88fce 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PublicKeySerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PublicKeySerializer.kt @@ -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::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 diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt index 3b4b03800e..b3082a629e 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt @@ -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::class.java, ThrowableProxy::class.java, factory) { +class ThrowableSerializer(factory: LocalSerializerFactory) : CustomSerializer.Proxy(Throwable::class.java, ThrowableProxy::class.java, factory) { companion object { private val logger = contextLogger() @@ -19,15 +22,23 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy> = 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 = 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 + 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) } -class StackTraceElementSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(StackTraceElement::class.java, StackTraceElementProxy::class.java, factory) { +class StackTraceElementSerializer(factory: LocalSerializerFactory) : CustomSerializer.Proxy(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) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/X509CRLSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/X509CRLSerializer.kt index 9a5a06b62e..e7374dd114 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/X509CRLSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/X509CRLSerializer.kt @@ -12,7 +12,7 @@ object X509CRLSerializer : CustomSerializer.Implements(X509CRL::class.j type.toString(), "", listOf(type.toString()), - SerializerFactory.primitiveTypeName(ByteArray::class.java)!!, + AMQPTypeIdentifiers.primitiveTypeName(ByteArray::class.java), descriptor, emptyList() ))) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/X509CertificateSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/X509CertificateSerializer.kt index 5d00cef9b0..90063aaa99 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/X509CertificateSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/X509CertificateSerializer.kt @@ -12,7 +12,7 @@ object X509CertificateSerializer : CustomSerializer.Implements( type.toString(), "", listOf(type.toString()), - SerializerFactory.primitiveTypeName(ByteArray::class.java)!!, + AMQPTypeIdentifiers.primitiveTypeName(ByteArray::class.java), descriptor, emptyList() ))) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/AMQPSchemaExtensions.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/AMQPSchemaExtensions.kt deleted file mode 100644 index 37736abcde..0000000000 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/AMQPSchemaExtensions.kt +++ /dev/null @@ -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().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>() - 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 = 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 = 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, Class> = 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 - } -} - diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Exceptions.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Exceptions.kt index ac1346b053..ffd6096f04 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Exceptions.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Exceptions.kt @@ -23,14 +23,3 @@ class NullablePrimitiveException(val name: String, val field: Class) : 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}") diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/MetaCarpenter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/MetaCarpenter.kt deleted file mode 100644 index 445c3ce7da..0000000000 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/MetaCarpenter.kt +++ /dev/null @@ -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, - val dependencies: MutableMap>>, - val dependsOn: MutableMap>) { - companion object CarpenterSchemaConstructor { - fun buildWith(classLoader: ClassLoader, types: List) = - 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>() - - 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)) - } -} - diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Schema.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Schema.kt index 8690668367..90a1034f70 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Schema.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Schema.kt @@ -74,7 +74,7 @@ fun EnumMap.simpleFieldAccess(): Boolean { class ClassSchema( name: String, fields: Map, - superclass: Schema? = null, + superclass: Schema? = null, // always null for now, but retained because non-null superclass is supported by carpenter. interfaces: List> = 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, - superclass: Schema? = null, interfaces: List> = 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) } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/CarpentryDependencyGraph.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/CarpentryDependencyGraph.kt index 6b6edfac88..d9959672e1 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/CarpentryDependencyGraph.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/CarpentryDependencyGraph.kt @@ -6,8 +6,7 @@ import java.lang.reflect.Type /** * Once we have the complete graph of types requiring carpentry to hand, we can use it to sort those types in reverse- * dependency order, i.e. beginning with those types that have no dependencies on other types, then the types that - * depended on those types, and so on. This means we can feed types directly to the [RemoteTypeCarpenter], and don't - * have to use the [CarpenterMetaSchema]. + * depended on those types, and so on. This means we can feed types in this order directly to the [RemoteTypeCarpenter]. * * @param typesRequiringCarpentry The set of [RemoteTypeInformation] for types that are not reachable by the current * classloader. diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformation.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformation.kt index 206417138b..c7473623d4 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformation.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformation.kt @@ -56,8 +56,19 @@ sealed class LocalTypeInformation { * @param type The [Type] to obtain [LocalTypeInformation] for. * @param lookup The [LocalTypeLookup] to use to find previously-constructed [LocalTypeInformation]. */ - fun forType(type: Type, lookup: LocalTypeLookup): LocalTypeInformation = - LocalTypeInformationBuilder(lookup).build(type, TypeIdentifier.forGenericType(type)) + fun forType(type: Type, lookup: LocalTypeLookup): LocalTypeInformation { + val builder = LocalTypeInformationBuilder(lookup) + val result = builder.build(type, TypeIdentifier.forGenericType(type)) + + // Patch every cyclic reference with a `follow` property pointing to the type information it refers to. + builder.cycles.forEach { cycle -> + cycle.follow = lookup.findOrBuild(cycle.observedType, cycle.typeIdentifier) { + throw IllegalStateException("Should not be attempting to build new type information when populating a cycle") + } + } + + return result + } } /** @@ -71,6 +82,29 @@ sealed class LocalTypeInformation { */ abstract val typeIdentifier: TypeIdentifier + /** + * Get the map of [LocalPropertyInformation], for all types that have it, or an empty map otherwise. + */ + val propertiesOrEmptyMap: Map get() = when(this) { + is LocalTypeInformation.Composable -> properties + is LocalTypeInformation.Abstract -> properties + is LocalTypeInformation.AnInterface -> properties + is LocalTypeInformation.NonComposable -> properties + is LocalTypeInformation.Opaque -> expand.propertiesOrEmptyMap + else -> emptyMap() + } + + /** + * Get the list of interfaces, for all types that have them, or an empty list otherwise. + */ + val interfacesOrEmptyList: List get() = when(this) { + is LocalTypeInformation.Composable -> interfaces + is LocalTypeInformation.Abstract -> interfaces + is LocalTypeInformation.AnInterface -> interfaces + is LocalTypeInformation.NonComposable -> interfaces + else -> emptyList() + } + /** * Obtain a multi-line, recursively-indented representation of this type information. * @@ -101,11 +135,10 @@ sealed class LocalTypeInformation { */ data class Cycle( override val observedType: Type, - override val typeIdentifier: TypeIdentifier, - private val _follow: () -> LocalTypeInformation) : LocalTypeInformation() { - val follow: LocalTypeInformation get() = _follow() + override val typeIdentifier: TypeIdentifier) : LocalTypeInformation() { + lateinit var follow: LocalTypeInformation - // Custom equals / hashcode because otherwise the "follow" lambda makes equality harder to reason about. + // Custom equals / hashcode omitting "follow" override fun equals(other: Any?): Boolean = other is Cycle && other.observedType == observedType && @@ -121,7 +154,10 @@ sealed class LocalTypeInformation { */ data class Opaque(override val observedType: Class<*>, override val typeIdentifier: TypeIdentifier, private val _expand: () -> LocalTypeInformation) : LocalTypeInformation() { - val expand: LocalTypeInformation get() = _expand() + /** + * In some rare cases, e.g. during Exception serialisation, we may want to "look inside" an opaque type. + */ + val expand: LocalTypeInformation by lazy { _expand() } // Custom equals / hashcode because otherwise the "expand" lambda makes equality harder to reason about. override fun equals(other: Any?): Boolean = @@ -202,6 +238,7 @@ sealed class LocalTypeInformation { * * @param constructor [LocalConstructorInformation] for the constructor used when building instances of this type * out of dictionaries of typed values. + * @param evolutionConstructors Evolution constructors in ascending version order. * @param properties [LocalPropertyInformation] for the properties of the interface. * @param superclass [LocalTypeInformation] for the superclass of the underlying class of this type. * @param interfaces [LocalTypeInformation] for the interfaces extended by this interface. @@ -211,7 +248,7 @@ sealed class LocalTypeInformation { override val observedType: Type, override val typeIdentifier: TypeIdentifier, val constructor: LocalConstructorInformation, - val evolverConstructors: List, + val evolutionConstructors: List, val properties: Map, val superclass: LocalTypeInformation, val interfaces: List, @@ -312,7 +349,7 @@ data class LocalConstructorInformation( * Represents information about a constructor that is specifically to be used for evolution, and is potentially matched * with a different set of properties to the regular constructor. */ -data class EvolverConstructorInformation( +data class EvolutionConstructorInformation( val constructor: LocalConstructorInformation, val properties: Map) @@ -330,16 +367,16 @@ private data class LocalTypeInformationPrettyPrinter(private val simplifyClassNa with(typeInformation) { when (this) { is LocalTypeInformation.Abstract -> - typeIdentifier.prettyPrint() + + typeIdentifier.prettyPrint(simplifyClassNames) + printInheritsFrom(interfaces, superclass) + indentAnd { printProperties(properties) } is LocalTypeInformation.AnInterface -> - typeIdentifier.prettyPrint() + printInheritsFrom(interfaces) - is LocalTypeInformation.Composable -> typeIdentifier.prettyPrint() + + typeIdentifier.prettyPrint(simplifyClassNames) + printInheritsFrom(interfaces) + is LocalTypeInformation.Composable -> typeIdentifier.prettyPrint(simplifyClassNames) + printConstructor(constructor) + printInheritsFrom(interfaces, superclass) + indentAnd { printProperties(properties) } - else -> typeIdentifier.prettyPrint() + else -> typeIdentifier.prettyPrint(simplifyClassNames) } } @@ -366,7 +403,7 @@ private data class LocalTypeInformationPrettyPrinter(private val simplifyClassNa " ".repeat(indent) + key + (if(!value.isMandatory) " (optional)" else "") + (if (value.isCalculated) " (calculated)" else "") + - ": " + value.type.prettyPrint(simplifyClassNames) + ": " + prettyPrint(value.type) private inline fun indentAnd(block: LocalTypeInformationPrettyPrinter.() -> String) = copy(indent = indent + 1).block() diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformationBuilder.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformationBuilder.kt index 88df121a43..5e596d6465 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformationBuilder.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformationBuilder.kt @@ -32,7 +32,10 @@ import kotlin.reflect.jvm.javaType * this is not a [MutableSet], as we want to be able to backtrack while traversing through the graph of related types, and * will find it useful to revert to earlier states of knowledge about which types have been visited on a given branch. */ -internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup, val resolutionContext: Type? = null, val visited: Set = emptySet()) { +internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup, + val resolutionContext: Type? = null, + val visited: Set = emptySet(), + val cycles: MutableList = mutableListOf()) { companion object { private val logger = contextLogger() @@ -42,9 +45,7 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup, val * Recursively build [LocalTypeInformation] for the given [Type] and [TypeIdentifier] */ fun build(type: Type, typeIdentifier: TypeIdentifier): LocalTypeInformation = - if (typeIdentifier in visited) LocalTypeInformation.Cycle(type, typeIdentifier) { - LocalTypeInformationBuilder(lookup, resolutionContext).build(type, typeIdentifier) - } + if (typeIdentifier in visited) LocalTypeInformation.Cycle(type, typeIdentifier).apply { cycles.add(this) } else lookup.findOrBuild(type, typeIdentifier) { isOpaque -> copy(visited = visited + typeIdentifier).buildIfNotFound(type, typeIdentifier, isOpaque) } @@ -184,13 +185,13 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup, val interfaceInformation, typeParameterInformation) } - val evolverConstructors = evolverConstructors(type).map { ctor -> + val evolutionConstructors = evolutionConstructors(type).map { ctor -> val constructorInformation = buildConstructorInformation(type, ctor) - val evolverProperties = buildObjectProperties(rawType, constructorInformation) - EvolverConstructorInformation(constructorInformation, evolverProperties) + val evolutionProperties = buildObjectProperties(rawType, constructorInformation) + EvolutionConstructorInformation(constructorInformation, evolutionProperties) } - return LocalTypeInformation.Composable(type, typeIdentifier, constructorInformation, evolverConstructors, properties, + return LocalTypeInformation.Composable(type, typeIdentifier, constructorInformation, evolutionConstructors, properties, superclassInformation, interfaceInformation, typeParameterInformation) } @@ -395,7 +396,10 @@ private fun constructorForDeserialization(type: Type): KFunction? { } } -private fun evolverConstructors(type: Type): List> { +/** + * Obtain evolution constructors in ascending version order. + */ +private fun evolutionConstructors(type: Type): List> { val clazz = type.asClass() if (!clazz.isConcreteClass || clazz.isSynthetic) return emptyList() diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/RemoteTypeCarpenter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/RemoteTypeCarpenter.kt index 75e4b9363d..e7b97cf45b 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/RemoteTypeCarpenter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/RemoteTypeCarpenter.kt @@ -23,7 +23,10 @@ class SchemaBuildingRemoteTypeCarpenter(private val carpenter: ClassCarpenter): try { when (typeInformation) { is RemoteTypeInformation.AnInterface -> typeInformation.carpentInterface() - is RemoteTypeInformation.Composable -> typeInformation.carpentComposable() + is RemoteTypeInformation.Composable -> + // We cannot carpent parameterised types, and if the type is parameterised assume we are really here + // because a type parameter needed carpenting. + if (typeInformation.typeIdentifier !is TypeIdentifier.Parameterised) typeInformation.carpentComposable() is RemoteTypeInformation.AnEnum -> typeInformation.carpentEnum() else -> { } // Anything else, such as arrays, will be taken care of by the above @@ -31,7 +34,14 @@ class SchemaBuildingRemoteTypeCarpenter(private val carpenter: ClassCarpenter): } catch (e: ClassCarpenterException) { throw NotSerializableException("${typeInformation.typeIdentifier.name}: ${e.message}") } - return typeInformation.typeIdentifier.getLocalType(classLoader) + + return try { + typeInformation.typeIdentifier.getLocalType(classLoader) + } catch (e: ClassNotFoundException) { + // This might happen if we've been asked to carpent up a parameterised type, and it's the rawtype itself + // rather than any of its type parameters that were missing. + throw NotSerializableException("Could not carpent ${typeInformation.typeIdentifier.prettyPrint(false)}") + } } private val RemoteTypeInformation.erasedLocalClass get() = typeIdentifier.getLocalType(classLoader).asClass() diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/RemoteTypeInformation.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/RemoteTypeInformation.kt index 93c0e72307..7cdae243a8 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/RemoteTypeInformation.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/RemoteTypeInformation.kt @@ -1,9 +1,5 @@ package net.corda.serialization.internal.model -import net.corda.serialization.internal.amqp.Transform -import net.corda.serialization.internal.amqp.TransformTypes -import java.util.* - typealias TypeDescriptor = String /** @@ -88,9 +84,9 @@ sealed class RemoteTypeInformation { /** * The [RemoteTypeInformation] emitted if we hit a cycle while traversing the graph of related types. */ - data class Cycle(override val typeIdentifier: TypeIdentifier, private val _follow: () -> RemoteTypeInformation) : RemoteTypeInformation() { - override val typeDescriptor = typeIdentifier.name - val follow: RemoteTypeInformation get() = _follow() + data class Cycle(override val typeIdentifier: TypeIdentifier) : RemoteTypeInformation() { + override val typeDescriptor by lazy { follow.typeDescriptor } + lateinit var follow: RemoteTypeInformation override fun equals(other: Any?): Boolean = other is Cycle && other.typeIdentifier == typeIdentifier override fun hashCode(): Int = typeIdentifier.hashCode() @@ -176,14 +172,14 @@ private data class RemoteTypeInformationPrettyPrinter(private val simplifyClassN } private fun printProperties(properties: Map) = - properties.entries.sortedBy { it.key }.joinToString("\n", "\n", "") { + properties.entries.joinToString("\n", "\n", "") { it.prettyPrint() } private fun Map.Entry.prettyPrint(): String = " ".repeat(indent) + key + (if(!value.isMandatory) " (optional)" else "") + - ": " + value.type.prettyPrint(simplifyClassNames) + ": " + prettyPrint(value.type) } data class EnumTransforms(val defaults: Map, val renames: Map) { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeModellingFingerPrinter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeModellingFingerPrinter.kt new file mode 100644 index 0000000000..8afebf0c15 --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeModellingFingerPrinter.kt @@ -0,0 +1,233 @@ +package net.corda.serialization.internal.model + +import com.google.common.hash.Hashing +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.toBase64 +import net.corda.serialization.internal.amqp.* +import java.io.NotSerializableException + +/** + * A fingerprinter that fingerprints [LocalTypeInformation]. + */ +interface FingerPrinter { + /** + * Traverse the provided [LocalTypeInformation] graph and emit a short fingerprint string uniquely representing + * the shape of that graph. + * + * @param typeInformation The [LocalTypeInformation] to fingerprint. + */ + fun fingerprint(typeInformation: LocalTypeInformation): String +} + +/** + * A [FingerPrinter] that consults a [CustomTypeDescriptorLookup] to obtain type descriptors for + * types that do not need to be traversed to calculate their fingerprint information. (Usually these will be the type + * descriptors supplied by custom serializers). + * + * @param customTypeDescriptorLookup The [CustomTypeDescriptorLookup] to use to obtain custom type descriptors for + * selected types. + */ +class TypeModellingFingerPrinter( + private val customTypeDescriptorLookup: CustomSerializerRegistry, + private val debugEnabled: Boolean = false) : FingerPrinter { + + private val cache: MutableMap = DefaultCacheProvider.createCache() + + override fun fingerprint(typeInformation: LocalTypeInformation): String = + cache.computeIfAbsent(typeInformation.typeIdentifier) { + FingerPrintingState( + customTypeDescriptorLookup, + FingerprintWriter(debugEnabled)).fingerprint(typeInformation) + } +} + +/** + * Wrapper for the [Hasher] we use to generate fingerprints, providing methods for writing various kinds of content + * into the hash. + */ +internal class FingerprintWriter(debugEnabled: Boolean) { + + 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 logger = contextLogger() + } + + private val debugBuffer: StringBuilder? = if (debugEnabled) StringBuilder() else null + private var hasher = Hashing.murmur3_128().newHasher() // FIXUP: remove dependency on Guava Hasher + + fun write(chars: CharSequence) = append(chars) + fun write(words: List) = append(words.joinToString()) + fun writeAlreadySeen() = append(ALREADY_SEEN_HASH) + fun writeEnum() = append(ENUM_HASH) + fun writeArray() = append(ARRAY_HASH) + fun writeNullable() = append(NULLABLE_HASH) + fun writeNotNullable() = append(NOT_NULLABLE_HASH) + fun writeAny() = append(ANY_TYPE_HASH) + + private fun append(chars: CharSequence) = apply { + debugBuffer?.append(chars) + hasher = hasher.putUnencodedChars(chars) + } + + val fingerprint: String by lazy { + val fingerprint = hasher.hash().asBytes().toBase64() + if (debugBuffer != null) logger.info("$fingerprint from $debugBuffer") + fingerprint + } +} + +/** + * Representation of the current state of fingerprinting, which keeps track of which types have already been visited + * during fingerprinting. + */ +private class FingerPrintingState( + private val customSerializerRegistry: CustomSerializerRegistry, + private val writer: FingerprintWriter) { + + companion object { + private var CHARACTER_TYPE = LocalTypeInformation.Atomic( + Character::class.java, + TypeIdentifier.forClass(Character::class.java)) + } + + private val typesSeen: MutableSet = mutableSetOf() + + /** + * Fingerprint the type recursively, and return the encoded fingerprint written into the hasher. + */ + fun fingerprint(type: LocalTypeInformation): String = + fingerprintType(type).writer.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: LocalTypeInformation): FingerPrintingState = apply { + // Don't go round in circles. + when { + hasSeen(type.typeIdentifier) -> writer.writeAlreadySeen() + type is LocalTypeInformation.Cycle -> fingerprintType(type.follow) + else -> ifThrowsAppend({ type.observedType.typeName }, { + typesSeen.add(type.typeIdentifier) + 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: LocalTypeInformation) = apply { + when (type) { + is LocalTypeInformation.Cycle -> + throw IllegalStateException("Cyclic references must be dereferenced before fingerprinting") + is LocalTypeInformation.Unknown, + is LocalTypeInformation.Top -> writer.writeAny() + is LocalTypeInformation.AnArray -> { + fingerprintType(type.componentType) + writer.writeArray() + } + is LocalTypeInformation.ACollection -> fingerprintCollection(type) + is LocalTypeInformation.AMap -> fingerprintMap(type) + is LocalTypeInformation.Atomic -> fingerprintName(type) + is LocalTypeInformation.Opaque -> fingerprintOpaque(type) + is LocalTypeInformation.AnEnum -> fingerprintEnum(type) + is LocalTypeInformation.AnInterface -> fingerprintInterface(type) + is LocalTypeInformation.Abstract -> fingerprintAbstract(type) + is LocalTypeInformation.Singleton -> fingerprintName(type) + is LocalTypeInformation.Composable -> fingerprintComposable(type) + is LocalTypeInformation.NonComposable -> throw NotSerializableException( + "Attempted to fingerprint non-composable type ${type.typeIdentifier.prettyPrint(false)}") + } + } + + private fun fingerprintCollection(type: LocalTypeInformation.ACollection) { + fingerprintName(type) + fingerprintType(type.elementType) + } + + private fun fingerprintMap(type: LocalTypeInformation.AMap) { + fingerprintName(type) + fingerprintType(type.keyType) + fingerprintType(type.valueType) + } + + private fun fingerprintOpaque(type: LocalTypeInformation) = + fingerprintWithCustomSerializerOrElse(type) { + fingerprintName(type) + } + + private fun fingerprintInterface(type: LocalTypeInformation.AnInterface) = + fingerprintWithCustomSerializerOrElse(type) { + fingerprintName(type) + writer.writeAlreadySeen() // FIXUP: this replicates the behaviour of the old fingerprinter for compatibility reasons. + fingerprintInterfaces(type.interfaces) + fingerprintTypeParameters(type.typeParameters) + } + + private fun fingerprintAbstract(type: LocalTypeInformation.Abstract) = + fingerprintWithCustomSerializerOrElse(type) { + fingerprintName(type) + fingerprintProperties(type.properties) + fingerprintInterfaces(type.interfaces) + fingerprintTypeParameters(type.typeParameters) + } + + private fun fingerprintComposable(type: LocalTypeInformation.Composable) = + fingerprintWithCustomSerializerOrElse(type) { + fingerprintName(type) + fingerprintProperties(type.properties) + fingerprintInterfaces(type.interfaces) + fingerprintTypeParameters(type.typeParameters) + } + + private fun fingerprintName(type: LocalTypeInformation) { + val identifier = type.typeIdentifier + when (identifier) { + is TypeIdentifier.ArrayOf -> writer.write(identifier.componentType.name).writeArray() + else -> writer.write(identifier.name) + } + } + + private fun fingerprintTypeParameters(typeParameters: List) = + typeParameters.forEach { fingerprintType(it) } + + private fun fingerprintProperties(properties: Map) = + properties.asSequence().sortedBy { it.key }.forEach { (propertyName, propertyType) -> + val (neverMandatory, adjustedType) = adjustType(propertyType.type) + fingerprintType(adjustedType) + writer.write(propertyName) + if (propertyType.isMandatory && !neverMandatory) writer.writeNotNullable() else writer.writeNullable() + } + + // Compensate for the serialisation framework's forcing of char to Character + private fun adjustType(propertyType: LocalTypeInformation): Pair = + if (propertyType.typeIdentifier.name == "char") true to CHARACTER_TYPE else false to propertyType + + private fun fingerprintInterfaces(interfaces: List) = + interfaces.forEach { fingerprintType(it) } + + // ensures any change to the enum (adding constants) will trigger the need for evolution + private fun fingerprintEnum(type: LocalTypeInformation.AnEnum) { + writer.write(type.members).write(type.typeIdentifier.name).writeEnum() + } + + // Give any custom serializers loaded into the factory the chance to supply their own type-descriptors + private fun fingerprintWithCustomSerializerOrElse(type: LocalTypeInformation, defaultAction: () -> Unit) { + val customTypeDescriptor = customSerializerRegistry.findCustomSerializer(type.observedType.asClass(), type.observedType)?.typeDescriptor?.toString() + if (customTypeDescriptor != null) writer.write(customTypeDescriptor) + else defaultAction() + } + + // Test whether we are in a state in which we have already seen the given type. + // + // We don't include Example and Example where type is ? or T in this otherwise we + // generate different fingerprints for class Outer(val a: Inner) 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 and Example to have the same fingerprint + private fun hasSeen(type: TypeIdentifier) = (type in typesSeen) + && (type != TypeIdentifier.UnknownType) +} diff --git a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaPrivatePropertyTests.java b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaPrivatePropertyTests.java index a68e14b572..a811807dcb 100644 --- a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaPrivatePropertyTests.java +++ b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaPrivatePropertyTests.java @@ -1,5 +1,6 @@ package net.corda.serialization.internal.amqp; +import net.corda.serialization.internal.amqp.testutils.TestDescriptorBasedSerializerRegistry; import net.corda.serialization.internal.amqp.testutils.TestSerializationContext; import org.junit.Test; @@ -133,8 +134,9 @@ public class JavaPrivatePropertyTests { } @Test - public void singlePrivateWithConstructor() throws NotSerializableException, NoSuchFieldException, IllegalAccessException { - SerializerFactory factory = testDefaultFactory(); + public void singlePrivateWithConstructor() throws NotSerializableException { + TestDescriptorBasedSerializerRegistry registry = new TestDescriptorBasedSerializerRegistry(); + SerializerFactory factory = testDefaultFactory(registry); SerializationOutput ser = new SerializationOutput(factory); DeserializationInput des = new DeserializationInput(factory); @@ -144,22 +146,14 @@ public class JavaPrivatePropertyTests { assertEquals (c.a, c2.a); - // - // Now ensure we actually got a private property serializer - // - Map> serializersByDescriptor = factory.getSerializersByDescriptor(); - - assertEquals(1, serializersByDescriptor.size()); - ObjectSerializer cSerializer = ((ObjectSerializer)serializersByDescriptor.values().toArray()[0]); - assertEquals(1, cSerializer.getPropertySerializers().getSerializationOrder().size()); - Object[] propertyReaders = cSerializer.getPropertySerializers().getSerializationOrder().toArray(); - assertTrue (((PropertyAccessor)propertyReaders[0]).getSerializer().getPropertyReader() instanceof PrivatePropertyReader); + assertEquals(1, registry.getContents().size()); } @Test public void singlePrivateWithConstructorAndGetter() - throws NotSerializableException, NoSuchFieldException, IllegalAccessException { - SerializerFactory factory = testDefaultFactory(); + throws NotSerializableException { + TestDescriptorBasedSerializerRegistry registry = new TestDescriptorBasedSerializerRegistry(); + SerializerFactory factory = testDefaultFactory(registry); SerializationOutput ser = new SerializationOutput(factory); DeserializationInput des = new DeserializationInput(factory); @@ -169,15 +163,6 @@ public class JavaPrivatePropertyTests { assertEquals (c.a, c2.a); - // - // Now ensure we actually got a private property serializer - // - Map> serializersByDescriptor = factory.getSerializersByDescriptor(); - - assertEquals(1, serializersByDescriptor.size()); - ObjectSerializer cSerializer = ((ObjectSerializer)serializersByDescriptor.values().toArray()[0]); - assertEquals(1, cSerializer.getPropertySerializers().getSerializationOrder().size()); - Object[] propertyReaders = cSerializer.getPropertySerializers().getSerializationOrder().toArray(); - assertTrue (((PropertyAccessor)propertyReaders[0]).getSerializer().getPropertyReader() instanceof PublicPropertyReader); + assertEquals(1, registry.getContents().size()); } } diff --git a/serialization/src/test/java/net/corda/serialization/internal/carpenter/JavaCalculatedValuesToClassCarpenterTest.java b/serialization/src/test/java/net/corda/serialization/internal/carpenter/JavaCalculatedValuesToClassCarpenterTest.java index 1171d7713d..d0e39bb214 100644 --- a/serialization/src/test/java/net/corda/serialization/internal/carpenter/JavaCalculatedValuesToClassCarpenterTest.java +++ b/serialization/src/test/java/net/corda/serialization/internal/carpenter/JavaCalculatedValuesToClassCarpenterTest.java @@ -7,6 +7,8 @@ import net.corda.core.serialization.SerializedBytes; import net.corda.serialization.internal.AllWhitelist; import net.corda.serialization.internal.amqp.*; import net.corda.serialization.internal.amqp.Schema; +import net.corda.serialization.internal.model.RemoteTypeInformation; +import net.corda.serialization.internal.model.TypeIdentifier; import net.corda.testing.core.SerializationEnvironmentRule; import org.junit.Before; import org.junit.Rule; @@ -66,42 +68,23 @@ public class JavaCalculatedValuesToClassCarpenterTest extends AmqpCarpenterBase ObjectAndEnvelope objAndEnv = new DeserializationInput(factory) .deserializeAndReturnEnvelope(serialized, C.class, context); - C amqpObj = objAndEnv.getObj(); - Schema schema = objAndEnv.getEnvelope().getSchema(); - - assertEquals(2, amqpObj.getI()); - assertEquals("4", amqpObj.getSquared()); - assertEquals(2, schema.getTypes().size()); - assertTrue(schema.getTypes().get(0) instanceof CompositeType); - - CompositeType concrete = (CompositeType) schema.getTypes().get(0); - assertEquals(3, concrete.getFields().size()); - assertEquals("doubled", concrete.getFields().get(0).getName()); - assertEquals("int", concrete.getFields().get(0).getType()); - assertEquals("i", concrete.getFields().get(1).getName()); - assertEquals("int", concrete.getFields().get(1).getType()); - assertEquals("squared", concrete.getFields().get(2).getName()); - assertEquals("string", concrete.getFields().get(2).getType()); - - assertEquals(0, AMQPSchemaExtensions.carpenterSchema(schema, ClassLoader.getSystemClassLoader()).getSize()); - Schema mangledSchema = ClassCarpenterTestUtilsKt.mangleNames(schema, singletonList(C.class.getTypeName())); - CarpenterMetaSchema l2 = AMQPSchemaExtensions.carpenterSchema(mangledSchema, ClassLoader.getSystemClassLoader()); - String mangledClassName = ClassCarpenterTestUtilsKt.mangleName(C.class.getTypeName()); - - assertEquals(1, l2.getSize()); - net.corda.serialization.internal.carpenter.Schema carpenterSchema = l2.getCarpenterSchemas().stream() - .filter(s -> s.getName().equals(mangledClassName)) + TypeIdentifier typeToMangle = TypeIdentifier.Companion.forClass(C.class); + Envelope env = objAndEnv.getEnvelope(); + RemoteTypeInformation typeInformation = getTypeInformation(env).values().stream() + .filter(it -> it.getTypeIdentifier().equals(typeToMangle)) .findFirst() - .orElseThrow(() -> new IllegalStateException("No schema found for mangled class name " + mangledClassName)); + .orElseThrow(IllegalStateException::new); - Class pinochio = new ClassCarpenterImpl(AllWhitelist.INSTANCE).build(carpenterSchema); + RemoteTypeInformation renamed = rename(typeInformation, typeToMangle, mangle(typeToMangle)); + + Class pinochio = load(renamed); Object p = pinochio.getConstructors()[0].newInstance(4, 2, "4"); - assertEquals(pinochio.getMethod("getI").invoke(p), amqpObj.getI()); - assertEquals(pinochio.getMethod("getSquared").invoke(p), amqpObj.getSquared()); - assertEquals(pinochio.getMethod("getDoubled").invoke(p), amqpObj.getDoubled()); + assertEquals(2, pinochio.getMethod("getI").invoke(p)); + assertEquals("4", pinochio.getMethod("getSquared").invoke(p)); + assertEquals(4, pinochio.getMethod("getDoubled").invoke(p)); Parent upcast = (Parent) p; - assertEquals(upcast.getDoubled(), amqpObj.getDoubled()); + assertEquals(4, upcast.getDoubled()); } } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/ListsSerializationTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/ListsSerializationTest.kt index 0164664800..55c0254095 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/ListsSerializationTest.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/ListsSerializationTest.kt @@ -88,7 +88,7 @@ class ListsSerializationTest { payload.add(2) val wrongPayloadType = WrongPayloadType(payload) Assertions.assertThatThrownBy { wrongPayloadType.serialize() } - .isInstanceOf(NotSerializableException::class.java).hasMessageContaining("Cannot derive collection type for declaredType") + .isInstanceOf(NotSerializableException::class.java).hasMessageContaining("Cannot derive collection type for declared type") } @CordaSerializable @@ -107,7 +107,9 @@ class ListsSerializationTest { val container = CovariantContainer(payload) fun verifyEnvelopeBody(envelope: Envelope) { - envelope.schema.types.single { typeNotation -> typeNotation.name == java.util.List::class.java.name + "" } + envelope.schema.types.single { typeNotation -> + typeNotation.name == "java.util.List<${Parent::class.java.name}>" + } } assertEqualAfterRoundTripSerialization(container, { bytes -> verifyEnvelope(bytes, ::verifyEnvelopeBody) }) diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/AbstractAMQPSerializationSchemeTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/AbstractAMQPSerializationSchemeTest.kt index 8c38cbeb26..eaaeffb043 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/AbstractAMQPSerializationSchemeTest.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/AbstractAMQPSerializationSchemeTest.kt @@ -37,7 +37,7 @@ class AbstractAMQPSerializationSchemeTest { null) - val factory = TestSerializerFactory(TESTING_CONTEXT.whitelist, TESTING_CONTEXT.deserializationClassLoader) + val factory = SerializerFactoryBuilder.build(TESTING_CONTEXT.whitelist, TESTING_CONTEXT.deserializationClassLoader) val maxFactories = 512 val backingMap = AccessOrderLinkedHashMap, SerializerFactory>({ maxFactories }) val scheme = object : AbstractAMQPSerializationScheme(emptySet(), backingMap, createSerializerFactoryFactory()) { @@ -55,7 +55,6 @@ class AbstractAMQPSerializationSchemeTest { } - IntStream.range(0, 2048).parallel().forEach { val context = if (ThreadLocalRandom.current().nextBoolean()) { genesisContext.withClassLoader(URLClassLoader(emptyArray())) diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/CorDappSerializerTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/CorDappSerializerTests.kt index d38187f6b7..d8c263d113 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/CorDappSerializerTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/CorDappSerializerTests.kt @@ -13,16 +13,13 @@ import kotlin.test.assertEquals class CorDappSerializerTests { data class NeedsProxy(val a: String) - private fun proxyFactory(serializers: List>): SerializerFactory { - val factory = SerializerFactoryBuilder.build(AllWhitelist, - ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader()), - DefaultEvolutionSerializerProvider) - + private fun proxyFactory( + serializers: List> + ) = SerializerFactoryBuilder.build(AllWhitelist, + ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader())).apply { serializers.forEach { - factory.registerExternal(CorDappCustomSerializer(it, factory)) + registerExternal(CorDappCustomSerializer(it, this)) } - - return factory } class NeedsProxyProxySerializer : SerializationCustomSerializer { diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializeNeedingCarpentryOfEnumsTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializeNeedingCarpentryOfEnumsTest.kt index 5520d3ea6c..7fc19130e5 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializeNeedingCarpentryOfEnumsTest.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializeNeedingCarpentryOfEnumsTest.kt @@ -20,15 +20,15 @@ class DeserializeNeedingCarpentryOfEnumsTest : AmqpCarpenterBase(AllWhitelist) { // Setup the test // val setupFactory = testDefaultFactoryNoEvolution() - + val classCarpenter = ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader()) val enumConstants = listOf("AAA", "BBB", "CCC", "DDD", "EEE", "FFF", "GGG", "HHH", "III", "JJJ").associateBy({ it }, { EnumField() }) // create the enum - val testEnumType = setupFactory.classCarpenter.build(EnumSchema("test.testEnumType", enumConstants)) + val testEnumType = classCarpenter.build(EnumSchema("test.testEnumType", enumConstants)) // create the class that has that enum as an element - val testClassType = setupFactory.classCarpenter.build(ClassSchema("test.testClassType", + val testClassType = classCarpenter.build(ClassSchema("test.testClassType", mapOf("a" to NonNullableField(testEnumType)))) // create an instance of the class we can then serialise @@ -59,16 +59,16 @@ class DeserializeNeedingCarpentryOfEnumsTest : AmqpCarpenterBase(AllWhitelist) { // Setup the test // val setupFactory = testDefaultFactoryNoEvolution() - + val classCarpenter = ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader()) val enumConstants = listOf("AAA", "BBB", "CCC", "DDD", "EEE", "FFF", "GGG", "HHH", "III", "JJJ").associateBy({ it }, { EnumField() }) // create the enum - val testEnumType1 = setupFactory.classCarpenter.build(EnumSchema("test.testEnumType1", enumConstants)) - val testEnumType2 = setupFactory.classCarpenter.build(EnumSchema("test.testEnumType2", enumConstants)) + val testEnumType1 = classCarpenter.build(EnumSchema("test.testEnumType1", enumConstants)) + val testEnumType2 = classCarpenter.build(EnumSchema("test.testEnumType2", enumConstants)) // create the class that has that enum as an element - val testClassType = setupFactory.classCarpenter.build(ClassSchema("test.testClassType", + val testClassType = classCarpenter.build(ClassSchema("test.testClassType", mapOf( "a" to NonNullableField(testEnumType1), "b" to NonNullableField(testEnumType2), diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializeNeedingCarpentrySimpleTypesTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializeNeedingCarpentrySimpleTypesTest.kt index d8e3fc57df..5b00c4bf30 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializeNeedingCarpentrySimpleTypesTest.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializeNeedingCarpentrySimpleTypesTest.kt @@ -441,7 +441,4 @@ class DeserializeNeedingCarpentrySimpleTypesTest : AmqpCarpenterBase(AllWhitelis assertEquals(0b1010.toByte(), deserializedObj::class.java.getMethod("getByteB").invoke(deserializedObj)) assertEquals(null, deserializedObj::class.java.getMethod("getByteC").invoke(deserializedObj)) } -} - - - +} \ No newline at end of file diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializeSimpleTypesTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializeSimpleTypesTests.kt index 6dc46436b9..e419963a2b 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializeSimpleTypesTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializeSimpleTypesTests.kt @@ -74,7 +74,7 @@ class DeserializeSimpleTypesTests { val ia = IA(arrayOf(1, 2, 3)) assertEquals("class [Ljava.lang.Integer;", ia.ia::class.java.toString()) - assertEquals(SerializerFactory.nameForType(ia.ia::class.java), "int[]") + assertEquals(AMQPTypeIdentifiers.nameForType(ia.ia::class.java), "int[]") val serialisedIA = TestSerializationOutput(VERBOSE, sf1).serialize(ia) val deserializedIA = DeserializationInput(sf1).deserialize(serialisedIA) @@ -93,7 +93,7 @@ class DeserializeSimpleTypesTests { val ia = IA(arrayOf(Integer(1), Integer(2), Integer(3))) assertEquals("class [Ljava.lang.Integer;", ia.ia::class.java.toString()) - assertEquals(SerializerFactory.nameForType(ia.ia::class.java), "int[]") + assertEquals(AMQPTypeIdentifiers.nameForType(ia.ia::class.java), "int[]") val serialisedIA = TestSerializationOutput(VERBOSE, sf1).serialize(ia) val deserializedIA = DeserializationInput(sf1).deserialize(serialisedIA) @@ -116,7 +116,7 @@ class DeserializeSimpleTypesTests { val ia = IA(v) assertEquals("class [I", ia.ia::class.java.toString()) - assertEquals(SerializerFactory.nameForType(ia.ia::class.java), "int[p]") + assertEquals(AMQPTypeIdentifiers.nameForType(ia.ia::class.java), "int[p]") val serialisedIA = TestSerializationOutput(VERBOSE, sf1).serialize(ia) val deserializedIA = DeserializationInput(sf1).deserialize(serialisedIA) @@ -134,7 +134,7 @@ class DeserializeSimpleTypesTests { val c = C(arrayOf('a', 'b', 'c')) assertEquals("class [Ljava.lang.Character;", c.c::class.java.toString()) - assertEquals(SerializerFactory.nameForType(c.c::class.java), "char[]") + assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "char[]") val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c) val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) @@ -154,7 +154,7 @@ class DeserializeSimpleTypesTests { val c = C(v) assertEquals("class [C", c.c::class.java.toString()) - assertEquals(SerializerFactory.nameForType(c.c::class.java), "char[p]") + assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "char[p]") val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c) var deserializedC = DeserializationInput(sf1).deserialize(serialisedC) @@ -183,7 +183,7 @@ class DeserializeSimpleTypesTests { val c = C(arrayOf(true, false, false, true)) assertEquals("class [Ljava.lang.Boolean;", c.c::class.java.toString()) - assertEquals(SerializerFactory.nameForType(c.c::class.java), "boolean[]") + assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "boolean[]") val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c) val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) @@ -203,7 +203,7 @@ class DeserializeSimpleTypesTests { c.c[0] = true; c.c[1] = false; c.c[2] = false; c.c[3] = true assertEquals("class [Z", c.c::class.java.toString()) - assertEquals(SerializerFactory.nameForType(c.c::class.java), "boolean[p]") + assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "boolean[p]") val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c) val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) @@ -222,7 +222,7 @@ class DeserializeSimpleTypesTests { val c = C(arrayOf(0b0001, 0b0101, 0b1111)) assertEquals("class [Ljava.lang.Byte;", c.c::class.java.toString()) - assertEquals(SerializerFactory.nameForType(c.c::class.java), "byte[]") + assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "byte[]") val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c) val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) @@ -241,7 +241,7 @@ class DeserializeSimpleTypesTests { c.c[0] = 0b0001; c.c[1] = 0b0101; c.c[2] = 0b1111 assertEquals("class [B", c.c::class.java.toString()) - assertEquals(SerializerFactory.nameForType(c.c::class.java), "binary") + assertEquals("binary", AMQPTypeIdentifiers.nameForType(c.c::class.java)) val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c) val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) @@ -267,7 +267,7 @@ class DeserializeSimpleTypesTests { val c = C(arrayOf(1, 2, 3)) assertEquals("class [Ljava.lang.Short;", c.c::class.java.toString()) - assertEquals(SerializerFactory.nameForType(c.c::class.java), "short[]") + assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "short[]") val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c) val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) @@ -286,7 +286,7 @@ class DeserializeSimpleTypesTests { c.c[0] = 1; c.c[1] = 2; c.c[2] = 5 assertEquals("class [S", c.c::class.java.toString()) - assertEquals(SerializerFactory.nameForType(c.c::class.java), "short[p]") + assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "short[p]") val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c) val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) @@ -304,7 +304,7 @@ class DeserializeSimpleTypesTests { val c = C(arrayOf(2147483650, -2147483800, 10)) assertEquals("class [Ljava.lang.Long;", c.c::class.java.toString()) - assertEquals(SerializerFactory.nameForType(c.c::class.java), "long[]") + assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "long[]") val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c) val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) @@ -323,7 +323,7 @@ class DeserializeSimpleTypesTests { c.c[0] = 2147483650; c.c[1] = -2147483800; c.c[2] = 10 assertEquals("class [J", c.c::class.java.toString()) - assertEquals(SerializerFactory.nameForType(c.c::class.java), "long[p]") + assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "long[p]") val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c) val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) @@ -341,7 +341,7 @@ class DeserializeSimpleTypesTests { val c = C(arrayOf(10F, 100.023232F, -1455.433400F)) assertEquals("class [Ljava.lang.Float;", c.c::class.java.toString()) - assertEquals(SerializerFactory.nameForType(c.c::class.java), "float[]") + assertEquals("float[]", AMQPTypeIdentifiers.nameForType(c.c::class.java)) val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c) val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) @@ -360,7 +360,7 @@ class DeserializeSimpleTypesTests { c.c[0] = 10F; c.c[1] = 100.023232F; c.c[2] = -1455.433400F assertEquals("class [F", c.c::class.java.toString()) - assertEquals(SerializerFactory.nameForType(c.c::class.java), "float[p]") + assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "float[p]") val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c) val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) @@ -378,7 +378,7 @@ class DeserializeSimpleTypesTests { val c = C(arrayOf(10.0, 100.2, -1455.2)) assertEquals("class [Ljava.lang.Double;", c.c::class.java.toString()) - assertEquals(SerializerFactory.nameForType(c.c::class.java), "double[]") + assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "double[]") val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c) val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) @@ -397,7 +397,7 @@ class DeserializeSimpleTypesTests { c.c[0] = 10.0; c.c[1] = 100.2; c.c[2] = -1455.2 assertEquals("class [D", c.c::class.java.toString()) - assertEquals(SerializerFactory.nameForType(c.c::class.java), "double[p]") + assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "double[p]") val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c) val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializedParameterizedTypeTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializedParameterizedTypeTests.kt deleted file mode 100644 index e2e718cc8d..0000000000 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializedParameterizedTypeTests.kt +++ /dev/null @@ -1,105 +0,0 @@ -package net.corda.serialization.internal.amqp - -import org.junit.Test -import java.io.NotSerializableException -import kotlin.test.assertEquals - -class DeserializedParameterizedTypeTests { - private fun normalise(string: String): String { - return string.replace(" ", "") - } - - private fun verify(typeName: String) { - val type = DeserializedParameterizedType.make(typeName) - assertEquals(normalise(type.typeName), normalise(typeName)) - } - - @Test - fun `test nested`() { - verify(" java.util.Map < java.util.Map< java.lang.String, java.lang.Integer >, java.util.Map < java.lang.Long , java.lang.String > >") - } - - @Test - fun `test simple`() { - verify("java.util.List") - } - - @Test - fun `test multiple args`() { - verify("java.util.Map") - } - - @Test - fun `test trailing whitespace`() { - verify("java.util.Map ") - } - - @Test - fun `test list of commands`() { - verify("java.util.List>") - } - - @Test(expected = NotSerializableException::class) - fun `test trailing text`() { - verify("java.util.Mapfoo") - } - - @Test(expected = NotSerializableException::class) - fun `test trailing comma`() { - verify("java.util.Map") - } - - @Test(expected = NotSerializableException::class) - fun `test leading comma`() { - verify("java.util.Map<,java.lang.String, java.lang.Integer>") - } - - @Test(expected = NotSerializableException::class) - fun `test middle comma`() { - verify("java.util.Map<,java.lang.String,, java.lang.Integer>") - } - - @Test(expected = NotSerializableException::class) - fun `test trailing close`() { - verify("java.util.Map>") - } - - @Test(expected = NotSerializableException::class) - fun `test empty params`() { - verify("java.util.Map<>") - } - - @Test(expected = NotSerializableException::class) - fun `test mid whitespace`() { - verify("java.u til.List") - } - - @Test(expected = NotSerializableException::class) - fun `test mid whitespace2`() { - verify("java.util.List") - } - - @Test(expected = NotSerializableException::class) - fun `test wrong number of parameters`() { - verify("java.util.List") - } - - @Test - fun `test no parameters`() { - verify("java.lang.String") - } - - @Test(expected = NotSerializableException::class) - fun `test parameters on non-generic type`() { - verify("java.lang.String") - } - - @Test(expected = NotSerializableException::class) - fun `test excessive nesting`() { - var nested = "java.lang.Integer" - for (i in 1..DeserializedParameterizedType.MAX_DEPTH) { - nested = "java.util.List<$nested>" - } - verify(nested) - } -} \ No newline at end of file diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumEvolvabilityTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumEvolvabilityTests.kt index 03978266b6..ef0c6d38c4 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumEvolvabilityTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumEvolvabilityTests.kt @@ -392,27 +392,10 @@ class EnumEvolvabilityTests { data class C1(val annotatedEnum: AnnotatedEnumOnce) val sf = testDefaultFactory() - val f = sf.javaClass.getDeclaredField("transformsCache") - f.isAccessible = true - - @Suppress("UNCHECKED_CAST") - val transformsCache = f.get(sf) as ConcurrentHashMap>> - - assertEquals(0, transformsCache.size) val sb1 = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C1(AnnotatedEnumOnce.D)) - - assertEquals(2, transformsCache.size) - assertTrue(transformsCache.containsKey(C1::class.java.name)) - assertTrue(transformsCache.containsKey(AnnotatedEnumOnce::class.java.name)) - val sb2 = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C2(AnnotatedEnumOnce.D)) - assertEquals(3, transformsCache.size) - assertTrue(transformsCache.containsKey(C1::class.java.name)) - assertTrue(transformsCache.containsKey(C2::class.java.name)) - assertTrue(transformsCache.containsKey(AnnotatedEnumOnce::class.java.name)) - assertEquals(sb1.transformsSchema.types[AnnotatedEnumOnce::class.java.name], sb2.transformsSchema.types[AnnotatedEnumOnce::class.java.name]) } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerFactoryTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerFactoryTests.kt new file mode 100644 index 0000000000..0dda90607b --- /dev/null +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerFactoryTests.kt @@ -0,0 +1,55 @@ +package net.corda.serialization.internal.amqp + +import net.corda.serialization.internal.amqp.testutils.deserializeAndReturnEnvelope +import net.corda.serialization.internal.amqp.testutils.serialize +import net.corda.serialization.internal.amqp.testutils.testDefaultFactory +import net.corda.serialization.internal.model.RemoteTypeInformation +import net.corda.serialization.internal.model.TypeIdentifier +import org.junit.Test +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class EvolutionSerializerFactoryTests { + + private val factory = testDefaultFactory() + + @Test + fun preservesDataWhenFlagSet() { + val nonStrictEvolutionSerializerFactory = DefaultEvolutionSerializerFactory( + factory, + ClassLoader.getSystemClassLoader(), + false) + + val strictEvolutionSerializerFactory = DefaultEvolutionSerializerFactory( + factory, + ClassLoader.getSystemClassLoader(), + true) + + @Suppress("unused") + class C(val importantFieldA: Int) + val (_, env) = DeserializationInput(factory).deserializeAndReturnEnvelope( + SerializationOutput(factory).serialize(C(1))) + + val remoteTypeInformation = AMQPRemoteTypeModel().interpret(SerializationSchemas(env.schema, env.transformsSchema)) + .values.find { it.typeIdentifier == TypeIdentifier.forClass(C::class.java) } + as RemoteTypeInformation.Composable + + val withAddedField = remoteTypeInformation.copy(properties = remoteTypeInformation.properties.plus( + "importantFieldB" to remoteTypeInformation.properties["importantFieldA"]!!)) + + val localTypeInformation = factory.getTypeInformation(C::class.java) + + // No evolution required with original fields. + assertNull(strictEvolutionSerializerFactory.getEvolutionSerializer(remoteTypeInformation, localTypeInformation)) + + // Returns an evolution serializer if the fields have changed. + assertNotNull(nonStrictEvolutionSerializerFactory.getEvolutionSerializer(withAddedField, localTypeInformation)) + + // Fails in strict mode if the remote type information includes a field not included in the local type. + assertFailsWith { + strictEvolutionSerializerFactory.getEvolutionSerializer(withAddedField, localTypeInformation) + } + } + +} \ No newline at end of file diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerProviderTesting.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerProviderTesting.kt deleted file mode 100644 index 7239e6a467..0000000000 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerProviderTesting.kt +++ /dev/null @@ -1,22 +0,0 @@ -package net.corda.serialization.internal.amqp - -import java.io.NotSerializableException - -/** - * An implementation of [EvolutionSerializerProvider] that disables all evolution within a - * [SerializerFactory]. This is most useful in testing where it is known that evolution should not be - * occurring and where bugs may be hidden by transparent invocation of an [EvolutionSerializer]. This - * prevents that by simply throwing an exception whenever such a serializer is requested. - */ -object FailIfEvolutionAttempted : EvolutionSerializerProvider { - override fun getEvolutionSerializer(factory: SerializerFactory, - typeNotation: TypeNotation, - newSerializer: AMQPSerializer, - schemas: SerializationSchemas): AMQPSerializer { - throw NotSerializableException("No evolution should be occurring\n" + - " ${typeNotation.name}\n" + - " ${typeNotation.descriptor.name}\n" + - " ${newSerializer.type.typeName}\n" + - " ${newSerializer.typeDescriptor}\n\n${schemas.schema}") - } -} diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolvabilityTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolvabilityTests.kt index e0109515d6..ac35801b27 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolvabilityTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolvabilityTests.kt @@ -461,6 +461,17 @@ class EvolvabilityTests { assertEquals(oa, outer.a) assertEquals(ia, outer.b.a) assertEquals(null, outer.b.b) + + // Repeat, but receiving a message with the newer version of Inner + val newVersion = SerializationOutput(sf).serializeAndReturnSchema(Outer(oa, Inner(ia, "new value"))) + val model = AMQPRemoteTypeModel() + val remoteTypeInfo = model.interpret(SerializationSchemas(newVersion.schema, newVersion.transformsSchema)) + println(remoteTypeInfo) + + val newOuter = DeserializationInput(sf).deserialize(SerializedBytes(newVersion.obj.bytes)) + assertEquals(oa, newOuter.a) + assertEquals(ia, newOuter.b.a) + assertEquals("new value", newOuter.b.b) } @Test diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/FingerPrinterTesting.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/FingerPrinterTesting.kt index 1e5f7c3d82..cad38afa53 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/FingerPrinterTesting.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/FingerPrinterTesting.kt @@ -1,23 +1,25 @@ package net.corda.serialization.internal.amqp import org.junit.Test -import java.lang.reflect.Type import kotlin.test.assertEquals import net.corda.serialization.internal.AllWhitelist import net.corda.serialization.internal.amqp.testutils.TestSerializationOutput import net.corda.serialization.internal.amqp.testutils.serializeAndReturnSchema import net.corda.serialization.internal.carpenter.ClassCarpenterImpl +import net.corda.serialization.internal.model.ConfigurableLocalTypeModel +import net.corda.serialization.internal.model.LocalTypeInformation +import net.corda.serialization.internal.model.FingerPrinter class FingerPrinterTesting : FingerPrinter { private var index = 0 - private val cache = mutableMapOf() + private val cache = mutableMapOf() - override fun fingerprint(type: Type): String { - return cache.computeIfAbsent(type) { index++.toString() } + override fun fingerprint(typeInformation: LocalTypeInformation): String { + return cache.computeIfAbsent(typeInformation) { index++.toString() } } @Suppress("UNUSED") - fun changeFingerprint(type: Type) { + fun changeFingerprint(type: LocalTypeInformation) { cache.computeIfAbsent(type) { "" }.apply { index++.toString() } } } @@ -30,10 +32,14 @@ class FingerPrinterTestingTests { @Test fun testingTest() { val fpt = FingerPrinterTesting() - assertEquals("0", fpt.fingerprint(Integer::class.java)) - assertEquals("1", fpt.fingerprint(String::class.java)) - assertEquals("0", fpt.fingerprint(Integer::class.java)) - assertEquals("1", fpt.fingerprint(String::class.java)) + val descriptorBasedSerializerRegistry = DefaultDescriptorBasedSerializerRegistry() + val customSerializerRegistry: CustomSerializerRegistry = CachingCustomSerializerRegistry(descriptorBasedSerializerRegistry) + val typeModel = ConfigurableLocalTypeModel(WhitelistBasedTypeModelConfiguration(AllWhitelist, customSerializerRegistry)) + + assertEquals("0", fpt.fingerprint(typeModel.inspect(Integer::class.java))) + assertEquals("1", fpt.fingerprint(typeModel.inspect(String::class.java))) + assertEquals("0", fpt.fingerprint(typeModel.inspect(Integer::class.java))) + assertEquals("1", fpt.fingerprint(typeModel.inspect(String::class.java))) } @Test @@ -42,7 +48,7 @@ class FingerPrinterTestingTests { val factory = SerializerFactoryBuilder.build(AllWhitelist, ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader()), - fingerPrinterProvider = { _ -> FingerPrinterTesting() }) + overrideFingerPrinter = FingerPrinterTesting()) val blob = TestSerializationOutput(VERBOSE, factory).serializeAndReturnSchema(C(1, 2L)) diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/GenericsTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/GenericsTests.kt index 544d467bd8..c85fc2a343 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/GenericsTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/GenericsTests.kt @@ -40,15 +40,6 @@ class GenericsTests { private fun BytesAndSchemas.printSchema() = if (VERBOSE) println("${this.schema}\n") else Unit - private fun MutableMap>.printKeyToType() { - if (!VERBOSE) return - - forEach { - println("Key = ${it.key} - ${it.value.type.typeName}") - } - println() - } - @Test fun twoDifferentTypesSameParameterizedOuter() { data class G(val a: A) @@ -57,12 +48,8 @@ class GenericsTests { val bytes1 = SerializationOutput(factory).serializeAndReturnSchema(G("hi")).apply { printSchema() } - factory.serializersByDescriptor.printKeyToType() - val bytes2 = SerializationOutput(factory).serializeAndReturnSchema(G(121)).apply { printSchema() } - factory.serializersByDescriptor.printKeyToType() - listOf(factory, testDefaultFactory()).forEach { f -> DeserializationInput(f).deserialize(bytes1.obj).apply { assertEquals("hi", this.a) } DeserializationInput(f).deserialize(bytes2.obj).apply { assertEquals(121, this.a) } @@ -94,15 +81,11 @@ class GenericsTests { val bytes = ser.serializeAndReturnSchema(G("hi")).apply { printSchema() } - factory.serializersByDescriptor.printKeyToType() - assertEquals("hi", DeserializationInput(factory).deserialize(bytes.obj).a) assertEquals("hi", DeserializationInput(altContextFactory).deserialize(bytes.obj).a) val bytes2 = ser.serializeAndReturnSchema(Wrapper(1, G("hi"))).apply { printSchema() } - factory.serializersByDescriptor.printKeyToType() - printSeparator() DeserializationInput(factory).deserialize(bytes2.obj).apply { @@ -161,21 +144,18 @@ class GenericsTests { ser.serialize(Wrapper(Container(InnerA(1)))).apply { factories.forEach { DeserializationInput(it).deserialize(this).apply { assertEquals(1, c.b.a_a) } - it.serializersByDescriptor.printKeyToType(); printSeparator() } } ser.serialize(Wrapper(Container(InnerB(1)))).apply { factories.forEach { DeserializationInput(it).deserialize(this).apply { assertEquals(1, c.b.a_b) } - it.serializersByDescriptor.printKeyToType(); printSeparator() } } ser.serialize(Wrapper(Container(InnerC("Ho ho ho")))).apply { factories.forEach { DeserializationInput(it).deserialize(this).apply { assertEquals("Ho ho ho", c.b.a_c) } - it.serializersByDescriptor.printKeyToType(); printSeparator() } } } @@ -217,7 +197,6 @@ class GenericsTests { ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader()) )): SerializedBytes<*> { val bytes = SerializationOutput(factory).serializeAndReturnSchema(a) - factory.serializersByDescriptor.printKeyToType() bytes.printSchema() return bytes.obj } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/PrivatePropertyTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/PrivatePropertyTests.kt index b4012b5c00..43c248c360 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/PrivatePropertyTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/PrivatePropertyTests.kt @@ -3,19 +3,20 @@ package net.corda.serialization.internal.amqp import junit.framework.TestCase.assertTrue import junit.framework.TestCase.assertEquals import net.corda.core.serialization.ConstructorForDeserialization -import net.corda.serialization.internal.amqp.testutils.deserialize -import net.corda.serialization.internal.amqp.testutils.serializeAndReturnSchema -import net.corda.serialization.internal.amqp.testutils.serialize -import net.corda.serialization.internal.amqp.testutils.testDefaultFactoryNoEvolution +import net.corda.serialization.internal.amqp.testutils.* +import net.corda.serialization.internal.model.ConfigurableLocalTypeModel +import net.corda.serialization.internal.model.LocalPropertyInformation +import net.corda.serialization.internal.model.LocalTypeInformation import org.junit.Test -import org.apache.qpid.proton.amqp.Symbol import org.assertj.core.api.Assertions import java.io.NotSerializableException -import java.util.concurrent.ConcurrentHashMap import java.util.* class PrivatePropertyTests { - private val factory = testDefaultFactoryNoEvolution() + + private val registry = TestDescriptorBasedSerializerRegistry() + private val factory = testDefaultFactoryNoEvolution(registry) + val typeModel = ConfigurableLocalTypeModel(WhitelistBasedTypeModelConfiguration(factory.whitelist, factory)) @Test fun testWithOnePrivateProperty() { @@ -125,21 +126,13 @@ class PrivatePropertyTests { val schemaAndBlob = SerializationOutput(factory).serializeAndReturnSchema(c1) assertEquals(1, schemaAndBlob.schema.types.size) - val serializersByDescriptor = factory.serializersByDescriptor + val typeInformation = typeModel.inspect(C::class.java) + assertTrue(typeInformation is LocalTypeInformation.Composable) + typeInformation as LocalTypeInformation.Composable - val schemaDescriptor = schemaAndBlob.schema.types.first().descriptor.name - serializersByDescriptor.filterKeys { (it as Symbol) == schemaDescriptor }.values.apply { - assertEquals(1, this.size) - assertTrue(this.first() is ObjectSerializer) - val propertySerializers = (this.first() as ObjectSerializer).propertySerializers.serializationOrder.map { it.serializer } - assertEquals(2, propertySerializers.size) - // a was public so should have a synthesised getter - assertTrue(propertySerializers[0].propertyReader is PublicPropertyReader) - - // b is private and thus won't have teh getter so we'll have reverted - // to using reflection to remove the inaccessible property - assertTrue(propertySerializers[1].propertyReader is PrivatePropertyReader) - } + assertEquals(2, typeInformation.properties.size) + assertTrue(typeInformation.properties["a"] is LocalPropertyInformation.ConstructorPairedProperty) + assertTrue(typeInformation.properties["b"] is LocalPropertyInformation.PrivateConstructorPairedProperty) } @Test @@ -153,22 +146,14 @@ class PrivatePropertyTests { val schemaAndBlob = SerializationOutput(factory).serializeAndReturnSchema(c1) assertEquals(1, schemaAndBlob.schema.types.size) - val serializersByDescriptor = factory.serializersByDescriptor - val schemaDescriptor = schemaAndBlob.schema.types.first().descriptor.name - serializersByDescriptor.filterKeys { (it as Symbol) == schemaDescriptor }.values.apply { - assertEquals(1, this.size) - assertTrue(this.first() is ObjectSerializer) - val propertySerializers = (this.first() as ObjectSerializer).propertySerializers.serializationOrder.map { it.serializer } - assertEquals(2, propertySerializers.size) + val typeInformation = typeModel.inspect(C::class.java) + assertTrue(typeInformation is LocalTypeInformation.Composable) + typeInformation as LocalTypeInformation.Composable - // as before, a is public so we'll use the getter method - assertTrue(propertySerializers[0].propertyReader is PublicPropertyReader) - - // the getB() getter explicitly added means we should use the "normal" public - // method reader rather than the private oen - assertTrue(propertySerializers[1].propertyReader is PublicPropertyReader) - } + assertEquals(2, typeInformation.properties.size) + assertTrue(typeInformation.properties["a"] is LocalPropertyInformation.ConstructorPairedProperty) + assertTrue(typeInformation.properties["b"] is LocalPropertyInformation.ConstructorPairedProperty) } @Suppress("UNCHECKED_CAST") @@ -179,9 +164,8 @@ class PrivatePropertyTests { val c1 = Outer(Inner(1010101)) val output = SerializationOutput(factory).serializeAndReturnSchema(c1) - println (output.schema) - val serializersByDescriptor = factory.serializersByDescriptor + val serializersByDescriptor = registry.contents // Inner and Outer assertEquals(2, serializersByDescriptor.size) @@ -198,24 +182,13 @@ class PrivatePropertyTests { @Test fun allCapsProprtyNotPrivate() { data class C (val CCC: String) + val typeInformation = typeModel.inspect(C::class.java) - val output = SerializationOutput(factory).serializeAndReturnSchema(C("this is nice")) + assertTrue(typeInformation is LocalTypeInformation.Composable) + typeInformation as LocalTypeInformation.Composable - val serializersByDescriptor = factory.serializersByDescriptor - - val schemaDescriptor = output.schema.types.first().descriptor.name - serializersByDescriptor.filterKeys { (it as Symbol) == schemaDescriptor }.values.apply { - assertEquals(1, size) - - assertTrue(this.first() is ObjectSerializer) - val propertySerializers = (this.first() as ObjectSerializer).propertySerializers.serializationOrder.map { it.serializer } - - // CCC is the only property to be serialised - assertEquals(1, propertySerializers.size) - - // and despite being all caps it should still be a public getter - assertTrue(propertySerializers[0].propertyReader is PublicPropertyReader) - } + assertEquals(1, typeInformation.properties.size) + assertTrue(typeInformation.properties["CCC"] is LocalPropertyInformation.ConstructorPairedProperty) } } \ No newline at end of file diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt index 0ee30b0373..3adba6ac32 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt @@ -21,7 +21,6 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.node.serialization.amqp.AMQPServerSerializationScheme import net.corda.nodeapi.internal.crypto.ContentSignerBuilder import net.corda.serialization.internal.* -import net.corda.serialization.internal.amqp.SerializerFactory.Companion.isPrimitive import net.corda.serialization.internal.amqp.testutils.* import net.corda.serialization.internal.carpenter.ClassCarpenterImpl import net.corda.testing.contracts.DummyContract @@ -210,7 +209,7 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi private fun defaultFactory(): SerializerFactory { return SerializerFactoryBuilder.build(AllWhitelist, ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader()), - evolutionSerializerProvider = FailIfEvolutionAttempted + allowEvolution = false ) } @@ -258,27 +257,27 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi @Test fun isPrimitive() { - assertTrue(isPrimitive(Character::class.java)) - assertTrue(isPrimitive(Boolean::class.java)) - assertTrue(isPrimitive(Byte::class.java)) - assertTrue(isPrimitive(UnsignedByte::class.java)) - assertTrue(isPrimitive(Short::class.java)) - assertTrue(isPrimitive(UnsignedShort::class.java)) - assertTrue(isPrimitive(Int::class.java)) - assertTrue(isPrimitive(UnsignedInteger::class.java)) - assertTrue(isPrimitive(Long::class.java)) - assertTrue(isPrimitive(UnsignedLong::class.java)) - assertTrue(isPrimitive(Float::class.java)) - assertTrue(isPrimitive(Double::class.java)) - assertTrue(isPrimitive(Decimal32::class.java)) - assertTrue(isPrimitive(Decimal64::class.java)) - assertTrue(isPrimitive(Decimal128::class.java)) - assertTrue(isPrimitive(Char::class.java)) - assertTrue(isPrimitive(Date::class.java)) - assertTrue(isPrimitive(UUID::class.java)) - assertTrue(isPrimitive(ByteArray::class.java)) - assertTrue(isPrimitive(String::class.java)) - assertTrue(isPrimitive(Symbol::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(Character::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(Boolean::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(Byte::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(UnsignedByte::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(Short::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(UnsignedShort::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(Int::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(UnsignedInteger::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(Long::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(UnsignedLong::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(Float::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(Double::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(Decimal32::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(Decimal64::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(Decimal128::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(Char::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(Date::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(UUID::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(ByteArray::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(String::class.java)) + assertTrue(AMQPTypeIdentifiers.isPrimitive(Symbol::class.java)) } @Test @@ -475,10 +474,11 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi @Test fun `class constructor is invoked on deserialisation`() { compression == null || return // Manipulation of serialized bytes is invalid if they're compressed. - val ser = SerializationOutput(SerializerFactoryBuilder.build(AllWhitelist, + val serializerFactory = SerializerFactoryBuilder.build(AllWhitelist, ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader()) - )) - val des = DeserializationInput(ser.serializerFactory) + ) + val ser = SerializationOutput(serializerFactory) + val des = DeserializationInput(serializerFactory) val serialisedOne = ser.serialize(NonZeroByte(1), compression).bytes val serialisedTwo = ser.serialize(NonZeroByte(2), compression).bytes diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationPropertyOrdering.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationPropertyOrdering.kt index 750a2f2c5c..5ffcfe2c90 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationPropertyOrdering.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationPropertyOrdering.kt @@ -1,12 +1,8 @@ package net.corda.serialization.internal.amqp import net.corda.core.serialization.ConstructorForDeserialization -import net.corda.serialization.internal.amqp.testutils.TestSerializationOutput -import net.corda.serialization.internal.amqp.testutils.deserialize -import net.corda.serialization.internal.amqp.testutils.serializeAndReturnSchema -import net.corda.serialization.internal.amqp.testutils.testDefaultFactoryNoEvolution +import net.corda.serialization.internal.amqp.testutils.* import org.junit.Test -import java.util.concurrent.ConcurrentHashMap import kotlin.test.assertEquals import org.apache.qpid.proton.amqp.Symbol import java.lang.reflect.Method @@ -17,7 +13,8 @@ class SerializationPropertyOrdering { companion object { val VERBOSE get() = false - val sf = testDefaultFactoryNoEvolution() + val registry = TestDescriptorBasedSerializerRegistry() + val sf = testDefaultFactoryNoEvolution(registry) } // Force object references to be ued to ensure we go through that code path @@ -100,25 +97,6 @@ class SerializationPropertyOrdering { assertEquals("e", this.fields[4].name) } - // Test needs to look at a bunch of private variables, change the access semantics for them - val fields : Map = mapOf ( - "setter" to PropertyAccessorGetterSetter::class.java.getDeclaredField("setter")).apply { - this.values.forEach { - it.isAccessible = true - } - } - - val serializersByDescriptor = sf.serializersByDescriptor - val schemaDescriptor = output.schema.types.first().descriptor.name - - // make sure that each property accessor has a setter to ensure we're using getter / setter instantiation - serializersByDescriptor.filterKeys { (it as Symbol) == schemaDescriptor }.values.apply { - assertEquals(1, this.size) - assertTrue(this.first() is ObjectSerializer) - val propertyAccessors = (this.first() as ObjectSerializer).propertySerializers.serializationOrder as List - propertyAccessors.forEach { property -> assertNotNull(fields["setter"]!!.get(property) as Method?) } - } - val input = DeserializationInput(sf).deserialize(output.obj) assertEquals(100, input.a) assertEquals(200, input.b) diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationSchemaTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationSchemaTests.kt index 7a56cbb953..a504b25a8c 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationSchemaTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationSchemaTests.kt @@ -17,87 +17,4 @@ val TESTING_CONTEXT = SerializationContextImpl(amqpMagic, emptyMap(), true, SerializationContext.UseCase.Testing, - null) - -// Test factory that lets us count the number of serializer registration attempts -class TestSerializerFactory( - wl: ClassWhitelist, - cl: ClassLoader -) : DefaultSerializerFactory(wl, ClassCarpenterImpl(wl, cl, false), DefaultEvolutionSerializerProvider, ::SerializerFingerPrinter) { - var registerCount = 0 - - override fun register(customSerializer: CustomSerializer) { - ++registerCount - return super.register(customSerializer) - } -} - -// Instance of our test factory counting registration attempts. Sucks its global, but for testing purposes this -// is the easiest way of getting access to the object. -val testFactory = TestSerializerFactory(TESTING_CONTEXT.whitelist, TESTING_CONTEXT.deserializationClassLoader) - -// Serializer factory factory, plugs into the SerializationScheme and controls which factory type -// we make for each use case. For our tests we need to make sure if its the Testing use case we return -// the global factory object created above that counts registrations. -class TestSerializerFactoryFactory : SerializerFactoryFactoryImpl() { - override fun make(context: SerializationContext) = - when (context.useCase) { - SerializationContext.UseCase.Testing -> testFactory - else -> super.make(context) - } -} - -class AMQPTestSerializationScheme : AbstractAMQPSerializationScheme(emptySet(), AccessOrderLinkedHashMap { 128 }, TestSerializerFactoryFactory()) { - override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory { - throw UnsupportedOperationException() - } - - override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory { - throw UnsupportedOperationException() - } - - override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase) = true -} - -// Test SerializationFactory that wraps a serialization scheme that just allows us to call .serialize. -// Returns the testing scheme we created above that wraps the testing factory. -class TestSerializationFactory : SerializationFactory() { - private val scheme = AMQPTestSerializationScheme() - - override fun deserialize( - byteSequence: ByteSequence, - clazz: Class, context: - SerializationContext - ): T { - throw UnsupportedOperationException() - } - - override fun deserializeWithCompatibleContext( - byteSequence: ByteSequence, - clazz: Class, - context: SerializationContext - ): ObjectWithCompatibleContext { - throw UnsupportedOperationException() - } - - override fun serialize(obj: T, context: SerializationContext) = scheme.serialize(obj, context) -} - -// The actual test -class SerializationSchemaTests { - @Test - fun onlyRegisterCustomSerializersOnce() { - @CordaSerializable - data class C(val a: Int) - - val c = C(1) - val testSerializationFactory = TestSerializationFactory() - val expectedCustomSerializerCount = 41 - - assertEquals(0, testFactory.registerCount) - c.serialize(testSerializationFactory, TESTING_CONTEXT) - assertEquals(expectedCustomSerializerCount, testFactory.registerCount) - c.serialize(testSerializationFactory, TESTING_CONTEXT) - assertEquals(expectedCustomSerializerCount, testFactory.registerCount) - } -} \ No newline at end of file + null) \ No newline at end of file diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/StaticInitialisationOfSerializedObjectTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/StaticInitialisationOfSerializedObjectTest.kt index f8450faf49..ab13ffa712 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/StaticInitialisationOfSerializedObjectTest.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/StaticInitialisationOfSerializedObjectTest.kt @@ -6,6 +6,7 @@ import net.corda.serialization.internal.AllWhitelist import net.corda.serialization.internal.amqp.testutils.deserialize import net.corda.serialization.internal.carpenter.ClassCarpenterImpl import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Ignore import org.junit.Test import java.io.NotSerializableException import java.lang.reflect.Type @@ -44,6 +45,7 @@ class StaticInitialisationOfSerializedObjectTest { C() } + @Ignore("Suppressing this, as it depends on obtaining internal access to serialiser cache") @Test fun kotlinObjectWithCompanionObject() { data class D(val c: C) @@ -63,7 +65,7 @@ class StaticInitialisationOfSerializedObjectTest { // build a serializer for type D without an instance of it to serialise, since // we can't actually construct one - sf.get(null, D::class.java) + sf.get(D::class.java) // post creation of the serializer we should have two elements in the map, this // proves we didn't statically construct an instance of C when building the serializer diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/testutils/AMQPTestUtils.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/testutils/AMQPTestUtils.kt index b248d11c2d..77dc08ab79 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/testutils/AMQPTestUtils.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/testutils/AMQPTestUtils.kt @@ -18,20 +18,45 @@ import java.io.File.separatorChar import java.io.NotSerializableException import java.nio.file.StandardCopyOption.REPLACE_EXISTING -fun testDefaultFactory() = SerializerFactoryBuilder.build(AllWhitelist, - ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader()) -) +/** + * For tests that want to see inside the serializer registry + */ +class TestDescriptorBasedSerializerRegistry : DescriptorBasedSerializerRegistry { + val contents = mutableMapOf>() -fun testDefaultFactoryNoEvolution(): SerializerFactory { - return SerializerFactoryBuilder.build( - AllWhitelist, - ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader()), - FailIfEvolutionAttempted) + override fun get(descriptor: String): AMQPSerializer? = contents[descriptor] + + override fun set(descriptor: String, serializer: AMQPSerializer) { + contents.putIfAbsent(descriptor, serializer) + } + + override fun getOrBuild(descriptor: String, builder: () -> AMQPSerializer): AMQPSerializer = + get(descriptor) ?: builder().also { set(descriptor, it) } } -fun testDefaultFactoryWithWhitelist() = SerializerFactoryBuilder.build(EmptyWhitelist, - ClassCarpenterImpl(EmptyWhitelist, ClassLoader.getSystemClassLoader()) -) +@JvmOverloads +fun testDefaultFactory(descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry = + DefaultDescriptorBasedSerializerRegistry()) = + SerializerFactoryBuilder.build( + AllWhitelist, + ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader()), + descriptorBasedSerializerRegistry = descriptorBasedSerializerRegistry) + +@JvmOverloads +fun testDefaultFactoryNoEvolution(descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry = + DefaultDescriptorBasedSerializerRegistry()): SerializerFactory = + SerializerFactoryBuilder.build( + AllWhitelist, + ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader()), + descriptorBasedSerializerRegistry = descriptorBasedSerializerRegistry, + allowEvolution = false) + +@JvmOverloads +fun testDefaultFactoryWithWhitelist(descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry = + DefaultDescriptorBasedSerializerRegistry()) = + SerializerFactoryBuilder.build(EmptyWhitelist, + ClassCarpenterImpl(EmptyWhitelist, ClassLoader.getSystemClassLoader()), + descriptorBasedSerializerRegistry = descriptorBasedSerializerRegistry) class TestSerializationOutput( private val verbose: Boolean, diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/CalculatedValuesToClassCarpenterTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/CalculatedValuesToClassCarpenterTests.kt deleted file mode 100644 index 893f81833b..0000000000 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/CalculatedValuesToClassCarpenterTests.kt +++ /dev/null @@ -1,101 +0,0 @@ -package net.corda.serialization.internal.carpenter - -import net.corda.core.serialization.SerializableCalculatedProperty -import net.corda.serialization.internal.AllWhitelist -import net.corda.serialization.internal.amqp.CompositeType -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.testutils.deserializeAndReturnEnvelope -import net.corda.serialization.internal.amqp.testutils.testDefaultFactoryNoEvolution -import org.junit.Test -import kotlin.test.assertEquals - -class CalculatedValuesToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) { - - interface Parent { - @get:SerializableCalculatedProperty - val doubled: Int - } - - @Test - fun calculatedValues() { - data class C(val i: Int): Parent { - @get:SerializableCalculatedProperty - val squared = (i * i).toString() - - override val doubled get() = i * 2 - } - - val factory = testDefaultFactoryNoEvolution() - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(C(2))) - val amqpObj = obj.obj - val serSchema = obj.envelope.schema - - assertEquals(2, amqpObj.i) - assertEquals("4", amqpObj.squared) - assertEquals(2, serSchema.types.size) - require(serSchema.types[0] is CompositeType) - - val concrete = serSchema.types[0] as CompositeType - assertEquals(3, concrete.fields.size) - assertEquals("doubled", concrete.fields[0].name) - assertEquals("int", concrete.fields[0].type) - assertEquals("i", concrete.fields[1].name) - assertEquals("int", concrete.fields[1].type) - assertEquals("squared", concrete.fields[2].name) - assertEquals("string", concrete.fields[2].type) - - val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) - assertEquals(0, l1.size) - val mangleSchema = serSchema.mangleNames(listOf((classTestName("C")))) - val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) - val aName = mangleName(classTestName("C")) - - assertEquals(1, l2.size) - val aSchema = l2.carpenterSchemas.find { it.name == aName }!! - - val pinochio = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema) - val p = pinochio.constructors[0].newInstance(4, 2, "4") - - assertEquals(pinochio.getMethod("getI").invoke(p), amqpObj.i) - assertEquals(pinochio.getMethod("getSquared").invoke(p), amqpObj.squared) - assertEquals(pinochio.getMethod("getDoubled").invoke(p), amqpObj.doubled) - - val upcast = p as Parent - assertEquals(upcast.doubled, amqpObj.doubled) - } - - @Test - fun implementingClassDoesNotCalculateValue() { - class C(override val doubled: Int): Parent - - val factory = testDefaultFactoryNoEvolution() - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(C(5))) - val amqpObj = obj.obj - val serSchema = obj.envelope.schema - - assertEquals(2, serSchema.types.size) - require(serSchema.types[0] is CompositeType) - - val concrete = serSchema.types[0] as CompositeType - assertEquals(1, concrete.fields.size) - assertEquals("doubled", concrete.fields[0].name) - assertEquals("int", concrete.fields[0].type) - - val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) - assertEquals(0, l1.size) - val mangleSchema = serSchema.mangleNames(listOf((classTestName("C")))) - val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) - val aName = mangleName(classTestName("C")) - - assertEquals(1, l2.size) - val aSchema = l2.carpenterSchemas.find { it.name == aName }!! - - val pinochio = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema) - val p = pinochio.constructors[0].newInstance(5) - - assertEquals(pinochio.getMethod("getDoubled").invoke(p), amqpObj.doubled) - - val upcast = p as Parent - assertEquals(upcast.doubled, amqpObj.doubled) - } -} \ No newline at end of file diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTest.kt index ac510642d7..d9a7b19fd6 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTest.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTest.kt @@ -27,7 +27,7 @@ class ClassCarpenterTest { @Test fun empty() { - val clazz = cc.build(ClassSchema("gen.EmptyClass", emptyMap(), null)) + val clazz = cc.build(ClassSchema("gen.EmptyClass", emptyMap())) assertEquals(0, clazz.nonSyntheticFields.size) assertEquals(2, clazz.nonSyntheticMethods.size) // get, toString assertEquals(0, clazz.declaredConstructors[0].parameterCount) @@ -97,8 +97,8 @@ class ClassCarpenterTest { @Test(expected = DuplicateNameException::class) fun duplicates() { - cc.build(ClassSchema("gen.EmptyClass", emptyMap(), null)) - cc.build(ClassSchema("gen.EmptyClass", emptyMap(), null)) + cc.build(ClassSchema("gen.EmptyClass", emptyMap())) + cc.build(ClassSchema("gen.EmptyClass", emptyMap())) } @Test diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTestUtils.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTestUtils.kt index 0068399ed6..0ec2fed1ab 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTestUtils.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTestUtils.kt @@ -1,41 +1,14 @@ package net.corda.serialization.internal.carpenter +import com.google.common.reflect.TypeToken import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.SerializedBytes import net.corda.serialization.internal.amqp.* -import net.corda.serialization.internal.amqp.Field -import net.corda.serialization.internal.amqp.Schema +import net.corda.serialization.internal.amqp.testutils.deserializeAndReturnEnvelope import net.corda.serialization.internal.amqp.testutils.serialize import net.corda.serialization.internal.amqp.testutils.testName - -fun mangleName(name: String) = "${name}__carpenter" - -/** - * given a list of class names work through the amqp envelope schema and alter any that - * match in the fashion defined above - */ -fun Schema.mangleNames(names: List): Schema { - val newTypes: MutableList = mutableListOf() - - for (type in types) { - val newName = if (type.name in names) mangleName(type.name) else type.name - val newProvides = type.provides.map { if (it in names) mangleName(it) else it } - val newFields = mutableListOf() - - (type as CompositeType).fields.forEach { - val fieldType = if (it.type in names) mangleName(it.type) else it.type - val requires = - if (it.requires.isNotEmpty() && (it.requires[0] in names)) listOf(mangleName(it.requires[0])) - else it.requires - - newFields.add(it.copy(type = fieldType, requires = requires)) - } - - newTypes.add(type.copy(name = newName, provides = newProvides, fields = newFields)) - } - - return Schema(types = newTypes) -} +import net.corda.serialization.internal.model.* +import org.junit.Assert.assertTrue /** * Custom implementation of a [SerializerFactory] where we need to give it a class carpenter @@ -48,7 +21,78 @@ open class AmqpCarpenterBase(whitelist: ClassWhitelist) { var cc = ClassCarpenterImpl(whitelist = whitelist) var factory = serializerFactoryExternalCarpenter(cc) - fun serialise(obj: T): SerializedBytes = SerializationOutput(factory).serialize(obj) - @Suppress("NOTHING_TO_INLINE") - inline fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz" + protected val remoteTypeModel = AMQPRemoteTypeModel() + protected val typeLoader = ClassCarpentingTypeLoader(SchemaBuildingRemoteTypeCarpenter(cc), cc.classloader) + + protected inline fun T.roundTrip(): ObjectAndEnvelope = + DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(this)) + + protected val Envelope.typeInformation: Map get() = + remoteTypeModel.interpret(SerializationSchemas(schema, transformsSchema)) + + protected inline fun Envelope.typeInformationFor(): RemoteTypeInformation { + val interpreted = typeInformation + val type = object : TypeToken() {}.type + return interpreted.values.find { it.typeIdentifier == TypeIdentifier.forGenericType(type) } + as RemoteTypeInformation + } + + protected inline fun Envelope.getMangled(): RemoteTypeInformation = + typeInformationFor().mangle() + + protected fun serialise(obj: T): SerializedBytes = SerializationOutput(factory).serialize(obj) + + protected inline fun RemoteTypeInformation.mangle(): RemoteTypeInformation { + val from = TypeIdentifier.forGenericType(object : TypeToken() {}.type) + return rename(from, from.mangle()) + } + + protected fun TypeIdentifier.mangle(): TypeIdentifier = when(this) { + is TypeIdentifier.Unparameterised -> copy(name = name + "_carpenter") + is TypeIdentifier.Parameterised -> copy(name = name + "_carpenter") + is TypeIdentifier.Erased -> copy(name = name + "_carpenter") + is TypeIdentifier.ArrayOf -> copy(componentType = componentType.mangle()) + else -> this + } + + protected fun TypeIdentifier.rename(from: TypeIdentifier, to: TypeIdentifier): TypeIdentifier = when(this) { + from -> to.rename(from, to) + is TypeIdentifier.Parameterised -> copy(parameters = parameters.map { it.rename(from, to) }) + is TypeIdentifier.ArrayOf -> copy(componentType = componentType.rename(from, to)) + else -> this + } + + protected fun RemoteTypeInformation.rename(from: TypeIdentifier, to: TypeIdentifier): RemoteTypeInformation = when(this) { + is RemoteTypeInformation.Composable -> copy( + typeIdentifier = typeIdentifier.rename(from, to), + properties = properties.mapValues { (_, property) -> property.copy(type = property.type.rename(from, to)) }, + interfaces = interfaces.map { it.rename(from, to) }, + typeParameters = typeParameters.map { it.rename(from, to) }) + is RemoteTypeInformation.Unparameterised -> copy(typeIdentifier = typeIdentifier.rename(from, to)) + is RemoteTypeInformation.Parameterised -> copy( + typeIdentifier = typeIdentifier.rename(from, to), + typeParameters = typeParameters.map { it.rename(from, to) }) + is RemoteTypeInformation.AnInterface -> copy( + typeIdentifier = typeIdentifier.rename(from, to), + properties = properties.mapValues { (_, property) -> property.copy(type = property.type.rename(from, to)) }, + interfaces = interfaces.map { it.rename(from, to) }, + typeParameters = typeParameters.map { it.rename(from, to) }) + is RemoteTypeInformation.AnArray -> copy(componentType = componentType.rename(from, to)) + is RemoteTypeInformation.AnEnum -> copy( + typeIdentifier = typeIdentifier.rename(from, to)) + else -> this + } + + protected fun RemoteTypeInformation.load(): Class<*> = + typeLoader.load(listOf(this))[typeIdentifier]!!.asClass() + + protected fun assertCanLoadAll(vararg types: RemoteTypeInformation) { + assertTrue(typeLoader.load(types.asList()).keys.containsAll(types.map { it.typeIdentifier })) + } + + protected fun Class<*>.new(vararg constructorParams: Any?) = + constructors[0].newInstance(*constructorParams)!! + + protected fun Any.get(propertyName: String): Any = + this::class.java.getMethod("get${propertyName.capitalize()}").invoke(this) } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/CompositeMemberCompositeSchemaToClassCarpenterTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/CompositeMemberCompositeSchemaToClassCarpenterTests.kt index 5e5b262a6f..b1700ae8c2 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/CompositeMemberCompositeSchemaToClassCarpenterTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/CompositeMemberCompositeSchemaToClassCarpenterTests.kt @@ -2,13 +2,11 @@ package net.corda.serialization.internal.carpenter import net.corda.core.serialization.CordaSerializable import net.corda.serialization.internal.AllWhitelist -import net.corda.serialization.internal.amqp.CompositeType -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.testutils.deserializeAndReturnEnvelope import org.junit.Test +import java.io.NotSerializableException +import java.util.* import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue +import kotlin.test.assertFailsWith @CordaSerializable interface I_ { @@ -16,258 +14,96 @@ interface I_ { } class CompositeMembers : AmqpCarpenterBase(AllWhitelist) { - @Test - fun bothKnown() { - val testA = 10 - val testB = 20 + @Test + fun parentIsUnknown() { @CordaSerializable data class A(val a: Int) @CordaSerializable data class B(val a: A, var b: Int) - val b = B(A(testA), testB) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) + val (_, envelope) = B(A(10), 20).roundTrip() - val amqpObj = obj.obj - - assertEquals(testB, amqpObj.b) - assertEquals(testA, amqpObj.a.a) - assertEquals(2, obj.envelope.schema.types.size) - require(obj.envelope.schema.types[0] is CompositeType) - require(obj.envelope.schema.types[1] is CompositeType) - - var amqpSchemaA: CompositeType? = null - var amqpSchemaB: CompositeType? = null - - for (type in obj.envelope.schema.types) { - when (type.name.split("$").last()) { - "A" -> amqpSchemaA = type as CompositeType - "B" -> amqpSchemaB = type as CompositeType - } - } - - require(amqpSchemaA != null) - require(amqpSchemaB != null) - - // Just ensure the amqp schema matches what we want before we go messing - // around with the internals - assertEquals(1, amqpSchemaA?.fields?.size) - assertEquals("a", amqpSchemaA!!.fields[0].name) - assertEquals("int", amqpSchemaA.fields[0].type) - - assertEquals(2, amqpSchemaB?.fields?.size) - assertEquals("a", amqpSchemaB!!.fields[0].name) - assertEquals(classTestName("A"), amqpSchemaB.fields[0].type) - assertEquals("b", amqpSchemaB.fields[1].name) - assertEquals("int", amqpSchemaB.fields[1].type) - - val metaSchema = obj.envelope.schema.carpenterSchema(ClassLoader.getSystemClassLoader()) - - // if we know all the classes there is nothing to really achieve here - require(metaSchema.carpenterSchemas.isEmpty()) - require(metaSchema.dependsOn.isEmpty()) - require(metaSchema.dependencies.isEmpty()) + // We load an unknown class, B_mangled, which includes a reference to a known class, A. + assertCanLoadAll(envelope.getMangled()) } - // you cannot have an element of a composite class we know about - // that is unknown as that should be impossible. If we have the containing - // class in the class path then we must have all of it's constituent elements - @Test(expected = UncarpentableException::class) - fun nestedIsUnknown() { - val testA = 10 - val testB = 20 - + @Test + fun bothAreUnknown() { @CordaSerializable data class A(override val a: Int) : I_ @CordaSerializable data class B(val a: A, var b: Int) - val b = B(A(testA), testB) + val (_, envelope) = B(A(10), 20).roundTrip() - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) - val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"))) - - amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) + // We load an unknown class, B_mangled, which includes a reference to an unknown class, A_mangled. + // For this to work, we must include A_mangled in our set of classes to load. + assertCanLoadAll(envelope.getMangled().mangle(), envelope.getMangled()) } @Test - fun ParentIsUnknown() { - val testA = 10 - val testB = 20 - + fun oneIsUnknown() { @CordaSerializable data class A(override val a: Int) : I_ @CordaSerializable data class B(val a: A, var b: Int) - val b = B(A(testA), testB) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) + val (_, envelope) = B(A(10), 20).roundTrip() - val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("B"))) - val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) + // We load an unknown class, B_mangled, which includes a reference to an unknown class, A_mangled. + // This will fail, because A_mangled is not included in our set of classes to load. + assertFailsWith { assertCanLoadAll(envelope.getMangled().mangle()) } + } - assertEquals(1, carpenterSchema.size) + // See https://github.com/corda/corda/issues/4107 + @Test + fun withUUID() { + @CordaSerializable + data class IOUStateData( + val value: Int, + val ref: UUID, + val newValue: String? = null + ) - val metaCarpenter = MetaCarpenter(carpenterSchema, ClassCarpenterImpl(whitelist = AllWhitelist)) - - metaCarpenter.build() - - require(mangleName(classTestName("B")) in metaCarpenter.objects) + val uuid = UUID.randomUUID() + val(_, envelope) = IOUStateData(10, uuid, "new value").roundTrip() + val recarpented = envelope.getMangled().load() + val instance = recarpented.new(null, uuid, 10) + assertEquals(uuid, instance.get("ref")) } @Test - fun BothUnknown() { - val testA = 10 - val testB = 20 + fun mapWithUnknown() { + data class C(val a: Int) + data class D(val m: Map) + val (_, envelope) = D(mapOf("c" to C(1))).roundTrip() - @CordaSerializable - data class A(override val a: Int) : I_ + val infoForD = envelope.typeInformationFor().mangle() + val mangledMap = envelope.typeInformation.values.find { it.typeIdentifier.name == "java.util.Map" }!!.mangle() + val mangledC = envelope.getMangled() - @CordaSerializable - data class B(val a: A, var b: Int) + assertEquals( + "java.util.Map", + mangledMap.prettyPrint(false)) - val b = B(A(testA), testB) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) - val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) - val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) - - // just verify we're in the expected initial state, A is carpentable, B is not because - // it depends on A and the dependency chains are in place - assertEquals(1, carpenterSchema.size) - assertEquals(mangleName(classTestName("A")), carpenterSchema.carpenterSchemas.first().name) - assertEquals(1, carpenterSchema.dependencies.size) - require(mangleName(classTestName("B")) in carpenterSchema.dependencies) - assertEquals(1, carpenterSchema.dependsOn.size) - require(mangleName(classTestName("A")) in carpenterSchema.dependsOn) - - val metaCarpenter = TestMetaCarpenter(carpenterSchema, ClassCarpenterImpl(whitelist = AllWhitelist)) - - assertEquals(0, metaCarpenter.objects.size) - - // first iteration, carpent A, resolve deps and mark B as carpentable - metaCarpenter.build() - - // one build iteration should have carpetned up A and worked out that B is now buildable - // given it's depedencies have been satisfied - assertTrue(mangleName(classTestName("A")) in metaCarpenter.objects) - assertFalse(mangleName(classTestName("B")) in metaCarpenter.objects) - - assertEquals(1, carpenterSchema.carpenterSchemas.size) - assertEquals(mangleName(classTestName("B")), carpenterSchema.carpenterSchemas.first().name) - assertTrue(carpenterSchema.dependencies.isEmpty()) - assertTrue(carpenterSchema.dependsOn.isEmpty()) - - // second manual iteration, will carpent B - metaCarpenter.build() - require(mangleName(classTestName("A")) in metaCarpenter.objects) - require(mangleName(classTestName("B")) in metaCarpenter.objects) - - // and we must be finished - assertTrue(carpenterSchema.carpenterSchemas.isEmpty()) + assertCanLoadAll(infoForD, mangledMap, mangledC) } - @Test(expected = UncarpentableException::class) - @Suppress("UNUSED") - fun nestedIsUnknownInherited() { - val testA = 10 - val testB = 20 - val testC = 30 - - @CordaSerializable - open class A(val a: Int) - - @CordaSerializable - class B(a: Int, var b: Int) : A(a) - - @CordaSerializable - data class C(val b: B, var c: Int) - - val c = C(B(testA, testB), testC) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(c)) - - val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) - - amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) - } - - @Test(expected = UncarpentableException::class) - @Suppress("UNUSED") - fun nestedIsUnknownInheritedUnknown() { - val testA = 10 - val testB = 20 - val testC = 30 - - @CordaSerializable - open class A(val a: Int) - - @CordaSerializable - class B(a: Int, var b: Int) : A(a) - - @CordaSerializable - data class C(val b: B, var c: Int) - - val c = C(B(testA, testB), testC) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(c)) - - val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) - - amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) - } - - @Suppress("UNUSED") - @Test(expected = UncarpentableException::class) - fun parentsIsUnknownWithUnknownInheritedMember() { - val testA = 10 - val testB = 20 - val testC = 30 - - @CordaSerializable - open class A(val a: Int) - - @CordaSerializable - class B(a: Int, var b: Int) : A(a) - - @CordaSerializable - data class C(val b: B, var c: Int) - - val c = C(B(testA, testB), testC) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(c)) - - val carpenterSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) - TestMetaCarpenter(carpenterSchema.carpenterSchema( - ClassLoader.getSystemClassLoader()), ClassCarpenterImpl(whitelist = AllWhitelist)) - } - - /* - * TODO serializer doesn't support inheritnace at the moment, when it does this should work @Test - fun `inheritance`() { - val testA = 10 - val testB = 20 + fun parameterisedNonCollectionWithUnknown() { + data class C(val a: Int) + data class NotAMap(val key: K, val value: V) + data class D(val m: NotAMap) + val (_, envelope) = D(NotAMap("c" , C(1))).roundTrip() - @CordaSerializable - open class A(open val a: Int) + val infoForD = envelope.typeInformationFor().mangle() + val mangledNotAMap = envelope.typeInformationFor>().mangle() + val mangledC = envelope.getMangled() - @CordaSerializable - class B(override val a: Int, val b: Int) : A (a) - - val b = B(testA, testB) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) - - require(obj.obj is B) - - val carpenterSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) - val metaCarpenter = TestMetaCarpenter(carpenterSchema.carpenterSchema()) - - assertEquals(1, metaCarpenter.schemas.carpenterSchemas.size) - assertEquals(mangleNames(classTestName("B")), metaCarpenter.schemas.carpenterSchemas.first().name) - assertEquals(1, metaCarpenter.schemas.dependencies.size) - assertTrue(mangleNames(classTestName("A")) in metaCarpenter.schemas.dependencies) + assertCanLoadAll(infoForD, mangledNotAMap, mangledC) } - */ } - diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/InheritanceSchemaToClassCarpenterTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/InheritanceSchemaToClassCarpenterTests.kt index 6a1a2c6e3e..336471c9ac 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/InheritanceSchemaToClassCarpenterTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/InheritanceSchemaToClassCarpenterTests.kt @@ -2,10 +2,9 @@ package net.corda.serialization.internal.carpenter import net.corda.core.serialization.CordaSerializable import net.corda.serialization.internal.AllWhitelist -import net.corda.serialization.internal.amqp.DeserializationInput import org.junit.Test import kotlin.test.* -import net.corda.serialization.internal.amqp.testutils.deserializeAndReturnEnvelope +import java.io.NotSerializableException @CordaSerializable interface J { @@ -39,172 +38,68 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) { fun interfaceParent1() { class A(override val j: Int) : J - val testJ = 20 - val a = A(testJ) + val (_, env) = A(20).roundTrip() + val mangledA = env.getMangled() - assertEquals(testJ, a.j) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - val serSchema = obj.envelope.schema - assertEquals(2, serSchema.types.size) - val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) + val carpentedA = mangledA.load() + val carpentedInstance = carpentedA.new(20) - // since we're using an envelope generated by seilaising classes defined locally - // it's extremely unlikely we'd need to carpent any classes - assertEquals(0, l1.size) + assertEquals(20, carpentedInstance.get("j")) - val mangleSchema = serSchema.mangleNames(listOf(classTestName("A"))) - val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) - assertEquals(1, l2.size) - - val aSchema = l2.carpenterSchemas.find { it.name == mangleName(classTestName("A")) } - assertNotEquals(null, aSchema) - assertEquals(mangleName(classTestName("A")), aSchema!!.name) - assertEquals(1, aSchema.interfaces.size) - assertEquals(J::class.java, aSchema.interfaces[0]) - - val aBuilder = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema) - val objJ = aBuilder.constructors[0].newInstance(testJ) - val j = objJ as J - - assertEquals(aBuilder.getMethod("getJ").invoke(objJ), testJ) - assertEquals(a.j, j.j) + val asJ = carpentedInstance as J + assertEquals(20, asJ.j) } @Test fun interfaceParent2() { class A(override val j: Int, val jj: Int) : J - val testJ = 20 - val testJJ = 40 - val a = A(testJ, testJJ) + val (_, env) = A(23, 42).roundTrip() + val carpentedA = env.getMangled().load() + val carpetedInstance = carpentedA.constructors[0].newInstance(23, 42) - assertEquals(testJ, a.j) - assertEquals(testJJ, a.jj) + assertEquals(23, carpetedInstance.get("j")) + assertEquals(42, carpetedInstance.get("jj")) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - - val serSchema = obj.envelope.schema - - assertEquals(2, serSchema.types.size) - - val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) - - assertEquals(0, l1.size) - - val mangleSchema = serSchema.mangleNames(listOf(classTestName("A"))) - val aName = mangleName(classTestName("A")) - val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) - - assertEquals(1, l2.size) - - val aSchema = l2.carpenterSchemas.find { it.name == aName } - - assertNotEquals(null, aSchema) - - assertEquals(aName, aSchema!!.name) - assertEquals(1, aSchema.interfaces.size) - assertEquals(J::class.java, aSchema.interfaces[0]) - - val aBuilder = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema) - val objJ = aBuilder.constructors[0].newInstance(testJ, testJJ) - val j = objJ as J - - assertEquals(aBuilder.getMethod("getJ").invoke(objJ), testJ) - assertEquals(aBuilder.getMethod("getJj").invoke(objJ), testJJ) - - assertEquals(a.j, j.j) + val asJ = carpetedInstance as J + assertEquals(23, asJ.j) } @Test fun multipleInterfaces() { - val testI = 20 - val testII = 40 - class A(override val i: Int, override val ii: Int) : I, II - val a = A(testI, testII) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) + val (_, env) = A(23, 42).roundTrip() + val carpentedA = env.getMangled().load() + val carpetedInstance = carpentedA.constructors[0].newInstance(23, 42) - val serSchema = obj.envelope.schema + assertEquals(23, carpetedInstance.get("i")) + assertEquals(42, carpetedInstance.get("ii")) - assertEquals(3, serSchema.types.size) + val i = carpetedInstance as I + val ii = carpetedInstance as II - val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) - - // since we're using an envelope generated by serialising classes defined locally - // it's extremely unlikely we'd need to carpent any classes - assertEquals(0, l1.size) - - // pretend we don't know the class we've been sent, i.e. it's unknown to the class loader, and thus - // needs some carpentry - val mangleSchema = serSchema.mangleNames(listOf(classTestName("A"))) - val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) - val aName = mangleName(classTestName("A")) - - assertEquals(1, l2.size) - - val aSchema = l2.carpenterSchemas.find { it.name == aName } - - assertNotEquals(null, aSchema) - assertEquals(aName, aSchema!!.name) - assertEquals(2, aSchema.interfaces.size) - assertTrue(I::class.java in aSchema.interfaces) - assertTrue(II::class.java in aSchema.interfaces) - - val aBuilder = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema) - val objA = aBuilder.constructors[0].newInstance(testI, testII) - val i = objA as I - val ii = objA as II - - assertEquals(aBuilder.getMethod("getI").invoke(objA), testI) - assertEquals(aBuilder.getMethod("getIi").invoke(objA), testII) - assertEquals(a.i, i.i) - assertEquals(a.ii, ii.ii) + assertEquals(23, i.i) + assertEquals(42, ii.ii) } @Test fun nestedInterfaces() { class A(override val i: Int, override val iii: Int) : III - val testI = 20 - val testIII = 60 - val a = A(testI, testIII) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) + val (_, env) = A(23, 42).roundTrip() + val carpentedA = env.getMangled().load() + val carpetedInstance = carpentedA.constructors[0].newInstance(23, 42) - val serSchema = obj.envelope.schema + assertEquals(23, carpetedInstance.get("i")) + assertEquals(42, carpetedInstance.get("iii")) - assertEquals(3, serSchema.types.size) + val i = carpetedInstance as I + val iii = carpetedInstance as III - val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) - - // since we're using an envelope generated by serialising classes defined locally - // it's extremely unlikely we'd need to carpent any classes - assertEquals(0, l1.size) - - val mangleSchema = serSchema.mangleNames(listOf(classTestName("A"))) - val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) - val aName = mangleName(classTestName("A")) - - assertEquals(1, l2.size) - - val aSchema = l2.carpenterSchemas.find { it.name == aName } - - assertNotEquals(null, aSchema) - assertEquals(aName, aSchema!!.name) - assertEquals(2, aSchema.interfaces.size) - assertTrue(I::class.java in aSchema.interfaces) - assertTrue(III::class.java in aSchema.interfaces) - - val aBuilder = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema) - val objA = aBuilder.constructors[0].newInstance(testI, testIII) - val i = objA as I - val iii = objA as III - - assertEquals(aBuilder.getMethod("getI").invoke(objA), testI) - assertEquals(aBuilder.getMethod("getIii").invoke(objA), testIII) - assertEquals(a.i, i.i) - assertEquals(a.i, iii.i) - assertEquals(a.iii, iii.iii) + assertEquals(23, i.i) + assertEquals(23, iii.i) + assertEquals(42, iii.iii) } @Test @@ -212,237 +107,60 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) { class A(override val i: Int) : I class B(override val i: I, override val iiii: Int) : IIII - val testI = 25 - val testIIII = 50 - val a = A(testI) - val b = B(a, testIIII) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) + val (_, env) = B(A(23), 42).roundTrip() + val carpentedA = env.getMangled().load() + val carpentedB = env.getMangled().load() - val serSchema = obj.envelope.schema + val carpentedAInstance = carpentedA.new(23) + val carpentedBInstance = carpentedB.new(carpentedAInstance, 42) - // Expected classes are - // * class A - // * class A's interface (class I) - // * class B - // * class B's interface (class IIII) - assertEquals(4, serSchema.types.size) - - val mangleSchema = serSchema.mangleNames(listOf(classTestName("A"), classTestName("B"))) - val cSchema = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) - val aName = mangleName(classTestName("A")) - val bName = mangleName(classTestName("B")) - - assertEquals(2, cSchema.size) - - val aCarpenterSchema = cSchema.carpenterSchemas.find { it.name == aName } - val bCarpenterSchema = cSchema.carpenterSchemas.find { it.name == bName } - - assertNotEquals(null, aCarpenterSchema) - assertNotEquals(null, bCarpenterSchema) - - val cc = ClassCarpenterImpl(whitelist = AllWhitelist) - val cc2 = ClassCarpenterImpl(whitelist = AllWhitelist) - val bBuilder = cc.build(bCarpenterSchema!!) - bBuilder.constructors[0].newInstance(a, testIIII) - - val aBuilder = cc.build(aCarpenterSchema!!) - val objA = aBuilder.constructors[0].newInstance(testI) - - // build a second B this time using our constructed instance of A and not the - // local one we pre defined - bBuilder.constructors[0].newInstance(objA, testIIII) - - // whittle and instantiate a different A with a new class loader - val aBuilder2 = cc2.build(aCarpenterSchema) - val objA2 = aBuilder2.constructors[0].newInstance(testI) - - bBuilder.constructors[0].newInstance(objA2, testIIII) + val iiii = carpentedBInstance as IIII + assertEquals(23, iiii.i.i) + assertEquals(42, iiii.iiii) } - // if we remove the nested interface we should get an error as it's impossible - // to have a concrete class loaded without having access to all of it's elements - @Test(expected = UncarpentableException::class) + @Test fun memberInterface2() { class A(override val i: Int) : I - class B(override val i: I, override val iiii: Int) : IIII - val testI = 25 - val testIIII = 50 - val a = A(testI) - val b = B(a, testIIII) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) + val (_, env) = A(23).roundTrip() - val serSchema = obj.envelope.schema - - // The classes we're expecting to find: - // * class A - // * class A's interface (class I) - // * class B - // * class B's interface (class IIII) - assertEquals(4, serSchema.types.size) - - // ignore the return as we expect this to throw - serSchema.mangleNames(listOf( - classTestName("A"), "${this.javaClass.`package`.name}.I")).carpenterSchema(ClassLoader.getSystemClassLoader()) + // if we remove the nested interface we should get an error as it's impossible + // to have a concrete class loaded without having access to all of it's elements + assertFailsWith { assertCanLoadAll(env.getMangled().mangle()) } } @Test fun interfaceAndImplementation() { class A(override val i: Int) : I - val testI = 25 - val a = A(testI) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) + val (_, env) = A(23).roundTrip() - val serSchema = obj.envelope.schema - - // The classes we're expecting to find: - // * class A - // * class A's interface (class I) - assertEquals(2, serSchema.types.size) - - val amqpSchema = serSchema.mangleNames(listOf(classTestName("A"), "${this.javaClass.`package`.name}.I")) - val aName = mangleName(classTestName("A")) - val iName = mangleName("${this.javaClass.`package`.name}.I") - val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) - - // whilst there are two unknown classes within the envelope A depends on I so we can't construct a - // schema for A until we have for I - assertEquals(1, carpenterSchema.size) - assertNotEquals(null, carpenterSchema.carpenterSchemas.find { it.name == iName }) - - // since we can't build A it should list I as a dependency - assertTrue(aName in carpenterSchema.dependencies) - assertEquals(1, carpenterSchema.dependencies[aName]!!.second.size) - assertEquals(iName, carpenterSchema.dependencies[aName]!!.second[0]) - - // and conversly I should have A listed as a dependent - assertTrue(iName in carpenterSchema.dependsOn) - assertEquals(1, carpenterSchema.dependsOn[iName]!!.size) - assertEquals(aName, carpenterSchema.dependsOn[iName]!![0]) - - val mc = MetaCarpenter(carpenterSchema, ClassCarpenterImpl(whitelist = AllWhitelist)) - mc.build() - - assertEquals(0, mc.schemas.carpenterSchemas.size) - assertEquals(0, mc.schemas.dependencies.size) - assertEquals(0, mc.schemas.dependsOn.size) - assertEquals(2, mc.objects.size) - assertTrue(aName in mc.objects) - assertTrue(iName in mc.objects) - - mc.objects[aName]!!.constructors[0].newInstance(testI) + // This time around we will succeed, because the mangled I is included in the type information to be loaded. + assertCanLoadAll(env.getMangled().mangle(), env.getMangled()) } @Test fun twoInterfacesAndImplementation() { class A(override val i: Int, override val ii: Int) : I, II - val testI = 69 - val testII = 96 - val a = A(testI, testII) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - - val amqpSchema = obj.envelope.schema.mangleNames(listOf( - classTestName("A"), - "${this.javaClass.`package`.name}.I", - "${this.javaClass.`package`.name}.II")) - - val aName = mangleName(classTestName("A")) - val iName = mangleName("${this.javaClass.`package`.name}.I") - val iiName = mangleName("${this.javaClass.`package`.name}.II") - val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) - - // there is nothing preventing us from carpenting up the two interfaces so - // our initial list should contain both interface with A being dependent on both - // and each having A as a dependent - assertEquals(2, carpenterSchema.carpenterSchemas.size) - assertNotNull(carpenterSchema.carpenterSchemas.find { it.name == iName }) - assertNotNull(carpenterSchema.carpenterSchemas.find { it.name == iiName }) - assertNull(carpenterSchema.carpenterSchemas.find { it.name == aName }) - - assertTrue(iName in carpenterSchema.dependsOn) - assertEquals(1, carpenterSchema.dependsOn[iName]?.size) - assertNotNull(carpenterSchema.dependsOn[iName]?.find({ it == aName })) - - assertTrue(iiName in carpenterSchema.dependsOn) - assertEquals(1, carpenterSchema.dependsOn[iiName]?.size) - assertNotNull(carpenterSchema.dependsOn[iiName]?.find { it == aName }) - - assertTrue(aName in carpenterSchema.dependencies) - assertEquals(2, carpenterSchema.dependencies[aName]!!.second.size) - assertNotNull(carpenterSchema.dependencies[aName]!!.second.find { it == iName }) - assertNotNull(carpenterSchema.dependencies[aName]!!.second.find { it == iiName }) - - val mc = MetaCarpenter(carpenterSchema, ClassCarpenterImpl(whitelist = AllWhitelist)) - mc.build() - - assertEquals(0, mc.schemas.carpenterSchemas.size) - assertEquals(0, mc.schemas.dependencies.size) - assertEquals(0, mc.schemas.dependsOn.size) - assertEquals(3, mc.objects.size) - assertTrue(aName in mc.objects) - assertTrue(iName in mc.objects) - assertTrue(iiName in mc.objects) + val (_, env) = A(23, 42).roundTrip() + assertCanLoadAll( + env.getMangled().mangle().mangle(), + env.getMangled(), + env.getMangled() + ) } @Test fun nestedInterfacesAndImplementation() { class A(override val i: Int, override val iii: Int) : III - val testI = 7 - val testIII = 11 - val a = A(testI, testIII) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - - val amqpSchema = obj.envelope.schema.mangleNames(listOf( - classTestName("A"), - "${this.javaClass.`package`.name}.I", - "${this.javaClass.`package`.name}.III")) - - val aName = mangleName(classTestName("A")) - val iName = mangleName("${this.javaClass.`package`.name}.I") - val iiiName = mangleName("${this.javaClass.`package`.name}.III") - val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) - - // Since A depends on III and III extends I we will have to construct them - // in that reverse order (I -> III -> A) - assertEquals(1, carpenterSchema.carpenterSchemas.size) - assertNotNull(carpenterSchema.carpenterSchemas.find { it.name == iName }) - assertNull(carpenterSchema.carpenterSchemas.find { it.name == iiiName }) - assertNull(carpenterSchema.carpenterSchemas.find { it.name == aName }) - - // I has III as a direct dependent and A as an indirect one - assertTrue(iName in carpenterSchema.dependsOn) - assertEquals(2, carpenterSchema.dependsOn[iName]?.size) - assertNotNull(carpenterSchema.dependsOn[iName]?.find({ it == iiiName })) - assertNotNull(carpenterSchema.dependsOn[iName]?.find({ it == aName })) - - // III has A as a dependent - assertTrue(iiiName in carpenterSchema.dependsOn) - assertEquals(1, carpenterSchema.dependsOn[iiiName]?.size) - assertNotNull(carpenterSchema.dependsOn[iiiName]?.find { it == aName }) - - // conversly III depends on I - assertTrue(iiiName in carpenterSchema.dependencies) - assertEquals(1, carpenterSchema.dependencies[iiiName]!!.second.size) - assertNotNull(carpenterSchema.dependencies[iiiName]!!.second.find { it == iName }) - - // and A depends on III and I - assertTrue(aName in carpenterSchema.dependencies) - assertEquals(2, carpenterSchema.dependencies[aName]!!.second.size) - assertNotNull(carpenterSchema.dependencies[aName]!!.second.find { it == iiiName }) - assertNotNull(carpenterSchema.dependencies[aName]!!.second.find { it == iName }) - - val mc = MetaCarpenter(carpenterSchema, ClassCarpenterImpl(whitelist = AllWhitelist)) - mc.build() - - assertEquals(0, mc.schemas.carpenterSchemas.size) - assertEquals(0, mc.schemas.dependencies.size) - assertEquals(0, mc.schemas.dependsOn.size) - assertEquals(3, mc.objects.size) - assertTrue(aName in mc.objects) - assertTrue(iName in mc.objects) - assertTrue(iiiName in mc.objects) + val (_, env) = A(23, 42).roundTrip() + assertCanLoadAll( + env.getMangled().mangle().mangle(), + env.getMangled(), + env.getMangled().mangle() + ) } } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/MultiMemberCompositeSchemaToClassCarpenterTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/MultiMemberCompositeSchemaToClassCarpenterTests.kt index 3a560566bb..f5848a8f3b 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/MultiMemberCompositeSchemaToClassCarpenterTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/MultiMemberCompositeSchemaToClassCarpenterTests.kt @@ -1,57 +1,24 @@ package net.corda.serialization.internal.carpenter import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SerializableCalculatedProperty import net.corda.serialization.internal.AllWhitelist -import net.corda.serialization.internal.amqp.CompositeType -import net.corda.serialization.internal.amqp.DeserializationInput import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertNotEquals -import net.corda.serialization.internal.amqp.testutils.deserializeAndReturnEnvelope class MultiMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) { @Test - fun twoInts() { + fun anIntAndALong() { @CordaSerializable - data class A(val a: Int, val b: Int) + data class A(val a: Int, val b: Long) - val testA = 10 - val testB = 20 - val a = A(testA, testB) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) + val (_, env) = A(23, 42).roundTrip() + val carpentedInstance = env.getMangled().load().new(23, 42) - val amqpObj = obj.obj - - assertEquals(testA, amqpObj.a) - assertEquals(testB, amqpObj.b) - assertEquals(1, obj.envelope.schema.types.size) - require(obj.envelope.schema.types[0] is CompositeType) - - val amqpSchema = obj.envelope.schema.types[0] as CompositeType - - assertEquals(2, amqpSchema.fields.size) - assertEquals("a", amqpSchema.fields[0].name) - assertEquals("int", amqpSchema.fields[0].type) - assertEquals("b", amqpSchema.fields[1].name) - assertEquals("int", amqpSchema.fields[1].type) - - val carpenterSchema = CarpenterMetaSchema.newInstance() - amqpSchema.carpenterSchema( - classloader = ClassLoader.getSystemClassLoader(), - carpenterSchemas = carpenterSchema, - force = true) - - assertEquals(1, carpenterSchema.size) - val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") } - - assertNotEquals(null, aSchema) - - val pinochio = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema!!) - val p = pinochio.constructors[0].newInstance(testA, testB) - - assertEquals(pinochio.getMethod("getA").invoke(p), amqpObj.a) - assertEquals(pinochio.getMethod("getB").invoke(p), amqpObj.b) + assertEquals(23, carpentedInstance.get("a")) + assertEquals(42L, carpentedInstance.get("b")) } @Test @@ -59,42 +26,65 @@ class MultiMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhi @CordaSerializable data class A(val a: Int, val b: String) - val testA = 10 - val testB = "twenty" - val a = A(testA, testB) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) + val (_, env) = A(23, "skidoo").roundTrip() + val carpentedInstance = env.getMangled().load().new(23, "skidoo") - val amqpObj = obj.obj + assertEquals(23, carpentedInstance.get("a")) + assertEquals("skidoo", carpentedInstance.get("b")) + } - assertEquals(testA, amqpObj.a) - assertEquals(testB, amqpObj.b) - assertEquals(1, obj.envelope.schema.types.size) - require(obj.envelope.schema.types[0] is CompositeType) + interface Parent { + @get:SerializableCalculatedProperty + val doubled: Int + } - val amqpSchema = obj.envelope.schema.types[0] as CompositeType + @Test + fun calculatedValues() { + data class C(val i: Int): Parent { + @get:SerializableCalculatedProperty + val squared = (i * i).toString() - assertEquals(2, amqpSchema.fields.size) - assertEquals("a", amqpSchema.fields[0].name) - assertEquals("int", amqpSchema.fields[0].type) - assertEquals("b", amqpSchema.fields[1].name) - assertEquals("string", amqpSchema.fields[1].type) + override val doubled get() = i * 2 + } - val carpenterSchema = CarpenterMetaSchema.newInstance() - amqpSchema.carpenterSchema( - classloader = ClassLoader.getSystemClassLoader(), - carpenterSchemas = carpenterSchema, - force = true) + val (amqpObj, envelope) = C(2).roundTrip() + val remoteTypeInformation = envelope.typeInformationFor() - assertEquals(1, carpenterSchema.size) - val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") } + assertEquals(""" + C: Parent + doubled: int + i: int + squared: String + """.trimIndent(), remoteTypeInformation.prettyPrint()) - assertNotEquals(null, aSchema) + val pinochio = remoteTypeInformation.mangle().load() + assertNotEquals(pinochio.name, C::class.java.name) + assertNotEquals(pinochio, C::class.java) - val pinochio = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema!!) - val p = pinochio.constructors[0].newInstance(testA, testB) + // Note that params are given in alphabetical order: doubled, i, squared + val p = pinochio.new(4, 2, "4") - assertEquals(pinochio.getMethod("getA").invoke(p), amqpObj.a) - assertEquals(pinochio.getMethod("getB").invoke(p), amqpObj.b) + assertEquals(2, p.get("i")) + assertEquals("4", p.get("squared")) + assertEquals(4, p.get("doubled")) + + val upcast = p as Parent + assertEquals(upcast.doubled, amqpObj.doubled) + } + + @Test + fun implementingClassDoesNotCalculateValue() { + class C(override val doubled: Int): Parent + + val (_, env) = C(5).roundTrip() + + val pinochio = env.getMangled().load() + val p = pinochio.new(5) + + assertEquals(5, p.get("doubled")) + + val upcast = p as Parent + assertEquals(5, upcast.doubled) } } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/SingleMemberCompositeSchemaToClassCarpenterTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/SingleMemberCompositeSchemaToClassCarpenterTests.kt deleted file mode 100644 index 5642b0b824..0000000000 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/SingleMemberCompositeSchemaToClassCarpenterTests.kt +++ /dev/null @@ -1,205 +0,0 @@ -package net.corda.serialization.internal.carpenter - -import net.corda.core.serialization.CordaSerializable -import net.corda.serialization.internal.AllWhitelist -import net.corda.serialization.internal.amqp.CompositeType -import net.corda.serialization.internal.amqp.DeserializationInput -import org.junit.Test -import kotlin.test.assertEquals -import net.corda.serialization.internal.amqp.testutils.deserializeAndReturnEnvelope - -class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) { - @Test - fun singleInteger() { - @CordaSerializable - data class A(val a: Int) - - val test = 10 - val a = A(test) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - val amqpObj = obj.obj - - assertEquals(test, amqpObj.a) - assertEquals(1, obj.envelope.schema.types.size) - require(obj.envelope.schema.types[0] is CompositeType) - - val amqpSchema = obj.envelope.schema.types[0] as CompositeType - - assertEquals(1, amqpSchema.fields.size) - assertEquals("a", amqpSchema.fields[0].name) - assertEquals("int", amqpSchema.fields[0].type) - - val carpenterSchema = CarpenterMetaSchema.newInstance() - amqpSchema.carpenterSchema( - classloader = ClassLoader.getSystemClassLoader(), - carpenterSchemas = carpenterSchema, - force = true) - - val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!! - val aBuilder = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema) - val p = aBuilder.constructors[0].newInstance(test) - - assertEquals(aBuilder.getMethod("getA").invoke(p), amqpObj.a) - } - - @Test - fun singleString() { - @CordaSerializable - data class A(val a: String) - - val test = "ten" - val a = A(test) - - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - val amqpObj = obj.obj - - assertEquals(test, amqpObj.a) - assertEquals(1, obj.envelope.schema.types.size) - require(obj.envelope.schema.types[0] is CompositeType) - - val amqpSchema = obj.envelope.schema.types[0] as CompositeType - val carpenterSchema = CarpenterMetaSchema.newInstance() - amqpSchema.carpenterSchema( - classloader = ClassLoader.getSystemClassLoader(), - carpenterSchemas = carpenterSchema, - force = true) - - val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!! - val aBuilder = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema) - val p = aBuilder.constructors[0].newInstance(test) - - assertEquals(aBuilder.getMethod("getA").invoke(p), amqpObj.a) - } - - @Test - fun singleLong() { - @CordaSerializable - data class A(val a: Long) - - val test = 10L - val a = A(test) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - val amqpObj = obj.obj - - assertEquals(test, amqpObj.a) - assertEquals(1, obj.envelope.schema.types.size) - require(obj.envelope.schema.types[0] is CompositeType) - - val amqpSchema = obj.envelope.schema.types[0] as CompositeType - - assertEquals(1, amqpSchema.fields.size) - assertEquals("a", amqpSchema.fields[0].name) - assertEquals("long", amqpSchema.fields[0].type) - - val carpenterSchema = CarpenterMetaSchema.newInstance() - amqpSchema.carpenterSchema( - classloader = ClassLoader.getSystemClassLoader(), - carpenterSchemas = carpenterSchema, - force = true) - - val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!! - val aBuilder = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema) - val p = aBuilder.constructors[0].newInstance(test) - - assertEquals(aBuilder.getMethod("getA").invoke(p), amqpObj.a) - } - - @Test - fun singleShort() { - @CordaSerializable - data class A(val a: Short) - - val test = 10.toShort() - val a = A(test) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - val amqpObj = obj.obj - - assertEquals(test, amqpObj.a) - assertEquals(1, obj.envelope.schema.types.size) - require(obj.envelope.schema.types[0] is CompositeType) - - val amqpSchema = obj.envelope.schema.types[0] as CompositeType - - assertEquals(1, amqpSchema.fields.size) - assertEquals("a", amqpSchema.fields[0].name) - assertEquals("short", amqpSchema.fields[0].type) - - val carpenterSchema = CarpenterMetaSchema.newInstance() - amqpSchema.carpenterSchema( - classloader = ClassLoader.getSystemClassLoader(), - carpenterSchemas = carpenterSchema, - force = true) - - val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!! - val aBuilder = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema) - val p = aBuilder.constructors[0].newInstance(test) - - assertEquals(aBuilder.getMethod("getA").invoke(p), amqpObj.a) - } - - @Test - fun singleDouble() { - @CordaSerializable - data class A(val a: Double) - - val test = 10.0 - val a = A(test) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - val amqpObj = obj.obj - - assertEquals(test, amqpObj.a) - assertEquals(1, obj.envelope.schema.types.size) - require(obj.envelope.schema.types[0] is CompositeType) - - val amqpSchema = obj.envelope.schema.types[0] as CompositeType - - assertEquals(1, amqpSchema.fields.size) - assertEquals("a", amqpSchema.fields[0].name) - assertEquals("double", amqpSchema.fields[0].type) - - val carpenterSchema = CarpenterMetaSchema.newInstance() - amqpSchema.carpenterSchema( - classloader = ClassLoader.getSystemClassLoader(), - carpenterSchemas = carpenterSchema, - force = true) - - val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!! - val aBuilder = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema) - val p = aBuilder.constructors[0].newInstance(test) - - assertEquals(aBuilder.getMethod("getA").invoke(p), amqpObj.a) - } - - @Test - fun singleFloat() { - @CordaSerializable - data class A(val a: Float) - - val test = 10.0F - val a = A(test) - val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) - val amqpObj = obj.obj - - assertEquals(test, amqpObj.a) - assertEquals(1, obj.envelope.schema.types.size) - require(obj.envelope.schema.types[0] is CompositeType) - - val amqpSchema = obj.envelope.schema.types[0] as CompositeType - - assertEquals(1, amqpSchema.fields.size) - assertEquals("a", amqpSchema.fields[0].name) - assertEquals("float", amqpSchema.fields[0].type) - - val carpenterSchema = CarpenterMetaSchema.newInstance() - amqpSchema.carpenterSchema( - classloader = ClassLoader.getSystemClassLoader(), - carpenterSchemas = carpenterSchema, - force = true) - - val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!! - val aBuilder = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema) - val p = aBuilder.constructors[0].newInstance(test) - - assertEquals(aBuilder.getMethod("getA").invoke(p), amqpObj.a) - } -} diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/model/ClassCarpentingTypeLoaderTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/model/ClassCarpentingTypeLoaderTests.kt index 1e79409733..f7b942ccd1 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/model/ClassCarpentingTypeLoaderTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/model/ClassCarpentingTypeLoaderTests.kt @@ -54,7 +54,7 @@ class ClassCarpentingTypeLoaderTests { val person = personType.make("Arthur Putey", 42, address, listOf(previousAddress)) val personJson = ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(person) .replace("\r\n", "\n") - + assertEquals(""" { "name" : "Arthur Putey", diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/model/LocalTypeModelTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/model/LocalTypeModelTests.kt index d14646337f..7a10fdb560 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/model/LocalTypeModelTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/model/LocalTypeModelTests.kt @@ -59,9 +59,9 @@ class LocalTypeModelTests { assertInformation(""" Nested(collectionHolder: StringKeyedCollectionHolder?, intArray: int[], optionalParam: Short?) collectionHolder (optional): StringKeyedCollectionHolder(list: List, map: Map, array: List[]): CollectionHolder - array: List[] - list: List - map: Map + array: List[] + list: List + map: Map intArray: int[] """) diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/model/TypeIdentifierTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/model/TypeIdentifierTests.kt index f07f88526a..f4bc6b3f3a 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/model/TypeIdentifierTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/model/TypeIdentifierTests.kt @@ -43,6 +43,19 @@ class TypeIdentifierTests { TypeIdentifier.forGenericType(fieldType, HasStringArray::class.java).prettyPrint()) } + @Test + fun `roundtrip`() { + assertRoundtrips(Int::class.javaPrimitiveType!!) + assertRoundtrips() + assertRoundtrips() + assertRoundtrips(List::class.java) + assertRoundtrips>() + assertRoundtrips>>() + assertRoundtrips() + assertRoundtrips(HasArray::class.java) + assertRoundtrips>() + } + private fun assertIdentified(type: Type, expected: String) = assertEquals(expected, TypeIdentifier.forGenericType(type).prettyPrint()) @@ -50,4 +63,12 @@ class TypeIdentifierTests { assertEquals(expected, TypeIdentifier.forGenericType(typeOf()).prettyPrint()) private inline fun typeOf() = object : TypeToken() {}.type + + private inline fun assertRoundtrips() = assertRoundtrips(typeOf()) + + private fun assertRoundtrips(original: Type) { + val identifier = TypeIdentifier.forGenericType(original) + val localType = identifier.getLocalType(classLoader = ClassLoader.getSystemClassLoader()) + assertIdentified(localType, identifier.prettyPrint()) + } } \ No newline at end of file diff --git a/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt b/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt index 7fb3af428a..aed7852dd3 100644 --- a/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt +++ b/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt @@ -57,7 +57,7 @@ class InteractiveShellTest { }, input, FlowA::class.java, om) assertEquals(expected, output!!, input) } - + @Test fun flowStartSimple() { check("a: Hi there", "Hi there") From 9b15d4dafbb1ac75c10fd6e2227219a93899066d Mon Sep 17 00:00:00 2001 From: Roger Willis Date: Thu, 22 Nov 2018 14:31:34 +0000 Subject: [PATCH 16/17] CORDA-2232: external id to pubkey mapping (#4210) * First pass Update test. Address review comments. Added docs and kdocs. Clean-up. * Addressed review comments. Changes to docsite. * First pass at account service. Added new hibernate schemas and liquibase scripts. Added indexes to new tables. Removed mock network. Removed fresh key for external id from key management service. Removed some redundant changes. Rebase to master. * Clean up. * Added try/catch block as recommended by Andras. * Removed accounts test to another branch. Removed element collections from fungible states and linear states table. Added a new state_parties table which stores x500 names and public key hashes. Added a view which can be used to query by external ID. * Removed try catch block. It's not required as the checkpoint serialiser deals with this. Re-used existing DB session instead of creating a new session. Entity manager auto flushes. * Added java friendly api. *  This is a combination of 10 commits.  This is the 1st commit message: Shortened table name.  This is the commit message #2: Minor changes.  This is the commit message #3: Common criteria parser now returns a predicate set which is concatenated to the predicate sets of sub-class criteria.  This is the commit message #4: Fixed api compatibility issue. Reverted some changes to reduce size of PR.  This is the commit message #5: Multiple states can now be mapped to the same externalId. Multiple externalIds can now be mapped to the same state.  This is the commit message #6: Relaxed upper bound type constraint in some of the vault types.  This is the commit message #7: Added comment to test.  This is the commit message #8: Changed name of external id to public key join table. Removed some comments/TODOs.  This is the commit message #9: Added docs. General clean up.  This is the commit message #10: Fixed participants query bug and updated unit test. * Removed unused code. --- .ci/api-current.txt | 12 +- .../core/node/services/vault/QueryCriteria.kt | 16 ++- .../node/services/vault/QueryCriteriaUtils.kt | 3 +- docs/source/api-persistence.rst | 6 +- docs/source/api-vault-query.rst | 80 +++++++++++ .../source/resources/state-to-external-id.png | Bin 0 -> 100647 bytes .../keys/PersistentKeyManagementService.kt | 26 +++- .../node/services/schema/NodeSchemaService.kt | 16 +-- .../vault/HibernateQueryCriteriaParser.kt | 49 +++++-- .../node/services/vault/NodeVaultService.kt | 9 +- .../corda/node/services/vault/VaultSchema.kt | 96 ++++++++----- .../vault-schema.changelog-master.xml | 1 + .../migration/vault-schema.changelog-v8.xml | 36 +++++ .../services/vault/ExternalIdMappingTest.kt | 131 ++++++++++++++++++ 14 files changed, 401 insertions(+), 80 deletions(-) create mode 100644 docs/source/resources/state-to-external-id.png create mode 100644 node/src/main/resources/migration/vault-schema.changelog-v8.xml create mode 100644 node/src/test/kotlin/net/corda/node/services/vault/ExternalIdMappingTest.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 0028c8444c..58d8d92ba5 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -4041,7 +4041,7 @@ public static final class net.corda.core.node.services.vault.QueryCriteria$Fungi @Nullable public final java.util.List getOwner() @Nullable - public final java.util.List getParticipants() + public java.util.List getParticipants() @Nullable public final net.corda.core.node.services.vault.ColumnPredicate getQuantity() @NotNull @@ -4078,7 +4078,7 @@ public static final class net.corda.core.node.services.vault.QueryCriteria$Linea @Nullable public final java.util.List getExternalId() @Nullable - public final java.util.List getParticipants() + public java.util.List 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 (Class, String) + public (Class, String) @NotNull - public final Class component1() + public final Class component1() @NotNull public final String component2() @NotNull - public final net.corda.core.node.services.vault.SortAttribute$Custom copy(Class, String) + public final net.corda.core.node.services.vault.SortAttribute$Custom copy(Class, String) public boolean equals(Object) @NotNull - public final Class getEntityStateClass() + public final Class getEntityStateClass() @NotNull public final String getEntityStateColumnName() public int hashCode() diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index 553320c7fa..d85bc7b3f4 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -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 = emptySet() open val constraints: Set = emptySet() + open val participants: List? = null abstract val contractStateTypes: Set>? override fun visit(parser: IQueryCriteriaParser): Collection { return parser.parseCriteria(this) @@ -94,7 +95,8 @@ sealed class QueryCriteria : GenericQueryCriteria = emptySet(), - override val constraints: Set = emptySet() + override val constraints: Set = emptySet(), + override val participants: List? = null ) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { super.visit(parser) @@ -124,7 +126,7 @@ sealed class QueryCriteria : GenericQueryCriteria? = null, + override val participants: List? = null, val uuid: List? = null, val externalId: List? = null, override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, @@ -172,7 +174,7 @@ sealed class QueryCriteria : GenericQueryCriteria? = null, + override val participants: List? = null, val quantity: ColumnPredicate? = null, override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, override val contractStateTypes: Set>? = null, @@ -188,7 +190,7 @@ sealed class QueryCriteria : GenericQueryCriteria? = null, + override val participants: List? = null, val owner: List? = null, val quantity: ColumnPredicate? = null, val issuer: List? = null, @@ -231,7 +233,7 @@ sealed class QueryCriteria : GenericQueryCriteria @JvmOverloads constructor( + data class VaultCustomQueryCriteria @JvmOverloads constructor( val expression: CriteriaExpression, override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, override val contractStateTypes: Set>? = null, @@ -299,7 +301,7 @@ interface IQueryCriteriaParser : BaseQueryCriteriaParser fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection - fun parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria): Collection + fun parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria): Collection } diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt index 2492313d0b..7bf52cffe8 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt @@ -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, + data class Custom(val entityStateClass: Class, val entityStateColumnName: String) : SortAttribute() } diff --git a/docs/source/api-persistence.rst b/docs/source/api-persistence.rst index f1ca9ef1df..70008c75e4 100644 --- a/docs/source/api-persistence.rst +++ b/docs/source/api-persistence.rst @@ -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; }); diff --git a/docs/source/api-vault-query.rst b/docs/source/api-vault-query.rst index 2243a6bfea..7b5c63e2ff 100644 --- a/docs/source/api-vault-query.rst +++ b/docs/source/api-vault-query.rst @@ -1,3 +1,9 @@ +.. highlight:: kotlin +.. raw:: html + + + + 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 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(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``. \ No newline at end of file diff --git a/docs/source/resources/state-to-external-id.png b/docs/source/resources/state-to-external-id.png new file mode 100644 index 0000000000000000000000000000000000000000..78cda151e4014f8df7dcc427c737fe45008e550a GIT binary patch literal 100647 zcmeFZby!qU*9QuSN(h34Qc?m+jdTy)-3=nb&?p@Ph#;NPInv!FIVvDX!w}Mv14DPi zJ;wWf?|1JP@AKS$Kb{BX%$&XV*=y~!e(Sf_UPGvwiYy)uISv{c8lJqIlm;5wT`(FN zMg-O!;FDCy;}|qFoDyqENi}&%Ng6dLM@wrv3p6yj(1gUhT14xlUGNlnP^}ndZs1&Q z1KNn~+K9N8gakH2kQf>vNkS)y5iT>`$I@=Pkar~-uh_7dNlC>!%PC%|tB-fS!%Lw0 zNjTT$KIe^`b>4=;osnrbGiM*X2dL2$L=9==0xZzvkPvLimf1(@!yNZ6iNr7z-eT>3 z4(b0+{OQi!i(A~Qn2WRWS)D8A=S?9Lm&ih!HD&^MM<38 zd+f%O@le-KAD>O5qFpHcHcFb3h7zkAO{md#4@VU z?mk?AG%u)mfWwggh7}Y}!rS#Vf%buqufa#Sl&C+$LZH&%kbugvXVoe{NPyKTs>YMh z-#I;n9K{FkGS+w)Ch?c>RiG+n2Tzx;ig46UW zXFrp}&D;^~H`EyH!%q-Sdw>09mH+(~g>VHCs{l0tJ}VlL2Zrs7yM1Wg3f=vw4&y#~ zFA`#s;?p}{OBXQse_3=^BIRK1>q|jX8l2lyy+{OeX%bwaplnCjXm0pupB<65Z`K-X zJ21p}QY*-Ga?Cq#eKbs^e_%N{{v7d;Rd8%X>~V%2&&S(`{v1#Mh1Oe}*94;hIY0TWZWVm-e2r~%>*@u8Fxt!K7$)!D z>^@eKAP5XDB*4>nH%d#@jaB_NIfiH%`%IEnjhpx`gE&+Ub48LO>p>xL@mqW`yrIFN zXKTVPx5?jbi%SkYJ-l;%kN)}X#9(bR;~&J9U&|}0Der&+;(p%azt0}n@O9-i9u(8z z{b2jVPjS6_@IWSlw=sl*+5V5{l{%=fC0@ozNrjY3hsAQ{-m#VtjCs7wNGJ0xM)eV8 zP>|V+2oC&5)Jpt80pCBDcG`W>dV%67OU6x3OQ!APU!_P4)BZwkwpGPyN6d?z7Qps3 z(QKoV?ML_z@lC8_5&YnsrNh_t{M1I=?6(`p8*Vq;pNQD#!222WhR8$M1luvtz2ok} zA(qB!6Ux{DwlemXW@{=9{*O2258oH!U zVV@%P6gO#1n4$=n8TK56%*A=1{j?wou5{l zs;`l_$`=J%30mDOjfIGJ$x%bD>#SbnJ``8{SG-pT7-P4~aUYP{nGDtb{PymC7%qu1 zF%fY%NgByr5>Xb8c;N(%1h=Pk&qOr7J*8jEY%Sx94X<ZDx#`$zW#n(?4rtEj`Knyqmh@lf zku#6yVt@XPM~fTEEk%Bp`<$DR=ZL$VhtG=F7Cb^{%~AKD&du7@I?}ep_O2Dumd|E> zf)@4M^@Hp2+Tj{>XbcfKI-5J4e_^Dm9-Ws|Fj07-1}okvg=LxM`1PVeK6A_Q{D2HO zVo?}SFjDA{oAAAhw2SbNxrn$!YW8rFZ4p$g@6wh0a?>W=e#T1pl?2}&-{gxC%l#g$ zY3^?>!Pe31d|b9Q+%*O@cUPTO+g2(1g!<<79vQr5WoC2G*I;X9ozSm0Y^&$2R;ZkE zbe%}Gq|sj-#*^9oe>ftxbGPKG8(KZF!*{9XRym8)kNMT(qw6Fe6FNb z$fpwQ3l@TIS8C}~>fExOnHYDE+_^0pW?E?aexArr(l5(T7LD>JMJ3lyvyUn?qLpk| zm{srG?ug)w;|^e5TVOlL?|$fEP?hYH+?Vu{u+CzUxF6hh-x+tB^q9s-)QEVFgqDhs zVwhl$c7(o~TZ7Vqyp9oNQ0v4EY7)6aO+={qkgICcd3`&NzG}Qmx~d^*ovqi{&7yIC zzjg(pBUkaFf}b!rGCV?v^5i{LN4c45N6li%Vwb>nx=4DG(Zg-_ZL4?Y64qa9q`Epr zI*{^F@cBXo#V1orxy^@3BUOReko2{tUw zy7*WTn9>+;f_%PhN4&{`Rk9QH7$r_-GDN4Qo5Z%Q2@R{&Yr0t+ccq7<6E#*hUe3&E z2bJ(vcqJkeP7pSIHUp?~XyoP5nWU}J1f?EsS)}Q7or+&kOVmUn*NL+h9YKLR~plxT`uF-R6)vyY z)i9ULNlwvE(X*d`9Op|~-7n@qB;QE_5*!lxwVAckO2C^-+s#$;7Jiy_Hm>qVNNP{f zIgy_}AM0cq@;7O=UmSlas3WWs5#{i+Dby@yDZ20q@sl&6*6_>vQlGU@*|T6D`8ucY z&7s_8>~+p#+3B>1G<czM!qSjf=?>CTe2g z4lt|llY_#|s-C6P`Fp;$q|+gT_dM?%fw6t|UnFx~?w|C{!y|~nd0;m0{Oi(%#?Avo zj!DiHD6#F4=Q9rh0iDC(O=|6mL(_~)#LD2@Ug2Ks#McQ;$NCm+FYmRCvF7s7pP^kl zh}XQiWTT;+spq%RrDs?r6_$ z_R8_41-pm+YoIk6ny`l;aA|MhVn*X(Z|C4F=pjP)yM-Wd{p({89nJ41F18|cddg}v zl8#OmG(7Cw?9b`II5adg!cMO&1vR8({&ol6iO^ZOxV#nwf!y8Q+1;p-feuR9)z8heR zfktU9rK$zIV*FYgVC7_hKTrR>{+I+eG)BiAb;Kd6x+ubVHMKmOb#n0S6@eyTHk^un=ua!#B8At+Bv)AP+-&D#yO2dD~@I`9k#P7u9#OJ$(>BJPgQ}cLgW8cJg zr`c_M3V!7`PtR%`cpHP@EgB{b8akF3+JAc~7Klyhe;V<6L*3Ry} zXu#{Y{)hSfV+gTjY#K2Ae4y69qbM=x$lZTK@BZCHVo;ej?4o~1QEyLk@Bce`k7as$ z8UZEJ_;(Zq7%26>qxb(a(El^ge>lQ_Ypeffp#M9J{2zk;|3=VNl^3T;bZOhE7ia0% z{{*>Jg`dSN(%{oBXOXmnmvTTnZI{VY_^%e^qaCL2u4F7Oswa_(O#Gi{p7go?u2gx; zj888P5Ip{yFyr3{0QNuz6-451Keb0dL^Qj6>A}0m6%?tE@Lz$x*d!JWxHl*@;vYA$ z2*sd5Z+A)mTkuU5LhnG%Ouk|K2O)V6^dS8P3WCss&3_bXVgFY|AwCjqvBs|OJJ#Qu z{sh3(u&^8n|4*3zePz_@FsG=AJx@3cS_0_ODjuvCPkqpMjwMFp02sp?+u?T`0jI2w zXtA|;{O4~>mX9V%nnH;2|7cnbfw?;PPykh!DkHW+`v)ff5rqApJ~-t{2!{~~nCui- zXuO(!D7EcJTSFJhBHdPe(`RFYTU^>o06{}wI*g7H0M4JQAFzBU3 zEK#%ywdiqmce$)I8189_bSC~~6aZnPkphey0iIO6QmN?nq9=>L*7m$aPsL)ra7%SUlhZIim&^&sY{~Sd+i!z6A72 zBB}cAkB9n`0R}#RMRfPCKNhC~wp^SR{IxUrb7fc`BI9|lX&B{Ro0^5T$M#KDchNBi z$VYRzmPMkAc*&l9uTg_uyumUWb4!cXJB9vLO}tq&4k9pAr9Vz45#i)ubJ5n#SFk&Kv197bI8A+-m0h%HRNV?X*~f8W7@7!JAyom0W?K= z{e3sXU03VcRns5Csm-o(Q@HLH_^vE`R)H&LUgeMsG0_PxsPb~)?Y3;CP^C4v=Mvq< zhSWCa2d$0i0&8P}FbN-v3jE;zd~GBv@h}BLJha z2&9TEC4dcEF)0Y>Vq65JEIF5N^56d}IzAw}lN#gwKifAkYk<=$_9l(e#zcRv#OX)a zWAo)Z|QG?dmROna9zad^jN)1o$4+!F815wvJ#ovsEwIy-@9bsZSh&Z z_cE!g1{&&>73(pdnLRUT{kfvFLz`B$L}U)8*TX*^QFrY&`jVGQery3GT3Mm;pCNz| zT0IHV13VvszROSYSDZV5IGkdPxPQe7{Uy%5@y1;vZtG9sU2{N)*FtnPP6ys$=E@0 zYzt>GC&05}OB{z=P=}w+$l1JI$`_oKF<1fc_zr-FVi0dJHa$3J$|>I463`0QQiu9q z@K6B2Lnlo5>~ByGlLfp%I58K~fARs=efKnSuj3u5RBiIPQVI6D!dxNU;6|z;zVCQ+ zN6be$e#3JR6Bm>I@4Tw}d8><~{f%^B2pza$HVYf7hFII96PgtOD`&E3zpY1xRBO>T zdX^2`6*MMirkbt;gWSSrUPjd=4N!+0S@xU8PfbfNz)J3ORqRs|!nEhah%3Z^ z_Za9-f`1sm#|}VZhC538e0>KZKDJ7V=jOO#(wSxsMYe2)+|HYX8Z@f>*n_4EzzUr|N@QSum-&i{%s z0x+t?Ob!7t8fyQ~uN#=R%g?IuiM$J#wlwGvQ6Y9Sbo*o4XBtG$(oS8%vgqm$cq2z@ zb67I^2QJeZF-t?AQjL0Lm@BFvg>N=WI}2`J%{OrcBj4g5GJd+>2T@6kPx1IVrDz17O{!wy&yO+unMIAQ&WV!Fab4Gw!lE(nSI@wVWIwyRr zIF^)0~LB}&!8Ew@$Zh|_d=9!_8Frs-wAK^m)Sx+tBH-wYTCDR*5wOuS80+ZWqPTIl&~CLP3~USfIyVky7) zGp3;6BtRh$I`J;-fCV{ceG@=n)XL^vMprwtv*^1$RH7llR4kbJcJUn|S0OAg{QZ5(S%HR#kEEELTz zg~(IR9>=n9H*wWj#jjeHO_QY}O@E9a?A)IemzFzRWQmZW zh6&yUqM`J_{_JKSEw)N1Gx`;YiEPeXZcxw76nd-pn3<77%0}+Nx#DN}E0Wv;;h6KI zg(x+~wuWm8TL?I0fO;cxZ*n~VqrK#GptnAC=x%%>oLIoJt=sI%b)gH4q_Q~n(LU+t zyD%Be(snVyF0)imf}Ry{j@*5_T{qdQ8}aUyc?yrQea-+J2Tw6>OU&XZ72nJ`Z)YNJ z5+BzN;X77zh3ft+vlBTvm(`AIU*mVRz7;xeP=B0UA07Do-de!=huiCs-VGtF$4Bc! zSuK$qfYnD(Bo+TAjbi-(CXOpjV`Dke&azp$@yV>WZyxCvL3b;4f@=z1XJ|E!fGQ?#AJ9B%aiJ2pYkRGN8;`|@Wt-!{w9!{j2LMjYPY zErEjugaV2HBvf8Kf7;6z+)|zKA=09L31g+(?X6#0ScCK6z6j>|iG{x7bz*n_L&)U9 zG#JQpRnf6_P444F=X*@? z;CDA&2>@p7LKQC*QO57DmQ{EME(6Y(qmqU+~V@Z<%Do1K301#90x zo9J+%yuoj9EdkH^D`@??#+7&-S*)zzrO+p1WN&RlnoD2R{2D?|(U3nMGf$yg{i?h} zppW`2xkY-}uSAP`QlJ<2zHLYDV`!)B;YC6WamL7wZQB?rB?g}^d3gB57F}{+1SRbGomcENzKgy&IXPwwln~{b$f7+9XZ9Z%!i|e|YupF4Sj^c3+ zp3AnrTy>P%zS`*Hq#nJzK1O)8<+o#?W5vS&%6!>ZA}rehN#G;sWQqCQW=03bm3lOZ zm_Gb=LVod$55jO5boxzHW&Y=?Fk?#&+TQwQPo=CMN8`+O=W@s6*qoz>0bKf*+46_t zFY@w-^O)R(mnf#B=Wq7Y1Z#Uqxa8lsbuIVI%ja!VyR1D#`uFI9A%0mE*Oyd@lOwEo zr|I(W)Pz%`BEk+s$Yb&JvbKb?e~=}RAOT{1#gWB2H|TL;9xj;{{b0??rOdjMwk zt;V=Wy9Il6qg#EyO3;Bzy7qIY^4OT zdOgn(mF*1!qZXp++#4;czrpj!*!udBnr=4 zqaiJi#U)@H6l^-q1LeJX`}$&H1o{5Pw197`3uQ!PF1YjlfM7wjSB`;RHClV}Q+sS= zs$4mi29wC}-M8%Xw`BO|p*!toLW^Sp5XMD>u$WaH#*x#2yGeh*cuG;gJBuTvpH)MC zj&+JTk=Ld#HPgXoM7_-N)3~1$=d1_UKhr6q9SJ=rH-$8M_cZO{{i~4;{Pro43GjQX z5qEcnOh2G(nL#yX63ctaQ{Y-hEw=O@9tvisnk5%76eAv!$d%SXg9W=~Bi68&rB{6hBVwL{y z&Yni?84$}AVk@Sg08yOgCN6ynpuF^#N8hC|Fl3N%de;tyGGt`S6vf9RR~B>AH|l;_ z6GiPQeTd>>wae=3sa-cie77sTWsuf*vPA<W|EZlUY+D??!X%@${elpoejI|27bNzr&7AFFqGcJBpQ++)t<=Ru| z>cBj=qi^YpxwH0>c*euUo~WOcu*v0^n*Llwv4&j+Tj$sH)TQl-$a0TST3t7Ixzm=* zPWP6nTIgChjVoq}yVPs@LDE=1O?|f%PI)%w&*O!q)0XfpZBO?qwjIv>K{{~U^_nK+ z1{NA2`XIfxU1iRPqlH?bFv57p0jE@HYuHmwl=2lB|L2oc274Gn=Yn?)RU2DKtu=h= z=aQ-1_4&4{YOC|I#V6C0LPOUDGQ|^TIu@vX&5A~BoufLbp$O?sQOi8);YgcSy5L|g z9ucg!U8WP6RH3<7l)=Ox;o}*o3%^t+x$YV$Z7rWA>_e<~nSrAyPjHW4w+(oA6i8+t zn-Ie4CRRru{dSl|VEab~=gpJ(yTK>~c!5k_UXPB-FZLg&E{(NGAp_OHgp#}>CwR^% zxIeot_t`7*?TpuZ-OqNM2oRWSl}4*-H7U0!UW=V-#1Gf!0ZSTIPlK{cTf}!>(0PCE z#aENa5yRZ|;utQv%{h74>0xnb*RO^5pKRtTdEvwJZ5}M{cl!6P#%u!l>1G?+pwK7h3rbfX&3XOvYxQa}spfl@E zNV5!`i3QSLCGGi&@JGot6tCY%zG=%F^O!Gzs|EcZ45GjW%j*?3qlKZzV_6YQJBI_J zxvr<8jym6=Q9hNcd!h-Q5K&@g_%^SPH9YimGgF_!B**DIx{ZQ0DMJxuVICm!#zqKq z=s4=y&!`56)KLvgfH~ui6|8D2;Ud*X_I!()7fE^Q4c99qt&FN}=UJ_{_STuS3^zgs zr)(T;aDQji_MQNzl#pf(mcM%vV!#t}h7>H==of2BKYcIV#5~5M2lwK)DsUbul9_uP zAKHGI%aY7F&)!v1*Njb}7f%tO*b~T;*sDHSkEu#cH2okr0%=s0vxt+NtWR;WSN~qG zlY{CT$qI#iBn%XVKW&(4+R=!$g=c{0F5%J>eEh>X%qcUp`r3?9S3lxjFv7xl6b|hS zrvz7u=Cj{T3y<%IsIvl#}*4kXMPbC=!( zb2m#w(7jMWujYxvFs;)>!9@Q}Wb*V8dPVx&W5<5b&R2;sAq6D*n3G@MJ+_kJ{KnPO zbY+UD*+;AsAa_({H8TZBSX;k^baOsvr$8BIT;w{B8oPQQEKO>kuJ4_4rhT=wa7b~( zizGj-goA7nGP;+)vC)~jo+mq;)F2vId4T+}zGS4b_V1K6O+64ts_YGz@BB@8dI3;W z?3nHJkYG>WVR2d41bDx{U!FgMfx#&IWyVZhd+{`fA2^V>`dVcc&c~D=3%1n~^m&25 z=xkFVBT)PT7vzwkC3Q#QsL!9dWf-#p{jn+r-aEYrzLASk!-mQ-nX zKq7hg$NtbVyVlXL3V3l6usynYI;JXJi>wN2$I@7%g6Cv0RkZts1}rYaG<2UO|3h%dNOGN>VhrD@~% zn$QzxSV*nRem|z5?`hbeaQ|dFn`9fMioD$RLo|Z5-(Dm^ct#E`$qq&Qe6Ene6Ur3l z^x#`|eP29JPY>iW8hlr`RzxSPC2x3{76GhPcBu#@bxn-e*a#nZY+?%}E@~1D(wcs! z^2Ce)CbTqFJ^GIf%@v@!v3VH_cG|_7zR%yw`~HALM2k%|F4iNqF_{EO(TRyG!G^_h z>cB9v2a>*LV+EYs7>3{S=ti`B&*(Av{IsM~GE81v53vqAOfIX9+k3Zk3;D_4a@Fwk z-nO8-Y3L$yUeznu^f!0CSTfkAk3KBOSNDgLoKcq`+7Hh+$oOr(QV8BvX2ofTmFBGjiL;9w?OQEJ%TW8Zq;PazMg-Za z*}!z^aLG2ocP|@&K_(4;N5I+APVdQbU0hHR!t$$ZoW-Hzmd%>BzwtEWS3;;|DsJj8 zZOs04R+Kl+tf)^2v-qOKNJ9ViwDi$=7=)7Gx$kg%*(+L(T0{DM_R)}*KY1QUtx3DZ zgO5GgM4M}UVjYXDosj7h?dR-!I7lp|n!GDR^1ajSZcS#~_Tiv{^_DopiroP&$+^c_ zl5=-P5a-R#&~yBw3SPTCJjBNILiFQ%&oi=4TaE=HJ!*sdj$|*+dLg-!x5=$NR?!pu zLAhLfn>)=tEZs6Bu&x9-Xq4z8Uff04K;+qRZpMyaWZ;-5CvwtJId@g_;n|c}V`d44 z^Jf;z7iEeY+xH$){>XAXb|BQh6fnjaXfjUPY1v8xyK@t zV3YXS?+mnFd}DG^pX|p<5Q2@~;CPTi6-Sy!9`u<|HiX}Yf{x(1@z;^t4+uio0L9y- zxlM(gy^kXDOyuGOJqc1;ysOPkfGORt;KBIt=ZaGhK^ma68Nua2+I#l%OJ-ELt4US0aOtl{u@Rf@-us zwu#bKtKs@=Q%^0bkiANac0$U;Mms1JEP~v5^C#(8ZbtZmbJuoT(x#;b^ok^gm^|^4 zDh5@<=O4N}9d+D*6M+aCm2L#W3BNeiFi}w@7F}f>AIK`FUQPC_?BHG(N;+S%cuQxF zRbQ`nW)kpBx9^YH2^Dr($K*CApD9i!T-+2u{jxbH^xcZNMXby4SDJ_BIL%~7n8WwV zOu&|pR@Ie(92XsImhXV~a${%pg?V*vd)It^&^o+jGVOrN)Vp5>8dW5Gd?L*SLgD62 z-RChFhZF3r7pVVdy6}g~{RvG#!IA=Sz^^ay>51`e00Fp0?Mb*LrNf5u>DE&7+{VH3 z$tBMxS6|+vPE&*iMRVID=P@KI@{u05Q&sHFLdHbr{re#&@( zBmy9fUt%Xb?Oscd+*C$z^fNvhgsLrzck>NGWo{8aah0jZDpQRyAbf%&k7K zOUStHoSevU4jjnE3+EZNM_i}W>6ZIa{#@EHy#+eUt$=5@xpoxXmx(N+1KXBMvU{N>TmdB$H*5TiQ|s8Pco?*J=EyQ zRMq5@>BY}Y+YC6Tc_1BcUOxwKel-dm4c0zjq(f|=%<7d?4oOC5sDbO;-Tm&bwFA_X$m#j42j@735~@~hxLW(Im3&m5 zHY2>lr5nDCPCKTy#5Qthk4Kk)OpJkF0!*OsnP!2ql9wfT2z2$iG%v|$#pyV0BDZJo zqT*FYb2q-OdJ`gU&MKy)$-^Uz{`AGHRl& z$bzorr)P{z@r}0Vr~PuPeHx#I9?LrWOr|3YPyNb%5G+`a z;)5IK@CtDvq1?zci_Ze46E*gVRIjcJ9@H-wDXZ$9TuH6usV;`*Ck$;}*Y-Ut>Wo*^ z#kT_AI%Y)XnQR=bw$oKm=NQ)dPPc7s?wQo(jg@T)AD;txgT9QpbeIaKKx0a6tunq<(?XLFa$tF^cBOGDDnET^ z82lPOd)Ly*;pd|QHW%|xWlm#RCL6wMJZ129rkIc2F80YR#!M-r*Q`GD8=hQt6{Sc) zGE2{l;iH5VVcyO(AcZ)$rMUHvVwis|KNKSa0)8)6S}F6d18wxRS7iDa#)H-_YwK=% zJR@%+d%Emn%kXXk$J*HvKtM}K%ufG*0mGlfBnlX&q%HL63vqxj++J8(HlX`B9JFNE zx}ej)TVu(u38cr-F!C6Iq!f?Y$UjcW(?8$$-Mw|T8!T$H5A8<7SfSh9ZNIO`nG&z+ zgVNkz%ZW~8G2HhTlgDN?Y0*30oTL}Mm>hf(`@B80N{r?)=JgruM!)>iVwi_kkOA>~ zfXO_4B%f>ZYcBQHPxTTA< zzpVyyE^yZ+;L|V8T4#YY0sXE+vi!R{izocO!?sEJ#nKF6O@ERYO2F35d?ZGz9jH2R zQLlVJ519QU9!70!z&Y-`-jx9quzqI`m`a5Eg@(`{egX!t>u@@T{5L-l>j%UihdvCU zLhFERrpDtu1L|95Y081Ays@8boWR`^aCb{5yA6!{wDy5T;O}gw0We_c%6P%{ZwnwG z0&XUAKN))sJk6N6v?ByCGnq(vZ46)#bjlWWj1bi(@fK{#M?`J1 zBAkVkS0)*F{TeAd>A+dvOEUX>r+Kci#2Xj zO^G&Pr=@x0eCN>AV1`>*=G71IGam+Jj^WCpxq@jaFv1nO2&i138y zQ~;0IyTdBJM+jqhtMK8!bPOFVDOu>aqW46?{x-erN*2iY*~$G;1zh#Pzh@MC(G`vk zNp-z*Y@nM;t z;;R$U*{3kjVg>fpoo_3t47jgsc1Tm;9$PNA&2H`iW(GkEm81g%Ci`y5Vx0vD+Nrqs zUkOSxhlxW3I~o~}jVD@Rm~>k~TGX?1PV&+SmFv@S*2#|dsmjfLs%+vVoAr#O1O+d* zC3N-F)%3H@^~-HRF^lz~jA>K3*AlHS&+ES<9YLy00~rvFJB;q#A3W*28%kq4Z4XTR z!Cw&-^~cdYO?h5*+l{JG%0@z0*pwUq1w__{@Y7@irblT1=>aLlAFkqwQPFl@EWG>q zRssl%q0kd!!pJ$YWrfW#w!WTM0DAhm{{T(Q0voy?2a6Xy4G@iyrS{Rv^t(RmAq|?b z&HOx)rUcM@T_Lm3{SYX@^9mL3a9#?i%Z9c0J8wvrGi$p#w035}=raG}TwG+ZX zN9&ACiEpc-a{3|>D{@j6+ffIc2D^>wTLzt`Gd#zSz6bc?tJGrX?K(Xg0&@)bX(x!a zHTCpvK-h_Yl8M(d*Srj;o@XQ=+zCt=_H=`z>QR?=iOw+f8MM@c`qSPw3Uerv#MoaO zs1tv2vOVhzFOaJ>$a$t@u4DW1_yolaM#OYolr6iQxTQ`cXXqL_9eeJvY76ygLG!&7 zI8)NS=QGPi2j2K8!hAXp8xqUx8V{5VeAdHz$gN61HlEyzTt0R9wIn>V25nK=(EQ}< z!vfLw>y#C6gKg0}jU16T>oQkaOsulzB4`|6-nq0I!{^>md!;hqYuqvvR6OgLQ9mXvzZ5 zRy5rOOw7nsHhgtU$6GUlDHCN-x=3_(A#GQR40!a*p9_cEaCZD&reO zGVfJ~4F=&GzHX8a)cM9r2QGlCmaKSBz6=>$&0J7}E;kE1uOm+n&yCsY{n9rgPWw)!O3(0s-Q5nx-krCn%lEzNF4bkBBM(dR zK&ehZY>FNmr6z>!wLQ#n`;8@P6qu`wU~L0lZoNo0b=Vf0|YtBRnZXi?PBB~c@@k|fO>(Vrw zIR4|~*tnh}!vj|xP49}8Z_H8m^^RmD#_Jl}MS|Z$UwoR78nLmms%@fF=O4+LP{rt6l;TY=q>cGTPpE>O+OAFL=oC(_B!{g%ra)J)o`+b} z&lb6$s^2bta!F(@B2tJnz0G9c4!hWwkGYo1Sa7&5EX1JX^2IkZ_6q{wSr_h!Yqt|} z4kWjmz;J=v3s{)ayleAm0dvezlx)%J>cTdG|k zLgo32lOR78Y3cLm{zoS6+^$vqPX}8TH8+TuWo=EL+ZV7W%2Y5a3HaPlK10Q~i|8zY z?_OM#`TP5i!hMY!S_>Xh6|XCuOfC$T=!_G>zJPW93MzOa*CGh`F+R45IS?~}<+M3l ziUJl>N70mf1z2V3k|I$$u(>C$Ox_>f{0%E!0~%KQsogNY690_8yE;TwckPwT)Jk}1 zN&W?WkH*=>(bo=0>)4JI%kkX{+hTi)rdibx6MEz<*id+nER#hGCA1~XRJNTjwYQ(H z0#%{crb-bYjWQWS>bRTQ6dKN7wz0SNCA@*?&BTlK-%P59PmnFv?8DgVe(dz9LpmU< zNV76WAeH5HKg9Ej5OygJ>Y|=VS@s8}J&+5F-+0u)bX8^KW6yX7kWODNnWH~5XZ$sD zOBwq}y=z(yE;_JrD7S+vZ{YUDjM;0h+X$s?Lw?2%E{3GV#HVbFdV=&N2npK?*7%7zbNKElUsbnz(S^w1c@aeZ0b*%oS$f8UzY| zF#REkCn$mdrrz9~-SXSr(EOD!ItgJZ7~ep)-(+RW4maN>@(9eVgNa^+sLrc3v>Qim zzgY#BwUy1Y1Ld5{CuM|5IPXJM>6z^1Ar=F&DRvZ<7g&N;Rt-a-QC3MuY{fQj*iE?e~m4^`uLH>01S$$+)fK z=ax%4--eXPwqkS2G`q$Osb-ng+OKHnBOAeo%e_JwmXfvwtMj(5^=qI{;fWRbA}lh_ zWu~5klF)B^_z{ACs*1`8pUOd-FuqlvYgXqE*jPP3Q8O7$x7cb(J6>L@oYeP2ovlXUlxqhguvG*ire+=p6443x+MPF&T>)OvHwtMxF$NG4yKbjaz22(zb9O}&zl-sM^rQT4bGAtO5E|QalopKA@bKxy0 zkCieHwh)r)Wp>NF9X6PIVUF2LzHY1NcavzRv%Y@W_obxyr2V!?J)C}*%BZ@k6Scin zpQ<7HF#`a5Hj`VO1hD>rmfodyop36%>5(+jjkpW%O}Am*AFykDh+QSuZmSKOQ^7#q zO%(c`^^NgD=ieQjEXMl~)su%Fao46F7|PIh=2&p6GiS2s0_^1+2(7Va<9?H8sF#R23neB^j zkxY^!kZbDS7Jb@x^ku;9Px3{~1&jCzI8VOL(n?*x*}3au^P9@{vn)sr&!$}lrNVbL zU1%|;)3Qduj*+v+s`=&GJzuMbk>1ac*Krez^}1P$np&Sv8tXY`al5(#M5-D(y&vy9 z!r69iYCJ$agqa-X*G%|szk!joY>mHqK8wy(TkbPx8ZV4X45PyvfAKNIpRXt^qG=xe zN!c6$qN<9cH@dDu+K+#s`TD@eji1tHz3$N>f@G(71F={$SM66&o;Y&uaMg1u~m}zfzNI$1t$M5DX&vX7) zThvEwulU56f}1hm!b|r@5B)wa4ivuFTR)WDGQGHaUgnR!&3pd-kSe>XnSB~7@w0Eqr(1LE~U7l!{KHRx(c)e%c zGxFl-UgKVd13ZIr+R>HQdxUGuAylTs@5$-x$dAgXSFdE5FFFh4C#dmk9&B?x81vD2 z)Nt;7Ue|T;aF+CX?&BlR^EM`~3G-rh2LaBC8lCAocTsV6bwS_WedThW-6$*fkANvy zNi&tjW&z&>Im#R>wFV2>?0G#4I<;GOJ+I}PuNJyN>A|k3id8jsc2 zx$XAeltyq!s@q(WK!da8+=A0UDSTkTx$lPIYi-18%yoK7p9rXf(uu6h{x>E!Dr>Gkk4>qOhgC;9BaJGTgE+@A6|1Q@ zhko;CDs#72tVep?l-KK}qHMV#J}8KZJ2nU2{54xu>5e6|)oM684VtvJ+rgINRJBPBXr*dfgH)wzKT=VNGQ4FhXy*Vw3GdC? zwsbUwgby?p!MR=iS_B;MPTcQ|#7YH>4GVo-A$RwSwI7)k9U_T|;Ubcsd>?3hH6gk&8#E=5@D}qC5}$ z_-9i4D|YKl@lR@VQ=0Fia&%-h!^bvM-R(M1)(3eT7YsXMCBB|9R!6Iy!{|e)lK0IR z2QRM#t&i?S|3B<~^;=Zk_vkAj1|=yCB3&v-Ga!w$2uK@rN(&4i@(M^dk^@pACEXxB zUJ%FF@=ehT}|G<6jFC6yFnSIt?Ywgv0o%G|q*P6x{j^5{eqq-9B1Wn}n zU}ao|zq4Xox`qrWv;}j#{_K@}Jtq_}3tC;eF_Ek9P$*DVTf*jW7NmWqr*;2nnYQ~$ zO+ivilx+)FYmvLg%Msot6XNklsuRq={6F11mh!DC`-{`~Euc-i*q*f@QttAyr>wY0ma_I>9? zyzO;Fn{Zq!5j!NpD&cw_OQ*Lge z%d;&YV2-o5ujSrdckYo`l{yiXa(hYzW>)e0bp?7w+z|(X9MwWb?+STwbDFiqz@egI zkVc;*{Ha6bu72<;^Z`y*3eRfJ0Yup+V*yw7-jW~O!FTw`PEb3ywA(tnsD!TA<|X|o z&)G3aN88Yg$7AN1i3A$tSwn+E3FP0HRENKZId_-$PI9Ryp6bd)t4q5+>7|;cp2v)Ti7yWt?Vbb(<3ruA;0L_WvX`gs!FqB`Jnu) z&n`8oru5)^2!wS~mfBwGbFU3gLRC``o5#3(7Y=nh^HvLPP77oEH5+A(35n<0!{H>b z#xp$)dvDySj7G@Q;)BX%!PXL--@^+_9|sM6a{A~J;znI_=z=~f?N8vRqMYH!vBlbz zH}$Yn-JKjvhb+>LkGW_1tpqi$V^d+koXM$mV_!^)AFe1-eAt{BTv{|0Sjtl|6*Zb__rUu5lQ)-3dmrlW7y=-9(GRhlN{~)xndBUE_8T6Dq5ok>*+F zfWfu6*o6o>qbWDic3juK3bj~1y3Pq*x66 z(Ou8?L#(;`=3cuS%)k<9IXOxh-DHwit(65PTE*A%SN@QDzC8C}x)iocts^TSwBM)q z&ZXTS$&P1Ymxb^3+z%nCRwgo>=#MaNGAR|6E-BCbFr!94S}B;Dm&bWxP&No``#_bK zF5GL~S+TXq10j3nQ9SYBTXu#4{fymC^807W(h<17?^&KzS_=_(2($_~LBBT=Oa5_a z5r3=hK;5_Jm-6PehRM^?CEXOg2@PfCkimW-Qv;bvTO|*rb0z(HO=a0GB`9MKCsy2- zz@Q`aci>=NsWPkG@ZnZ+Z&fsy?xdqzU7^S=4OYq?+uOCa*R5Pr zsyk>Z7?P#0KB*d8hg-zkQcLF^e5h8^RyxZ*BL=3KzW%kb)C+iVZvwCNRO{R5CP#oe z1f1xteC3`M*Y^zCmk%6sk|K6L#L}ch^p81kS~HAc;s>qD?QN9%wwiRr;Q3gzM~+RK zj}0)O%OY~`E6<_Z1God+i90ZN^MRni1r~6>`JT6M6h)yp-L!l<28zX1G@Sax(mpxR)lzU zD$QV<*b=KHjs^j^Q)_+}T@&$G1AC7*%L6y&$ENozxL8LDkF_1XYvqRQ3VqT)Y-QJv zJIfvMoF+(`?T<`#gR3^w2Cl1bYRkP^KkGJ3?Y4~=5;L(sCO>#=ezyQIy>qgYW}t8K{Cf1p)>daAr)&pEbWIF+5YA!L*^wsWv@ge>is~CRi%${c9rMvsJsmGEL99$>B$hZ1p5XT!Ldv3a?<^8Nw+5w zQgIk)nSF551D%(E_Id8^hkV}v|Kz8RI%l#0Fbm5<8QAz|iVRKV%E@mM~P75N{Ln+t~^AVES92LS95!{Lfv8TQ6TFoXJ9zhy)?*@2=17#b< zj%%Mte{cia(g>%R4+C5 z8GAoLG{pOV?8E2V0s|w43@a-+hJ};nmewV%H)~9(3D=iHcxOU1-*{Hf{K_|-QTuH8 z?#C2E$)?N2%8G-m5;5gtd%ash#(Rxdj(iK9bvOZW^W>6coB%( zh(GTopR;@Mf`iuJgNLyAUJ#`_lH2-V0em0P=JAY6{=b@ilwF+;i#wq<2K9r zvmqHHH7Gbw8QQ zDe8_>n+U&7a!DZC+utE#@?NsaEz^wA$&)U~?<8AZ?67LNpX5QY3i4ZSeMd&BDG5j^ ziR`~T+RyhCSskr9dpsPA1&f?#It>^XI#(Ak@uKD_kjWDeq&r+SS3CSUUOKZk5X&We zY@`)gtyi`_Y?XQQ#aNL``a=j;pHQU)CuDNuDT6O=P{(O1Y5OOaWvePaj+nc3#quu; z;g#vF9VsG*Z3SN}5(!`A3mguXwEgb3FifEgKhdCO8+Lly=CR6a``m76`dO^!>Gsou42a0#(1F6TBaMjH3Ka{6wOc>#}1CNIpJU`d_Qup?EG47&g!j(@5J#t(cxld zP}VH*WWigJ4`zynNz>;B?Cf6n5>b&29o-(wbrm94%GcRVMLVyj9mKj5gv7@Fv+MxQ z2Fr%*k7#gd7nL=4x-9M0x!N#mItTaoh&3h3{m5@7tF6#a?SA5W%%f^UFCnra?0fXB z!)_zn{((x3Y`39(j8Vvkl_ArM>5o)jmZ@U670Z$`YR_ebGa=-?D>P_>T}FN6G&ChX zSSL9L3v0VPuGm!3+aEaen-;As8jLPKS=|+;jo0N#3)LHBNUzs* z$Pba7D$OHeqvF-ePo3V6%E=#IeWtZ+JGNK;EBtMmnQvao%RHC7HZgMbJs&>=t2wqm zI2MS)-dB3Bsk05|ofZn4&TUI2Z?XM*{5x=ZN3nR9EDx(=zPlJRV$*)~!J(-k5N(f( zlc|oTCP~U4l2o*z52m5U-sot!H^8ZtPs7)R;q+Qrc3%*(4JkcFOe!z8W62B4*VLJ9 zefIlAYt^IU9>*BC=ytoGwA!_g6Og7chJykQNp3Z{OIZ8_yr#|%%AN=clJ9E2sL_6~ zHB?rsPQw0*7RynPB+Y}P1X{g%oWWNkrzE)iFfq&cxyNq&R8v*qKwiczO6PG~mMjCc zsEk4vU}){myfGO7*VM^R+bp6p+z=}=-#C<0EvUH~yr-M1l}bmC%hEeeFIwT^@`J?9 zF|V)rKsWtfzQ!O4&9+vFqD1{QS8;>VoP@`$xPiAoWq~BQ|J!bVa2{QwoS$DDsM>qx zIZud*x3w614Asa$5lj1freOteRbaHtqV)RIY@!#i=K0n;ft9OmL0%~WVG{UEr=pUW ze$h}q-*W1GL9ktwrgepB!T3f{sczt$96hSG8LdI*+GfN6OQ8R4N+iGq9GRR2?aD0ms0tHX@iuyXl($FCdH%)sh(57Hqutc*IJbIljwS@tkOkt#qU?*=tN_IY#s`M<%9&5X}o zKZE)w67dlG0FX=7viz2Yb;Uo-qTqK&u@Ue7HXmV=0N;~s%xR;JT=a1VYvY1LU?unG z6Ny0~NzQxQ_qV~4eA%(k3h|J*&wUNqb8H)o`}bP zRq5Yfq9lmo`-7S@Er+x)W&Er5rAN2E!Yg}w zd!fER@z9Lu-Z$K4@~fy9en0*3^!%`nOFqdE%}W;}SmfXp=$4f_H>hbqyvsm*7lT!9 z8dY6c28yAg*wI|Bs~~ArXi@9?BZ3G2CT~rmRjqokQLP<(H)ejT$Zq6YNXMaJtnfhu zR|hHLtQ^}Xr0u|Cux4lcDb}1j<9bfVlZw@Xs`;JtO?YKYP>Evl{Tcfe&8oS6nsV_I z>xDw|{&#kVap|!_ztOx1Q2PX*Q%*Ayu<-BH(DLt#`w=jBfCWl#x4QjW6* zMBCtNmCE^)=7+s&xX*Y1$1%}=fY))yOp2jW`9>^`KopLMjVVi2X z;3sb!ZI&*&Pt~$S7c=dG<#b{kDfrrij1)ZfcWK%eazipp%`*Cq#+@qsriJ3MxNyBb z!D2If<`10RWqC6RbQ$?3+-ed|hBW7TgMtbNH}d11aNFXV$wVeu(DLz_1p%A&hK0Y5 zLe-ATNxW2jj1N;ExKEUhLl*f>)Rr*^21>no}A2lP8n;VycaYnGL_ ze*f*;5}_Ik#8(HlFxm(mguX|X$aVw3YH!jSi%L_nb1J`J3lvMi+x|S?J%WD=Q1qtD z)^&$Ad?~ajLYS{Ql=Jx)1Th6H^dLJ1W2<6DEmiK&oF|JfaO0;X|GY$@dWgPL|GUC7 zS0758kF!BU2c0|&Rn~QqdB1Z~5(VdWYOA=2b=)%G5I2O;&bE%@KjY1-^D;tpwAiHz zzHC=bSR#}wP+%%nAMXMW(vw290Sy!`-u>8`C4~Z$Rf&KtRO}7jHl}g+GXCFAfLABI z@t>hd0Rt$iLOJ!x*(c`DZ_QP#KMe2esg!}^{@g?`Hi!EYPE(I6v7FkBHsujDvP~u! zLb_!By1%*m-;Dea_wbf}kpDYP+(S_XUU$5PZr9z~HD1Iv_jtWu9$mH5=sTs>06QRj z7q9yoTQD-voe)v^$q%7LKl?8#$|s#wGNY#f&JB(6nU}Dw(Lu~h(yIqiTYQ~E^5~kW z`w5x-k-F95)|GE#MVvFa5PbTPST;2VH@hfd_QbG`p^omVywJ~2=&*b}A0Fn`ay}g} zeJ;GNzCHK()A1AIlIYt2tKeN9PY)q1@tiog^}>&lp$Qe^*d>9WoEM_;rIH@P_AF7j z1J!*6x4pL={ko+Y(7LE?aq zmPIWn1qQj$!6V3MBc!CjylkIpesD`cH5_+U%EbX;?=rxTn!$;Db`rThmYZG;)H4C9 zXnh|)fJ?sY|B8iP^yGg5r!)L3-$x8s zD2Ms{MQ^W!p>lf8@fz)#ur#x+WgQ7DlY zY|Z5JT$-+%@ng`k?ZX!~w&t7Wf^MLu%bv~ZTqaMIo z*sDmoO*T=QI>!xKnmH5U1uHctnpaKUxN$<1XXmd z1!(f4DMyuh;zC3yqa*(jB0}JwQ&dpo+Y({e~(8X(_+UvS6SoFVa3INJKH@`Qm zfu;z*nTfJKprqJ4x#w4XGee33Z8(?XEG zLWjSpx4qkvictS*c4tme<&FC<53*|+Pd3i~W%u*oR`Gs7z`Pf`yhWYZx*Hy^#-Lqd z>wDTbNNP$GA6_rR+rG29p=@PPH!E<|fXPHsFz^u5>lDm=bo7%C71-*vs(KLxxP`Sa zO@|o6pAo{OAFVer6=hK4lyF!5Fb5DkykaiJb`ON#CiH6r8>K@uqI!0EnH#v8> zA%qN~6Y9}pWuA;$e1|+)`5NjuCz3l){cYJg0OFjPj#z(LD)qO>{yKEdi{dNdc*F1RL5Ab&M?RJA zmf!WFZ{lu!Thy?GijU^&6POb66>`guO5jb*cZ--&O9Y{uvVE2**AfJY! zQ*@>1RIs;?vQ22h;zd0YzutJ>X%~U(Z7N!G?ThqN@J?}>4%@Y*3F7Ldl%LE-Eeq^z{lb5 z_{^ttldA?55W+*@_p!QmyN8Aa-Ddr)29fgCUA<%yJsa-F0s>o_n;m1$G}Y^Nf84V0 z%Wu_5L8Z&LnXh}s{bE!h+Kx2|MwAG)F3y6}50DB|y7+$d5jEjF0o zus~?Vx(?MHF1y>Y;0fMvDml;CGGS!k-)D0%o$c!TP`4ZOB5vi!zClEO&dI-9@yFVm zM)UcisP;H*1=l^M(m{_YRdbGZK?_tx`t6!$H8rX>Ds`lA=UGC*i`MY*UOO*u7xWwP zVvm~_)ylYH2IED(6xZ&`?0gQWCX?vXqK@_*S|R=C7+Uh7%e7(wSgvKhEry4)W$lel zG5Q5^p7&~6j->V?g&h|&jrOUOJPjCEdGn&}l}>Zl*3DwN@G^Hh7%EJcOz-^%b691Q zv>klH>95jjx&2>P{53H@JReLfGLAlq(XU!-R^)o6l=(wC-IJ|R;lWN zb)DeT;_jv%db@?@;dH`R1*%C$qk}t2Q>5tmxa;9{Hsvx9hYQ|{0mTj&&8q4H6{JM< zaGjky<8iae2Y5kNitn5rDIaU0df{$!l~F-{{+ADCz9$=#s>h((4DrQaJ>0Es8@7{W!5PIJOcqX|RKd>>Z|B}d^O%RItP{hZ0fM(yt2SBo zg5k6W+;w5qPu2n{+k$WAyY_$yVK!%CcM3Cp;(a>GW;Ex5UzKzPEax9 zK_98nMkwLj5&o-Mz{p3+iGc|$7P@ghPXvJ>9egSK|Fjo&_0LJNf%WB|J9i;S5U zj9e!VdQO3us+I=51G&vPq+PCMQY;X)u?oKltk6D-a!Xa6M?#XoC)<{q<_w>ZX{7&x zr8Y`_pkYHT?H|8Jpb@2FXd&n-AGj${<(z~&`Fsg1u)+5TBSGlWL3W7K=hHYhShQkH z(jT^PK+g36KSZ+@NL$FN20fTaR!w6`bm+$&@eq;lI~HrPE7|h>U~q9qlE|&xhr03^ z=TKX8r%Kr$Kjk_k^sVgTcN{+_gHa*h;!Rf}Smo{$H~t03ozDFDxkP=Zoc}|-ULF9j z#<=5Y>`!rTg%Aq}LjK|hj&KlNhz)vfWg`I{ayn_PJwUM^D+D14ZK-XH{P_u(uK63$ z9ok+7t9?Ho6)0S$qx*64iSF6rwaG|_>Vldoh%-xyG z9-S|G#!(^Jk57DJp}rR1MFG3tCtxYk8>tj<(HbQ72X9;dB3ymdu>VB)c~|f9;@-?5 zb>xZTi*B&}8f%o{Q_!Uku^-lxPnUX6<|>8yyoI^OkYD{T($Cjkg**bv_g{*TZ`WR> zCF&%Y`1mcN^G+hZ2LnvFfKIliQY)b-ic&<4PWm8o)(=A5*FllH%?$kI&-*jGnit1Y z4qp)`gIakpH0!6IswFHuKOoQrNps0IH0CT2)y%N>ObdHovIZu%E=W4Ol|9u~x>ee~ z{Kjkia(2={Gn2V$K;0nTlo{h+R%-q(vsl+XEO}9cQPlthW@<25M6b*=F^p|FE9THB6K~!!`B0UAqI5&UJ zdWYQ!%i@>VLT=`-!jDVJ-=sn}&X`tW?&Y`HcogVZPb37#$xFFIQdMC#Zj+?F;7F>W zp!y9=j!RwBF=Q2<9Zks<5fNeiWdQ->s#~j3v!2j?)s)GuPiJxd%5Zh)K zYM<|hF^-fPSD3r!m0?Z$3E9-EQ^Q2chx<>^r=%m1GJ!teL zJAw$N7`;rI|7U+hRtzjuzWXF_DY>*VCoF!bu8~@M59jEdDqgK|${)EXvez__(L)(X z*r+Uf<_ltxOiwkJJc*sZkHtF)GYuQOe=GU@MP~(&uym8db!hf*R|Zya4&mUmI3Krt zJ%*E%`$y68g>r4V97)&i&RdRAlu2YQ`W){n7WW7Y_q@x2(uifTmi(?v2`v8E9{&M4 zRI5;4){+{WZLP6&nv+q`cYUfUEbDd3jQ#snoI%aDB1!F(oP-f#5G@n~ zV{$ev9xDV|{o*{!-q3EGOv9nJGlq2jd|orHX@15rv9M_QLGDKb>dz-@**hO`~xe3jX7wV3DOcyC#uJ0w}@CFT}KaCU2sL&U?PjNZ;fNekiYHi_WNd=*5df^Zty4) z{v2ae@k1uuca^1|AXj5?Uymi7C*15@b4Eskq4$KpKg|DoSk452`F*TaOLeJ+04bBv( z&FU1bYcJepq#{RscL?Nw>~C^AW8?nDAC!iiQJb63+=$%OmXT|>lqp2_ zLFJ@w{NcFv%sv4Qyl2VdpAdsuyGw)#69y|PymoKRQ2VO8bMPw?U=uvO-<(JVY=TS! zUL(ZQG5nyB5JNUpb-l6~&w@E&Pz93F#d>i+-0G&%#iQg^Agq_vb=&YvI`G9Rx-w=t-5KkPL|lP8^3UVZeax zI3{2REftYv9DT_U-0dfdzCPc|0=br0V2dx`w53-;a;uH|slfO_IH@ItI7f|MJ%#{AzP@FPnga zJVGofo)Eok@|H=)EseMbt*BQm9VqG+5rh3^*}s^>Kf}#L?$qQZMh67=ekSgq#On=k zpeK2jRe7o7lbNr^(CpqXm%9tZl$d-^ZC_0yC*tQ6ZH^NeKLjr5iLkTC{lW0x!56iv zO9*GF5;bPqjvUDved1O&WWS5e1bG@Z8aq2~Sy7N12hg8g;mAyJMUB^g`(pRfsYIG& zEWL~gA3{a=gLoWpGzhn~yn`T$QTJf||GafBM#<(04_j9EXEWSNwBO~`@u%r(S>wFK zQKTSk1|e=x^1#XBRk>k`LcYd3%oFkmtS0#((PdFk>L%&X>>9 z11eG)r&2Methd>8BP#3f+sdDosn4-mPCQ}AqsXRx!`9kV+2r>{LzkCS!b7{hYX)|f zTRGtG7L;L_>gP`l-jZiyp9Gn5Q94Wz;GW>#b*QUqS5UriIj>F{>a>USiY=Rz%ZTge7=OyDd@%_1vm9kr8J+FIT zV&IGYa|7cE#w`f<>)(_)|JY}|_6_zKzDJA9@ovmZ#=HgbrmhtySKiXP#rD}L06e2C z5M!MdIY++4c!P0!f``2L^K^s-m;+4YwFT~Pq>*q{Vf&SkHRSC)k57waZ{_a`_=AQ4 zYgGJE`+2%(MN_Qp1kAT{1NXZd2J8lo24-CQ!&)?NN)^)E5Oi|(IRAe@aPXs;4|v#3 z?09eD*WE;GJ-u6kU=GLkzln>%=RJ*@va`1#ci*JQDCH|-($q?~t&ryCdzr2HWM zUk#24|8f48n)VTMvZtiz@lvW0B1xogYAWO|S~`Bu^e)IRgf30VUm#mWqrWLTpxq$; zD(q6I@b%FRYz%@MpAi3kih6Pf9~y1@vT=>D-sJ17#*hXfG}_Uyag7u;l+L~a`x5r+ zKVH^E0AVevDT)=h@u3USa?d!x6s5P|%N5|d&@T7p?-&pPAke|LJn}HPE#tKlA@js2eyDX6}@Rxr6!h&qwSYBSfSCg@Clfix~QtH7qgj*of^XgaWx!@ zlpTF{!Gi(~1&*I>X$9M+kzOKw5U^9u0WzlG5CdjJseUZE)%NuNgY^FlmiQf(LaPra z-EI^|GDhpYZus~16XVYNBV{%)N(i)kZ6e)MkMoho=?Ac(!VafA4Q}4NSy^4elfQ=M z%CYja?bD@v{0Hq1*snf9PQ}7{AE&t|KIgsxTg^wrqZpzC#$y>j+-^a#1$Xu+u4T@p zq`&$D2TaI~CF#G!3fMf49G5&I0`RU*I8&@bw`R%(YS>-nFwwdI;ZCjCOO{{%}_r9&vGD@nmCe zWansLwc1PC_7>V-@ukNGBuWL)Jb^|dFpO=F{7_l)i30-wI296doFL4 z6h81L%l!DZWp1^YwSC69C_EK(o`~nQ6nPtGo2W@m`LXmCkd~igp9|6g*l+A{=7&!g zbzu1|lmO{85=UdQ$RxHE3u9m?;S3#nSi7I0f`%r()h2bR=st=e(Ks++oWB!_UX)H2 zshqlTmrd2YinhA{oF^vlkCqos5(tO3H+WX|sAaPIwo7b42jN zN?`g_Ol^MBdSa^%Mjz(RSMIYnt;R>2233kad6VDs0#}7=h%KdP4FK0bMObp(fZ0%G z!d{uqaHZ{1@xd43uULw2gDYbXfBGXYVaDyxPlG?P0l2j)=rS8GvB*Hg%#K`}V#)5- zVGns#6kr`=Rn`?V8V~XPgowhkjQHF?G0A7NyJBQF#u$_o4OfQga`u<6_L~1lE$-1pVXW9@W+qQ0W{$ntt|Zu*qLdds!Hh-VuPASd-xj| z2YgIOiU(KgNTNt@vDFA9CHb>^x3Iw`vSaycDu}7pFUAXPMBJ@&vLPbTV_UtLN}Xft z_$D*cPN`KkPaUhXt1zB2ZQtC&Vb|Q5lIZ!nP@81@Ckw>pY{<GO0GEdXpQeQei>hsl2SswkbeC`S8ANE{mBi`tZ(t zDH*_rwVhQd4U~kBHy;&=sXM{cA6ympEP!hHrwI->QJXpW8(K34?5xme(8bQ%Sc;9e zT||?TBYT0QFVq0W0--*YJKOx?_v(z*KaH**O`JoI@LwHA{5{LvjD5=92MhswT zkZn!(I$RmIH!=NhK`wsI+kBH5IQY`}inVU(1W! z2{GC_x)EKqsLxLE;QQp#c9})qR{>eDK(d;`_oeU*MIFNKD2mk9sqTjd3SzIwRB1N_ z2X0m~O^m>i5EfI97O^^|J>b4G`F?z9?wz8jvFAdIn-)U?>k*#12a zFqdO%QWE{8sU?Gwpsx>Scjf~$W%pFZOtqMYITKk=X7@dgGTr!^Lh5qQy1D_c8%>oi z@j+Sx)+Sh-x%AkN4RtwYy_huH9h|*QbxAI~#Y)8{?HNBIEG-U-mBH%WhXw}p0d@{t zeN`yxO>U)1Qp5v$#xgFL&>mbn$s%X83E*x7Q!n1}SL^^HM%7_}S01A*C@-GPn<7j?I$VY; z^zfm_9>+8`h7UlOFZ(MA6dYo-FTJ4NhzYx?nSlApC8aI_`8u|#*o%7t$n*MVan#FU z!)Z3#e1>YpMlIovePvMV;%I8<==u|r&z2R7cLWH&_^>KgjWM9$x(#tlowmoRH>qj~ zb2SIFu@L#7Ne$4m6KJF_LWk^>54Wp)kvtESinYtevdLU?j3<{(b&)}^pmkj5`EREA zwPm%+!aANGZkXD*kQ3*Q;Hk6hYNlBQCO5-|G#0xR*!dTQL`sqNHiH)kGU26PH)6Lk zO&EuFHWG#A3=D#vmo>NgQim>y9W{;gf6f#?`t_&Q#+^WvwyN~P#^nI-fv3}?iD66R zAI;WUO(*y)LJCWA$|%XP9`gyuI#bk({9fuAuZ*+VB+22~)n3B6P(hC4<%i`%xsGr2EZr-0AJb?L+}5hkgm0`Lwl#Nb z1a(xMd09R2bZsAJxo<{3%83%1qkT7)H%ZB8^z0B*7gQDIR(n-Ns{dTb<4m7mNxHVQ z|Nax>TvaJ6LJWMEoatoEvywD}j8WsWi^htCPx0n)kHvfLT^%XlkrZefyT~Yy`{E;-7_6?AGn)_X-r6 zpIH^Y9(rkiGusFN$v*qNj|nA*ynfuCBY@&g-ah0y)T0N>hsbu9Yj}(}m!!F`|AsU; zEms=AH5Zj4yN37MtyJ?2X&fm+BrcS+94FtpJapxL*QU2~*nu$OG$^@S{N;S%vB_vP zYbE1;L&qyuP3yn~Id1NQLNc#Yz+t<5=}jBJVOa~dUCPkFhdKe@GkP2|4#*wpXFb*Q z&w2s_44b21(O+!Ni^dN`+`o2*%~y#|Fz#k-E2=KmTey2gdm|F#)10?g%EO!4G(g#& z!Q|0w_Kq^evEd^f|AVfu645i5UH!CU1=oB+TGYv5N0Vrk3Q6bSR5{tPQ}G^Wn1zY* zbKHm}egWIy)1yyCpR?Wl81_nZ8Mn1OU%g0}P=?P0)X~XBL*8Ybe%s8Rk@1K_0V32e$Nxv#rV{+}!F+NlV-iiWeK)ebyYt)Z}VgG(DroxqdtfbEsY(LwoKI;xDFrAmtX8Z z)yfq$D%v}-LuexM=sIhq4Z;TP>zG}o5EZ<~HtU0@Wu-Us3~hiJ#GCAZb!i5LqgC95 zMG>X-PcWh=IOZzphL2B&oMUCq2C7a=ru4|;9+cOnHTK4>IcjKagX5 z`HEx9q`LdRjCzO_JGqhLorc% zM$Apx6}H*zd(?N0LJVst(Q=h%eDlaIf)VGM)KR;&AJi}g*(l0#!|=}SqM(m!PwWeX zx8}+WE054Tz}7!cQnS;mJCfX+8ntP<$ej;H*y-Buy8^#o+1LHbyZ9H_uVgL!z3nY1 zdOX!9m3h#^33P>1T0^MeKmQ1;%1J(ZDW|4f1$Oxu$K=^Q@N6#b_coAV?BGJ2r9*6V zg^|dXP3vHf+1<1?=B&@g_U5-S@8CgFeV>U`t_W8w-wPv?-w3ZW@W|V%nnDJ8W;*35 zIOJjB!o4o+e4uY8bLt;CfMwbZqwho&1xI+eV-HL}Ise*br?7`)j^4Rde94!m)4LTk z`h;g5FZMc$NTupv4mx`$a#b`T9-H=3r*5#}yr~D82=Yn;f!mNp_Ndx;t_VEo;q9c0 z-Go!q5k&&M_TE0W)v}y>_Rn2zdL=ys`s6ky+Z7HlfZIlfiuYlo6apD$5t$b*Fg%^` zVL$E?1A!XJ2XD9y@uuV@pZG48SD#rirp8$*a_oJ6D(KwvbW}Vz+h1%OsF_{WX$N&8 z{;pTm8t3Oaq}Z6SEckGH^tvV<E%3!h}`L`wr`} zyArDH1=tsE#2mtPlk9YkSD6sFBs5T5IH9wrR&BwXOnAeRQdQKh;jf8p7H-2qy{T}# zw)%QbDtw^scfJ&5;@wn{po^w{^sXf^C2w<_rATwXtsT>NtVe$cjTpEQq7GfZ|D*os zqdxEaI*5AW-eYn|=-kmf^&7t?U}Jime4<|9tI_DReRsp>C)=XDZ9~YqqTZBVoBj;Q zkjqa6{dRgFWReQfm@bJm2ndq1@jIquF&7cr&MHwSj*5f2?3ucWfOWbG4vx!gX?5A= zE9CFq21ajc_F|i2eDKd)Kaa89J04?2Pli?WsW>3WakfJl=fT_{@zlvE3Zt%u1&>*K zsXYpq<`xCa1;=eTy8rkggaaAaa9H=H;joq&8WR0kK0hj}P*i_&>P)%tO@ElA{k2Wl zJQgwCe~P=_&EX7WEN{~Z>EyYZmVl>rb@xT2CJ<`X@8l7e8srCREr|tr&y{sx-0D*P z9zG8N>-CxT4)p>zJXkuLyp;f|@h*XFtYgR<;o)++!B8l^`7I#2dCFa^_Nk34n347o zxmSa?@~)6Ssg$$nB*{cd?7z?HCk0|i-UekjQ&PmC7%S2u^>#?qX;O$h(&V)<;FNjWCumg2W(x3^>>JW@X{*!X1CPv(E>bah zUqEd{a!HvC7^@wR{P*Y-K^#XheRK>&dPL6CH{C7j7e89oZPv9Fx`m9V7jGRb*}}lP z6it@7;ywv}1v-dA0%opXqU?`V- z25TfyqSVF2fYsxW;ZO&mOu5~7jW+dc8i5xR;U$IhT%s%@n+HsuaMI!SP_FI5GLfNQ6f!QR&~`Wx;9)Xj@f zRYqLwfu0a5A^nyR8VV~4>bHZG@c6#lK)4;|$5OjEsOOKTU9{dSC8Ek4TS zDt{-_(T|en=`kG|7=kQ7zsk9!qj251<~oKP48U zrI~LzUMg+8GmKQAbHctoM*_tq7O-6ZM!Y(gpZ7H$LsD)_U=3c6Lpni?`FuBZ_6$Fu zYPAsFURbj)SMA)Dfc&Iu|EG|DK$rXscSlpr6Kh1c!KtNWC!jXlXy!6nRdW7l{ijf@ zvz0zWfOY@Fr488bqax4$qGF9IPXRan2>NhzG6vM_sr23>Ver9L zOCSj!DlYX^Eh_i{GN;c}^JhL-1bRMv!r=TH<&l|kjhpqBQzd!h=e{zl^iCivRR1Ve zie=ZET;$2jS4;7^d`5%{*q>*23OeWQd6a4zD`w-c_ah!lu?KFX>0~+gjo@2k>poWV zbPr4KM5$Yg{8;&U)#V!2JCD-OJxQm=w~=ZN2Z_pQH|MzpKd3d%w#0oi+xAZfwaA zgXpF0HhR5h+qS)_4XwEP7UZ1o)`NZ7-dLP|kWSfd~^ar|A(^Z(AN7#9JjT%;ol@F9_f>?w~SJ z5@Fz-x2W|jGd+~8nFh2icGhsrZtDzy_oFdaRC zt=L~^Z~!fA{JwaP0=+T9XbN7rc}ZF^@N+am<fwZ^+Oh>>oGbCDVLb}pFd)NAu4d6a{xN7{K5HQ zj{$pzqq$5Wzom6dhx@}DH6#s^`k(v%{#S|eO0s=@TS|blGxCFWaS^yHvHyj$x$E&2 z?k*9CpM=UzdJRa^K%o4NLKIh7zx#54)gICvU;!A&_O_yaY=dkadH1xC#1`2pe)JD? z)KE##`;vh#|NQz6m{6v}-vzX$aZw41l&Lk*+!=`JyF#YcYNN%YSt zFIV)49MGuO)YkGnAYwg^mKlY@meyzrPT@O%dR!ed3Psc~{oU1myIe#d*mMF9?qOXG zfcCPFd`(GxB{tO;h6te1E)I>WAmI1moyGzQ0N=%7nc}~|-v=aLJ^4wB;)d6I;IXNE zvwu0o?bEK^vDmcJ01zUlbkAR!$|1V2`rP4{4!57BCZQIZn_*i+nr?6jy{)_s6 zVQdA%KC(rl5PU)L2^fR0+_DEZcyfnk;7%ReQ_8-7X8RkD+)YF})$_1MJq~0Y=RP9B z`eoma^CyTKWN!Z=B9@$PzS=-t`|GaI5zuUA0nnHy+mZQD5SqO6{E1yopKZl{9V^tm zzD=#vVy9k~T78cPPq;hBKVhl(#*JsLb1P=3LofVQ0J{#}O*G#x0t$vUY&r~Ox@UiM zc|zmA1Rh8ZJxcy_;RC_=_?PYVWwjFvtf*sKXff*-8NLsXGdEA(Vy!9G0Dk^ZyaIZ) z`>*snpxLBPK-muS_*I30UOhhZ;jv)Jfz;YRg>1R|e0zJD1sgOxMR)MTw@TL|tY5Zo ziQezIA>r(w!WC2ny}D9xCkhC>U>|@5UkdsT9(W>ouY{%Qzl=P{apU+(pX4;l zP1r(-g3D5EtZ-y9a07eFz9c>VKZUz>=lOQA9g+8-W_N)lum5m%N(Ovse=K<5yYkD$ zKj-h&xesy*_`h}N8?;O&z8bQ|%Dw;YRm?_@Y!+AHmkCMY1HIb%QC9>sd(#4_BY+D` zp@hy=_PbXP;huGv{W*U{Tcamz@C83Hm|qn*Y4fHV&tw9%-u)@uL#IE;q51BB!mN#3 z&O)KY(8mlCBxqakz*Wu*I7o)S>bhbXP(w$~Mb_Ugf!s=8Jf{uIn0Y7yUElsa>)@{% z1#|)Utmp}j{}#u8i{rn=@&D@LC=&`AsP=AIXVAw!vunRiOiX;={Nps(qSOH2Fwqcr zJXeclH}?ikO%cVr*nCqroL$Bjer=?@sTq7K zeUp8%>X!Ys&>lnfZ5AuO<{&ff(Xp@C2=QQNksq%{B0pNU}W*XL9;UpRG<(@u4R+00{2Vd~|%Gh z+V|0>TRDC`(hxX+)>I7wWW3&h7OvhB7q9vy-u&5h?5;zwc0!p`Ts*k*M1q0-@z?HA zjGp#86&|1K;I^fRY0jfZUtPHW^)$s(i5HO_uGkIz&wJB&ZAofa4c_GIJb;R%vlp_m zfls^5?XwyR+T9{}a{;8&55~Uy)9w~*QfjAW+=zMeIIe-mH+S-si!U60P;75?-Ru*w z#x8Azb9Nsm`fQNglrHucyX5gYDxDVk;vg2eXxAI0_%oV3wpSTUXtpqTM`l&g0@t6G zdU5xm%k5Z|#xQPK$tes8g%;Jz(9#^lzk9Tut!GP$GjcD#xzE_UWi6d@F%;Gd^V-0; z#RNz#MkT85HbA_J?N4b1Kpru0>x=mPTU)Po{pp%_=q5G#tOI(Y1COcij}JkZ2N(Cw zfL@160e+=yAEJTVyU{Aj^bCW+m{Z`T_cqu!V`n|UeNyu|<_vYOnSLP`G@}eUuflC* zEUF~~Tqp$&to?3Hd=T34YQ{!^9y6Kh=h*>=DngKqX`A5gk+u#43Fu6OQ*83fNj0m) zoD{S%e5P{ZE2Fdeew?Y9-`tl97|!J!b)ic;yal7OE8`q|B2R{~SdP}jx%L>^bi-60 zO(U6W6RxftW)G8J@+z2tjhBGJN|W~JbAiNXwe6QDxD1Od)vP}x^c*QacWxO|I71?F zdak5SA$#c61V;XQ^L)+q92xZNr`_TfpSG;@PsmSDBCSy&7F!E927Lgtw3$~iHXO|P zEw!Y^ojUKhri6d@yBjtQ@ZB7hBcMm9Gu4zxtCq;nUMJ4LZrSC+2A+_5fxL+p^L00_ zSKYGfF*b>>FSg5Fl(#JLtZ2!}#4N+VU=c4M;_Qtnwu32e|j!VEvs)!SV|y zR;UV0szt-p(>RwpL+8q|NDRV|lN@ck!*?W9Dy(YFdew%qTs!(k!;(7wElU$~7tvqg z+9Xf69oNqfNmJC{FZdc5iRarQ@*$&O#Ubc+^_G|HYVeHzbvXnX_z6K1eAdG>wp=c_tic7P)*k#erFR zvsZ8c3M89Fpk>zTweXW)^6y#;a%LBVCn7z!C-UxD67xP) zhU|e=cWq$$cJfHyW@oqfr}zIX9!F zm$y_ok;q=_u2*l)aStc^z!MLh3G(@7y=-YIqicjr8~RJl0O^aEN{-wP|9+%x;JD= z#dr6z)s!I@>uUzuph)?bO#zs650s+p_=;%s4ZQ~zra1Wl>-saCmvAEz9)5u0c>Pg( z$(J6N&BES}r#x*{?XUGp4L&@zbFb;;ReKn%oUdJaoIRn z1;gFgpxiUHr6&m9v89hwV;Wt-4!RfTv?2D+c;G4LRa#T zX`Oydw}OJ|;$~xdv~le=ni1wp*tLsTC6gV1__GsO6bv6) z|9=sDlw$S7ilpK^f9KVr+@xj&Y#kP@GfhC7U#dAS>y#(t`m+I>XiRWPr=^dPR0@*bWc z;6l?q1vO1&6jO2f6q zxK{MWlA%)4E#M&kfqdQ?fAHT?0^?sUIiCA71@o^S?SHD6F}`BA(Y81_O`SyG_n&>l z+X!5J?3Qr5Q{NrAC70EOrJUmS%6p;_@!2r5aO1*zO7n3@FY}%%r*Phz)(InJ4j(en z+I?8;gk*Z%>KN{>__vP3osRhVv*^BcQ||%KA}=zof^#w71o3vhLxq=IB|t$l{pU09 z;<2NF2lUrS_a4{xg(|)RUB0r9GnD0Tg}!I))R8EilK4~WzwGjGOGH_Wxi)Y|^;=}Z zN|WEzm4AH0`+3UW6ml`}a;sAsMt8BQ+>6o5#E$#Z2|+XQ-{PcN(xc3XgFQ9YG}L&~ zN2xMc>Sao#vGeo#p%QV1lYgbp)(C&t41#B@yiqUj<5H-jH3=Z!Qri_2bSVWStd;77T2hR=P|G8clV`*`aG-!2121DQBtFXh}XXzzy6QTd_|MIV3J5@f^g ziq~4Hl$9Cz=PY`wm~hbidV>{Jfy_^U6G~ZoS<$-+8;o}G zcsl%F7pHUd1YWXceU+3l6ld&MFuL6p{*pI-b%J!^)K>db-N{zD#^qX(V&?fdZouV) z3>ro^q->n*uXK+{F?%?)_43Hei*J?;g%_pdO@Bms=Hlx-rgT@T(e&zNGJllGK|<5y zVkH)Sw`>9_ z+dUpXY$c(IP}(SatdJq_|DffsscYAQ_cc{h>VzZCXf}T?u!!OrRUj^wbZKe$7gPTZ z0E*v>)ZW((@8BmpYYnUqhln++D6Hz3YQn73WESK&A578ZnERl^FQMQV^+nx?cZ7S8 zc?p*YqP?$I5}tZWViqX5o?nc2E7+FZe`l39)(c+~H-Xuq@*-VGPVWP7@5>4PvUPgB zr4*7m6MpcB7`SNEe<78((sa%tb>yb|L6`lReVk$Og=aixKG%PGFV%`{`Cj%p@DDFT&bk>gIi6S{GH0p zwLP}mep?qTpoU1x>2kxb9%}4)%f9tqeX}cPiMdYT)ZkZ?{ao+;@NP$UmlWCE5k;x8 z@&n^lD3_W!Ww)?J**sXseZ!IaDTMTwEk(36ZWMl+w*N__gi}F}UD(WB<8br8C(o(& z+d_M(8r_;390u0AjKKAlz)dS;e3X(LH}!)977QM5h1@%^`X|vlk3Zdz_rIVVxlKxK zRC2=0vq!e7&vOLKakzoQ#u`tB%bjFXSbMP766bQ;xwqg--TXV$>4vHMrrHV&kChzs zs<01;R7b(_uvtC}nLL@^nu#8rYAI-%4=Jzw%mnd8*%ivwk``6Z3IbQ{OclwJj4N%* z(&0M;Y8YJng6{_epeHE`_Q(N>S5-p_F#&bIAxyK)80}x{H+~U$pjLP;J11W~47)ky zd&vxUk=lt4br%e9Qbwdw~3k zleR%pK)H2}w`G-Ev6=BXboY9tm(yU4p=j-b?vkWW=wDfu1NoPHO?$=fr~pxc#3iTB zDzuS$dk2bgpZ853|I;Lz3V|q>33deU`igC&FfMzAqRtshPTyfl$&ojTMpHM7`MyLa z9`TdZ4Hj--;~ZElG1x1Yfb!LpZF6c7e?Vs#AYSG%7-|7%U(^-S8-9(?Yhd{~Dw@zY?Wv=;)8)^gv~e{lk(O6p)rIA1dKQE-pT<9`9a zNdPh34)eRj1HL$^4#u9(b>uh;B!30r(V9=3{tQ$f3jrLr)it#nLP@2dj30IKyCkF@ zLB|0~F2Pm9LXm$B25kY3dxecR6p9k7Aa;Rp@DpVgW|{`Ddv!gFU$Zf+_gU;Z-_8ce z2T;WD{To;~19CDYtbIh1GC*1PZg&Ci`)yKo7y-=t@6x%Te2{RQZnj9)VKrt4pxn+| zy-)w#T15jSzMf0U2*MpaiotG7Dtw<43--x8aQEu1`j4D{D*ay|Yr(rP+X2q2QcMyC zF=n;f9os|D5>|v@?@Ncveo1Sw;6A1SNL71NBEgDi0ib{&3NOXWD^Cr;I^ ztjYQJ$2pKuaCsgc!3w=}!A3*xW-p86@jU@dU{QnBKWN+b8-Js1(~mAL-iQ%b0%#LE zrJGvHRc$K>pZ#&=GO*uPtt)EOw&D|cP$CGWcl(ztWs!R5-1wq;qIc3T4Bv7oGd}5UEcbyE1-%{n7h}2sW ze7}3^f|~y7)Y4*QivzC*L##JCFB!FAm z6<#iZJQtKR|FKz>3c-D6!Gu1}eEmZY?IZ0q-D_F#Pmnykv`Rl1E;p}Qlh0dG%=3s2 zqRHcY}_ZZUl~05L|g}&*g~p zovC!|vx>3`%OVIc5f>BTHZv%HTF%2gCb9sZ&aL3iNQ7sbE3FNh8kaFa;bQLEkL?Po zt3|dQCu5{s5-l0N(MAuNl^N#_b4LjzK0H~cybxDG!`XaOSS$=z)93A{B}L#Nj4plr z6f+Szi8o{kF|^xUHUE*{D@KqX>rf1`u(Wi+`wV= zpGoUX+YjY7?THVmA3i7^#7%DFJY@FZNWGGTYTEer`K3dP8J zJ6royT|e>BF!LX~9{b46AlbVEF z8x?4ZaD!Y%hk&$Zf=suB4n{CMgM)u7TzIBQdNE68^$TV%r{uGluCe(Rv8X4zIo|uv zVUA#5hU5@nF{1lU=W-rF-4`s}`4nw)3d2?@LBh03e!$1zPt~ z+QtCs!wu|2H~(F)t+;Lf#}passHlMp%b@4Rf8|RwBMvd#y%^lE{>^sYDiR3hGu`oYfZl5%J+7^Bl2SB%0; zOZFMt9Lhe!@C7$Vu&H<}pP@6n@NUy^eMHCSGeR096R;J7kqdg9@F`;zO5dD#Y0~Ak zmlx&A2kLN=Q-%3rUTdjI7y9xiluwxUpOLWju=}cqAI9-+jdQKdubZ4{un>i7_I~m1 zC|WSWxYKwE46ykx%m}ZT2@iqGDx4!6w;#aqRQEbydEH@cE&&TFyM5s`JX=SfzbkQ^w=EgM&=Drxl5-p>21+*Aj93^7`5U8H)|(feV&i>ELjP zfKShP4IqOn{&M8>biRcOl~SR?$|wE0B2kp$BeG&H9{$-><8$T{_hnp)sp7_{d=+zf z&Ah6^90uq}q<+}U(!`X1b-ypSUU{z@1C6M-PYex;%yL%^FeA5huVm&(k#C!j}o^78sB)6G?I^w(E~VFIgtFJqOb zUGr<_db7E$`9%Gu@<57+%J z=9yM~Yjbs~v~v&eTxcnrs6g0L#MTMt7LGFe<<^~lU|W2_#^>8j<{mt}dh zO*feJ=yQa`32n95f^MQZ(sir*>aCDQv8*Ajf8-y{!XVQbI<(b2HTsqODj|* zS;F@pE&k8-CPWuCqK(2L?V?H~iLPQw=FZ*syT;{a{Ll=L{-U)kF%$2Gmy)~^M^{ni zJ9@l9a}*YGJUn8&5%_WkN*cWp9? zTMXUqwVn&EZAO%CEH$bSquW>Cfpio^nxOO<+HWfIscfWg?989Iqok+QcekNT*a=3C zd*^lVUOf-`!eV`;f=;2;q^n^FObc(*QaDEET_^i^iH~USntsfevp3Yyn^w8M#fWHm z!l`g$+M<#`5sr->?%cIlJ)c{y5L1fi5bp08+ATF+P_j2lME9S;!;#ewH(>RFua_)UUKV2*?>t zE8{ZF3$%opt60s2K(Coi`z@{pe<+8M)4pmL{I%@15!9K}w*y z?vq{XqH2$Lh{l!m%HthAZbQm2t*>kdYTJofAIoXaVHl$6=MVONSb|1*?YSNd_a_kn==RJ1AE?q;Z3H9?z?zeS6PSF zg6A8eSXn4Asi+VTA^*FhIDh(wJerQWrczR74TPzJyN4FcYDn7}?f=1cqSMZ56;J$j z`M6zbq)q6;K9%y`B1W|igIarXsHt~U~YY{Qd|un8;eC9WJzy-qv;3#z!FlW!KLO+PIpp+UTP^` z3hW3dk{86E1rprmx_H#IoVRUYyMy=3bM@Qgfk5-542#F;5s!1{$zCu8)R#zy1|w|o zstulkjGzr7Yx)Aa{HwbXQ6sTs%i%SJ6U&Kq6C_DIr2~$;`@JUt+}_xLTcMj6JK>AI ze&BX6cj_}zuKn_~%zrkET!iRwTVOlr9Jl-0fG@%`pr8Oh&AY?I*pZVugU)$m@N>?8 zcKs;8@N@9AAr;B;FKzdW#_xo3MI83BDz(3){lhNJKTp76a9H7qDxMfw;`0_S)tQrr z8Z>F+F`&2{HzL@YI93``FX4lg!`o%F@Ybj+h|X6%BxWj}?7B#~I1^Ioj9w2t;k3^l z{|)%RQNVkTK@0J-_ECqxd`YjOw%*1*D3n~r&^koD-sri!xvLP5XX`OSo=Y_ov>SXO zb554rp}HtXZ%ir);K0ut;u@`A&kB|0dIx&CCK?WIZq#q;XV|lvQ zs+GqtgmR_2U&G7GMtS9RxlKo1CPR+f{gN)a=e2~(9NrpPa(_a_j%tftEWzfkuH7fQ zM8AhwWEe&%lf21x3(L799wyV$5>?Kqt@J}Ded~IfxRadI9x5e01ZHnIG>cU@0j|TM zCwJse-16)C();7$x!hw}cV(M8*y;^K_I6EgcYxJ(h&w8d2P+!#t~2JHCdZg((ziMK zh?FVh%Li=AN{%v}mVW7V6_`Y9WY2}p`?};xuYsp-M%Jey;;mt1%bvk3d%T{DABpU4 zl@|IOZ$Rk|iz_Q0yeH6V#}w@=T5vX%m7JNePZ+JffLV34P1Jv1meIJ5C5h30>Jtkw z%d85dPtTyo$2uG3-oL)T_Mih9LE^$tuJzDs9%+|0$Fk!jEn&5SEmg05-hjKOyBvM; zDlQqJb5I>4#bL$RAWu#^Q`Fw-$*6F0e@`M(!n}N<7;MDB%~44m6i>Mjq<*JPrO}0`D?loq*5H;Fc}bFm`+}6OMur z2oufC$JxsjNZo^9vW~2$eNV{n&X5sCXgUP*o~a&jdWK>;SCCijDIVX7go>E?0e_c5 z{j^gb9&Dud4dk;a6jh!JZM}c|@>gU|-{nfiUxkZTCOA@kHff)tIZ#O7j2(~RwmG5N zvf{H6E=rs=M9CXJg|)3~rrqW+&R?`jONYcn-Ai*hO1wis!ks!VI~&dB;8{L0XqD;7SbR6~Xr6no9EWP%{&>&`PStacN`{9k`Abg z!LsL-m@G*hC@l&YDInVb;moryqc?2Wz+}#y)YAIrqVeegw&>Hh5wrAEQRb7~TW)?7 zd?pqv_(oaUVn+=r(BKxV(9wHFqbad?U}8G%qHe1&LqlrGsrgHRn=8Q zOji44Q&aS6H}B!upNMnP=}qcce58PkoB)su{z|I?$Y9T{gw1Sn=S8oHjU2OyKhXbh zB=jT5;TcHui!gVK)!dXoHt+_>x!17y{n-2wa9TDq9N$Z{-`?8D>1zf63ft}muN6R> zv+TA3G!4t_>Ez^OMPmr@$zFA|v&*u_)ku|&m-|r1v&Bo_`BYH5@^N#?!92T;aByrM z(63tm`oZ(kSnm<*e!O$_H$N@~($u$!RUbDu@$X($VT*nXHZ3hJa~`E;xnH_w9>L9U zSEAh8VM|+_37yDlvV$0k9+TyeCwXCH6h@Nlq?R%rgiX;QVs#bv6+9~Cw+BZ+HGyg< z3Xk=llq{b}Hz&!*EZHv2Pkf6S`o+yJR0UQKI1iRLr>k=5S;m{M4RhSS@RVBvH}_an zm}Joq?A&_Wa=q$?ue|fIRd*@TOOdN9AkWpEdTF6xB4eZ=H#k2S#>*WX8w_I@nM_X= zM`BwWU{>Bq|WSPA$|^J!pl``%8^^}DswbGKLyenlJm_b69s-HR!5fK}7l*vq2? z9Q>XC>e5m_V(71@!rf~L>mSYLQ9#YS&oyeV3coaOi|N{zV&7fwH@+>j6gP;eHj9g% zn@=S3VKApim1YmJ6udLs@yY@*1Frp}nWpWuJN~}@H4ypXAJZH3;&16>K+@ro60&aK z^OAe*`5~RhI#NTSdhX8a6$iGTvPtMKnYeF8n(CPcf@$e2EaSdb?V360VxcfSg&n@9 zjV!HMtXXjIRS5D=SjxPeZ~D~CTK2U=R0g+)Tep@Vi6H{NOt!E-?u*p!k>7WbryYbv z$2T5C=DDUFf;trBx&J&Td4?*}S~V^*efoClPSNVSzSnxql#4$)w>Ru7@;K&|lqS{c zIV>RDk+%6=%|$kHXn^;~pr|<#7EAli$uP9xAX;_ow2)P7jveIDxAgZyn0hC0wd924 zhwoGFwN7!G~jc@-gCvdm(N?uXn z{g#@?daqskk9LGLksl?~#3zrMHApIno1!l)EKKAV^HEz4K-DfbAJ3a9$Q^SP$d(GrZvpT$rj9<7^WylV&!ii)+8T-@ z0+qOZrwixkH5^ss1vk+Cl4=DYo?}$XgI(PvgFdUKF8_H#i;5VlNy`*g^;U=+CA!I9OtxRjR5SG ze6y8igQs6qhxOFv&)&9t?dpM;KC07TG+rC_(Sz&GLNY0dbdN`A9Myp6iW^LuK{MAM zh?not>lyGSCs(P0l1;oy1J5jg*|Og{t!c{Qwf>FVTTQXx|3 zq2{&(+3t_qdAasQ6>scDEDb~*Z_xB3rX`wsBOXln66j_UQc_W|`fW#UwaZa<<% zzS0d$^YL(lv@rN6IH&J|DB)cFq4A!JE-~MhOA`mS41t40vj9LCk2wuke5o=ZccfQO z3$!+wUYR(DlI^NVOE-C{&+YO3%Fv`zbGI4>*lL4;2labrL_RIaxWXhQBaLR_oU7D8 zJ3bb^)$GL4S*g0YP-%-9|26=7*d}av-ri&QcXBCi>vYnm#KUSr#*eUh`=!A>ZkRGB z{HK)Bd1<1s`HH&mi}b&XP5-e}8#V62hW(1$`CQE2%fM@_8|=n+$yM=uYa?j-9jS%C zzEFT9Z%IacP1D*H8;p2z;-Uzt@>mr7j3Kql6Jw)Kyy#j?ypxuvS?Oh1RRde zj;M`cCASsXH*LAD`Qw7nudo=@qr{#2C(84$7$ImU>C+tmTvX+DZ@rM0Bf<}*lAR9m z&GjjpykL0}+q?}MbsS)1H4=kuByw7ef3Z z=I=^6s{!TM6gk0iC_509!iwUIgcTJrM%wCs7CIzFZ2G*CfUy~bp77rFYI(ZN%PI?U z8z8ODM{KsXO8y^J`RU%^w*sZnDx8U5pl;#>6^DyMD-JmYl(hfU}kCJ+y6x(5ZL;RDx1|_13fVV%~k@rSr?i*Qz0PU zci)E1XQ8}hR*3KbR4okNuX}|dLAw9((k?62%XFjPf$aQvxo{A zK-p&;4I+1eM*w4EQ%b(0{t@VxUsNJV1@y|dWXKR)f~uGl3WknR+`0Vp+ z3^nkj4pF#T-*Q|4lhF;R!nmTN=;beB7;XRghyp(tFV7!KFOLH(nA)ERL9Hm5W32Cn z7KpL`RuzcmdT96iY-d89X)R!I?a@$}{jj+km`=QSMrxRhx)Z+aoCqDzMQ=k4exYWN{ZtHQrw1lbJ$KmK3sN2+LM4-Dz(}wu|Ho%kb26+_Y?x(A|*;o_z(9SLrT{Y3O z$B>vn@w4fcp5a8eXdog-yz8jkrJkgoiZwtIyvry6y;M^G(E@4Kp@ng#)w%4B(>1%O=vV?B7C! ziZE6|MF%%_Ov-;m3iIJZpv+{_qfZ^BsiJh``V} zoV3oCBwoOi8Y*6Q2N~ffo*mThOv2i@R7N}vc7;+gvd0X?1C#?p-R z1G5r*olTWCi9TmMFgDwPdvke7z*mg#QA#-7)pXHa=(aBxg3z3vo__0-hW15i=}hN} zp>4hk7>9%%9_+dBv=mRwh^UxY@bjCDRUorl?(XiZ=`tWc3Yo{lbzaQLEh9X>(+Al6 zLHMBC^l|wb>4}y#k#08*%deSN2Zs!D2+})?eMX2wGS9Pl9p;L}QaO*jp-zBn(R0+N z2R*tpiS@ke588Uwr@WZqAWwfQj<~Z;j@laQR8>`#E$P_%%2<;3cc)`@WLBb0tXRks zVnV@9!P-YHZHVLrIw$x#5VcXKBQ5@YPt&!8_)E__Mb>k-jDVzPz9{fv*&Uv5F6G4M zS{k16#9X|v^D`xYNhjvHAROcfxka>Kz(cKuxBCvVhugdl-)>p+AR--pDB-l?*b{H!m(8{C5kY*1&RFFP@?Cn?RxK(-8(JB`B-{HB2LRtud)GuWilcu`qncjk}!D z#h1R>_<#%VqykbG{Akw%7>WL?QxDB#c4mnYaw780L6G|CqI78!6n)7re`NdGwFa2> z`Z7V;yuRd81IIFbF@%~aL;6SH=1(>Z8$ePX;t!-*In(M*{IQyEiHjJj1Y-eWnUk_J zy2hXqtW~S%J8kaWHaUZUR4`N{x<9J1*^jR+SsW>d&I zdJT8U=d|~UoBAOCbx7ax_&)I1SAEvmg0y|c%bi* z6ohqZLjP`o{J93L9&6J|V0N7u;&736$L9*66I;D!y_{o)^swpKC9zr>*Y6UE6p0b3)!Kin)r)_DzfZoWY}G zE{?5JA$|XT^PwRDY|j*K@7w&lV4V*Y+}HG7@z~Sw^L&z^(jBjsO+t=$u{6r#I;+=f zR{Twu^k2J9YoM+99F}}BCVAES7t`Ga+WSvCb$re+HZM1!GmL7c z;k{PfzIkG<4omkFr2Q$rgq-p5aovdV0jWz8C;t%Hn_azguWNV2&)GT5{tTrK0DTO# zy&wv$Lq(OXV2!;vEfMlkPk3%hl>W}ZEK5t4e97~}#_f-nUc@7j;bNTCk=GNQ@5sg& zp!P&Wmr_i##g^c}r=u#v{g`wDTa+47(@r7Lcjhv8-LBVu>Q4Xm?RH4i+oDh$x2 zGGY+p#l`}@X{Oa)SrAuhsIO*m7Jz7X5K|iFMce28kl^v48tLbsZLpUv?1oOw&#eYw zou`1!Qjmr2GwD!Zwf$MVlJ2HQ^;cmH>B0x{+8OIu@*bk zZ)55l5D?(E%~s#Is%sV^0srfuLERL})!z`71;x*Sfx5ss|D_iE;0#d-m?l%E_;4u5nsj0hAX54pR@EJ8i?m zDKd+__v^7rA}?O*wYSJebSI(>&tvZa%S!}={|mfQG)IBk#kl0GPccV->)EX82dp&V z)X8kteFNW?hJXi|Z=+zwbi{L3&TB@0DnfMW?TKM0w1@!b8 zVpjhBQ*88HW!S*zXxi#G!kQ)x+=-A=+$N(jRV?}KzK@R~8Q?KP#@MUoZU#s@I4CxJ zI${#Qpjo7Jz%G{Rpd9iml@r|CF2CPpF;A61`Gl^nO(^i5xezWrykqQ)1+8(l6&sG2 zzE6C0ZJ&;Dx$|uh__?O^)|qD@lt};#PgYh|{zQ*%Yi<2(Q$?I#uz|-s{5>cd5ClOW zA+$$?9r7K*APDqLPdC&8st6fxXyG1ajTob86QGY0Dt;5GH0~lGQQUl<>WLpJY7y0g zs*+bKW{ZurdLGLZ`hw8y7UHdUPnPL~b!|WWG460d1b(qEW>MM@<&=SUL7Uc2z$i_} z^Xz*8IRDDmXDIuC&+p~!Z*TI6i8?O)z#SBox&3qV9RajV^5Um#IwiH5`vDY4p?A0VjmOwv zb_&ENfr6F%?70`1jyg+c{bTPH>5fV;fcL9G*w6ULr}klkjwBG zz?pMl3o|E8fpGWfa;^VRB6r`*C=lpoO^Kqk-T?blnYB78zHm*zvf=Aj3CU^_+Tr3i!g(5IdUWt7pNJKw1S%Tr1ghtKSbR4miG^q|S{{c7N1%cM z!Q#O7jj!d8OQ8I}_Hz{)Inibnm0`GSgrU87$b&PdLDwX%+387mxsS&D1xATlsepQ~U) zeB_>_&f9@~09?CIyz%wS3u|zB^KYw+=U~BzhHs&oAMW^1jNbM-SZSw>Ua}5*`K|-n z2%8VS=q<)v?RRg_s$In8P!Fg6+?o6x*8!oWv-7oynMUYXfYv|w0)I0ob>J|Iw|XAz zJy^fAyTcG3sDyv=JPLR%9- zP^b3R_!Pq@WC?bI^S|J)&`1LnuK_@0uj>VI)-M~u`?tP2$|}S9zy`HZULRGrz)$N! zuHUL1mq6)om^`>LA$)!BZ=!%`;b4!8d!ITCnhY!lJ*jm%ewKxD`~a{@<;j{qOM8=Q zzWbGk=FP~K$?!rbUo3TbE1m1gS=kLB5(w{tlc-X~-y@2#>5TGgxWp78P z2x|1jAASc!6!QSkiYg~(UIt(Kk+b2swz=_BIBO4|2GBj-(|dlMPVffBq!Q4nbSIg! zP^TUOjp)L^Prk?MRQ`sIuZ2GWT=e&6nxSzImZgqE-9UkHBb<&Ae-^$RvZ+jIs8e~MQ!a5&U$8n=0p>e@$8YRf=xIR|7`G2TEEXDf9W?Gzt4t1P z+=}Xffr3h(HvHMC|0d(V$@p(&{I@dxwN?L_ssDV&e?H?s2mWvH@?SLdUo`b!X#3wj z;}?tY-#+8NP2Ybz@c(w;ES%#1!*Iw4aQ;$9YcrEUU?RpJZ2Nft(oI0RvVonDXW>k< z%7LFVq%^N6{ksGD{S#%25pvmYR7HKjv)p^P`V-c_HjB;TUhlrhagP)w*3Q+Y_!Cw+ zeo%U0f4Y`Wz_p0;03<6h{K319l57tg{uOe+Yr4gsc`JRNbP~DP9x?1rR8HU4?&SL= zW+Q~tuz)t`$9LVJa&8o6&D+T6^pg{TiV>T{aOkE@(Z8J_TC;z72%%oPI9X=!>&AzrOTGDgc>yji1QSssI{mM%gH$@IReW zEC)%!>BvaS%fHGe=@0<2G&*kFDrx{Z=EXaYiYxwY5ZpRET(D=_fG^6I?eZjgf)BNWoA3DlPlG|u2SA4M+_@&VU#2urQ3zu6q{L)e zhVKLo7H+d~{blG-rWh-EZud{Zm!eZp6X_ki+h4p*D?MM@I4cI1nxOzQ%4!l5q`3)= ztTenmzU%=%^py32G$6|Nnn(Xq%z&n~YHKu@w}rG1C}9bY3SC*AR}U9nCmy<+%z7Ij zI1Lx?%{&C16Td9@ed$2D=sTv%!*{xL)vX5lZ&!zyIT%d3y6J>r;D!0a=~@pWHBcUHyc)fp89_F0r?E zAi0ksr7^ttxTQ!zWAD4WE_V7^k92uYZ}!p{b`SSHO(E6VzcZ{|DOqz@^AiPsV`lz1 z-?)}_`m_q8?1j{)?Z>|f$3vwJiX4DHHa+~+sskXi!^I_< zDqjvLCIx*$)%m%20ZnkA|MvFyf~DZpAdNwLM<#Xn@%dCHz68R*_iF*-%k$jn<;Goii3; zPe4MW>HGH6K?4RO^QD0EH=z0!5cCEBMqg}f965Hj7`#fnoCmOvg5aCjz14D=<1+Wj z`oxF^VOpsQ@g+7ToGz+MTo*h2M52h2#P0fVMlPXO#xR9w5I&HCd2OT5<}hyRjEEI_ zYX_7!NEBLD{Ck`3L0a##Db;P)AcvN^{D4SPZitI6v4U`mm>YhUW_@|AJw8iH^y7Dd zl)JXrT6OLR_rwzz+1hRKa>T&qS21|9_GqbJnTXA8^}?Z%7le1I$^)(`?W{YK zxV=yEoQg^oJL{QZB1!XEnVgtsA_DkqUMXjsn2F?@c1f$*yqFyy1Mk9SQ?}m2l68)` zT!=MT9^G=fy-7T*IVNv=tR9zDnvLaOVp0#@I_O;FR|(q3bvx@uwwV`qDwd&ZpcyeA zDvxdH6FB73$!XLRvmWEwe@^i@AO!-Jj{yBPV`u zJoZAhM3zq$EHV|bp^|pt6ctJcEfjz$<#F~q0+QeGYe~e$wi^2RX2V$7(L|o@8k-(G zvgpghxX%YX>fgQa$^SBu8g+W7ccm@^@doo!CHv*t=UR1BKDAClq-C#<%WI3CiE69F z>M@3-TG_g~heG24wK}t9U$-Vh;h{{+8rixmO!dXK^YP3=U6+u2ZC}b^^;#v-pb`0} zqy7;b8sEJt4wp>1da3KqqS9N%h}Q?2x}&_m3Fny4w7WJ4qz%GX#+Zj~t~VKzFJ)mY z;45aYiuB*%$|Ga6RGZ}{(w$u*0z3TWR+3JqG5M+#vR)Qn6NROM6NPHNZQd&E{Fc4%p-S~X**rS_^Ic2unrr1pr>icO?JghY5Q z_x<^O$8-FSz^M|$686;xr8N{LT#NpA*GgzpHwS~FF{f0$EoojQ`OcDFfwr&!6@1=e zo$9BopnvfUA4MK5NMhf8zTm41`1^l*g=@{eX(g59csFMm8F={Px_@ZTeI7;c@|u|a z-NSisuE6nFXPLPR>B#D@CLA(o=5QR&-rsCN|7_grYkad9jUyUMIyJ;7zoNxoO&KHY zS-fDkw1;<0wz0(4Y~aNb9|rC=E&lQhR*HZc7Fr&u&vS4pnfuRwh@Zk!ERMs3ARjgf z7ySKOZc}CLtvZhwvo2dK-3fI(9%!j0$Xn^L9=@$zxz?nZ-}6%nq5b=5s*JWr8Zxx) z;4S#XcQ*rjnx9cH;=zgcQnNnvlZ_bx^}Uo1uWHgS+2J+5&p@8L<=7B74$D3`c;4&q zYTy;sglnupGk#QITvZ{{tDo1t$Nuus-Hj{yJB@*%zi!DU>zt;eR^N`s$f#UIj$SL4 zSSaSX+V%$&{9$(k9cr{?A{c?UhjVOd30KE=O;LFi#xijBsfKGNHj!20&&h}nfxCd=y=TPT{)z%(h>3};#yhXS zQ7-B8g7A}gr-HPtCBLuX@83iY7r(g2~yZA=!$%ZDoY#>+wNxn@;;bjg$6k$tJN!Wt(LIMi1U;Z5^fY z1hpB4ok-1RgM9fOq02)giA+}FGujV=>>kNZt=y*5w7SunLrv5rB#|jLdp`Pe#FNAH zRZ$>3nH|d&KH2;}vb;(PaD2OxtGmwS5J6>^jnL(9_nWy54pDP-jX0D^SmC)ntYT>? z01iPNO*`St=adl72ls-NOJt5B&%SUBVI^l$Q*Nc$_^~#XTSyG-Wi7^HG}S6z$a-7b z?gwZ||7g4Srn9#Q{WynjIO*!nj{VcabtiV3z9ccw1(FeTNXPZ@*^v!U%1Cw_Vw$!@=DqMBA*)Jf{S2_n%^yyvwQkK!JNEP< za-rq-9+wbk$&;##JH8h~u3#f3Vq$ny@IkzKKN7RO-!9kD6*9qpPZbybt@vhtLsB#h zJ5yArEdLJdm}{ir_(MNS+wFROaoib3@^Mxt zmj9@5)9hn|R10r>|9E^vN4kFG6!nVk2(3gn?6zw64D<#()okL2M7nX^I4DYH#^CsXZ~TZ+_LV##7SqLF9?`)TJEh%M40kR8+qdx6pZ*wxHvAZM9EY9Nj#P}TZGZwyKV?^_j#Pprl0Kg zw)@a1qgxkjLe{h}@mc=VUknLHhkgpM%xFxhghP_+Ak2PC-(k9x`#I#}om1$#d9~%_ z{yRa;=kD5UYlWYd|5bX+h!H6kWasdra;gfQShHa5U9~q=;Fkjresub~m?2#VB?$?; zDg@c6@%AeW2_06_r?dBefuPL7Vcd_pBtcn5g|Y;E(zm0R?S4t?Md2EDSiHsWnL$$az$Ne)9u!0=VGcLfw;F-SS40-8# zL0@n01T}X0jGw9u+L{P6q%Wk!uyj_x>71M3t*(!@>EKN<)>XWFe3?BqOEyQtMJ%C} z@IAh+Omr=)8e?;i+=bgLD?kPA_`>XpHpK$=oeSbA1{;Rrx?m>t8?*I!sFxOPJzF2o zBrU0Fc)~h~V~94MdXPj_h4&}wkj~!3{uN7Z_q)`~-=lWlXmg6yKN!Y;JL$k&eFA(| zi^*c)6SlsOy!Qoq>%>(rHIcuK;xg5%N2g!Wqf_kXzVIwk;`P^I^cSLvxp`hDow?ef zGHw#F>@5{^wmF1$q&jtzflK$B4c{BSGn>x4D|LhWwdffU)kZ~MUD2RjbgLBMA*LnS zK98M)qJmTXQ=l|;@1nfk@t;_2RVOu&ScpC^OTGbji)&ht2gt6epQ-5tz{;>k7QOF}HXC7NPOfCQ}441U&vjArfn9nlFs z+kVn*_0kI1^`P9{kE~mPn7^|A;m*^)-jIO))4rC2b!<8N9kBW|T}P#CJcRx%{mMOY zVr%_9lCx~bgV@HLblDk(+u#?|bbo`j_|M>%s&TJ^8*-6=1;NdtwVSV`GJ&xUMsWmT z6=N_tf#(*`O5G8hzK*%nx#>sqQxEq}kTJNCX~xA`A$Ur72c*7NpS>5csHSB=suE4} zKiR&FPEy8DA5(Xx5AEZw!LQdB!?=$E{u>!h=z0FPAFKlDbn#`OU) zLofCNQ^3%L=YYFea?{pO<498fVM#)*wz_SGZJjPquGC_L@0q0c>W`Nt^tOVIhl8rD zrop9sSo4~@?FkxOb5#B5PI}QQibW&c20q9!HA*p|)WLW8GmT53F4|Advo1>GxRSq=n5L*BqNQ z;V7xBKTAvuKMKGOp_FnRnn06hmsW`3Se33u<>US8O|)?;l$9;dO9n0C7Z0+af17aL z4W(bgw7cXPcw%)HCkcQ$TUysi+zODr!9e0F4AYOFj78S5$2nP{46Sahv_EgetdzP5 zqYGRIx_$GbF=k8ehZV`?m1~?p4mUQEl39KJhGt0S*~*MsL2-TvN}mzlVYo1oz2?&G z4=>u%S?`}{e!3)J&^Wqj=KE40!CaDmf9hWQAbIi1L}}OT(-N-vd$8r!3Dpp_mtW2Z z?R;Iap@Vq53`)%ghLr>)O}aPaT;c(FrhPeZ)=SOw>MnA=lY5e5bOSzLdbw-z0^vyD z;Qw#|lt%$u0_mkAfKmE+@05JuD5OE(b}(uwusPf2ASbNeW$gPyL)bJwSo?_dRhQts zu%45(1jG(!3Zzr)xVojLzyd4`LQk5 zcWMB!!4vr4>%WUFCMZ%58d}*=GF*4}E(EXN0ng>~8+}f);8r+M&q^Rwo~qMCho885 z<~JqHDHKA|r^6jgZ)C+qr^O={?DA4Mrd0@+{`Pb0Z#Pe4GW3b43>B;zzVpFfh*Z=h zJoU}$z|gy7tiQ7;HaPUbd~{gy)lIwnG_S$z*hojvm|BQo>ODOxz8$K@gEmd?doXfu zK|n_oyFt+K@yEBNqZ8SiF8ufNtCRRmmLH|b?%Z+6fi&n&^hO0bRt~f>?zb$BWE2#{ zmrAKLWmdZmbkJWvKH5;D{qC8V)RIdV`nZ4P%qZE&Mf#Zg;ujw4OI)_zrRkFwH&&T) z|0%Pu15S|sg6FkAkAMmtUECvPAlP(2?Wa*vW$U|M=4~s(-|fj4&yiAg%>qonnhO1U zr=FLHOZFEaNkm)W1I9bsAw_II5<1HE7)Ga7tg|MmO1gA6!8{<>I4g8{B2*}j2UNDs zM9TZ@x)_UPC}`F<7?!4qKmBKMfr`@<^T3ZMC)#{)I>!6#O0`a*=Zc-0A__=Lt5O%| zBkeQquYUaXS@6Zj1g>PFmEUXKeAtgx0aO~#L#~GBC2U`2ekb&`%2s%PfAqZ2tWlcP z$VwWl+HKOPSCXJ8NS;Y~n-{p5hB<;HhlrkuPvd?xKgv%E(k;PR8bS5#D>}h#Jp6bG zlHv<#^>EUodjwpHU}h`IzSaPtny{D^^gEk;&#y%o(vvNr)30eBKH59|a< zKa}o~rfXc&Noc6tQaA73wX}o~?RDhy5*W(7#46cA(RAIvr$DWhI=A^6(_iuG(cVhZ z{-L?8RZUJd57=;cI{VrN=e<&DA}EA(y9}QSM57Nxy8rP$W`Or`=K?+d!~|@t@}CD4 zt^uz6>}EQ`a&x&m%=LqvcA9!csCt@KWsq+(j{;Yy<$|5+_YAYGx4kma@ARUzqxCkl z`0ML?vHnD(V}bhmgZyVFK~Tb6!6^TnIcvS3mjFb2Or5bIaECoLB0;56o&Kw|m#^;S zjWHGb`5(`!(^~^uv{4o%RU8H!-|WX9Cj~_$eQnv= zT-5V9juBz3Z3W4x8(Jyv`2ZD;-5cKhq3R`JfpA^2ReDM?N$|>tPClWp5;*K*mx0%A zF9u)HvcmDB0BZNohMGk%vP5&xN4a?^w%7V3vswZC`(DV6zrH;MEP+${lEIU|-lSdB zc#%YC5$H7%BTkXKfM~@VSp5(c9a*h=_;Y^?nOFSNJ|Wv<5gV~L$xJ+ ztV`%@dBT3I&pu#hZW;DutDuxieyMyYLc6lXPpmy(Y)78g-Jm$<%llCU*b=AMae{7? zde(ZB35XWtW?B-%lUT+TNp6>4begeBmUM+sllQN<@1L|UUk7S4CeNb1{x-G(gtf6T zFau0>g`QL9$))ZH1&6SXcix`@4B6c=(=Fq$W)a330&Oar z_E+KUz`^o_O{SmJySw??l^-E({TKzm-lMl?`j5^^e*7m%zkE_)waG`kIEiu!U!UcC zia6C`7iEsxb4dCtyA+jOuBh4Oc7f|NU5kEqqI#3CDHna{v$?~UN9uRSW~%ZD6HBe^ zI!*~i{6pmBNnS9^me|F*jXZ98(yN`H4tQ$0t`b%|@wfAhSpeq)j&pJyb!v< z&N8Wd=zLiI(w?dbrFvCMtb^*XKQaie>-UwV?BL>c*L74`)W=2{xIR7_5iv_h-zheM zTP0B*9>vw_4GND$I7~$qFwBa%L*9-R(uR+=4o8?I&jK;>qi;a0_JC1=`&R;RW)U_W zo(VK^DxDtau2!?{`h3hHlF)f%gg$!rBq4lcET#jXuyT4ZI1Z_j!8}3pb2CQ+Su}Xy ziy?Hz?941BuppFr$<@~KWP9)~GCa(+=KH%d$0_wP5{$F6s4WMr4!%>~VeEJ-47nZlS|KP6QcwhLz z+^|V4*qRZpA4oLA|2a9L33?1TTCrLQ^m{fxcUgm%=y<2#%_`>OGARb8%zwytX zEN*`y@CK1@q)a~*Lzn8#x4bObSxQs+Crq$B*~uGcn3+g8-1*t^tqO(G?LH=3?>Oi@@xQd&s#DTR_MjqK z1wN`^SH55QKg<5l+W+5P{x^@nf9cNu6z9Kc0Ms-9oF5l>`5#o%zwiA|ZTs&l{ue96 zf5iTOeKZO1gN>lU{(oQr|EkCT?f*_l8-8wgt^lK=N<~ro3(|%edDn`?UhV&mD)WSH zn4JupTPKGS$m`nQZk`;ZS{*G}joyi3&zl40QkE6LLz@PC@&nGgTs}b~8S>}&CQ&xQ zN@XYj7dgtAp?%G%=GzGrC^9N)>}1qVcTW#iLFR+XC*(J_xj*MgCm{Jcl?6Sqv*EX2 z0^ko+6M$D41?a)^r9EbfwPcr4AZ2DnD{UT^*V|*~_K@{~FE5w=bU%GU%v?sHP)1Kk zg*XF#z40(pNGs|%&2_xrGxqD(r>2DuLZhRj`cbYOo6VZPPGI0L3%`sefW&;Q&+7**_aanh910N`{^-^)C{>08Ud4jI+4 zX*(K92pt^?`eR{jVNLl4ki1;m>osawVBS?7Pv9V&K_p(=>tTSdKIN#2If(x=)4YK` zVO6jRJ4Ck~s#CZ(dg95aF8=H|`oYz9?gWCZZtdjao8 zC4`cjZ5YCOFE8Q0U7@m2GqU4Z;TWj4o6qNur1}R*=A3sQ5G3?mXp61foRCd&KIlns&NCZ8U zm4rN$7it=qX{BC50K_wE2!I&(yWO!xg)TeP@DWuPY8p43)-s>+POX;trDb-TGsW#w^7c;doNPvGlU~!pLUn zA3`7ie6$Iv1z6r1yp1da9oQx7!8)W$8DfF}7N}MRg|&nZ-t{ zNN4-^jlqW*ng)mRIIHvijBp+h?l$TQ$9TP5(!TxDx*42@s0!aL(N5a`qC^?x0;J9b zT~FWv4OD*cR;U6mNUDDs*};_R$sU*b*L=OIo#;{tZHR-qom(VvrLri}Xk z*X{qKRE|c^Wpm|o0ycLK(L-tj6T?N2vyipid10UqK9U9s6$374o1Xg5kG4z^tlNQ< z8U2XArxHJK|KS)hWpb$a2c!rfyhlSQgeC6eL6t^ULK0kn_VwJQ*|&Dz0pw|3g)+1U z{pV}>%MmBn4q*AInN<#n1X0`{vtcLTqpHH4YcUTbK)}6>xM>ALUMCN~g_qorMNMs+ zVFD$pyrKLrLX!4@LOLJi5i1isB0sr(bJOj>q__QC+Xxtx#jZRDuAS&x)@00-+ppgl>p2CR6g9B^F5T`iowTSk`{z7ZN5q<5j z;}<8&z7(+KyHGu{Bc>uGWrdDEob$6$2)82<+@~1fof2dCmEbTw;@Z<-2mx7viN-A3 zF>+C~f;WERl^xL-GU)-y)l;cz37Z^^LA4XD4gDtj2V?1=d_PyY=nL)n`}_N2Nnvv7 znd}UabkvCkyCR>fW@SSM3^8 z;(=2#PI<*+?^@lRI&-wYiFR_mg{%UI*=^dMV3mHcu&5}UIQ08d+pi|u3W#mf_~CNh z_v)Q!Om}F}NZZblF)~lFsp1MF{5chDHy;s6QN7`brDX5B*;}NN##9UbFM}Ry@s$MimfchD=IoF!!vHDrKy%T{RRGyFy#z7fMm$y zKx>hT#B)OsA&vE zAwlU%809vZxPCancpFNTCQd0`d*=GxI?rRl=eDtVh1+$vjB=1(Xpk!}v{cE0oy)01 zPAY7!P#Y<`Y z9AMWv?TS{c()?VIxy#h4Ri8aS2ZTp|<%G*uh~HfxrTx{EKhz{1x>B!Wnz+w?Gvh)b zxG;j4Sm)-+2OXMC8(8OBn?D$2a~w;yN%bSj5~s}DEX>-_@P9>S^xq<*$%{bU0_5aw zn}(Jc-59UcMp|0+=F}g|dfF|tT+0ta|HiF2QcH6nF<4B0f1qXd*Dx40g4LBvhg7WI zzG#iG++fR4rC5^Qw-1Ly^K=ng!}%2+Sy}{t7dtMOB79KFY*sXe_)ZOGs%q$R#xXqv z0rk3LY;Jx9>4Dwy{hn$)A^;AnROt~$E0_X)+WQZ?7gMxd3UN)@J&|Cj^V&vrDBkH}i}M4gy765XSPX}cab8m?Bk)LdTF zJ6EYO9@jpMw6?JkH=aRAO`=lR9~6|eG`UCCC~cIBO-zfTG$uZDf_>W%3gNDEePl1{xw3ZYsra(&t`d0tIUwRVn@>|UGsJUGSKsW{K#w&d z_30ZKHkuTyR4a{0t*O>AGvUrbj-bhF9IvW+EZ#GeYmZwv27+g2^x3E{g+%0aSfVjb z-11vLhA-mK^O11UIb4mI=6uo*d-eOHOKi(ItM46hz(7m^#St@9aIOz|6}OuA7y{xz z(|Dy22fTVkX?=Y(hL3uX8S+2nu%CLD=Si;j&UQ3v=we0 zD=2!_$0A8G!MMgs9Z7UU=tvN3zq~r$8#`4*UIQ7m!41c((we=6+r8gNR!&rf>5o-+ z`{|c;a}zVlmC=7n|CP-Z_t6h>l_Y9Tt3^gR+l}s0hTJW-TMU~_r;%C(Vy?*Xru#Wz zZEO`ULgEKhCIn4xYQxub$TC7}F^*;j>!mfx<4Qw&CAH9fz9oH z>ACIi#yRuzpRHq5&9~AkZev00meDLZoj$vlCpKcb`M{Q!-svzhLek;3^PR}E z>*`B;bWAm7;Y$WJ%c=Q#%iwTLv%J|j^L3<9TaSn;uz}J^UN(zZVjl5d?14DkGtSN_ zr+9JXq=E!#uGsRLQ$Lkh+$_eSzGnBwt(xH*Ao*LI<_KZ%D8qSfy`6Slh50e-p7BQO zNx9pf40iet=iZVmuNtPYL{ujdt!;w0XU$FKSTqu*VPAi4yzWNXbtKmqs2o&P3ZoM( z^oUFP-IqdF(6vYIa>RS?2u(@{qMV8DymG{WyL#$X#n$$E1b2E?ZD<0i0bSN!T054W zT+B9GTh~<8%@(QNKKjG>J{Z>A!kwP;wa=h8a(SU)vmxc1^wix+mRM8TNUbIw$$7!C zUPK+b_yd)ZI2z>SQye2V6^C#tcmMo zEo-Wbz)5Ps%`dSy<`HV#AINd`duqq)_aA}w+wdc$onZW)!DAX>JM26elzB|$I!+@e zoUJiiT<0d18dJLBMTWr@kVF&D-fIO+O^O3Qgv>fnB@apH+=~iJ z*4Xd9Ek_rrl`~e3E*|2OpC{&roPwtDF_H$@J8K}=yf!tGKSD%42~u(tI;b&_w4XxY ziq;;vfhNxUyEVicx}egQ{ac1s`tf~1ZNH&*y?bxj%@x`4Fel%+Si_nid`>SW75@$IBd3!|mY+2iermN-Q&lHO zl;y!DV(qgA+|Kl!xBprT6;Efcrk@eG={dGpZ-?N(M&7L2Qt(01U3C{i2Q__m#|B}y zX}EK39XT7pfE6M`!?e3PI(aCFPx;9{W7I@szad@)5mrP}ME0yCzYnEuItlU_L8dFi zG!|H+lMNS<0o*0DQyuN$N1_#?>bkCyl~X|eocc&^ogo5 zW7%LS@ytuz_Eu&C#$VrJ?>M48McTAUc6;m4Et1EL2^j^ZDy0>?eW4#m_J=BgCii8@ z_I{+3<_6z=hnZfHq14BQ^~%`DUeA3*s*-W7XUFI>F2uweoMem_*AP|chNIwoT)X%b z<~H?$Aj1^>6yjd0d|7>hy$IlkoL__}Te$`E-Xy$2wCx&NBc6O0pygYek$!G0Gs4Fv z)})0t`*v*qfXA=hmS5Jey=U@!ttJ8TI(x#uDDNy7(%9P^B!o9Cw-&IY$-Ky1&{eI)Ock5Xq6f`Z>6w>*lk`XSeDLr zvPB*YB(ak+%KcY*cvWw$92C%L*rTr`}h%>eHb2NyLLj=4cEBZw5UJ-t#|*YG=T; zsu}EW^Q|)>Ld+at)7BB7!8^OFqHAc8WPCHd5M@zvk=x9#nWsIN`3LXandl;Fv z{WjQ8c7@8wf&E$v>n(0ooWT{<_lAf&^ASxp=4{RQj)jdMQ19@@dC(umQ1e5?1nKbL zV=Wm*^uDdC;xMg+8St{Nyegu!=HBiR{lGyv4oF$jo((=!h|acqX%SO5^pB>G_ZH3t zN~t1KyHJVl*eBTSr+}`n`B_yFo&Y&9DK;XKOx9P>&`6r_^J^pq=`d>MGwl$0fQUqw z*rL@x5z|2_7kbo4*4pAy?}MkFp<4(#xlW=W&(()|zMxhod zwv^&;IIFyJ&?@=aj?NtE>9;t=e5Q@=zTq|VI zwEC=6>57)9a*V@x4WQYFodJl!uMC+nA9S;Qj}6&Y?L>-|0Z zvbd5s}njxPN3P>FSO zUB1+mjVfG3JxoX!@y-!L#Jxf9GZ(Y*Z>#iJsAReizF%~AdEa@zwgj<-L5_`5mD zftp+$ux|IS=f+KPX$UNeRuGRK#k55@Y8wYpbvKB~=)=5#s z21h$FlllURrh-R-bFw!Whg7TlE78rVo8xi(QkMTZ`gR-Uv!gS^ia+vgcQcc1GPJO+l2m8rf7ZCkvua_u0u z40U8}Z!0}12qwS7K91KJAUu8Z?Rykx3b0jcD$4<_vfff-yi2HkGhj(LK2$bX;)p{= zB}xuoi*Ogc$pyb6J)lMR)O)vj4WRAT9Hem2pFiIdzBpOT(H>B%b*bKm#G1jB|GTNJ zD$ICLOM2sIG1#H+0rVb0Bgxr^r!2&ZrLds?lx+T}DWMl4DiQvDF?i!C+!@UAB81O% zRHDV@0dYUZ>hSK*;~h>C$iK+d_h@UkMU3z3I3Z-&&tXu@FPO7;PzN0RiKne>5AjFt zm|T|8S+t(~F!V)bw_Rw%(sp3ScBuWcP|TA5+}wfX)EmX?oxxWY)bYI7WG_ZkBEXoL zJ}r*d3-biFer!F_BX3Ehz4OnfP6oysKUjSog}(j{DAQ`w6h#}G$naMiGl^Ta1}>K; z5BRwk@Tp57Qw@IpK)MgPAK1?SJ|w9(Q*B)ViuL*Vd{}c6-d-tbQybW7lJ=kPaX&ZG z{18@zA2=mjthDhVp=;XXzUrfQbi(}Z4;%(3V=4t$F=m5*%`S9q+n9?P?U#hmX}Vos z;#%)=7(aD3MlV%Jbq(sD#ZuolHY!Lbq|Y|CHILs?#|z9qSefnlTlN z=lFkGbE!cS{C=cvMTWO0uC1np5X%=`9B>FO8=mw8&u2~7HCAaC-jNe|(CH1eVK+~r zchRj+jnUf78qb8P48R4*+^gL!)5ir)oefi+;D=(3Ezcb!Z<{(l1Dc)PUJXw-!p;3U zL4xi;XBBvdYZ*Y4U<_C;>%&$Z`5_>$2$geyfs0 zNmmE-t<-3%YKl0-2u)$?Cia?!DOg`n3P=uImTkJF2sqHsUkWuS8Na`fc5|TZQnAVF zw1%t$>1_^42>pV(0S{h!Bc<$8$O_H@Il-KZ@^Dztv-;j`x%ZpZ$KJcv3pPe~_r}Qi zLfR%$E48`z-o&M(#^a<6?O9|Rq*^r?t8x(6snYD0Ao@AW?Kc0;U?{VgnU>Yf z1jZakh2(wnfs6ZY`9bq^%@!e{vu+DDuc+(N0^1gYW@6nP5!TpP=$hO;|IQgJ=}T`H zLd9rz=h`#M?H1(F8Mm3nDRmeT{-XI})Z&#;pT*L9BfAAa2T0u&^_?*?H(Q2yv%RKu z+UUf}(a3sJ;(os+>_E=fTjJpAu<8%f1b|-d;ZZB)8y)E5t-86Dl8g3_xOXs_xi(#A z$0>@KnaWxuSAYD3{=u1K9oxr)i}hms8TDF@0f)0{CP8i^`U{e#mHYKF%=x_z#Qt`- z;Q<(KUp>{?0$-{(#I?y0AHG?hOqIrba(v zqJY6|qACRc7ok=VsuVM>cein;--JJl=>X!-zNkBM;HS};_sHvj{ zZz9X;x$Yh1Af@S$+D7x|`OL!}{Pg1{8noJT8HIF}@C?phvv?xoI%gc`OSt0Ap0?sc@GJS-9^lBGX+P`13 zGaB=b$m8XbX5%!ynILgdpDJUX0B@PV%N2^%u~+G_Z9etjil$ayN;=}ux6%p=(915K zFfxs4i+aB2@At_naX#m!5<;^Zr#dT)SA1uPtX)1a?)93@^ETg(s;I2pRV?!F{=vcX z09`IIj20S`rI0`gIR5qOzPOYK_-NpKP1bHeY7<+8eNnjZQ0WZTXT;uD+Iy7Xdb4pp z3bEUp#tXV!>-m|0aCbY$+S~LZQ#qbzY~#ZpC+k4V$s}_Y@he|(5}O>^<4r_1vz{2yF8gdh=zSm<@|nn#_6@1=N^&1~n7N zPZCv!yAkf{`z@D>v$9dbe50a#s=bl?- ziL?MzuW?3U$7b+sRfWHI(FO?0qKhf*OhkKUp@`!+Vx!2rEVCg;k_q0+`W=0_mlbXT zfG9R}>r;<1Ed4XeLe|vuE7fE;@oYX510>4JP_AfuTOmh3Wo}<^(!0GXt2iF6CC)%s5hHyv;HGmRUr{@nONE3#9 zAkvV%0UGp>C|?G0rj7Ec#u)!%C8LHdnHGoJnw667}_Z6ECj7>>;b| zw45wX^kZj*xr`2K7 z8fJ?~gKpn5cH>K33kT^pdV|C*9Br8hGYM#I_>xuCVByy6k;cNvuE=q&jK_>IEm#i> zElh^*;eZ*#F+~`)5MYwm)BzUq8)A3b)q>EufiHz#WZh6M663|r^~(8GK&+eA@!W8Q z>0AKtCe`$jJ-aBF_62l?XUlU9b@MoFBF#!vF6+XU)pgge-|T)uq*Aa*qUV!x z80qeGS^}!5?j$4k1Wd~m+Ob=GqUiN?tz0GEb%+Md#n+V2`=7X-Z)_apHE9t8Of*>- zB;AXuuCQE9@g|)j5*@r@jyQWAbI0GoA!>@3x*eb$dc^kK!k{v5p{$|R*Wu>*4N66A z=tB3tKW=G_jZNdxQ~88q`2_J55Z+6EqvrnX8M$(jwj``cbKJt)+tQnXz46(>+kOM3 zq&?y3sBOxN76k(=5+VA58jniZeNLPzD-=QGmXQ<72^8S;ZuE^0+o#`mXb5!2UXXa8*evO?1XJxkUt%9y*5e-o-& zz&MHiHux`$Cr~r#QH~IAB)IxkzM7es1%df#xX@cLxYxNi$8nw|QM?;G7`HJYU;*^L$^zhq|SoDCqs zNu1{V(k_;TL@YpZ{t)M*G6oU~HbWw4*8ezh2gFualQe99lff~+Q`5VPb;Dn5F86Y{ zNb<1%0(tGGAhTz0Q2(x^%u%sHJhb_Jq+N^YNo+GwWq18@7uwPqi%fJ6Y(-U3Igr!# zIbmF;nZRRy-i?P(d~M_RXg<9(-8MqmtQ(T`!Ld$J9jtmw+L3IvgM(S@oH1hHCWzn^ zOYKSy>TGGP`wW0j7=L7LXtF}0%TLYHBGxMN$Jg*lG}L6 z&Q&~ebbKs%>pWm8A1O&l@)>+9ofh=LLN7tLx~t)O33qU zVd$FYE=pM3!#1?7cdl3?Fvwq^q)vP2xGN7RU(A2zQ$`UFLr;z_|5;rXo>bp+)pI!& zY%3Gfo!}tc(A=t-iRlM_jVh0m%~=!D!QyP5LmOW)}_3}|}st1*Mq>h2+iarGdf zPad00IhKWvjPRfcG*3u>fn3s3B8mx`o((l&{WV7pu)f>K>K{^Qe27cmT3L=VA;@wq>GbH&253lB-%9@;2+MxbvGpn>kXSqudwU91`yQI& z^weDhegD?{jZQa2Go4X^zkVa$bXJBMp*fToCS;a8e^GHDy>YRm<}n*g=00b7#Ca00 zIwLpETETU*^H2G47n)_P3`lXF;>{MDv+FHmsou58C26ka%1t@D+eYMsFIQ*xGzTB~ z`7Qq_Xcly(d^z;ZV5N5 zt@rXVtRiFvo;12-yv`n#t-mU{lfD$Q z+k@^J%}Lbns^PWS-;G&fMtS6thxDvV4G(hvFEw_LEZ5`r*T(2dLW@vIa)k=kZE^$RiR!g6)n%>39Sb$K3{X3pH~1g71l*oVXnEGC&_=eKqpN?0X8 zb>zr;*nVv(!Vm+vbsXR6O-MzvI8Y=Kp6V5&o9iVV;x}*%KXl$?ElZ!{{=HQE0lI<@ zIyh=djnUS_E@mln!!p_L9a$m8eR7j4eEeomc9wl1O&Ac!uOpsZ&w;Oo7Z?HtK z$p>!D@ZJeu4D_*JyC);3RvTcZ(q2~QRlaejXu4NRuyND9B~*=2tOhDe(V3M=?gW3$ z_qH-uJo?#b-g3=Se3ikGrT*MyNt|jJd-nSX_hNGP2m0%HSlN$Nq3+$d!EUQO ze3m`-HfrM2mg&x82mWA+e-lvaK`nXC54(@J0RfGcM_<}hT1GME@ED(Swf6d0Dxv<` zvpadp(Om6klQ*vTvq*G5O8F%5OM%5D;QYse#W#JyAGie*=)R+&*AQpH@83U*{`DU9 zj{Didr@T9YF|4ojC4N28NtA)HdY)!|{5bqlV#3C|8UNCzvNB+Xq^G|T#2`nn&94_< znc%Z`gty>YIo!xj+H2QzxunV8ob9J0g%@!!D`|lctR$J=j&}EK#3k$E??ud8e^Z0AC#Ov8VXk#D+g2+JozrBw z3dhgAOASpd;PbKuoboF(s~?OrLiXtOtC7SCGZFti|AZ(FZ1J$))UU!D)BeTeAkc$$ zccJCCn9X2*4&}zszl-Mb4)-Q;Zu#x=SBQQv2LoxNpN>W=Ez63{(-WeNRkT~+O9!Y3 zEFri(P~e%#N2mx48viPDP!OJtLsgAo7JR$FV6r!tU^V^4qJ*!W!J?t1#{Q$Ph*C2H zIZ-pA;%v$gS^HEyK^3)ScTs-{T5T#hu`}6jjRmBbFYj{-5`PQc26r?0?f17KnD6;iop<>So+z88V@%|!8|aZ9mQ4^~2@R|-{MMfKFz*G<|wiTW!Z2upl!oszAd zE?A^}g@t@i?t-9W9wxL}e=k}o6wZRWM==Dy)u?-^nUNT*rm0!+5oQvoRDL=;OJ&2@ zTgB5c#*ly-d`}vDCtYt2?G@^Cb~0H(d|cmE^zgp%XJIg)N0}U1ooT0`sF|*}CM<@X zW?+B#GjtNS*i9Wh!@$hB($i;_&#L_J%Y1Rl?HnN>__cm!SXy*UObS zbM?MKX6l^}5?%;fopi3o{bHH~R7NXg$6fZ5W5%D+Lg82P%Q5I(j2=ODps&ij&qQf6 zfqW~mC8W2yFW2h(QQV)k#ZzH-KC+*#<6J?JQ`a+p`z67)iOGA+njTRO6{H#~MAaRe z3-Ca)|CI0a_Ro2wjQkDPv0i+CHcl(KNKVVjNtfIkI_BK2q(3 z2s%c!fs9O@MQd07xfxHmdBtMCKT;vVB95l0mDQR2{6L(TvJsYpq_kyE_37U$)gBA_ z)P$I96eoU|NE7jNpeuQ}Qm6}&-)O~e#qOJ$G;e0M`BP)@$UGm#m(mYM(&b}YdI~H& zb>~Z)#a6z4q0^>*%pS*SJFmUw zBChYH9W<74n)k(OAQu+=ulnOsm15E*)twz=?Y)Fjxp{Y_Ve*52rS?%?xJ2vRq<%5m z@AqpmyM0;P&2ZX5bRS+zaGIM$*{5NvZ=Db5d&QueWAc_eq?E0g*ONJWan5OeQ$aH= zCu{cigKu4Zy4NNL6&2kH8^HRLGtcE<4?}H z>S`#Co}VZw8ytW!!)xY}txWgc=@b#X^pqb32w-pZ%X^f{b$6Fu^J`RnDKY5KxaDk8 zCMP(P#kvB$qLq7tb7#<-<0z6pgwrF;6>nzpxx2k6t>oJEK=#oKb@h9T{8fZNOf>0@ z2wpw3TAbi$D8=N>tuE=%TwOib7)UC)7Cfkzw?%D}{{T56Zp0BPVV4hyi_djtr+ko- zim8(8qd?cu9Z#O<1xSDq)ojW^iyot^eQ!Bg7h8vS4m&_?(&9T_=wRYz{ML8nIT0J% z`#f$YuYQkwuuvgFb_$a08bt?dA|T{>U0}Mi)PNJWym^sFcuvw z3IY+PS{%nOPx(g}*V%)bGwbcaAg`TI4H^?G-xXR3OKBgfuE}B<(}205mw%7+N*q18 zEqm5QaSlol8YAsXVal1}VealOa>I)9qs=L>+60I)!<@4q(Z&^L{z@?VeJZmo9M@erKVOkN)8!lV!@=Y*|h5YsH z?VeX5Jtpbj!IOZ=Y$D5zhf(4(>Y99E3}Y^(=I=1Z=pV&Fr=nF8$4=kTmPLpLtC!gqap&Wbm&bEtQJyV0Jh}sp)dJ-iSxr+ zal)5$J$+f0)IWjQscd~EKS3w6?Cdv<3&M8ZJc>P=^5snCtNvE@l)?Yg-g`zx`7C>*f=Ez_1|%l| z$x4tsAUO{@~9`JpRDZua(y!Tc|}lIq$w|X#@VN6zM%M#;A)$$59X+P zG2B?L(^jezp*i=^Bqt2YAmN;B0cuteYD^e;eE!jv3LTr_=a*d=Y+Dl!u4(7pfaD)8 z>fb+JGb!15msRQB(ArR5Clol)|JBVJD_!eaYMIMh;h2E5JQAw?5x@PFy0I5eKkc(u zU+rUVUie6tp*|mHb_dUx_V_eyPr^PoP`AqSdu!n{PrdHE6~-q8KC$}a+4jy&&GG$R zOHy^tqd1xOVbC;%;MQYs>_3SQRZ=7>&qEr?=6ODW$lj!zOYQ)?re2?yvZ2VBX~c>O()LqRO0=gJBAC-6E1=2 zW0G_iACk;`9#T#c2kHP?>UM6BT+v6P5TG(pQs{ZIp0?A~HqvTwFDQHU(n;TmqV+0| ze(li33=1Rf7o&l5t;W@Ev7+P4#gn3;rSK(x8u0G4&$y+i+oX7!q=!`{zvsTI(UqTp zqrnCRWb9MMKiq2QXV@Id`1;Jb5n~mx#>ILrey`@=CC}v7)iYWf%~{%>sTTzhjInEM zUrgKHpXP=^O&dJ(<<5?GL?3>AnO8EGo>#TEHo_h)SHHWb(uavN7>dr+TKfA*y6xb2_{G?5Boq?M$2Du*0Un zcwvJ(#`{qH+S0bJXYI*%?X|nLqaD^3-!VV$O&Pau(v{4q1@1Msnbhve3uIMr4_FR3 zuXfS#qF|*R7def7TxP*GJw=fW!=Asc4j2fT(-70{EF)}i)Sm$sfA`VRJT+_Jo zwiPZv><_I}LnQVx_u4Kh-r$0Ksm>qr)RMO~SUz=4ou21yrbTWziq-rq%kJ`JWt*OF zZiX~$jbsH%aY0_iZ}`2wdc?5B;L!WJnw^F!G%u5_@=K|U8~9`YL#4P#ddy=rL1+CKn;aA_E$jP>{Q*YbBxV!50jyhQxYcgfHz8(T zuyiMDKajmY+xJa`A!Ko3=@J?aq2A`QVkJk~)?VZzBgWGHXuh%OH)wE5j$AO5_cGVU z4&&}x$g`P$AZ$4%h zkrDM<&pX65k0Ln`=`(YWS};@T=%TWg#c-a(8qN)j=LUO{cb|=Rb@>4~l9nx*`CPZm zx#u0{vBRa$Nc$siu2tmX(n#FD>mwgYSK_)Q(mP-;r9%LaJfFD(ceD8Ck4qs(pF&=k z`?-@N7Y|-5e8xNSAtIgyLr+P1k#YEN4#ZOfvei{4<)i}{-`6}xFFiiTQHMc=KYACF zb`gbPYke z1>MJY$&sHK5+m~&I1woh%ClcF9StvTUbhb2r`Gn(VmHEvzkl-&dEl3R zIuYTbSqtV%bHE@&Y}7R#kZqVGGU^^p_>duX-+goXo|-AI&$5pWJAxA-`cq7iiXE}y zgVz3X;UYFEQ|Cc87Iqil&?}Ag#jx7dIHK zA?Em^S)CUd!w2R)Nl8pD9VEUc@jmwhMxFyot`mceF|PT~kao2T z@E1YFMi;*4f$TK=p}~i!;6zq=|1RsZUF%*#6*&Fgg4^T<+BUPzeouw; zH(aSh^(kUlWQ{=jm9S}}caa<@Dw~s{YfDp@qKJe9+D%m3@40uf_*&F9)yR{u>yr@^ za>u;aCu^y=VRq>ha$8@oc!CI956I@Zwb^>)o__yF01J5`+0oi`I@qk_KzT2p!cfj( z9UI(_&)c9zyUtjdIWMe0M^o$~0?Hl=>T7q_^)z;Dhla3)5-LD%?^XoQco-J;D$zrf zYva)D=ZMG^&k@svoFgH8{Hk~kD@OX;fP#pP80=S9B^amRl+pjz=;{@6OZ+h?%X70c zjzysS6*tYx*CC_#M~h&*Rixr<^8VuJ^rDNzrzE9_58KscW&OeDQ_4U}hRE26b)v|#6qg3F)16u zH+Bby{Zbu#cIrQ+dq8tb`;+rmli?_{_q@YVDXQ%~FUd^~92XY>RXUwPDt$!-;-Q4P zwR&<}DhGdtq4~A7wSOW?LlsF#N|!HzQue}R)U3%0r(5x~BO2FeZs~q~+t;K)`kCEaYoJ_a`g3q|+fPshPjIHuC+R#H zRacI3WiQHS` z98gp$uKMRe3&wX<+U+DX5COIJ2niZRd?^uQlZqOcbkUJXqcB{N+wa?Q6yWbkG#6ZP zbc*;tLq7th!T;yu&g}+Jup|7PaPsb;=O^kRhaCDkpy#&~+Rv{5?KSY4P76E*`y(Vi zgNCGV4T@bZpo$gh3X)@RPFgp&xUPd2t87q1qM*axPqu~&;K`+~7j93^jz|gpQrt!H zEF9A%0BBbHlzI-9fAG-ORuQ~IL{uGM3ZC{`m7?rm5FzI#TFSw@t5M@G(X^0QaxT5E zs$dMl+au;tfGvEYg7GiFlX;}5<|=6az|Mq~3_PU@VZ07qGCU-?jm3U<8k-iAb-49tF>#F(zO>I--z0 zm%w}1&tc)Eguk~#7in;S_T}jCa3aElk#ud?gJ(+nm!1&N;5@P2ej|{8QUNQji|TlB z&KN0LdcdNOT`&(&K^mFgRVhaWo=@~q1E4$^L_5LOj~P7Y-|1!1+=7C!%dw4(5tw_| zea(;oyxAjFW#|T4xXgE04H5<$Q8Q2r)H$=fYkU?M!5YV>5Mt2d1w}lA0BCQr1@j{U zPh$4t7y|D~@kPwUt)PRzs><8{kOM)`p!%5!!)wq$F=Wlk7)_-UE|EwK8J*Z*p>lc#$ynr7x3{fJtK=u`T=r<}&csfy&NkxZCu1 z0Z;)RIrw<2;d&S}D>rhz3dm86Jjo6Hf0F|dFm0yi!jM&fcQMx#-Y5f6Mx#WoU;l?3 zqyna!UZ)l~4;ox1e?7Ab1gfU|S?n!l>`s@v-{PXN{n8SzMq5dQ_WW{I47s45iZVL_ z>($m=<&cGVrk^(@J(rS0zrXEc=exsHz4$_TsG+8*(Q6!*w#~sPvmigY?h85kj1M&;ow)9<_*1^iB>{+A1ix4+X)N%Ob)I=%!&I3CN9piuMMv~f zLg03X%bB2u`wVwklyWNTQg;YZYOg9~W|O#1^miSqn+UU|*3p$|R5@npGBZE@zMz~n zOTzwGv*6iYQ5VwMJKJ^ccymQ*RyzW5_K>fUpUpMFy21}i)l(#j4_N9s3)uHf^u4!F z=j@VHK6e~vp;I1EbYqJJJN)wY&oEmOC0fX`^=(0c=S^2E@N)b@tH!(A2J1X9q^FS}#n(sY-61QR?FJ zTHY+&?sQ;6!(D$DeI!WaHASrho}%jzUw11$vGVL&B{iCl_oJs4_M)R2Q4g{?KU&8r zen&5T{*^y;+s?nP$F|zxh_2zsSEWl-J@d+pc4}VO?Y_;9C)7Pr4da@v%#T&Z6d*7) zjLAydaAlMI7p{8kiuaAjl5i(a3X*FO>el^_;+L~HhNR5v%9tcp~m&=tLO#id4LTUK-4*=-NFAF7zzN zi#cyZZ{t2jM;TX=5JO0wQy^s*c$cVNw?A!2T3C-yUl4?{97&2K&l=CUh#U;&)-0mx zi+2hSb)K!bHNY!Sc8`7$JDQNipTJfSYxP3CA2>pAMY@Hz z#Z^h3Uqt>9$-8A*{8-{6Zn^GEd)KsOA73l#-Qkh9wL!ng(V!hMYkvQd?sd2C{70t& z$m+mLA27w_PcLUga3F?eqc|R?8@$35X}){l%!7t&owp93TD}U2UrR7;Z=?=;SJ%IUw%l5aWOp0+&A`IeW{QN&zfeD(Cx6ER;FV9UbJWWdJ6Sqa;Mt8r~RqZ zajpV5x4bmSBC7sM_mbPj{WT4%W_A{j?iZ|3Q&TJP(rDlf z#@5UgtPf*a=jvRh-;djGyo@M~m9-z3I1%$J9>|)?KB(9(aS7=4Uag*z(w()EY`>eb`vQ8pRR% zl`@xnL=fZ`+ZeEht?S%CqlQmKM&YB)f2bHlEo;9yR3u2#*LK0q-kUi(NARu=A8uuM zo;@7(vk5df`@H7iT?N;7lK-Z9>Wp^uJ1d_}Bdjt8we%Av6R^tiO~2+!&|}5SW=D)z zfEr(Umj7hc;3AT2&b&}f+!Q)qqBqhomlS}4%T#I2s1)3}gKH2a&bf$GPk-}HT!lg_ zJKh715M!@@;vwnP~c1bQtok}c#a{D`_j+HODp;rrY@NEgXWb$NWF93qSBqSR&W zrH$cX^V_Bhky=cdBgV{%)0!Nv0uEINC zQ^3x0IDhZ%@%d0sLd#jQconto^pwbTFPq+;WzUJPeK8lK?-t4#4)d-V=Pm`9=Pr%H zC!@+%I12jYE@!Z@i5O4OJNpJJWMm+-g_U`hQkUylz%9ODt$D0fPFoPy64u(=j4QU! zuGiuk795s{e#h*pn`ftZiDZ-p#U%Q$$qB~#tqV@VZNEwlhyofu)c=gJ+Fn(|>jcFN zxpJW4>ABWvn?+!K6*}!)KbeP%ZjaFpm6%6QKiRW?y2OWu+iQL?5)Qo`jtTTAOt`6_ z&w2c*X|cWAa$#lAC~{fRv}&E9LEM8Kh#6nsgygSGA%)5awFxf0jv^K7RrZb9`&r8$@D zrTYp8W2Oyi6YP&YY&NeeZ?e==_-NWuQiLvBO4^1ybV(j|QSE*j z3h6tqeSS3A2Qxk3JphhU;lpuGYRp?`!M}6&4Vi7gm3rKAtDd@URRya>zX!ZwT@r9q z9g@Bc6zPZh_pcIL304eKlekG7E-m5~+0LCeXgI4VrZ54`)Y80;Cj!nCmIA51Fz%bl zTVIIPKL5t%PhUUpBFVuMAOOpM{QaDLnRva%i)cX_4i|(aK{1vyt6VK%(sO7AQ!)T6 zpxF%(-*y2zZ6%rgQQ~S8%1EwQji*E}D4=-iJuTAR{pn4_bv||+p52Y+ zY&A9BK)nlBAk;k%BqX-I^XU#=sT@g`(E1pgQ@_Of{LG7&VD;5uzl74n=2*zYC*FI&Jo!l=W}>n zhp_lzrSp~7{$`8ufyJ+-neQtbhM7KEQK5kh2e6N<_&EZ&Gs}8F+^?c=KX8&OOKy0~ zVa>ZeKXmwd_rhj2zgr|EDYlolhxlleC3c&d$tp3&xmf-_<>r>NjE2kBnuO2_OWoz% z5{a?KHTrbigp|(4DfZJ^ldSw~eGofzrpY9|%oma3B;|V(p=a_z&7J9SI`hFX%L+Dwq}L!>`wWY1&^s>{GGGtCODUy|8n-ROAkCZ^Cb{L9mN zCA#ZvI61N-j86SBCruPi^X&OO2HH3Q+lxq1=fu92%E*&Ay$T<;<6nHH&1SXV5Dqg% z2Kc-} zdDnb8DE0tv^vET4-^~4_4wWS$G3hlYAex7v9(}>R^3(}1Rypn z&M6T@V}g`^Y@0qZ9^q@?F0q#QJ*(QsZ{^zDURkW=#&00(=q_9{#4kl6Q_jq zo(?Gd0?v`RWZkm@f>Ry%DAlg_%T>ryd2e0LMP{lc7UT!a#Y*QbF+pt7UdnZB+_c8u zf*;p_cUThgW=NM7LJNVVA7VeUMBpa=GFN68icBYkeOG9W@q$5TpLDki!aZFlsAOQq!?DgHBIv}6NsQs5oept2dVaMFyGv5Y2lGov2A zRNW7`qRU0P#b4S`pJ}yHUG9Zr^5>b>_PMB<&0CIk_l|wrHY{?(-ZKg1I6im9|6pAX zx5+MFDE>U99|pxV)i9ytMFiA?Zncov4Or*0A}m>3%X7M_w>QTgWUx(_vOZ{>3}TZU zU*SY_2m@Z+{UB9v2k;{HMXJp(;6;nut%_bfq|&L*{WPJTxk&uJ&^O4=knY}JdZEm& zQ__9)Z(*BMf+HJ8wMr1F20cW=gmZd(Mw)R9SMEjP&*7LE(crH7#UP1Ew;vxm7f@UQ z+EcdhNk)mXLIits8P z4K8&>MJmye>1*yzDY^0ksoAJ^%m%clD(=O(xaEE)2H?rad(~WpJ8?dFWI#r6K{zw$ z<5EjDYYUXKc|3x}Rdrj|5L8ja8mSa#>}=Cw$-W#ij(GD#f-B?iqs!uE4a*wLP+uHh z3C%TMc1}t9xm@>ERrX$yJZ%H`7|U51p7B zlHEx@S);F?+=N#ffk@0bgFu;lkN9`MdR6Ha8$bd7`$WYenfl0V~a^}F}52~qs!Llpb+mmx*wSOeSo5reB!_A2Ke zCDrPDBF=q5FqvVSTg)#(O*|t;Pg0~###?QT`_AoH5FuBmR@AL z7ri~C8_dyZ7Juz+aI^Pnhji7ss67Q*NG&x7)eQ~ymxP@wl!7)<+aDO27^REBGtm_M zz?~}&c=@6H92T~UWAKneqqaDx)@=C0p%vd3px-<$s-Fp&rn?h^l{4Ly(9vYS+Bns| zNtrq8YlpDir(1tsVaxo%OCG>@xPf|{5)>H&=}#KZ&YJ( zg*b--`DMhQUb_eCbM&y6uHlx5{q+53LBoQ6NM_;mf@O`G=_j)^RXl-vSE$54$ERH& z800Hkmwj4+B>QIYHQr}@(`?s9H)f{)xLlwx8%fi}MWchCMmZS#euOFfA=y%W^g6NY z#p=lJhe3I1|8NX?=T|^{d}@xED7D}sL&pB2WPjYU?d}iQtWuDp9vJb?xreO@B+l0e zkqJ>2B`4GY`K;5N22LZ?cwkHN6wgr zvY(D=Z5(LS!JQ7}6Y>#$#L5!ib#+blkGP`J^~yQLnOwW0OUDR-U^b5kU zmO-dCElz56fg6p|8x);03WM%EDou(6fq&IrggF>&{2kU6b+DS--Y(nRL8GYd4~?r4 z>=D$9MbJovujMW$qCj%*jei`@l#j3OMF=(6qSyE z>4h4- zau7175Nt0XfG#pWOOsOng@aXktYDA5DzxLKiWgVtcvi>;SakBR;6*!ugPPW0rgvkr zs8YBPD>V6kCST&xX1^P+NTcETdEco(v=W5--&Ni+0`yk>L~Z{M_&^wdiP#Z5?Bt~B ze@WyOFw^ko&u!oF0Rl-!*;9cuTy)5?7&OAm`Tr?IY_-$-pp~j<3S&M!>15$0-1m9B z3=K}?QjasJyJCLtT^z|@IN(rf>&-|imf}Z3S1Ch#$TJ_KDTV;dd35ZU#e1O0u19Pn zY$iQ1Hs)}~h7uZ~Bw96dv#dD)fGPmyPrN)I+9qcPveVg=?D`(WMi4W< z2CpSO>8EQZEhV`K@?>rs-4<8*@cpe$wqER)$X@qeE%K2J#r=($#Sg=JNhe49y&y@| zXmjNj8?ilwNp3s_a>6k%JSy@>T4s1YN zHZ5ig$uPYHsKSW>6|)qLW&23-MdGo`z63aWoz^E{e?vDMBUMBl>VcSNs;ioATfS0L zC?pTi_cc@66-$rbXcV`<`&?_w>{A$oq=&9jrtENU-9^%7Hej=*da9a^_&!bL_Fzx1 z?_6MN5p(W2Jr;Wk#H#sRb$CHa%Jr~<>O2#M3q&RJThm^cYbEo8{!(GoBtLI{D>IjL za&?UvCRvF%M{<$8`QTT^1#LV<=noSA{S6`O)9uv!h6YnAR=FF^idrEdk&2jX`F4NI z3>h1OJz#Hbs}w6h^889=*lt7wskzD7x<@b*p`qeSlEd9qcxuOq`AdCDfnq2UlF)2L ziXE=}`Z)`rwNOzGCm_{H*Au*_Uf|L;@Vxy2z;9E%IkU%3@Y|jtb%?z)xy6JV&BJj; z0VGLo3TSxsd~TBuIkHjxo9hwKOK~1IW;qZM_Yy=O0F+UzHV0ZdPmX*=smo8PjMuUM zeq~i04S%Sdr1gac0%LfZsV4>0Vi=hQY=;ISL7BJ&Wv!GEMwBl1iVbGd+rU+kf zHc3A(W(4?hz`D7zD)lck?R}Mr#if1!mN#@e3}X<%g1Q2%CNV70(1=_ZRQEjU)dV>* zokR;JMhghG(CPy*OU-xZKZ5=29BY%(IcyTM1|t~LKiQ_a!-^GiqSdUICr2*FDa`UF z0c9jQ|GvqPz_!~Hdt3+}SxBRKB!Q>>b!^z-HGBjXoYJ{*m5-|yObCqW@T&}n;Qbd( z@>P1^ox9>i;+L^vZ)`i4!vN%`^DIPMf*K1O{^a!VE*ixXIy9m~AUKt-26&uwzt6dI zB1l6z%fS|SlPr;dtp68H!9&gJcpWp9AvbC?ym3B$zZ-0yX?-7myI&))O{eYz@H8%d zV~oJldo}9-bd_zjeHMvhAXV+r5CWqeiGWna{VPq-(&2xd_9rq}g1Tm9*X>s(%xwDG zSOSzHE^&oGaQ;`sO>CrwE4P1=8v%=ti3u=Vr&e;|qu zz{XY%uwQL5Ur-2Y(}o-zUy{ZO$megW()Wh9=v@j)vK&@pzlyE#sc5bNi&Ps<-Kys~ z)Q##MZw)H{^Lw_vM($|T7rEA0cUO9Om43m~&!bm%=s1Ue#=N=3DWi~bv59^!aX#9` z=2Zc~YU`x>(ACyFDd?$|dpsIKIyWu#VTZ-fZPi`1u;fp}vFGwc?Tr-cSs$5YJLw~r zx#Vnl#M4{C&$&ry$j$N7O>Qq-kL%;}b|wNB+mgGW=PR*FS>$0H2&1R&vh3!P&0NUB zRsX?!NsOcq3*-P}r--cCPGOyFQa-ibxqGl47r*FL`>WR)qB|hHk8nEP*%_55GG3E2 zVLVgBk4Qax>n<3oh~_=H)bUGIzw`MeWT#zcL2$yp!A{B6P+MZeaMb1nekbHB4>l!U z>4wgU?fxBqt?{1^Uo;M!(h?}lYq@ssn|LuX7M5$1|IHu_bCtA3D;36v*qHs3cNvNI zvNm{80O8rU49@X@w{21h>PUowg{eFh#P^~@UO(~Cmp8sW(9`kYq}gmzJ@q3E<)r3D zL|$S(Rkujz0-v&#)>*mWH?VR=Uj`p06Ma1eZ4bvHf1%cX67Enxs>qle`po?T@P{egyu zFV&5ei8_3H#^~ob`{$bnL2YLdGGi>vuyd#zuev)JXsu3-JV}Pg4gYv zVmJeVMX+*3Kv0)D)GQo&NI#UsRr`$50`I-4uwhYcU8y85kSEb)=|E->>L8rP z{pIKn&axN1Ku7);3Ovo;>BxYF$KZ+@m4W@i!l`va$_HY7w>W-B#n6mA&eu7??&nRR zJbFu!3aQ!=$Z~M`&7VIE_Ni0PMQFNc$W`&KetM*_X^-fo9GsIfxvaaEjv&eP*(Sp7 z<8TE(o5dHNes@Kv57v^RyXmbp>N|2oo7*IwFT5^oba~xZ{o(mN$?mK`=ztrKv#fLt z(n9iBNk-q2e=_)zAVQ|W5b(U|UWbK5_xd-FkA_xeK{X4K2{X+uAGeJd0PsA!!TJW-?sk=wT z>HYme-{OD~E4cTYleRNzzugP)vxMr0kCOiU{7f5}To(*nT}qY+CY^V8u}P;FUcfl7 zj-zQHSx50j+T%UDEtkSDQJ|6){FBoXUxoXF6DoL#A2(#ORWbApDkm%3th|C`~#<9dL z{IVCl7uK!~iI*C?sZ0nMk|ub#=%;zaxRF;-t}ayOA{FD#aYcxZ!ct-Ns88Qz)U6J z@B91*9w>MuzG3o6&Gw#MsuEvU2k9E?I%|@@^bQ8#3aRR4f0RM>8$-@5UL9;rQL)$5 zon4wC?mcA<;{gkfnuvXV-2PkFw+co)LKry)QO{p=X%LH7IezA~#ZDVzB~BYM+Gi!g zsWscizuda1oIF$+L}pf$@FPMGm#YB|Yq?e4J_Q`$)^~BWU47aeFK3W4KCk3F z1KH;KO~{EvLiNL;qX}(By0YfDZ<0ng=uTcii_UrA3d`jUf9; z+!YYcz_;*;*L^#_Tpw{0{24(F22b}{dA^9O@ioM;gh7LU2nuPl1GeGP%}%F(WnUrF zwdCI^-^_w9Rkc)fe=jMR%uPRVVTA6xbQ(0(~)r?_+uWKGSk1)OZFd!t1s4SDj;(`K z&CKJa&9zg@XYyU>fA#hf-T9#nSXKs|<(UsISLqc9=3j+da#)Vs%D*#lU&~k6R6ugU ze|4srNi3i!=4ha3$R8&*aR{n%Uk=EAkZ*pcdN#y$_4@^Iyq%#+)tdx;6;dKw->~$x z8V$ck23rk1@A>;}K!2SE%Uphw_hyfeY-4GWwJtew_mOrtP2n5Gw}kDtR_$~1$;v<2 zb>jYJQK3Ptwe*kya%58?C-EdgekX1Bu>iHra%wQ}zuR?qfnM^7GCFis#p_u2mF(WY z4s-tUV}zAwmdX5ye8X_hSm%dNpcVws<2=&*|u)OXTwp7Y!a4=CbC5S&(4 z3%+QhQ;oy@zS#4-Gg0B)Ai9&*{PwLKEgGdcD2|~Ap_PY>b?W5EYUAwYb^a>SJEohN z<{y61E@K4iZI{xj7FD?rtVxuYI(zbX4K|lFB!p`Aok8kZiz$xro2pL@W_ zCgtur-39)DDYVfcIm;8f6H=P(72a1l`?DMZ8*P%WSenUZxN&Sw8|oN_IlrwfQM^Gk zdW&J?gTb%Z(sdRjY*ZFf0f4bNs3(|Q7cV4I(H+JXY@_ya3#`~{OePfPd;s*cGM^{ zba`nk04F~GVJ>QKJ0wSr)?wt(OYhREgJvHGq|~I;Ub63t&#s(dln%%jr(89Sj$XUM z1-zUaSkyabV4;$jKB5QSAC;9#0m=mL`eyw4>`jyyPojVZD40LZ z>RVb8N?s|9IeuLXTNBu%G!2A z@J811fZK#G8RD5o@>FsYE^NwA3t(n^-t<+$DSS}%y*?p_8}qR*rzTd`^LE`o`__<; zDC7U=!Hbz72l_ zhItTo=jX6OV1djJx9N)yPeh; zm!{gbjEH|k#BkHZ;rw*6uBc`WtJIxr2lsv?Gk93!HOJ_p_07mS4Dd(8s%ASKdQqZCJ^9J{I!1~{v;Mru_d=WrkS_<$;!`blr!zn%cbRr z#^9~%EdhVvj0}pQ)X2S6fL)Vx8XnhG5D*BEP88!Lyrrj)vDTQC&FhVv^RBGL0nAik z^Fv*>VK)6_X-aYY2kdH8NIC(=vf*R65;)u!Q}iUqy!xIE#;_X@37*;C*YfW;{}{(pC`mKfQku{9$Q;{hYe6kF$j`kGq(5dK<$-d!G=blkEW z_F$Q(!R@z-`AD;23)z9g+&+6ifrj%(81PWaTm?B0S}xJ)cYR~z-Q2(EUb355KOYrt z(4ri&%L?#ls1Fqk9iT_OoxG0lnGIY!{v|@9MO%o%nVwER)##tS-Id*vlZ-1`(4D~H z^rs}(QzM23NH zzR-z*ae+}syy?UsZUE$Pe~MT9h=xln-K|&dmJN7;jBToK<-wfJd;0xc>|4Dt?RMDR zWquZY`Rw%eve)%-&(!GFIT{d+XGzJ>e#b*lYF<}3Z(`8bAl7fr_)AE(?0D&d*?zY1V&5cmvr(&Z&l{I;9M6#pa^5(_9p8R|7bhQ<~LkLw}l5X|9;l^>~?0 zR;gziS?Z$(1J2n??v}C;vQE;JC>KIn#g)5H!CpWyauYE&%6U;Tl)%3Gw;YGZ#~?LP zRaObGpAxheeBOS7P(px*G^|L$=0Z5FFCe`a{Tzf7uO{!7pU1*B@wS0|Bp-l$IH>Fn z(iMUTT^ZSk10ArflinA#iGbXLL$6OS>~#<@q=a9f?g;u z6%uU#(1}Lng$2NLZoMV__#Ee?NJHsKL;&%AM%^LMMq&S+8Jx8;kctrlk?W7Id3rd) zFe>5aA6*3A5Fv+z77)gHQ!qqvkw6)|J>}miyY}y-)4WUsEKL3|45KIPcPx=8!nrjl zGz-LQU)9EC1C;1St)rXTUy6wVjBmIygz*Oz8bx$Zyz2r15OWFvz#S>cAUiz)&fzJ) zp$re2sU>i_?l>K2aK+uL*0yw60UMI*KGaXt(oNEF##MZ?bf=(Aov zH~H&$?oX9c*NGP%5z^mChy>XF2Dm9^2uoMp?x)*O(1~`eKIUjkyhc4-%F(71T_}Aj z<;J%$*Z0avUhc_u9;>~qJu51aCVwdBv-Q8GKmIqc8X-SYQ5&pZvk1yuj{h}V^1p3P zIIt!{*{b}{q{{!c8R2KmfFCv@>Ux9nf62l8@0&jZ%N!)%vHVx6=6~Oe@a;FS&QK|~ zUmX8~p?w1I(XjT5&3`0-{?EOJgrtC#UVE8I?LQgX-!tpG56#| { return AppendOnlyPersistentMap( diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index 1259adf2d8..5c9b99ef19 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -44,8 +44,8 @@ class NodeSchemaService(private val extraSchemas: Set = 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 = 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) } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index f4d912ffb1..e4aec418df 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -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, Root<*>>(Pair(VaultSchemaV1.VaultStates::class.java, vaultStates)) + private val rootEntities = mutableMapOf, Root<*>>(Pair(VaultSchemaV1.VaultStates::class.java, vaultStates)) private val aggregateExpressions = mutableListOf>() private val commonPredicates = mutableMapOf, Predicate>() // schema attribute Name, operator -> predicate private val constraintPredicates = mutableSetOf() @@ -412,13 +413,25 @@ class HibernateQueryCriteriaParser(val contractStateType: Class("issuerRef").`in`(issuerRefs))) } - // participants + // Participants. criteria.participants?.let { - val participants = criteria.participants as List - val joinLinearStateToParty = vaultFungibleStates.joinSet("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("stateRef"), entityRoot.get("stateRef")) + val participantsPredicate = criteriaBuilder.and(entityRoot.get("x500Name").`in`(participants)) + predicateSet.add(statePartyJoin) + predicateSet.add(participantsPredicate) } + return predicateSet } @@ -452,17 +465,29 @@ class HibernateQueryCriteriaParser(val contractStateType: Class("externalId").`in`(externalIds))) } - // deal participants + // Participants. criteria.participants?.let { - val participants = criteria.participants as List - val joinLinearStateToParty = vaultLinearStates.joinSet("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("stateRef"), entityRoot.get("stateRef")) + val participantsPredicate = criteriaBuilder.and(entityRoot.get("x500Name").`in`(participants)) + predicateSet.add(statePartyJoin) + predicateSet.add(participantsPredicate) } + return predicateSet } - override fun parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria): Collection { + override fun parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria): Collection { log.trace { "Parsing VaultCustomQueryCriteria: $criteria" } val predicateSet = mutableSetOf() diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 0bfefa0f0f..f83bdfecac 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -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()) { diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index db23815db2..b2cf297557 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -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? = 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) : - 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? = 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) : - 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) } -} \ No newline at end of file + + @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 +} + diff --git a/node/src/main/resources/migration/vault-schema.changelog-master.xml b/node/src/main/resources/migration/vault-schema.changelog-master.xml index d1b1cd6f3d..9a049e261c 100644 --- a/node/src/main/resources/migration/vault-schema.changelog-master.xml +++ b/node/src/main/resources/migration/vault-schema.changelog-master.xml @@ -11,5 +11,6 @@ + diff --git a/node/src/main/resources/migration/vault-schema.changelog-v8.xml b/node/src/main/resources/migration/vault-schema.changelog-v8.xml new file mode 100644 index 0000000000..3f7adda2c2 --- /dev/null +++ b/node/src/main/resources/migration/vault-schema.changelog-v8.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + 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 + + + diff --git a/node/src/test/kotlin/net/corda/node/services/vault/ExternalIdMappingTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/ExternalIdMappingTest.kt new file mode 100644 index 0000000000..8e5b966e47 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/vault/ExternalIdMappingTest.kt @@ -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().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): 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().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(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(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(queryCriteria).states + } + assertEquals(dummyState, result.single().state.data) + } + +} \ No newline at end of file From 729469483f4b47f21288b0f959194caab875086d Mon Sep 17 00:00:00 2001 From: Dominic Fox Date: Fri, 23 Nov 2018 14:35:47 +0000 Subject: [PATCH 17/17] Resolve merge conflict --- .../kotlin/net/corda/node/services/schema/NodeSchemaService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index 5c9b99ef19..e835f34a35 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -45,7 +45,7 @@ class NodeSchemaService(private val extraSchemas: Set = emptySet() PersistentIdentityService.PersistentIdentityNames::class.java, ContractUpgradeServiceImpl.DBContractUpgrade::class.java, RunOnceService.MutualExclusion::class.java, - PersistentKeyManagementService.PublicKeyHashToExternalId::class.java) + PersistentKeyManagementService.PublicKeyHashToExternalId::class.java)) { override val migrationResource = "node-core.changelog-master" }