From c369680ccbe8b1be44cbaeb92f7fd7035456be45 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Wed, 9 May 2018 22:47:06 +0700 Subject: [PATCH 1/4] [CORDA-1383]: Make SignedTransaction fully Jackson de/serialisable. (#3097) --- .../corda/client/jackson/JacksonSupport.kt | 131 ++++++++++++++++-- .../client/jackson/JacksonSupportTest.kt | 42 +++--- docs/source/changelog.rst | 2 + 3 files changed, 148 insertions(+), 27 deletions(-) diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt index 51f39f967e..a9515a7522 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt @@ -2,17 +2,38 @@ package net.corda.client.jackson import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.core.* -import com.fasterxml.jackson.databind.* +import com.fasterxml.jackson.core.JsonFactory +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParseException +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.JsonToken +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.Module +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.deser.std.NumberDeserializers +import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.databind.ser.std.StdSerializer import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.KotlinModule import net.corda.core.contracts.Amount import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateRef -import net.corda.core.crypto.* -import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.AddressFormatException +import net.corda.core.crypto.Base58 +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.MerkleTree +import net.corda.core.crypto.PartialMerkleTree +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.SignatureMetadata +import net.corda.core.crypto.TransactionSignature +import net.corda.core.crypto.toStringShort import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.CordaX500Name @@ -24,12 +45,13 @@ import net.corda.core.node.services.IdentityService import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize -import net.corda.core.transactions.* +import net.corda.core.transactions.CoreTransaction +import net.corda.core.transactions.NotaryChangeWireTransaction +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.base58ToByteArray import net.corda.core.utilities.base64ToByteArray import net.corda.core.utilities.toBase64 -import net.i2p.crypto.eddsa.EdDSAPublicKey import java.math.BigDecimal import java.security.PublicKey import java.util.* @@ -99,12 +121,13 @@ object JacksonSupport { addDeserializer(OpaqueBytes::class.java, OpaqueBytesDeserializer) addSerializer(OpaqueBytes::class.java, OpaqueBytesSerializer) + listOf(TransactionSignatureSerde, SignedTransactionSerde).forEach { serde -> serde.applyTo(this) } + // For X.500 distinguished names addDeserializer(CordaX500Name::class.java, CordaX500NameDeserializer) addSerializer(CordaX500Name::class.java, CordaX500NameSerializer) // Mixins for transaction types to prevent some properties from being serialized - setMixInAnnotation(SignedTransaction::class.java, SignedTransactionMixin::class.java) setMixInAnnotation(WireTransaction::class.java, WireTransactionMixin::class.java) } } @@ -148,6 +171,90 @@ object JacksonSupport { registerModule(KotlinModule()) } + private interface JsonSerde { + val type: Class + val serializer: JsonSerializer + val deserializer: JsonDeserializer + + fun applyTo(module: SimpleModule) { + with(module) { + addSerializer(type, serializer) + addDeserializer(type, deserializer) + } + } + } + + private inline fun JsonNode.get(fieldName: String, condition: (JsonNode) -> Boolean, mapper: ObjectMapper, parser: JsonParser): RESULT { + + if (get(fieldName)?.let(condition) != true) { + JsonParseException(parser, "Missing required object field \"$fieldName\".") + } + return mapper.treeToValue(get(fieldName), RESULT::class.java) + } + + private object TransactionSignatureSerde : JsonSerde { + override val type: Class = TransactionSignature::class.java + + override val serializer = object : StdSerializer(type) { + override fun serialize(value: TransactionSignature, json: JsonGenerator, serializers: SerializerProvider) { + with(json) { + writeStartObject() + writeObjectField("by", value.by) + writeObjectField("signatureMetadata", value.signatureMetadata) + writeObjectField("bytes", value.bytes) + writeObjectField("partialMerkleTree", value.partialMerkleTree) + writeEndObject() + } + } + } + + override val deserializer = object : StdDeserializer(type) { + override fun deserialize(parser: JsonParser, context: DeserializationContext): TransactionSignature { + val mapper = parser.codec as ObjectMapper + val json = mapper.readTree(parser) + + if (json.get("by")?.isTextual != true) { + JsonParseException(parser, "Missing required text field \"by\".") + } + val by = PublicKeyDeserializer.deserializeValue(json.get("by").textValue()) + val signatureMetadata = json.get("signatureMetadata", JsonNode::isObject, mapper, parser) + val bytes = json.get("bytes", JsonNode::isObject, mapper, parser) + val partialMerkleTree = json.get("partialMerkleTree", JsonNode::isObject, mapper, parser) + + return TransactionSignature(bytes, by, signatureMetadata, partialMerkleTree) + } + } + } + + private object SignedTransactionSerde : JsonSerde { + override val type: Class = SignedTransaction::class.java + + override val serializer = object : StdSerializer(type) { + override fun serialize(value: SignedTransaction, json: JsonGenerator, serializers: SerializerProvider) { + with(json) { + writeStartObject() + writeObjectField("txBits", value.txBits.bytes) + writeObjectField("signatures", value.sigs) + writeEndObject() + } + } + } + + override val deserializer = object : StdDeserializer(type) { + override fun deserialize(parser: JsonParser, context: DeserializationContext): SignedTransaction { + val mapper = parser.codec as ObjectMapper + val json = mapper.readTree(parser) + + val txBits = json.get("txBits", JsonNode::isTextual, mapper, parser) + val signatures = json.get("signatures", JsonNode::isArray, mapper, parser) + + return SignedTransaction(SerializedBytes(txBits), signatures) + } + } + + private class TransactionSignatures : ArrayList() + } + object ToStringSerializer : JsonSerializer() { override fun serialize(obj: Any, generator: JsonGenerator, provider: SerializerProvider) { generator.writeString(obj.toString()) @@ -282,11 +389,15 @@ object JacksonSupport { object PublicKeyDeserializer : JsonDeserializer() { override fun deserialize(parser: JsonParser, context: DeserializationContext): PublicKey { + return deserializeValue(parser.text, parser) + } + + internal fun deserializeValue(value: String, parser: JsonParser? = null): PublicKey { return try { - val derBytes = parser.text.base64ToByteArray() + val derBytes = value.base64ToByteArray() Crypto.decodePublicKey(derBytes) } catch (e: Exception) { - throw JsonParseException(parser, "Invalid public key ${parser.text}: ${e.message}") + throw JsonParseException(parser, "Invalid public key $value: ${e.message}") } } } diff --git a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt index 14889e536d..26d2ef75c4 100644 --- a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt @@ -5,7 +5,11 @@ import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.Amount import net.corda.core.cordapp.CordappProvider -import net.corda.core.crypto.* +import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.SignatureMetadata +import net.corda.core.crypto.TransactionSignature import net.corda.core.identity.CordaX500Name import net.corda.core.node.ServiceHub import net.corda.core.transactions.SignedTransaction @@ -13,10 +17,12 @@ import net.corda.finance.USD import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity import net.corda.testing.internal.rigorousMock +import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Rule import org.junit.Test @@ -30,6 +36,7 @@ class JacksonSupportTest { val SEED = BigInteger.valueOf(20170922L)!! val mapper = JacksonSupport.createNonRpcMapper() val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).publicKey + val BOB_PUBKEY = TestIdentity(BOB_NAME, 70).publicKey val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party } @@ -99,26 +106,27 @@ class JacksonSupportTest { } @Test - fun writeTransaction() { + fun `wire transaction can be serialized and de-serialized`() { val attachmentRef = SecureHash.randomSHA256() doReturn(attachmentRef).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID) doReturn(testNetworkParameters()).whenever(services).networkParameters - fun makeDummyTx(): SignedTransaction { - val wtx = DummyContract.generateInitial(1, DUMMY_NOTARY, MINI_CORP.ref(1)) - .toWireTransaction(services) - val signatures = TransactionSignature( - ByteArray(1), - ALICE_PUBKEY, - SignatureMetadata( - 1, - Crypto.findSignatureScheme(ALICE_PUBKEY).schemeNumberID - ) - ) - return SignedTransaction(wtx, listOf(signatures)) - } val writer = mapper.writer() - // We don't particularly care about the serialized format, just need to make sure it completes successfully. - writer.writeValueAsString(makeDummyTx()) + val transaction = makeDummyTx() + val json = writer.writeValueAsString(transaction) + + val deserializedTransaction = mapper.readValue(json, SignedTransaction::class.java) + + assertThat(deserializedTransaction).isEqualTo(transaction) + } + + private fun makeDummyTx(): SignedTransaction { + val wtx = DummyContract.generateInitial(1, DUMMY_NOTARY, MINI_CORP.ref(1)) + .toWireTransaction(services) + val signatures = listOf( + TransactionSignature(ByteArray(1), ALICE_PUBKEY, SignatureMetadata(1, Crypto.findSignatureScheme(ALICE_PUBKEY).schemeNumberID)), + TransactionSignature(ByteArray(1), BOB_PUBKEY, SignatureMetadata(1, Crypto.findSignatureScheme(BOB_PUBKEY).schemeNumberID)) + ) + return SignedTransaction(wtx, signatures) } } diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 4deb351720..118dcbc475 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -8,6 +8,8 @@ Unreleased ========== * Fixed an error thrown by NodeVaultService upon recording a transaction with a number of inputs greater than the default page size. +* ``SignedTransaction`` can now be serialized to JSON and deserialized back into an object. + * Fixed incorrect computation of ``totalStates`` from ``otherResults`` in ``NodeVaultService``. * Refactor AMQP Serializer to pass context object down the serialization call hierarchy. Will allow per thread From 3f21c47f39ae6899fc93145741bdf5f4e00afca1 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Wed, 9 May 2018 20:20:43 +0100 Subject: [PATCH 2/4] Remove lingering Kryo reference from AMQP. (#3107) --- .../internal/serialization/amqp/DeserializationInput.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt index 3b1686fbbb..edbf8843cf 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt @@ -1,15 +1,11 @@ package net.corda.nodeapi.internal.serialization.amqp -import com.esotericsoftware.kryo.io.ByteBufferInputStream import net.corda.core.internal.VisibleForTesting import net.corda.core.serialization.EncodingWhitelist import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializedBytes import net.corda.core.utilities.ByteSequence -import net.corda.nodeapi.internal.serialization.CordaSerializationEncoding -import net.corda.nodeapi.internal.serialization.NullEncodingWhitelist -import net.corda.nodeapi.internal.serialization.SectionId -import net.corda.nodeapi.internal.serialization.encodingNotPermittedFormat +import net.corda.nodeapi.internal.serialization.* import org.apache.qpid.proton.amqp.Binary import org.apache.qpid.proton.amqp.DescribedType import org.apache.qpid.proton.amqp.UnsignedByte From 3bb95c3ed1ea49fdfd97c6ee0425186681243b20 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Wed, 9 May 2018 21:42:55 +0100 Subject: [PATCH 3/4] Cleanup and improvements to the serialisation format of JacksonSupport (needed for CORDA-1238) (#3102) Also deprecated all the public members that shouldn't have leaked into the public API. --- .ci/api-current.txt | 16 +- build.gradle | 1 - .../corda/client/jackson/JacksonSupport.kt | 343 ++++++++++-------- .../client/jackson/internal/JacksonUtils.kt | 21 ++ .../client/jackson/JacksonSupportTest.kt | 261 ++++++++++--- docs/source/changelog.rst | 14 +- docs/source/shell.rst | 44 ++- .../net/corda/irs/web/simulation/trade.json | 4 +- .../testing/internal/TestNodeInfoBuilder.kt | 2 +- tools/shell/build.gradle | 1 - .../net/corda/tools/shell/InteractiveShell.kt | 51 +-- .../corda/tools/shell/SerializationSupport.kt | 10 - .../tools/shell/CustomTypeJsonParsingTests.kt | 1 - 13 files changed, 488 insertions(+), 281 deletions(-) create mode 100644 client/jackson/src/main/kotlin/net/corda/client/jackson/internal/JacksonUtils.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index abe1f3254a..f0f7b9d2e3 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -3618,9 +3618,9 @@ public final class net.corda.client.jackson.JacksonSupport extends java.lang.Obj @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createDefaultMapper(net.corda.core.messaging.CordaRPCOps) @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createDefaultMapper(net.corda.core.messaging.CordaRPCOps, com.fasterxml.jackson.core.JsonFactory) @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createDefaultMapper(net.corda.core.messaging.CordaRPCOps, com.fasterxml.jackson.core.JsonFactory, boolean) - @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createInMemoryMapper(net.corda.core.node.services.IdentityService) - @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createInMemoryMapper(net.corda.core.node.services.IdentityService, com.fasterxml.jackson.core.JsonFactory) - @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createInMemoryMapper(net.corda.core.node.services.IdentityService, com.fasterxml.jackson.core.JsonFactory, boolean) + @kotlin.Deprecated @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createInMemoryMapper(net.corda.core.node.services.IdentityService) + @kotlin.Deprecated @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createInMemoryMapper(net.corda.core.node.services.IdentityService, com.fasterxml.jackson.core.JsonFactory) + @kotlin.Deprecated @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createInMemoryMapper(net.corda.core.node.services.IdentityService, com.fasterxml.jackson.core.JsonFactory, boolean) @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createNonRpcMapper() @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createNonRpcMapper(com.fasterxml.jackson.core.JsonFactory) @org.jetbrains.annotations.NotNull public final com.fasterxml.jackson.databind.Module getCordaModule() @@ -3650,7 +3650,7 @@ public static final class net.corda.client.jackson.JacksonSupport$CordaX500NameS public void serialize(net.corda.core.identity.CordaX500Name, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) public static final net.corda.client.jackson.JacksonSupport$CordaX500NameSerializer INSTANCE ## -public static final class net.corda.client.jackson.JacksonSupport$IdentityObjectMapper extends com.fasterxml.jackson.databind.ObjectMapper implements net.corda.client.jackson.JacksonSupport$PartyObjectMapper +@net.corda.core.DoNotImplement public static final class net.corda.client.jackson.JacksonSupport$IdentityObjectMapper extends com.fasterxml.jackson.databind.ObjectMapper implements net.corda.client.jackson.JacksonSupport$PartyObjectMapper public (net.corda.core.node.services.IdentityService, com.fasterxml.jackson.core.JsonFactory, boolean) public final boolean getFuzzyIdentityMatch() @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.IdentityService getIdentityService() @@ -3658,9 +3658,9 @@ public static final class net.corda.client.jackson.JacksonSupport$IdentityObject @org.jetbrains.annotations.Nullable public net.corda.core.identity.Party partyFromKey(java.security.PublicKey) @org.jetbrains.annotations.Nullable public net.corda.core.identity.Party wellKnownPartyFromX500Name(net.corda.core.identity.CordaX500Name) ## -public static final class net.corda.client.jackson.JacksonSupport$NoPartyObjectMapper extends com.fasterxml.jackson.databind.ObjectMapper implements net.corda.client.jackson.JacksonSupport$PartyObjectMapper +@net.corda.core.DoNotImplement public static final class net.corda.client.jackson.JacksonSupport$NoPartyObjectMapper extends com.fasterxml.jackson.databind.ObjectMapper implements net.corda.client.jackson.JacksonSupport$PartyObjectMapper public (com.fasterxml.jackson.core.JsonFactory) - @org.jetbrains.annotations.NotNull public Void partiesFromName(String) + @org.jetbrains.annotations.NotNull public Set partiesFromName(String) @org.jetbrains.annotations.Nullable public net.corda.core.identity.Party partyFromKey(java.security.PublicKey) @org.jetbrains.annotations.Nullable public net.corda.core.identity.Party wellKnownPartyFromX500Name(net.corda.core.identity.CordaX500Name) ## @@ -3684,7 +3684,7 @@ public static final class net.corda.client.jackson.JacksonSupport$PartyDeseriali @org.jetbrains.annotations.NotNull public net.corda.core.identity.Party deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) public static final net.corda.client.jackson.JacksonSupport$PartyDeserializer INSTANCE ## -public static interface net.corda.client.jackson.JacksonSupport$PartyObjectMapper +@net.corda.core.DoNotImplement public static interface net.corda.client.jackson.JacksonSupport$PartyObjectMapper @org.jetbrains.annotations.NotNull public abstract Set partiesFromName(String) @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party partyFromKey(java.security.PublicKey) @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party wellKnownPartyFromX500Name(net.corda.core.identity.CordaX500Name) @@ -3701,7 +3701,7 @@ public static final class net.corda.client.jackson.JacksonSupport$PublicKeySeria public void serialize(java.security.PublicKey, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) public static final net.corda.client.jackson.JacksonSupport$PublicKeySerializer INSTANCE ## -public static final class net.corda.client.jackson.JacksonSupport$RpcObjectMapper extends com.fasterxml.jackson.databind.ObjectMapper implements net.corda.client.jackson.JacksonSupport$PartyObjectMapper +@net.corda.core.DoNotImplement public static final class net.corda.client.jackson.JacksonSupport$RpcObjectMapper extends com.fasterxml.jackson.databind.ObjectMapper implements net.corda.client.jackson.JacksonSupport$PartyObjectMapper public (net.corda.core.messaging.CordaRPCOps, com.fasterxml.jackson.core.JsonFactory, boolean) public final boolean getFuzzyIdentityMatch() @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.CordaRPCOps getRpc() diff --git a/build.gradle b/build.gradle index 4476afa75d..4c998cce77 100644 --- a/build.gradle +++ b/build.gradle @@ -40,7 +40,6 @@ buildscript { ext.jackson_version = '2.9.3' ext.jetty_version = '9.4.7.v20170914' ext.jersey_version = '2.25' - ext.json_version = '20180130' ext.assertj_version = '3.8.0' ext.slf4j_version = '1.7.25' ext.log4j_version = '2.9.1' diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt index a9515a7522..04294c4043 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt @@ -2,56 +2,42 @@ package net.corda.client.jackson import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.core.JsonFactory -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.core.JsonParseException -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.core.JsonToken -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.databind.JsonDeserializer -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.JsonSerializer -import com.fasterxml.jackson.databind.Module -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.SerializationFeature -import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.core.* +import com.fasterxml.jackson.databind.* import com.fasterxml.jackson.databind.deser.std.NumberDeserializers import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.databind.ser.std.StdSerializer import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.fasterxml.jackson.module.kotlin.convertValue +import net.corda.client.jackson.internal.addSerAndDeser +import net.corda.client.jackson.internal.jsonObject +import net.corda.client.jackson.internal.readValueAs +import net.corda.core.CordaInternal +import net.corda.core.DoNotImplement import net.corda.core.contracts.Amount import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateRef -import net.corda.core.crypto.AddressFormatException -import net.corda.core.crypto.Base58 -import net.corda.core.crypto.Crypto -import net.corda.core.crypto.MerkleTree -import net.corda.core.crypto.PartialMerkleTree -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.SignatureMetadata +import net.corda.core.crypto.* import net.corda.core.crypto.TransactionSignature -import net.corda.core.crypto.toStringShort -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.AnonymousParty -import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.Party +import net.corda.core.identity.* +import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.NodeInfo import net.corda.core.node.services.IdentityService import net.corda.core.serialization.SerializedBytes -import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.base64ToByteArray -import net.corda.core.utilities.toBase64 +import net.corda.core.utilities.parsePublicKeyBase58 +import net.corda.core.utilities.toBase58String import java.math.BigDecimal import java.security.PublicKey import java.util.* @@ -62,73 +48,68 @@ import java.util.* * * Note that Jackson can also be used to serialise/deserialise other formats such as Yaml and XML. */ +@Suppress("DEPRECATION") object JacksonSupport { - // TODO: This API could use some tidying up - there should really only need to be one kind of mapper. // If you change this API please update the docs in the docsite (json.rst) + @DoNotImplement interface PartyObjectMapper { fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? fun partyFromKey(owningKey: PublicKey): Party? fun partiesFromName(query: String): Set + fun nodeInfoFromParty(party: AbstractParty): NodeInfo? } - class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory, val fuzzyIdentityMatch: Boolean) : PartyObjectMapper, ObjectMapper(factory) { + @Deprecated("This is an internal class, do not use", replaceWith = ReplaceWith("JacksonSupport.createDefaultMapper")) + class RpcObjectMapper(val rpc: CordaRPCOps, + factory: JsonFactory, + val fuzzyIdentityMatch: Boolean) : PartyObjectMapper, ObjectMapper(factory) { override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = rpc.wellKnownPartyFromX500Name(name) override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey) override fun partiesFromName(query: String) = rpc.partiesFromName(query, fuzzyIdentityMatch) + override fun nodeInfoFromParty(party: AbstractParty): NodeInfo? = rpc.nodeInfoFromParty(party) } - class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory, val fuzzyIdentityMatch: Boolean) : PartyObjectMapper, ObjectMapper(factory) { + @Deprecated("This is an internal class, do not use") + class IdentityObjectMapper(val identityService: IdentityService, + factory: JsonFactory, + val fuzzyIdentityMatch: Boolean) : PartyObjectMapper, ObjectMapper(factory) { override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = identityService.wellKnownPartyFromX500Name(name) override fun partyFromKey(owningKey: PublicKey): Party? = identityService.partyFromKey(owningKey) override fun partiesFromName(query: String) = identityService.partiesFromName(query, fuzzyIdentityMatch) + override fun nodeInfoFromParty(party: AbstractParty): NodeInfo? = null } + @Deprecated("This is an internal class, do not use", replaceWith = ReplaceWith("JacksonSupport.createNonRpcMapper")) class NoPartyObjectMapper(factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) { - override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = throw UnsupportedOperationException() - override fun partyFromKey(owningKey: PublicKey): Party? = throw UnsupportedOperationException() - override fun partiesFromName(query: String) = throw UnsupportedOperationException() + override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = null + override fun partyFromKey(owningKey: PublicKey): Party? = null + override fun partiesFromName(query: String): Set = emptySet() + override fun nodeInfoFromParty(party: AbstractParty): NodeInfo? = null } val cordaModule: Module by lazy { SimpleModule("core").apply { - addSerializer(AnonymousParty::class.java, AnonymousPartySerializer) - addDeserializer(AnonymousParty::class.java, AnonymousPartyDeserializer) - addSerializer(Party::class.java, PartySerializer) - addDeserializer(Party::class.java, PartyDeserializer) + addSerAndDeser(AnonymousPartySerializer, AnonymousPartyDeserializer) + addSerAndDeser(PartySerializer, PartyDeserializer) addDeserializer(AbstractParty::class.java, PartyDeserializer) - addSerializer(BigDecimal::class.java, ToStringSerializer) - addDeserializer(BigDecimal::class.java, NumberDeserializers.BigDecimalDeserializer()) - addSerializer(SecureHash::class.java, SecureHashSerializer) - addSerializer(SecureHash.SHA256::class.java, SecureHashSerializer) - addDeserializer(SecureHash::class.java, SecureHashDeserializer()) - addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer()) - - // Public key types - addSerializer(PublicKey::class.java, PublicKeySerializer) - addDeserializer(PublicKey::class.java, PublicKeyDeserializer) - - // For NodeInfo - // TODO this tunnels the Kryo representation as a Base58 encoded string. Replace when RPC supports this. - addSerializer(NodeInfo::class.java, NodeInfoSerializer) + addSerAndDeser(toStringSerializer, NumberDeserializers.BigDecimalDeserializer()) + addSerAndDeser(toStringSerializer, SecureHashDeserializer()) + addSerAndDeser(toStringSerializer, AmountDeserializer) + addSerAndDeser(OpaqueBytesSerializer, OpaqueBytesDeserializer) + addSerAndDeser(toStringSerializer, CordaX500NameDeserializer) + addSerAndDeser(PublicKeySerializer, PublicKeyDeserializer) + addDeserializer(CompositeKey::class.java, CompositeKeyDeseriaizer) + addSerAndDeser(toStringSerializer, NetworkHostAndPortDeserializer) + // TODO Add deserialization which follows the same lookup logic as Party + addSerializer(PartyAndCertificate::class.java, PartyAndCertificateSerializer) addDeserializer(NodeInfo::class.java, NodeInfoDeserializer) - // For Amount - addSerializer(Amount::class.java, AmountSerializer) - addDeserializer(Amount::class.java, AmountDeserializer) - - // For OpaqueBytes - addDeserializer(OpaqueBytes::class.java, OpaqueBytesDeserializer) - addSerializer(OpaqueBytes::class.java, OpaqueBytesSerializer) - listOf(TransactionSignatureSerde, SignedTransactionSerde).forEach { serde -> serde.applyTo(this) } - // For X.500 distinguished names - addDeserializer(CordaX500Name::class.java, CordaX500NameDeserializer) - addSerializer(CordaX500Name::class.java, CordaX500NameSerializer) - - // Mixins for transaction types to prevent some properties from being serialized + // Using mixins to fine-tune the default serialised output setMixInAnnotation(WireTransaction::class.java, WireTransactionMixin::class.java) + setMixInAnnotation(NodeInfo::class.java, NodeInfoMixin::class.java) } } @@ -141,8 +122,11 @@ object JacksonSupport { */ @JvmStatic @JvmOverloads - fun createDefaultMapper(rpc: CordaRPCOps, factory: JsonFactory = JsonFactory(), - fuzzyIdentityMatch: Boolean = false): ObjectMapper = configureMapper(RpcObjectMapper(rpc, factory, fuzzyIdentityMatch)) + fun createDefaultMapper(rpc: CordaRPCOps, + factory: JsonFactory = JsonFactory(), + fuzzyIdentityMatch: Boolean = false): ObjectMapper { + return configureMapper(RpcObjectMapper(rpc, factory, fuzzyIdentityMatch)) + } /** For testing or situations where deserialising parties is not required */ @JvmStatic @@ -156,19 +140,71 @@ object JacksonSupport { * match an identity known from the network map. If true, the name is matched more leniently but if the match * is ambiguous a [JsonParseException] is thrown. */ + @Deprecated("This is an internal method, do not use") @JvmStatic @JvmOverloads - fun createInMemoryMapper(identityService: IdentityService, factory: JsonFactory = JsonFactory(), - fuzzyIdentityMatch: Boolean = false) = configureMapper(IdentityObjectMapper(identityService, factory, fuzzyIdentityMatch)) + fun createInMemoryMapper(identityService: IdentityService, + factory: JsonFactory = JsonFactory(), + fuzzyIdentityMatch: Boolean = false): ObjectMapper { + return configureMapper(IdentityObjectMapper(identityService, factory, fuzzyIdentityMatch)) + } - private fun configureMapper(mapper: ObjectMapper): ObjectMapper = mapper.apply { - enable(SerializationFeature.INDENT_OUTPUT) - enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) - enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS) + @CordaInternal + @VisibleForTesting + internal fun createPartyObjectMapper(partyObjectMapper: PartyObjectMapper, factory: JsonFactory = JsonFactory()): ObjectMapper { + val mapper = object : ObjectMapper(factory), PartyObjectMapper by partyObjectMapper {} + return configureMapper(mapper) + } - registerModule(JavaTimeModule()) - registerModule(cordaModule) - registerModule(KotlinModule()) + private fun configureMapper(mapper: ObjectMapper): ObjectMapper { + return mapper.apply { + enable(SerializationFeature.INDENT_OUTPUT) + enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS) + disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + + registerModule(JavaTimeModule().apply { + addSerializer(Date::class.java, DateSerializer) + }) + registerModule(cordaModule) + registerModule(KotlinModule()) + } + } + + private val toStringSerializer = com.fasterxml.jackson.databind.ser.std.ToStringSerializer.instance + + private object DateSerializer : JsonSerializer() { + override fun serialize(value: Date, gen: JsonGenerator, serializers: SerializerProvider) { + gen.writeObject(value.toInstant()) + } + } + + private object NetworkHostAndPortDeserializer : JsonDeserializer() { + override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): NetworkHostAndPort { + return NetworkHostAndPort.parse(parser.text) + } + } + + private object CompositeKeyDeseriaizer : JsonDeserializer() { + override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): CompositeKey { + val publicKey = parser.readValueAs() + return publicKey as? CompositeKey ?: throw JsonParseException(parser, "Not a CompositeKey: $publicKey") + } + } + + private object PartyAndCertificateSerializer : JsonSerializer() { + override fun serialize(value: PartyAndCertificate, gen: JsonGenerator, serializers: SerializerProvider) { + gen.jsonObject { + writeObjectField("name", value.name) + writeObjectField("owningKey", value.owningKey) + // TODO Add configurable option to output the certPath + } + } + } + + @Suppress("unused") + private interface NodeInfoMixin { + @get:JsonIgnore val legalIdentities: Any // This is already covered by legalIdentitiesAndCerts } private interface JsonSerde { @@ -185,7 +221,6 @@ object JacksonSupport { } private inline fun JsonNode.get(fieldName: String, condition: (JsonNode) -> Boolean, mapper: ObjectMapper, parser: JsonParser): RESULT { - if (get(fieldName)?.let(condition) != true) { JsonParseException(parser, "Missing required object field \"$fieldName\".") } @@ -196,14 +231,12 @@ object JacksonSupport { override val type: Class = TransactionSignature::class.java override val serializer = object : StdSerializer(type) { - override fun serialize(value: TransactionSignature, json: JsonGenerator, serializers: SerializerProvider) { - with(json) { - writeStartObject() + override fun serialize(value: TransactionSignature, gen: JsonGenerator, serializers: SerializerProvider) { + gen.jsonObject { writeObjectField("by", value.by) writeObjectField("signatureMetadata", value.signatureMetadata) writeObjectField("bytes", value.bytes) writeObjectField("partialMerkleTree", value.partialMerkleTree) - writeEndObject() } } } @@ -212,11 +245,7 @@ object JacksonSupport { override fun deserialize(parser: JsonParser, context: DeserializationContext): TransactionSignature { val mapper = parser.codec as ObjectMapper val json = mapper.readTree(parser) - - if (json.get("by")?.isTextual != true) { - JsonParseException(parser, "Missing required text field \"by\".") - } - val by = PublicKeyDeserializer.deserializeValue(json.get("by").textValue()) + val by = mapper.convertValue(json["by"]) val signatureMetadata = json.get("signatureMetadata", JsonNode::isObject, mapper, parser) val bytes = json.get("bytes", JsonNode::isObject, mapper, parser) val partialMerkleTree = json.get("partialMerkleTree", JsonNode::isObject, mapper, parser) @@ -230,12 +259,10 @@ object JacksonSupport { override val type: Class = SignedTransaction::class.java override val serializer = object : StdSerializer(type) { - override fun serialize(value: SignedTransaction, json: JsonGenerator, serializers: SerializerProvider) { - with(json) { - writeStartObject() + override fun serialize(value: SignedTransaction, gen: JsonGenerator, serializers: SerializerProvider) { + gen.jsonObject { writeObjectField("txBits", value.txBits.bytes) writeObjectField("signatures", value.sigs) - writeEndObject() } } } @@ -255,110 +282,106 @@ object JacksonSupport { private class TransactionSignatures : ArrayList() } + + + // + // The following should not have been made public and are thus deprecated with warnings. + // + + @Deprecated("No longer used as jackson already has a toString serializer", + replaceWith = ReplaceWith("com.fasterxml.jackson.databind.ser.std.ToStringSerializer.instance")) object ToStringSerializer : JsonSerializer() { override fun serialize(obj: Any, generator: JsonGenerator, provider: SerializerProvider) { generator.writeString(obj.toString()) } } + @Deprecated("This is an internal class, do not use") object AnonymousPartySerializer : JsonSerializer() { - override fun serialize(obj: AnonymousParty, generator: JsonGenerator, provider: SerializerProvider) { - PublicKeySerializer.serialize(obj.owningKey, generator, provider) + override fun serialize(value: AnonymousParty, generator: JsonGenerator, provider: SerializerProvider) { + generator.writeObject(value.owningKey) } } + @Deprecated("This is an internal class, do not use") object AnonymousPartyDeserializer : JsonDeserializer() { override fun deserialize(parser: JsonParser, context: DeserializationContext): AnonymousParty { - if (parser.currentToken == JsonToken.FIELD_NAME) { - parser.nextToken() - } - - val key = PublicKeyDeserializer.deserialize(parser, context) - return AnonymousParty(key) + return AnonymousParty(parser.readValueAs(PublicKey::class.java)) } } + @Deprecated("This is an internal class, do not use") object PartySerializer : JsonSerializer() { - override fun serialize(obj: Party, generator: JsonGenerator, provider: SerializerProvider) { - generator.writeString(obj.name.toString()) + override fun serialize(value: Party, generator: JsonGenerator, provider: SerializerProvider) { + // TODO Add configurable option to output this as an object which includes the owningKey + generator.writeObject(value.name) } } + @Deprecated("This is an internal class, do not use") object PartyDeserializer : JsonDeserializer() { override fun deserialize(parser: JsonParser, context: DeserializationContext): Party { - if (parser.currentToken == JsonToken.FIELD_NAME) { - parser.nextToken() - } - val mapper = parser.codec as PartyObjectMapper - // The comma character is invalid in base64, and required as a separator for X.500 names. As Corda + // The comma character is invalid in Base58, and required as a separator for X.500 names. As Corda // X.500 names all involve at least three attributes (organisation, locality, country), they must // include a comma. As such we can use it as a distinguisher between the two types. - return if (parser.text.contains(",")) { + return if ("," in parser.text) { val principal = CordaX500Name.parse(parser.text) mapper.wellKnownPartyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name $principal") } else { val nameMatches = mapper.partiesFromName(parser.text) if (nameMatches.isEmpty()) { - val derBytes = try { - parser.text.base64ToByteArray() - } catch (e: AddressFormatException) { - throw JsonParseException(parser, "Could not find a matching party for '${parser.text}' and is not a base64 encoded public key: " + e.message) - } - val key = try { - Crypto.decodePublicKey(derBytes) - } catch (e: Exception) { - throw JsonParseException(parser, "Could not find a matching party for '${parser.text}' and is not a valid public key: " + e.message) - } - mapper.partyFromKey(key) ?: throw JsonParseException(parser, "Could not find a Party with key ${key.toStringShort()}") + val publicKey = parser.readValueAs() + mapper.partyFromKey(publicKey) + ?: throw JsonParseException(parser, "Could not find a Party with key ${publicKey.toStringShort()}") } else if (nameMatches.size == 1) { nameMatches.first() } else { - throw JsonParseException(parser, "Ambiguous name match '${parser.text}': could be any of " + nameMatches.map { it.name }.joinToString(" ... or ...")) + throw JsonParseException(parser, "Ambiguous name match '${parser.text}': could be any of " + + nameMatches.map { it.name }.joinToString(" ... or ... ")) } } } } + @Deprecated("This is an internal class, do not use") + // This is no longer used object CordaX500NameSerializer : JsonSerializer() { override fun serialize(obj: CordaX500Name, generator: JsonGenerator, provider: SerializerProvider) { generator.writeString(obj.toString()) } } + @Deprecated("This is an internal class, do not use") object CordaX500NameDeserializer : JsonDeserializer() { override fun deserialize(parser: JsonParser, context: DeserializationContext): CordaX500Name { - if (parser.currentToken == JsonToken.FIELD_NAME) { - parser.nextToken() - } - return try { CordaX500Name.parse(parser.text) - } catch (ex: IllegalArgumentException) { - throw JsonParseException(parser, "Invalid Corda X.500 name ${parser.text}: ${ex.message}", ex) + } catch (e: IllegalArgumentException) { + throw JsonParseException(parser, "Invalid Corda X.500 name ${parser.text}: ${e.message}", e) } } } + @Deprecated("This is an internal class, do not use") + // This is no longer used object NodeInfoSerializer : JsonSerializer() { override fun serialize(value: NodeInfo, gen: JsonGenerator, serializers: SerializerProvider) { gen.writeString(Base58.encode(value.serialize().bytes)) } } + @Deprecated("This is an internal class, do not use") object NodeInfoDeserializer : JsonDeserializer() { override fun deserialize(parser: JsonParser, context: DeserializationContext): NodeInfo { - if (parser.currentToken == JsonToken.FIELD_NAME) { - parser.nextToken() - } - try { - return Base58.decode(parser.text).deserialize() - } catch (e: Exception) { - throw JsonParseException(parser, "Invalid NodeInfo ${parser.text}: ${e.message}") - } + val mapper = parser.codec as PartyObjectMapper + val party = parser.readValueAs() + return mapper.nodeInfoFromParty(party) ?: throw JsonParseException(parser, "Cannot find node with $party") } } + @Deprecated("This is an internal class, do not use") + // This is no longer used object SecureHashSerializer : JsonSerializer() { override fun serialize(obj: SecureHash, generator: JsonGenerator, provider: SerializerProvider) { generator.writeString(obj.toString()) @@ -368,11 +391,9 @@ object JacksonSupport { /** * Implemented as a class so that we can instantiate for T. */ + @Deprecated("This is an internal class, do not use") class SecureHashDeserializer : JsonDeserializer() { override fun deserialize(parser: JsonParser, context: DeserializationContext): T { - if (parser.currentToken == JsonToken.FIELD_NAME) { - parser.nextToken() - } try { return uncheckedCast(SecureHash.parse(parser.text)) } catch (e: Exception) { @@ -381,69 +402,72 @@ object JacksonSupport { } } + @Deprecated("This is an internal class, do not use") object PublicKeySerializer : JsonSerializer() { - override fun serialize(obj: PublicKey, generator: JsonGenerator, provider: SerializerProvider) { - generator.writeString(obj.encoded.toBase64()) + override fun serialize(value: PublicKey, generator: JsonGenerator, provider: SerializerProvider) { + generator.writeString(value.toBase58String()) } } + @Deprecated("This is an internal class, do not use") object PublicKeyDeserializer : JsonDeserializer() { override fun deserialize(parser: JsonParser, context: DeserializationContext): PublicKey { - return deserializeValue(parser.text, parser) - } - - internal fun deserializeValue(value: String, parser: JsonParser? = null): PublicKey { return try { - val derBytes = value.base64ToByteArray() - Crypto.decodePublicKey(derBytes) + parsePublicKeyBase58(parser.text) } catch (e: Exception) { - throw JsonParseException(parser, "Invalid public key $value: ${e.message}") + throw JsonParseException(parser, "Invalid public key ${parser.text}: ${e.message}") } } } + @Deprecated("This is an internal class, do not use") + // This is no longer used object AmountSerializer : JsonSerializer>() { override fun serialize(value: Amount<*>, gen: JsonGenerator, serializers: SerializerProvider) { gen.writeString(value.toString()) } } + @Deprecated("This is an internal class, do not use") object AmountDeserializer : JsonDeserializer>() { override fun deserialize(parser: JsonParser, context: DeserializationContext): Amount<*> { - try { - return Amount.parseCurrency(parser.text) - } catch (e: Exception) { + return if (parser.currentToken == JsonToken.VALUE_STRING) { + Amount.parseCurrency(parser.text) + } else { try { - val tree = parser.readValueAsTree() - require(tree["quantity"].canConvertToLong() && tree["token"].asText().isNotBlank()) - val quantity = tree["quantity"].asLong() - val token = tree["token"].asText() + val tree = parser.readValueAsTree() + val quantity = tree["quantity"].apply { require(canConvertToLong()) } + val token = tree["token"] // Attempt parsing as a currency token. TODO: This needs thought about how to extend to other token types. - val currency = Currency.getInstance(token) - return Amount(quantity, currency) - } catch (e2: Exception) { - throw JsonParseException(parser, "Invalid amount ${parser.text}", e2) + val currency = (parser.codec as ObjectMapper).convertValue(token) + Amount(quantity.longValue(), currency) + } catch (e: Exception) { + throw JsonParseException(parser, "Invalid amount", e) } } } } + @Deprecated("This is an internal class, do not use") object OpaqueBytesDeserializer : JsonDeserializer() { override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): OpaqueBytes { - return OpaqueBytes(parser.text.toByteArray()) + return OpaqueBytes(parser.binaryValue) } } + @Deprecated("This is an internal class, do not use") object OpaqueBytesSerializer : JsonSerializer() { override fun serialize(value: OpaqueBytes, gen: JsonGenerator, serializers: SerializerProvider) { gen.writeBinary(value.bytes) } } + @Deprecated("This is an internal class, do not use") + @Suppress("unused") abstract class SignedTransactionMixin { @JsonIgnore abstract fun getTxBits(): SerializedBytes @JsonProperty("signatures") protected abstract fun getSigs(): List - @JsonProperty protected abstract fun getTransaction(): CoreTransaction + @JsonProperty protected abstract fun getTransaction(): CoreTransaction // TODO It seems this should be coreTransaction @JsonIgnore abstract fun getTx(): WireTransaction @JsonIgnore abstract fun getNotaryChangeTx(): NotaryChangeWireTransaction @JsonIgnore abstract fun getInputs(): List @@ -452,6 +476,8 @@ object JacksonSupport { @JsonIgnore abstract fun getRequiredSigningKeys(): Set } + @Deprecated("This is an internal class, do not use") + @Suppress("unused") abstract class WireTransactionMixin { @JsonIgnore abstract fun getMerkleTree(): MerkleTree @JsonIgnore abstract fun getAvailableComponents(): List @@ -459,4 +485,3 @@ object JacksonSupport { @JsonIgnore abstract fun getOutputStates(): List } } - diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/JacksonUtils.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/JacksonUtils.kt new file mode 100644 index 0000000000..04284557e9 --- /dev/null +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/JacksonUtils.kt @@ -0,0 +1,21 @@ +package net.corda.client.jackson.internal + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.module.SimpleModule + +inline fun SimpleModule.addSerAndDeser(serializer: JsonSerializer, deserializer: JsonDeserializer) { + addSerializer(T::class.java, serializer) + addDeserializer(T::class.java, deserializer) +} + +inline fun JsonGenerator.jsonObject(fieldName: String? = null, gen: JsonGenerator.() -> Unit) { + fieldName?.let { writeFieldName(it) } + writeStartObject() + gen() + writeEndObject() +} + +inline fun JsonParser.readValueAs(): T = readValueAs(T::class.java) diff --git a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt index 26d2ef75c4..196e610164 100644 --- a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt @@ -1,49 +1,61 @@ package net.corda.client.jackson import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.databind.node.ArrayNode +import com.fasterxml.jackson.databind.node.BinaryNode +import com.fasterxml.jackson.databind.node.ObjectNode +import com.fasterxml.jackson.databind.node.TextNode +import com.fasterxml.jackson.module.kotlin.convertValue import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.Amount import net.corda.core.cordapp.CordappProvider +import net.corda.core.crypto.* import net.corda.core.crypto.CompositeKey -import net.corda.core.crypto.Crypto -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.SignatureMetadata -import net.corda.core.crypto.TransactionSignature +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.toBase58String +import net.corda.core.utilities.toBase64 import net.corda.finance.USD import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.BOB_NAME -import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.core.SerializationEnvironmentRule -import net.corda.testing.core.TestIdentity +import net.corda.testing.core.* +import net.corda.testing.internal.createNodeInfoAndSigned import net.corda.testing.internal.rigorousMock import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Before import org.junit.Rule import org.junit.Test import java.math.BigInteger import java.security.PublicKey import java.util.* +import kotlin.collections.ArrayList import kotlin.test.assertEquals class JacksonSupportTest { private companion object { - val SEED = BigInteger.valueOf(20170922L)!! - val mapper = JacksonSupport.createNonRpcMapper() + val SEED: BigInteger = BigInteger.valueOf(20170922L) val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).publicKey val BOB_PUBKEY = TestIdentity(BOB_NAME, 70).publicKey val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party - val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party + val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")) } @Rule @JvmField val testSerialization = SerializationEnvironmentRule() + + private val partyObjectMapper = TestPartyObjectMapper() + private val mapper = JacksonSupport.createPartyObjectMapper(partyObjectMapper) + private lateinit var services: ServiceHub private lateinit var cordappProvider: CordappProvider @@ -54,37 +66,10 @@ class JacksonSupportTest { doReturn(cordappProvider).whenever(services).cordappProvider } - @Test - fun `should serialize Composite keys`() { - val expected = "\"MIHAMBUGE2mtoq+J1bjir/ONk6yd5pab0FoDgaYAMIGiAgECMIGcMDIDLQAwKjAFBgMrZXADIQAgIX1QlJRgaLlD0ttLlJF5kNqT/7P7QwCvrWc9+/248gIBATAyAy0AMCowBQYDK2VwAyEAqS0JPGlzdviBZjB9FaNY+w6cVs3/CQ2A5EimE9Lyng4CAQEwMgMtADAqMAUGAytlcAMhALq4GG0gBQZIlaKE6ucooZsuoKUbH4MtGSmA6cwj136+AgEB\"" - val innerKeys = (1..3).map { i -> - Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, SEED.plus(BigInteger.valueOf(i.toLong()))).public - } - // Build a 2 of 3 composite key - val publicKey = CompositeKey.Builder().let { - innerKeys.forEach { key -> it.addKey(key, 1) } - it.build(2) - } - val serialized = mapper.writeValueAsString(publicKey) - assertEquals(expected, serialized) - val parsedKey = mapper.readValue(serialized, PublicKey::class.java) - assertEquals(publicKey, parsedKey) - } - private class Dummy(val notional: Amount) @Test - fun `should serialize EdDSA keys`() { - val expected = "\"MCowBQYDK2VwAyEACFTgLk1NOqYXAfxLoR7ctSbZcl9KMXu58Mq31Kv1Dwk=\"" - val publicKey = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, SEED).public - val serialized = mapper.writeValueAsString(publicKey) - assertEquals(expected, serialized) - val parsedKey = mapper.readValue(serialized, PublicKey::class.java) - assertEquals(publicKey, parsedKey) - } - - @Test - fun readAmount() { + fun `read Amount`() { val oldJson = """ { "notional": { @@ -100,27 +85,186 @@ class JacksonSupportTest { } @Test - fun writeAmount() { + fun `write Amount`() { val writer = mapper.writer().without(SerializationFeature.INDENT_OUTPUT) - assertEquals("""{"notional":"25000000.00 USD"}""", writer.writeValueAsString(Dummy(Amount.parseCurrency("$25000000")))) + assertEquals("""{"notional":"25000000.00 GBP"}""", writer.writeValueAsString(Dummy(Amount.parseCurrency("£25000000")))) + assertEquals("""{"notional":"250000.00 USD"}""", writer.writeValueAsString(Dummy(Amount.parseCurrency("$250000")))) } @Test - fun `wire transaction can be serialized and de-serialized`() { + fun SignedTransaction() { val attachmentRef = SecureHash.randomSHA256() doReturn(attachmentRef).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID) doReturn(testNetworkParameters()).whenever(services).networkParameters val writer = mapper.writer() - val transaction = makeDummyTx() - val json = writer.writeValueAsString(transaction) + val stx = makeDummyStx() + val json = writer.writeValueAsString(stx) val deserializedTransaction = mapper.readValue(json, SignedTransaction::class.java) - assertThat(deserializedTransaction).isEqualTo(transaction) + assertThat(deserializedTransaction).isEqualTo(stx) } - private fun makeDummyTx(): SignedTransaction { + @Test + fun OpaqueBytes() { + val opaqueBytes = OpaqueBytes(secureRandomBytes(128)) + val json = mapper.valueToTree(opaqueBytes) + assertThat(json.binaryValue()).isEqualTo(opaqueBytes.bytes) + assertThat(json.asText()).isEqualTo(opaqueBytes.bytes.toBase64()) + assertThat(mapper.convertValue(json)).isEqualTo(opaqueBytes) + } + + @Test + fun CordaX500Name() { + testToStringSerialisation(CordaX500Name(commonName = "COMMON", organisationUnit = "ORG UNIT", organisation = "ORG", locality = "NYC", state = "NY", country = "US")) + } + + @Test + fun `SecureHash SHA256`() { + testToStringSerialisation(SecureHash.randomSHA256()) + } + + @Test + fun NetworkHostAndPort() { + testToStringSerialisation(NetworkHostAndPort("localhost", 9090)) + } + + @Test + fun UUID() { + testToStringSerialisation(UUID.randomUUID()) + } + + @Test + fun `Date is treated as Instant`() { + val date = Date() + val json = mapper.valueToTree(date) + assertThat(json.textValue()).isEqualTo(date.toInstant().toString()) + assertThat(mapper.convertValue(json)).isEqualTo(date) + } + + @Test + fun `Party serialization`() { + val json = mapper.valueToTree(MINI_CORP.party) + assertThat(json.textValue()).isEqualTo(MINI_CORP.name.toString()) + } + + @Test + fun `Party deserialization on full name`() { + fun convertToParty() = mapper.convertValue(TextNode(MINI_CORP.name.toString())) + + assertThatThrownBy { convertToParty() } + + partyObjectMapper.identities += MINI_CORP.party + assertThat(convertToParty()).isEqualTo(MINI_CORP.party) + } + + @Test + fun `Party deserialization on part of name`() { + fun convertToParty() = mapper.convertValue(TextNode(MINI_CORP.name.organisation)) + + assertThatThrownBy { convertToParty() } + + partyObjectMapper.identities += MINI_CORP.party + assertThat(convertToParty()).isEqualTo(MINI_CORP.party) + } + + @Test + fun `Party deserialization on public key`() { + fun convertToParty() = mapper.convertValue(TextNode(MINI_CORP.publicKey.toBase58String())) + + assertThatThrownBy { convertToParty() } + + partyObjectMapper.identities += MINI_CORP.party + assertThat(convertToParty()).isEqualTo(MINI_CORP.party) + } + + @Test + fun PublicKey() { + val json = mapper.valueToTree(MINI_CORP.publicKey) + assertThat(json.textValue()).isEqualTo(MINI_CORP.publicKey.toBase58String()) + assertThat(mapper.convertValue(json)).isEqualTo(MINI_CORP.publicKey) + } + + @Test + fun `EdDSA public key`() { + val publicKey = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, SEED).public + val json = mapper.valueToTree(publicKey) + assertThat(json.textValue()).isEqualTo(publicKey.toBase58String()) + assertThat(mapper.convertValue(json)).isEqualTo(publicKey) + } + + @Test + fun CompositeKey() { + val innerKeys = (1..3).map { i -> + Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, SEED + i.toBigInteger()).public + } + // Build a 2 of 3 composite key + val publicKey = CompositeKey.Builder().let { + innerKeys.forEach { key -> it.addKey(key, 1) } + it.build(2) + } + val json = mapper.valueToTree(publicKey) + assertThat(json.textValue()).isEqualTo(publicKey.toBase58String()) + assertThat(mapper.convertValue(json)).isEqualTo(publicKey) + } + + @Test + fun AnonymousParty() { + val anon = AnonymousParty(ALICE_PUBKEY) + val json = mapper.valueToTree(anon) + assertThat(json.textValue()).isEqualTo(ALICE_PUBKEY.toBase58String()) + assertThat(mapper.convertValue(json)).isEqualTo(anon) + } + + @Test + fun `PartyAndCertificate serialisation`() { + val json = mapper.valueToTree(MINI_CORP.identity) + assertThat(json.fieldNames()).containsOnly("name", "owningKey") + assertThat(mapper.convertValue(json["name"])).isEqualTo(MINI_CORP.name) + assertThat(mapper.convertValue(json["owningKey"])).isEqualTo(MINI_CORP.publicKey) + } + + @Test + fun `NodeInfo serialisation`() { + val (nodeInfo) = createNodeInfoAndSigned(ALICE_NAME) + val json = mapper.valueToTree(nodeInfo) + assertThat(json.fieldNames()).containsOnly("addresses", "legalIdentitiesAndCerts", "platformVersion", "serial") + val address = (json["addresses"] as ArrayNode).also { assertThat(it).hasSize(1) }[0] + assertThat(mapper.convertValue(address)).isEqualTo(nodeInfo.addresses[0]) + val identity = (json["legalIdentitiesAndCerts"] as ArrayNode).also { assertThat(it).hasSize(1) }[0] + assertThat(mapper.convertValue(identity["name"])).isEqualTo(ALICE_NAME) + assertThat(mapper.convertValue(json["platformVersion"])).isEqualTo(nodeInfo.platformVersion) + assertThat(mapper.convertValue(json["serial"])).isEqualTo(nodeInfo.serial) + } + + @Test + fun `NodeInfo deserialisation on name`() { + val (nodeInfo) = createNodeInfoAndSigned(ALICE_NAME) + + fun convertToNodeInfo() = mapper.convertValue(TextNode(ALICE_NAME.toString())) + + assertThatThrownBy { convertToNodeInfo() } + + partyObjectMapper.identities += nodeInfo.legalIdentities + partyObjectMapper.nodes += nodeInfo + assertThat(convertToNodeInfo()).isEqualTo(nodeInfo) + } + + @Test + fun `NodeInfo deserialisation on public key`() { + val (nodeInfo) = createNodeInfoAndSigned(ALICE_NAME) + + fun convertToNodeInfo() = mapper.convertValue(TextNode(nodeInfo.legalIdentities[0].owningKey.toBase58String())) + + assertThatThrownBy { convertToNodeInfo() } + + partyObjectMapper.identities += nodeInfo.legalIdentities + partyObjectMapper.nodes += nodeInfo + assertThat(convertToNodeInfo()).isEqualTo(nodeInfo) + } + + private fun makeDummyStx(): SignedTransaction { val wtx = DummyContract.generateInitial(1, DUMMY_NOTARY, MINI_CORP.ref(1)) .toWireTransaction(services) val signatures = listOf( @@ -129,4 +273,27 @@ class JacksonSupportTest { ) return SignedTransaction(wtx, signatures) } + + private inline fun testToStringSerialisation(value: T) { + val json = mapper.valueToTree(value) + assertThat(json.textValue()).isEqualTo(value.toString()) + assertThat(mapper.convertValue(json)).isEqualTo(value) + } + + private class TestPartyObjectMapper : JacksonSupport.PartyObjectMapper { + val identities = ArrayList() + val nodes = ArrayList() + override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? { + return identities.find { it.name == name } + } + override fun partyFromKey(owningKey: PublicKey): Party? { + return identities.find { it.owningKey == owningKey } + } + override fun partiesFromName(query: String): Set { + return identities.filter { query in it.name.toString() }.toSet() + } + override fun nodeInfoFromParty(party: AbstractParty): NodeInfo? { + return nodes.find { party in it.legalIdentities } + } + } } diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 118dcbc475..4e40d73eef 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -8,10 +8,20 @@ Unreleased ========== * Fixed an error thrown by NodeVaultService upon recording a transaction with a number of inputs greater than the default page size. -* ``SignedTransaction`` can now be serialized to JSON and deserialized back into an object. - * Fixed incorrect computation of ``totalStates`` from ``otherResults`` in ``NodeVaultService``. +* Changes to the JSON/YAML serialisation format from ``JacksonSupport``, which also applies to the node shell: + + * ``Instant`` and ``Date`` objects are serialised as ISO-8601 formatted strings rather than timestamps + * ``PublicKey`` objects are serialised and looked up according to their Base58 encoded string + * ``Party`` objects can be deserialised by looking up their public key, in addition to their name + * ``NodeInfo`` objects are serialised as an object and can be looked up using the same mechanism as ``Party`` + * ``NetworkHostAndPort`` serialised according to its ``toString()`` + * ``PartyAndCertificate`` is serialised as an object containing the name and owning key + * ``SignedTransaction`` can now be serialized to JSON and deserialized back into an object. + +* Several members of ``JacksonSupport`` have been deprecated to highlight that they are internal and not to be used + * Refactor AMQP Serializer to pass context object down the serialization call hierarchy. Will allow per thread extensions to be set and used by the RPC work (Observable Context Key) diff --git a/docs/source/shell.rst b/docs/source/shell.rst index 62c6301cae..da7bef0e8f 100644 --- a/docs/source/shell.rst +++ b/docs/source/shell.rst @@ -91,13 +91,13 @@ Windows Windows does not provide a built-in SSH tool. An alternative such as PuTTY should be used. The standalone shell ------------------------------- +-------------------- The standalone shell is a standalone application interacting with a Corda node via RPC calls. RPC node permissions are necessary for authentication and authorisation. Certain operations, such as starting flows, require access to CordApps jars. Starting the standalone shell -************************* +***************************** Run the following command from the terminal: @@ -177,7 +177,7 @@ The format of ``config-file``: Standalone Shell via SSH ------------------------------------------- +------------------------ The standalone shell can embed an SSH server which redirects interactions via RPC calls to the Corda node. To run SSH server use ``--sshd-port`` option when starting standalone shell or ``extensions.sshd`` entry in the configuration file. For connection to SSH refer to `Connecting to the shell`_. @@ -253,8 +253,8 @@ Where ``newCampaign`` is a parameter of type ``Campaign``. Mappings from strings to types ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Several parameter types can automatically be mapped from strings. See the `defined parsers`_ for more information. We -cover the most common types here. +In addition to the types already supported by Jackson, several parameter types can automatically be mapped from strings. +We cover the most common types here. Amount ~~~~~~ @@ -263,23 +263,44 @@ A parameter of type ``Amount`` can be written as either: * A dollar ($), pound (£) or euro (€) symbol followed by the amount as a decimal * The amount as a decimal followed by the ISO currency code (e.g. "100.12 CHF") +SecureHash +~~~~~~~~~~ +A parameter of type ``SecureHash`` can be written as a hexadecimal string: ``F69A7626ACC27042FEEAE187E6BFF4CE666E6F318DC2B32BE9FAF87DF687930C`` + OpaqueBytes ~~~~~~~~~~~ -A parameter of type ``OpaqueBytes`` can be provided as a string, which will be automatically converted to -``OpaqueBytes``. +A parameter of type ``OpaqueBytes`` can be provided as a string in Base64. + +PublicKey and CompositeKey +~~~~~~~~~~~~~~~~~~~~~~~~~~ +A parameter of type ``PublicKey`` can be written as a Base58 string of its encoded format: ``GfHq2tTVk9z4eXgyQXzegw6wNsZfHcDhfw8oTt6fCHySFGp3g7XHPAyc2o6D``. +``net.corda.core.utilities.EncodingUtils.toBase58String`` will convert a ``PublicKey`` to this string format. Party ~~~~~ A parameter of type ``Party`` can be written in several ways: -* By using the node's full name: ``"O=Monogram Bank,L=Sao Paulo,C=GB"`` +* By using the full name: ``"O=Monogram Bank,L=Sao Paulo,C=GB"`` * By specifying the organisation name only: ``"Monogram Bank"`` * By specifying any other non-ambiguous part of the name: ``"Sao Paulo"`` (if only one network node is located in Sao Paulo) +* By specifying the public key (see above) -Instant -~~~~~~~ -A parameter of type ``Instant`` can be written as follows: ``"2017-12-22T00:00:00Z"``. +NodeInfo +~~~~~~~~ +A parameter of type ``NodeInfo`` can be written in terms of one of its identities (see ``Party`` above) + +AnonymousParty +~~~~~~~~~~~~~~ +A parameter of type ``AnonymousParty`` can be written in terms of its ``PublicKey`` (see above) + +NetworkHostAndPort +~~~~~~~~~~~~~~~~~~ +A parameter of type ``NetworkHostAndPort`` can be written as a "host:port" string: ``"localhost:1010"`` + +Instant and Date +~~~~~~~~~~~~~~~~ +A parameter of ``Instant`` and ``Date`` can be written as an ISO-8601 string: ``"2017-12-22T00:00:00Z"`` Examples ^^^^^^^^ @@ -365,6 +386,5 @@ The shell will be enhanced over time. The currently known limitations include: * The ``jul`` command advertises access to logs, but it doesn't work with the logging framework we're using .. _Yaml: http://www.yaml.org/spec/1.2/spec.html -.. _defined parsers: api/kotlin/corda/net.corda.client.jackson/-jackson-support/index.html .. _Groovy: http://groovy-lang.org/ .. _CRaSH: http://www.crashub.org/ diff --git a/samples/network-visualiser/src/main/resources/net/corda/irs/web/simulation/trade.json b/samples/network-visualiser/src/main/resources/net/corda/irs/web/simulation/trade.json index dc4a5be379..2f3d62907c 100644 --- a/samples/network-visualiser/src/main/resources/net/corda/irs/web/simulation/trade.json +++ b/samples/network-visualiser/src/main/resources/net/corda/irs/web/simulation/trade.json @@ -1,6 +1,6 @@ { "fixedLeg": { - "fixedRatePayer": "MCowBQYDK2VwAyEAzswVB9wd3XKVlRwpCIjwla25BE0bc9aW5t8GXWg71Pw=", + "fixedRatePayer": "GfHq2tTVk9z4eXgyUEefbHpUFfpnDvsFoZVZe3ikrLbwdRA4jebSJPykJwgw", "notional": "$25000000", "paymentFrequency": "SemiAnnual", "effectiveDate": "2016-03-11", @@ -22,7 +22,7 @@ "interestPeriodAdjustment": "Adjusted" }, "floatingLeg": { - "floatingRatePayer": "MCowBQYDK2VwAyEAa3nFfmoJUjkoLASBjpYRLz8DpAAbqXpWTCOFKj8epfw=", + "floatingRatePayer": "GfHq2tTVk9z4eXgyMYwWRYKSgGpARSquPTt8V4Z54RmNe2SJ7BUq2jSUUfvT", "notional": { "quantity": 2500000000, "token": "USD" diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt index da7396192c..63194080fc 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt @@ -64,7 +64,7 @@ class TestNodeInfoBuilder(private val intermediateAndRoot: Pair() { - - override fun serialize(nodeInfo: NodeInfo, gen: JsonGenerator, serializers: SerializerProvider) { - - val json = JSONObject() - json["addresses"] = nodeInfo.addresses.map { address -> address.serialise() } - json["legalIdentities"] = nodeInfo.legalIdentities.map { address -> address.serialise() } - json["platformVersion"] = nodeInfo.platformVersion - json["serial"] = nodeInfo.serial - gen.writeRaw(json.toString()) - } - - private fun NetworkHostAndPort.serialise() = this.toString() - private fun Party.serialise() = JSONObject().put("name", this.name) - - private operator fun JSONObject.set(key: String, value: Any?): JSONObject { - return put(key, value) - } - } - private fun createOutputMapper(): ObjectMapper { - return JacksonSupport.createNonRpcMapper().apply { // Register serializers for stateful objects from libraries that are special to the RPC system and don't // make sense to print out to the screen. For classes we own, annotations can be used instead. - val rpcModule = SimpleModule() - rpcModule.addSerializer(Observable::class.java, ObservableSerializer) - rpcModule.addSerializer(InputStream::class.java, InputStreamSerializer) - rpcModule.addSerializer(NodeInfo::class.java, NodeInfoSerializer) + val rpcModule = SimpleModule().apply { + addSerializer(Observable::class.java, ObservableSerializer) + addSerializer(InputStream::class.java, InputStreamSerializer) + } registerModule(rpcModule) disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) @@ -240,7 +212,12 @@ object InteractiveShell { * the [runFlowFromString] method and starts the requested flow. Ctrl-C can be used to cancel. */ @JvmStatic - fun runFlowByNameFragment(nameFragment: String, inputData: String, output: RenderPrintWriter, rpcOps: CordaRPCOps, ansiProgressRenderer: ANSIProgressRenderer, om: ObjectMapper) { + fun runFlowByNameFragment(nameFragment: String, + inputData: String, + output: RenderPrintWriter, + rpcOps: CordaRPCOps, + ansiProgressRenderer: ANSIProgressRenderer, + om: ObjectMapper) { val matches = try { rpcOps.registeredFlows().filter { nameFragment in it } } catch (e: PermissionException) { diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/SerializationSupport.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/SerializationSupport.kt index 16504781b4..6397db484e 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/SerializationSupport.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/SerializationSupport.kt @@ -50,16 +50,6 @@ object UniqueIdentifierDeserializer : JsonDeserializer() { } } -/** - * String value deserialized to [UUID]. - * */ -object UUIDDeserializer : JsonDeserializer() { - override fun deserialize(p: JsonParser, ctxt: DeserializationContext): UUID { - //Create UUID object from string. - return UUID.fromString(p.text) - } -} - // An InputStream found in a response triggers a request to the user to provide somewhere to save it. object InputStreamSerializer : JsonSerializer() { var invokeContext: InvocationContext<*>? = null diff --git a/tools/shell/src/test/kotlin/net/corda/tools/shell/CustomTypeJsonParsingTests.kt b/tools/shell/src/test/kotlin/net/corda/tools/shell/CustomTypeJsonParsingTests.kt index e82f51ac24..ff510693e7 100644 --- a/tools/shell/src/test/kotlin/net/corda/tools/shell/CustomTypeJsonParsingTests.kt +++ b/tools/shell/src/test/kotlin/net/corda/tools/shell/CustomTypeJsonParsingTests.kt @@ -28,7 +28,6 @@ class CustomTypeJsonParsingTests { objectMapper = ObjectMapper() val simpleModule = SimpleModule() simpleModule.addDeserializer(UniqueIdentifier::class.java, UniqueIdentifierDeserializer) - simpleModule.addDeserializer(UUID::class.java, UUIDDeserializer) objectMapper.registerModule(simpleModule) } From 8419b24d80ac71d5609584cd43b4fca9ec2062e9 Mon Sep 17 00:00:00 2001 From: bpaunescu Date: Thu, 10 May 2018 09:42:48 +0100 Subject: [PATCH 4/4] CORDA-1443: re-wrote tests to ensure connection closes before node to avoid lengthy artemis timeouts (#3104) --- .../net/corda/node/services/rpc/RpcSslTest.kt | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt index 8252ef114c..c9728af7d7 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt @@ -35,14 +35,15 @@ class RpcSslTest { withKeyStores(server, client) { nodeSslOptions, clientSslOptions -> var successful = false driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) { - startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow().use { node -> - createCordaRPCClientWithSsl(node.rpcAddress, sslConfiguration = clientSslOptions).start(user.username, user.password).use { connection -> - connection.proxy.apply { - nodeInfo() - successful = true - } - } + val node = startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow() + val client = createCordaRPCClientWithSsl(node.rpcAddress, sslConfiguration = clientSslOptions) + val connection = client.start(user.username, user.password) + connection.proxy.apply { + nodeInfo() + successful = true } + + connection.close() } assertThat(successful).isTrue() } @@ -54,14 +55,15 @@ class RpcSslTest { val user = User("mark", "dadada", setOf(all())) var successful = false driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) { - startNode(rpcUsers = listOf(user)).getOrThrow().use { node -> - CordaRPCClient(node.rpcAddress).start(user.username, user.password).use { connection -> - connection.proxy.apply { - nodeInfo() - successful = true - } - } + val node = startNode(rpcUsers = listOf(user)).getOrThrow() + val client = CordaRPCClient(node.rpcAddress) + val connection = client.start(user.username, user.password) + connection.proxy.apply { + nodeInfo() + successful = true } + + connection.close() } assertThat(successful).isTrue() }