CORDA-1709 - The MVP blob inspector, able to inspect network service blobs (#3503)

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

(cherry picked from commit 3bb95c3)

* CORDA-1238: Updated JacksonSupport to support SerializedBytes, CertPath, X509Certificate and the signature classes (#3145)

SerializedBytes are first converted to the object it represents before being serialised as a pojo.

These changes will be needed to support the the blob inspector when it will output to YAML/JSON.

(cherry picked from commit b031e66)

* Cherry picked part of commit 824adca to port over *only* the JackSupport refactoring.

* CORDA-1238: Moved the blob inspector out of experimental and wired it to JackonSupport (#3224)

The existing output format was not complete and so was deleted to avoid it becoming a tech debt. We can always resurrect it at a later point.

(cherry picked from commit 4e0378d)

* Added back support for parsing OpaqueBytes as UTF-8 strings in JacksonSupport (#3240)

(cherry picked from commit d772bc8)

* Cleaned up blob inspector doc (#3284)

(cherry picked from commit b7fbebb)

* Blobinspector: trace level logging with --verbose (#3313)

(cherry picked from commit 6a2e50b)

* Cherry picked part of commit 3046843 to fix issue with --version

* Fixes to the api file
This commit is contained in:
Shams Asari
2018-07-03 19:58:13 +01:00
committed by Katelyn Baker
parent 00c9b8ce49
commit 9fc108aa1e
27 changed files with 1317 additions and 332 deletions

View File

@ -6,11 +6,8 @@ apply plugin: 'com.jfrog.artifactory'
dependencies {
compile project(':core')
testCompile project(':test-utils')
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
// Jackson and its plugins: parsing to/from JSON and other textual formats.
compile "com.fasterxml.jackson.module:jackson-module-kotlin:${jackson_version}"
// Yaml is useful for parsing strings to method calls.
@ -19,7 +16,9 @@ dependencies {
compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version"
compile "com.google.guava:guava:$guava_version"
testCompile project(':test-utils')
testCompile project(path: ':core', configuration: 'testArtifacts')
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testCompile "junit:junit:$junit_version"
}

View File

@ -4,35 +4,48 @@ 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.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.deser.std.NumberDeserializers
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.KotlinModule
import net.corda.client.jackson.internal.CordaModule
import net.corda.client.jackson.internal.ToStringSerialize
import net.corda.client.jackson.internal.jsonObject
import net.corda.client.jackson.internal.readValueAs
import net.corda.core.CordaInternal
import net.corda.core.CordaOID
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.*
import net.corda.core.crypto.CompositeKey
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.CertRole
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.*
import net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.NotaryChangeWireTransaction
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 net.corda.core.utilities.parsePublicKeyBase58
import net.corda.core.utilities.toBase58String
import org.bouncycastle.asn1.x509.KeyPurposeId
import java.lang.reflect.Modifier
import java.math.BigDecimal
import java.nio.charset.StandardCharsets.UTF_8
import java.security.PublicKey
import java.security.cert.CertPath
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.*
import javax.security.auth.x500.X500Principal
/**
* Utilities and serialisers for working with JSON representations of basic types. This adds Jackson support for
@ -40,91 +53,87 @@ import java.util.*
*
* Note that Jackson can also be used to serialise/deserialise other formats such as Yaml and XML.
*/
@Suppress("DEPRECATION", "MemberVisibilityCanBePrivate")
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 {
val isFullParties: Boolean
fun wellKnownPartyFromX500Name(name: CordaX500Name): Party?
fun partyFromKey(owningKey: PublicKey): Party?
fun partiesFromName(query: String): Set<Party>
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
@JvmOverloads constructor(val rpc: CordaRPCOps,
factory: JsonFactory,
val fuzzyIdentityMatch: Boolean,
override val isFullParties: Boolean = false) : 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
@JvmOverloads constructor(val identityService: IdentityService,
factory: JsonFactory,
val fuzzyIdentityMatch: Boolean,
override val isFullParties: Boolean = false) : 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
}
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()
@Deprecated("This is an internal class, do not use", replaceWith = ReplaceWith("JacksonSupport.createNonRpcMapper"))
class NoPartyObjectMapper
@JvmOverloads constructor(factory: JsonFactory,
override val isFullParties: Boolean = false) : PartyObjectMapper, ObjectMapper(factory) {
override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = null
override fun partyFromKey(owningKey: PublicKey): Party? = null
override fun partiesFromName(query: String): Set<Party> = 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)
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)
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)
// 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)
}
}
@Suppress("unused")
@Deprecated("Do not use this as it's not thread safe. Instead get a ObjectMapper instance with one of the create*Mapper methods.")
val cordaModule: Module by lazy(::CordaModule)
/**
* Creates a Jackson ObjectMapper that uses RPC to deserialise parties from string names.
*
* If [fuzzyIdentityMatch] is false, fields mapped to [Party] objects must be in X.500 name form and precisely
* @param fuzzyIdentityMatch If false, fields mapped to [Party] objects must be in X.500 name form and precisely
* 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.
*
* @param fullParties If true then [Party] objects will be serialised as JSON objects, with the owning key serialised
* in addition to the name. For [PartyAndCertificate] objects the cert path will be included.
*/
@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,
fullParties: Boolean = false): ObjectMapper {
return configureMapper(RpcObjectMapper(rpc, factory, fuzzyIdentityMatch, fullParties))
}
/** For testing or situations where deserialising parties is not required */
/**
* For testing or situations where deserialising parties is not required
*
* @param fullParties If true then [Party] objects will be serialised as JSON objects, with the owning key serialised
* in addition to the name. For [PartyAndCertificate] objects the cert path will be included.
*/
@JvmStatic
@JvmOverloads
fun createNonRpcMapper(factory: JsonFactory = JsonFactory()): ObjectMapper = configureMapper(NoPartyObjectMapper(factory))
fun createNonRpcMapper(factory: JsonFactory = JsonFactory(), fullParties: Boolean = false): ObjectMapper {
return configureMapper(NoPartyObjectMapper(factory, fullParties))
}
/**
* Creates a Jackson ObjectMapper that uses an [IdentityService] directly inside the node to deserialise parties from string names.
@ -133,139 +142,238 @@ 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))
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)
registerModule(JavaTimeModule())
registerModule(cordaModule)
registerModule(KotlinModule())
fun createInMemoryMapper(identityService: IdentityService,
factory: JsonFactory = JsonFactory(),
fuzzyIdentityMatch: Boolean = false): ObjectMapper {
return configureMapper(IdentityObjectMapper(identityService, factory, fuzzyIdentityMatch))
}
object ToStringSerializer : JsonSerializer<Any>() {
override fun serialize(obj: Any, generator: JsonGenerator, provider: SerializerProvider) {
generator.writeString(obj.toString())
@CordaInternal
@VisibleForTesting
internal fun createPartyObjectMapper(partyObjectMapper: PartyObjectMapper, factory: JsonFactory = JsonFactory()): ObjectMapper {
val mapper = object : ObjectMapper(factory), PartyObjectMapper by partyObjectMapper {}
return configureMapper(mapper)
}
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())
addMixIn(BigDecimal::class.java, BigDecimalMixin::class.java)
addMixIn(X500Principal::class.java, X500PrincipalMixin::class.java)
addMixIn(X509Certificate::class.java, X509CertificateMixin::class.java)
addMixIn(CertPath::class.java, CertPathMixin::class.java)
}
}
@ToStringSerialize
@JsonDeserialize(using = NumberDeserializers.BigDecimalDeserializer::class)
private interface BigDecimalMixin
private object DateSerializer : JsonSerializer<Date>() {
override fun serialize(value: Date, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeObject(value.toInstant())
}
}
@ToStringSerialize
private interface X500PrincipalMixin
@JsonSerialize(using = X509CertificateSerializer::class)
@JsonDeserialize(using = X509CertificateDeserializer::class)
private interface X509CertificateMixin
private object X509CertificateSerializer : JsonSerializer<X509Certificate>() {
val keyUsages = arrayOf(
"digitalSignature",
"nonRepudiation",
"keyEncipherment",
"dataEncipherment",
"keyAgreement",
"keyCertSign",
"cRLSign",
"encipherOnly",
"decipherOnly"
)
val keyPurposeIds = KeyPurposeId::class.java
.fields
.filter { Modifier.isStatic(it.modifiers) && it.type == KeyPurposeId::class.java }
.associateBy({ (it.get(null) as KeyPurposeId).id }, { it.name })
val knownExtensions = setOf(
"2.5.29.15",
"2.5.29.17",
"2.5.29.18",
"2.5.29.19",
"2.5.29.37",
CordaOID.X509_EXTENSION_CORDA_ROLE
)
override fun serialize(value: X509Certificate, gen: JsonGenerator, serializers: SerializerProvider) {
gen.jsonObject {
writeNumberField("version", value.version)
writeObjectField("serialNumber", value.serialNumber)
writeObjectField("subject", value.subjectX500Principal)
writeObjectField("publicKey", value.publicKey)
writeObjectField("issuer", value.issuerX500Principal)
writeObjectField("notBefore", value.notBefore)
writeObjectField("notAfter", value.notAfter)
writeObjectField("cordaCertRole", CertRole.extract(value))
writeObjectField("issuerUniqueID", value.issuerUniqueID)
writeObjectField("subjectUniqueID", value.subjectUniqueID)
writeObjectField("keyUsage", value.keyUsage?.asList()?.mapIndexedNotNull { i, flag -> if (flag) keyUsages[i] else null })
writeObjectField("extendedKeyUsage", value.extendedKeyUsage.map { keyPurposeIds.getOrDefault(it, it) })
jsonObject("basicConstraints") {
val isCa = value.basicConstraints != -1
writeBooleanField("isCA", isCa)
if (isCa) {
writeObjectField("pathLength", value.basicConstraints.let { if (it != Int.MAX_VALUE) it else null })
}
}
writeObjectField("subjectAlternativeNames", value.subjectAlternativeNames)
writeObjectField("issuerAlternativeNames", value.issuerAlternativeNames)
writeObjectField("otherCriticalExtensions", value.criticalExtensionOIDs - knownExtensions)
writeObjectField("otherNonCriticalExtensions", value.nonCriticalExtensionOIDs - knownExtensions)
writeBinaryField("encoded", value.encoded)
}
}
}
private class X509CertificateDeserializer : JsonDeserializer<X509Certificate>() {
private val certFactory = CertificateFactory.getInstance("X.509")
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): X509Certificate {
val encoded = if (parser.currentToken == JsonToken.START_OBJECT) {
parser.readValueAsTree<ObjectNode>()["encoded"].binaryValue()
} else {
parser.binaryValue
}
return certFactory.generateCertificate(encoded.inputStream()) as X509Certificate
}
}
@JsonSerialize(using = CertPathSerializer::class)
@JsonDeserialize(using = CertPathDeserializer::class)
private interface CertPathMixin
private class CertPathSerializer : JsonSerializer<CertPath>() {
override fun serialize(value: CertPath, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeObject(CertPathWrapper(value.type, uncheckedCast(value.certificates)))
}
}
private class CertPathDeserializer : JsonDeserializer<CertPath>() {
private val certFactory = CertificateFactory.getInstance("X.509")
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): CertPath {
val wrapper = parser.readValueAs<CertPathWrapper>()
return certFactory.generateCertPath(wrapper.certificates)
}
}
private data class CertPathWrapper(val type: String, val certificates: List<X509Certificate>) {
init {
require(type == "X.509") { "Only X.509 cert paths are supported" }
}
}
@Deprecated("This is an internal class, do not use")
object AnonymousPartySerializer : JsonSerializer<AnonymousParty>() {
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<AnonymousParty>() {
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<Party>() {
override fun serialize(obj: Party, generator: JsonGenerator, provider: SerializerProvider) {
generator.writeString(obj.name.toString())
override fun serialize(value: Party, gen: JsonGenerator, provider: SerializerProvider) {
val mapper = gen.codec as PartyObjectMapper
if (mapper.isFullParties) {
gen.writeObject(PartyAnalogue(value.name, value.owningKey))
} else {
gen.writeObject(value.name)
}
}
}
@Deprecated("This is an internal class, do not use")
object PartyDeserializer : JsonDeserializer<Party>() {
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
// 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(",")) {
val principal = CordaX500Name.parse(parser.text)
mapper.wellKnownPartyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name $principal")
return if (parser.currentToken == JsonToken.START_OBJECT) {
val analogue = parser.readValueAs<PartyAnalogue>()
Party(analogue.name, analogue.owningKey)
} 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()}")
} else if (nameMatches.size == 1) {
nameMatches.first()
// 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.
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 {
throw JsonParseException(parser, "Ambiguous name match '${parser.text}': could be any of " + nameMatches.map { it.name }.joinToString(" ... or ..."))
lookupByNameSegment(mapper, parser)
}
}
}
}
object CordaX500NameSerializer : JsonSerializer<CordaX500Name>() {
override fun serialize(obj: CordaX500Name, generator: JsonGenerator, provider: SerializerProvider) {
generator.writeString(obj.toString())
private fun lookupByNameSegment(mapper: PartyObjectMapper, parser: JsonParser): Party {
val nameMatches = mapper.partiesFromName(parser.text)
return when {
nameMatches.isEmpty() -> {
val publicKey = parser.readValueAs<PublicKey>()
mapper.partyFromKey(publicKey)
?: throw JsonParseException(parser, "Could not find a Party with key ${publicKey.toStringShort()}")
}
nameMatches.size == 1 -> nameMatches.first()
else -> throw JsonParseException(parser, "Ambiguous name match '${parser.text}': could be any of " +
nameMatches.map { it.name }.joinToString(" ... or ... "))
}
}
}
private class PartyAnalogue(val name: CordaX500Name, val owningKey: PublicKey)
@Deprecated("This is an internal class, do not use")
object CordaX500NameDeserializer : JsonDeserializer<CordaX500Name>() {
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)
}
}
}
object NodeInfoSerializer : JsonSerializer<NodeInfo>() {
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<NodeInfo>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): NodeInfo {
if (parser.currentToken == JsonToken.FIELD_NAME) {
parser.nextToken()
}
try {
return Base58.decode(parser.text).deserialize<NodeInfo>()
} catch (e: Exception) {
throw JsonParseException(parser, "Invalid NodeInfo ${parser.text}: ${e.message}")
}
val mapper = parser.codec as PartyObjectMapper
val party = parser.readValueAs<AbstractParty>()
return mapper.nodeInfoFromParty(party) ?: throw JsonParseException(parser, "Cannot find node with $party")
}
}
object SecureHashSerializer : JsonSerializer<SecureHash>() {
override fun serialize(obj: SecureHash, generator: JsonGenerator, provider: SerializerProvider) {
generator.writeString(obj.toString())
}
}
/**
* Implemented as a class so that we can instantiate for T.
*/
@Deprecated("This is an internal class, do not use")
class SecureHashDeserializer<T : SecureHash> : JsonDeserializer<T>() {
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) {
@ -274,61 +382,95 @@ object JacksonSupport {
}
}
@Deprecated("This is an internal class, do not use")
object PublicKeySerializer : JsonSerializer<PublicKey>() {
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<PublicKey>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): PublicKey {
return try {
val derBytes = parser.text.base64ToByteArray()
Crypto.decodePublicKey(derBytes)
parsePublicKeyBase58(parser.text)
} catch (e: Exception) {
throw JsonParseException(parser, "Invalid public key ${parser.text}: ${e.message}")
}
}
}
@Deprecated("This is an internal class, do not use")
object AmountDeserializer : JsonDeserializer<Amount<*>>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): Amount<*> {
return if (parser.currentToken == JsonToken.VALUE_STRING) {
Amount.parseCurrency(parser.text)
} else {
val wrapper = parser.readValueAs<CurrencyAmountWrapper>()
Amount(wrapper.quantity, wrapper.token)
}
}
}
private data class CurrencyAmountWrapper(val quantity: Long, val token: Currency)
@Deprecated("This is an internal class, do not use")
object OpaqueBytesDeserializer : JsonDeserializer<OpaqueBytes>() {
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): OpaqueBytes {
return OpaqueBytes(parser.text?.toByteArray(UTF_8) ?: parser.binaryValue)
}
}
//
// Everything below this point is no longer used but can't be deleted as they leaked into the public API
//
@Deprecated("No longer used as jackson already has a toString serializer",
replaceWith = ReplaceWith("com.fasterxml.jackson.databind.ser.std.ToStringSerializer.instance"))
object ToStringSerializer : JsonSerializer<Any>() {
override fun serialize(obj: Any, generator: JsonGenerator, provider: SerializerProvider) {
generator.writeString(obj.toString())
}
}
@Deprecated("This is an internal class, do not use")
object CordaX500NameSerializer : JsonSerializer<CordaX500Name>() {
override fun serialize(obj: CordaX500Name, generator: JsonGenerator, provider: SerializerProvider) {
generator.writeString(obj.toString())
}
}
@Deprecated("This is an internal class, do not use")
object NodeInfoSerializer : JsonSerializer<NodeInfo>() {
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 SecureHashSerializer : JsonSerializer<SecureHash>() {
override fun serialize(obj: SecureHash, generator: JsonGenerator, provider: SerializerProvider) {
generator.writeString(obj.toString())
}
}
@Deprecated("This is an internal class, do not use")
object AmountSerializer : JsonSerializer<Amount<*>>() {
override fun serialize(value: Amount<*>, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeString(value.toString())
}
}
object AmountDeserializer : JsonDeserializer<Amount<*>>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): Amount<*> {
try {
return Amount.parseCurrency(parser.text)
} catch (e: Exception) {
try {
val tree = parser.readValueAsTree<JsonNode>()
require(tree["quantity"].canConvertToLong() && tree["token"].asText().isNotBlank())
val quantity = tree["quantity"].asLong()
val token = tree["token"].asText()
// 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)
}
}
}
}
object OpaqueBytesDeserializer : JsonDeserializer<OpaqueBytes>() {
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): OpaqueBytes {
return OpaqueBytes(parser.text.toByteArray())
}
}
@Deprecated("This is an internal class, do not use")
object OpaqueBytesSerializer : JsonSerializer<OpaqueBytes>() {
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<CoreTransaction>
@JsonProperty("signatures") protected abstract fun getSigs(): List<TransactionSignature>
@ -341,6 +483,8 @@ object JacksonSupport {
@JsonIgnore abstract fun getRequiredSigningKeys(): Set<PublicKey>
}
@Deprecated("This is an internal class, do not use")
@Suppress("unused")
abstract class WireTransactionMixin {
@JsonIgnore abstract fun getMerkleTree(): MerkleTree
@JsonIgnore abstract fun getAvailableComponents(): List<Any>
@ -348,4 +492,3 @@ object JacksonSupport {
@JsonIgnore abstract fun getOutputStates(): List<ContractState>
}
}

View File

@ -0,0 +1,155 @@
@file:Suppress("DEPRECATION")
package net.corda.client.jackson.internal
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.node.ObjectNode
import net.corda.client.jackson.JacksonSupport
import net.corda.core.contracts.Amount
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature
import net.corda.core.identity.*
import net.corda.core.internal.DigitalSignatureWithCert
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.ByteSequence
import net.corda.core.utilities.NetworkHostAndPort
import java.security.PublicKey
import java.security.cert.CertPath
class CordaModule : SimpleModule("corda-core") {
override fun setupModule(context: SetupContext) {
super.setupModule(context)
context.setMixInAnnotations(PartyAndCertificate::class.java, PartyAndCertificateMixin::class.java)
context.setMixInAnnotations(NetworkHostAndPort::class.java, NetworkHostAndPortMixin::class.java)
context.setMixInAnnotations(CordaX500Name::class.java, CordaX500NameMixin::class.java)
context.setMixInAnnotations(Amount::class.java, AmountMixin::class.java)
context.setMixInAnnotations(AbstractParty::class.java, AbstractPartyMixin::class.java)
context.setMixInAnnotations(AnonymousParty::class.java, AnonymousPartyMixin::class.java)
context.setMixInAnnotations(Party::class.java, PartyMixin::class.java)
context.setMixInAnnotations(PublicKey::class.java, PublicKeyMixin::class.java)
context.setMixInAnnotations(ByteSequence::class.java, ByteSequenceMixin::class.java)
context.setMixInAnnotations(SecureHash.SHA256::class.java, SecureHashSHA256Mixin::class.java)
context.setMixInAnnotations(SerializedBytes::class.java, SerializedBytesMixin::class.java)
context.setMixInAnnotations(DigitalSignature.WithKey::class.java, ByteSequenceWithPropertiesMixin::class.java)
context.setMixInAnnotations(DigitalSignatureWithCert::class.java, ByteSequenceWithPropertiesMixin::class.java)
context.setMixInAnnotations(TransactionSignature::class.java, ByteSequenceWithPropertiesMixin::class.java)
context.setMixInAnnotations(SignedTransaction::class.java, JacksonSupport.SignedTransactionMixin::class.java)
context.setMixInAnnotations(WireTransaction::class.java, JacksonSupport.WireTransactionMixin::class.java)
context.setMixInAnnotations(NodeInfo::class.java, NodeInfoMixin::class.java)
}
}
@ToStringSerialize
@JsonDeserialize(using = NetworkHostAndPortDeserializer::class)
private interface NetworkHostAndPortMixin
private class NetworkHostAndPortDeserializer : JsonDeserializer<NetworkHostAndPort>() {
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): NetworkHostAndPort {
return NetworkHostAndPort.parse(parser.text)
}
}
@JsonSerialize(using = PartyAndCertificateSerializer::class)
// TODO Add deserialization which follows the same lookup logic as Party
private interface PartyAndCertificateMixin
private class PartyAndCertificateSerializer : JsonSerializer<PartyAndCertificate>() {
override fun serialize(value: PartyAndCertificate, gen: JsonGenerator, serializers: SerializerProvider) {
val mapper = gen.codec as JacksonSupport.PartyObjectMapper
if (mapper.isFullParties) {
gen.writeObject(PartyAndCertificateWrapper(value.name, value.certPath))
} else {
gen.writeObject(value.party)
}
}
}
private class PartyAndCertificateWrapper(val name: CordaX500Name, val certPath: CertPath)
@JsonSerialize(using = SerializedBytesSerializer::class)
@JsonDeserialize(using = SerializedBytesDeserializer::class)
private class SerializedBytesMixin
private class SerializedBytesSerializer : JsonSerializer<SerializedBytes<*>>() {
override fun serialize(value: SerializedBytes<*>, gen: JsonGenerator, serializers: SerializerProvider) {
val deserialized = value.deserialize<Any>()
gen.jsonObject {
writeStringField("class", deserialized.javaClass.name)
writeObjectField("deserialized", deserialized)
}
}
}
private class SerializedBytesDeserializer : JsonDeserializer<SerializedBytes<*>>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): SerializedBytes<Any> {
return if (parser.currentToken == JsonToken.START_OBJECT) {
val mapper = parser.codec as ObjectMapper
val json = parser.readValueAsTree<ObjectNode>()
val clazz = context.findClass(json["class"].textValue())
val pojo = mapper.convertValue(json["deserialized"], clazz)
pojo.serialize()
} else {
SerializedBytes(parser.binaryValue)
}
}
}
@JsonDeserialize(using = JacksonSupport.PartyDeserializer::class)
private interface AbstractPartyMixin
@JsonSerialize(using = JacksonSupport.AnonymousPartySerializer::class)
@JsonDeserialize(using = JacksonSupport.AnonymousPartyDeserializer::class)
private interface AnonymousPartyMixin
@JsonSerialize(using = JacksonSupport.PartySerializer::class)
private interface PartyMixin
@ToStringSerialize
@JsonDeserialize(using = JacksonSupport.CordaX500NameDeserializer::class)
private interface CordaX500NameMixin
@JsonIgnoreProperties("legalIdentities") // This is already covered by legalIdentitiesAndCerts
@JsonDeserialize(using = JacksonSupport.NodeInfoDeserializer::class)
private interface NodeInfoMixin
@ToStringSerialize
@JsonDeserialize(using = JacksonSupport.SecureHashDeserializer::class)
private interface SecureHashSHA256Mixin
@JsonSerialize(using = JacksonSupport.PublicKeySerializer::class)
@JsonDeserialize(using = JacksonSupport.PublicKeyDeserializer::class)
private interface PublicKeyMixin
@ToStringSerialize
@JsonDeserialize(using = JacksonSupport.AmountDeserializer::class)
private interface AmountMixin
@JsonDeserialize(using = JacksonSupport.OpaqueBytesDeserializer::class)
@JsonSerialize(using = ByteSequenceSerializer::class)
private interface ByteSequenceMixin
private class ByteSequenceSerializer : JsonSerializer<ByteSequence>() {
override fun serialize(value: ByteSequence, gen: JsonGenerator, serializers: SerializerProvider) {
val bytes = value.bytes
gen.writeBinary(bytes, value.offset, value.size)
}
}
@JsonIgnoreProperties("offset", "size")
@JsonSerialize
@JsonDeserialize
private interface ByteSequenceWithPropertiesMixin

View File

@ -0,0 +1,25 @@
package net.corda.client.jackson.internal
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer
import com.fasterxml.jackson.module.kotlin.convertValue
inline fun JsonGenerator.jsonObject(fieldName: String? = null, gen: JsonGenerator.() -> Unit) {
fieldName?.let { writeFieldName(it) }
writeStartObject()
gen()
writeEndObject()
}
inline fun <reified T> JsonParser.readValueAs(): T = readValueAs(T::class.java)
inline fun <reified T : Any> JsonNode.valueAs(mapper: ObjectMapper): T = mapper.convertValue(this)
@JacksonAnnotationsInside
@JsonSerialize(using = ToStringSerializer::class)
annotation class ToStringSerialize

View File

@ -1,42 +1,73 @@
package net.corda.client.jackson
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.core.JsonFactory
import com.fasterxml.jackson.databind.JsonNode
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.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.module.kotlin.convertValue
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.client.jackson.internal.valueAs
import net.corda.core.contracts.Amount
import net.corda.core.cordapp.CordappProvider
import net.corda.core.crypto.*
import net.corda.core.identity.CordaX500Name
import net.corda.core.crypto.CompositeKey
import net.corda.core.identity.*
import net.corda.core.internal.DigitalSignatureWithCert
import net.corda.core.node.NodeInfo
import net.corda.core.node.ServiceHub
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.serialize
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.*
import net.corda.finance.USD
import net.corda.nodeapi.internal.crypto.x509Certificates
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.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 org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.junit.runners.Parameterized.Parameters
import java.math.BigInteger
import java.nio.charset.StandardCharsets.*
import java.security.PublicKey
import java.security.cert.CertPath
import java.security.cert.X509Certificate
import java.util.*
import kotlin.test.assertEquals
import javax.security.auth.x500.X500Principal
import kotlin.collections.ArrayList
class JacksonSupportTest {
@RunWith(Parameterized::class)
class JacksonSupportTest(@Suppress("unused") private val name: String, factory: JsonFactory) {
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"))
@Parameters(name = "{0}")
@JvmStatic
fun factories() = arrayOf(arrayOf("JSON", JsonFactory()), arrayOf("YAML", YAMLFactory()))
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val partyObjectMapper = TestPartyObjectMapper()
private val mapper = JacksonSupport.createPartyObjectMapper(partyObjectMapper, factory)
private lateinit var services: ServiceHub
private lateinit var cordappProvider: CordappProvider
@ -48,54 +79,19 @@ class JacksonSupportTest {
}
@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<Currency>)
@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)
fun `Amount(Currency) serialization`() {
assertThat(mapper.valueToTree<TextNode>(Amount.parseCurrency("£25000000")).textValue()).isEqualTo("25000000.00 GBP")
assertThat(mapper.valueToTree<TextNode>(Amount.parseCurrency("$250000")).textValue()).isEqualTo("250000.00 USD")
}
@Test
fun readAmount() {
val oldJson = """
{
"notional": {
"quantity": 2500000000,
"token": "USD"
}
}
"""
val newJson = """ { "notional" : "$25000000" } """
assertEquals(Amount(2500000000L, USD), mapper.readValue(newJson, Dummy::class.java).notional)
assertEquals(Amount(2500000000L, USD), mapper.readValue(oldJson, Dummy::class.java).notional)
}
@Test
fun writeAmount() {
val writer = mapper.writer().without(SerializationFeature.INDENT_OUTPUT)
assertEquals("""{"notional":"25000000.00 USD"}""", writer.writeValueAsString(Dummy(Amount.parseCurrency("$25000000"))))
fun `Amount(Currency) deserialization`() {
val old = mapOf(
"quantity" to 2500000000,
"token" to "USD"
)
assertThat(mapper.convertValue<Amount<Currency>>(old)).isEqualTo(Amount(2_500_000_000, USD))
assertThat(mapper.convertValue<Amount<Currency>>(TextNode("$25000000"))).isEqualTo(Amount(2_500_000_000, USD))
}
@Test
@ -119,6 +115,382 @@ class JacksonSupportTest {
val writer = mapper.writer()
// We don't particularly care about the serialized format, just need to make sure it completes successfully.
writer.writeValueAsString(makeDummyTx())
println(writer.writeValueAsString(makeDummyTx()))
}
@Test
fun ByteSequence() {
val byteSequence: ByteSequence = OpaqueBytes.of(1, 2, 3, 4).subSequence(0, 2)
val json = mapper.valueToTree<BinaryNode>(byteSequence)
assertThat(json.binaryValue()).containsExactly(1, 2)
assertThat(json.asText()).isEqualTo(byteArrayOf(1, 2).toBase64())
assertThat(mapper.convertValue<ByteSequence>(json)).isEqualTo(byteSequence)
}
@Test
fun `OpaqueBytes serialization`() {
val opaqueBytes = OpaqueBytes(secureRandomBytes(128))
val json = mapper.valueToTree<BinaryNode>(opaqueBytes)
assertThat(json.binaryValue()).isEqualTo(opaqueBytes.bytes)
assertThat(json.asText()).isEqualTo(opaqueBytes.bytes.toBase64())
}
@Test
fun `OpaqueBytes deserialization`() {
assertThat(mapper.convertValue<OpaqueBytes>(TextNode("1234"))).isEqualTo(OpaqueBytes("1234".toByteArray(UTF_8)))
assertThat(mapper.convertValue<OpaqueBytes>(BinaryNode(byteArrayOf(1, 2, 3, 4)))).isEqualTo(OpaqueBytes.of(1, 2, 3, 4))
}
@Test
fun SerializedBytes() {
val data = TestData(BOB_NAME, "Summary", SubTestData(1234))
val serializedBytes = data.serialize()
val json = mapper.valueToTree<ObjectNode>(serializedBytes)
println(mapper.writeValueAsString(json))
assertThat(json["class"].textValue()).isEqualTo(TestData::class.java.name)
assertThat(json["deserialized"].valueAs<TestData>(mapper)).isEqualTo(data)
// Check that the entire JSON object can be converted back to the same SerializedBytes
assertThat(mapper.convertValue<SerializedBytes<*>>(json)).isEqualTo(serializedBytes)
assertThat(mapper.convertValue<SerializedBytes<*>>(BinaryNode(serializedBytes.bytes))).isEqualTo(serializedBytes)
}
// This is the class that was used to serialise the message for the test below. It's commented out so that it's no
// longer on the classpath.
// @CordaSerializable
// data class ClassNotOnClasspath(val name: CordaX500Name, val value: Int)
@Test
fun `SerializedBytes of class not on classpath`() {
// The contents of the file were written out as follows:
// ClassNotOnClasspath(BOB_NAME, 54321).serialize().open().copyTo("build" / "class-not-on-classpath-data")
val serializedBytes = SerializedBytes<Any>(javaClass.getResource("class-not-on-classpath-data").readBytes())
val json = mapper.valueToTree<ObjectNode>(serializedBytes)
println(mapper.writeValueAsString(json))
assertThat(json["class"].textValue()).isEqualTo("net.corda.client.jackson.JacksonSupportTest\$ClassNotOnClasspath")
assertThat(json["deserialized"].valueAs<Map<*, *>>(mapper)).isEqualTo(mapOf(
"name" to BOB_NAME.toString(),
"value" to 54321
))
assertThat(mapper.convertValue<SerializedBytes<*>>(BinaryNode(serializedBytes.bytes))).isEqualTo(serializedBytes)
}
@Test
fun DigitalSignature() {
val digitalSignature = DigitalSignature(secureRandomBytes(128))
val json = mapper.valueToTree<BinaryNode>(digitalSignature)
assertThat(json.binaryValue()).isEqualTo(digitalSignature.bytes)
assertThat(json.asText()).isEqualTo(digitalSignature.bytes.toBase64())
assertThat(mapper.convertValue<DigitalSignature>(json)).isEqualTo(digitalSignature)
}
@Test
fun `DigitalSignature WithKey`() {
val digitalSignature = DigitalSignature.WithKey(BOB_PUBKEY, secureRandomBytes(128))
val json = mapper.valueToTree<ObjectNode>(digitalSignature)
val (by, bytes) = json.assertHasOnlyFields("by", "bytes")
assertThat(by.valueAs<PublicKey>(mapper)).isEqualTo(BOB_PUBKEY)
assertThat(bytes.binaryValue()).isEqualTo(digitalSignature.bytes)
assertThat(mapper.convertValue<DigitalSignature.WithKey>(json)).isEqualTo(digitalSignature)
}
@Test
fun DigitalSignatureWithCert() {
val digitalSignature = DigitalSignatureWithCert(MINI_CORP.identity.certificate, secureRandomBytes(128))
val json = mapper.valueToTree<ObjectNode>(digitalSignature)
val (by, bytes) = json.assertHasOnlyFields("by", "bytes")
assertThat(by.valueAs<X509Certificate>(mapper)).isEqualTo(MINI_CORP.identity.certificate)
assertThat(bytes.binaryValue()).isEqualTo(digitalSignature.bytes)
assertThat(mapper.convertValue<DigitalSignatureWithCert>(json)).isEqualTo(digitalSignature)
}
@Test
fun TransactionSignature() {
val metadata = SignatureMetadata(1, 1)
val transactionSignature = TransactionSignature(secureRandomBytes(128), BOB_PUBKEY, metadata)
val json = mapper.valueToTree<ObjectNode>(transactionSignature)
val (bytes, by, signatureMetadata, partialMerkleTree) = json.assertHasOnlyFields(
"bytes",
"by",
"signatureMetadata",
"partialMerkleTree"
)
assertThat(bytes.binaryValue()).isEqualTo(transactionSignature.bytes)
assertThat(by.valueAs<PublicKey>(mapper)).isEqualTo(BOB_PUBKEY)
assertThat(signatureMetadata.valueAs<SignatureMetadata>(mapper)).isEqualTo(metadata)
assertThat(partialMerkleTree.isNull).isTrue()
assertThat(mapper.convertValue<TransactionSignature>(json)).isEqualTo(transactionSignature)
}
@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<TextNode>(date)
assertThat(json.textValue()).isEqualTo(date.toInstant().toString())
assertThat(mapper.convertValue<Date>(json)).isEqualTo(date)
}
@Test
fun `Party serialization`() {
val json = mapper.valueToTree<TextNode>(MINI_CORP.party)
assertThat(json.textValue()).isEqualTo(MINI_CORP.name.toString())
}
@Test
fun `Party serialization with isFullParty = true`() {
partyObjectMapper.isFullParties = true
val json = mapper.valueToTree<ObjectNode>(MINI_CORP.party)
val (name, owningKey) = json.assertHasOnlyFields("name", "owningKey")
assertThat(name.valueAs<CordaX500Name>(mapper)).isEqualTo(MINI_CORP.name)
assertThat(owningKey.valueAs<PublicKey>(mapper)).isEqualTo(MINI_CORP.publicKey)
}
@Test
fun `Party deserialization on full name`() {
fun convertToParty() = mapper.convertValue<Party>(TextNode(MINI_CORP.name.toString()))
// Check that it fails if it can't find the party
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<Party>(TextNode(MINI_CORP.name.organisation))
// Check that it fails if it can't find the party
assertThatThrownBy { convertToParty() }
partyObjectMapper.identities += MINI_CORP.party
assertThat(convertToParty()).isEqualTo(MINI_CORP.party)
}
@Test
fun `Party deserialization on public key`() {
fun convertToParty() = mapper.convertValue<Party>(TextNode(MINI_CORP.publicKey.toBase58String()))
// Check that it fails if it can't find the party
assertThatThrownBy { convertToParty() }
partyObjectMapper.identities += MINI_CORP.party
assertThat(convertToParty()).isEqualTo(MINI_CORP.party)
}
@Test
fun `Party deserialization on name and key`() {
val party = mapper.convertValue<Party>(mapOf(
"name" to MINI_CORP.name,
"owningKey" to MINI_CORP.publicKey
))
// Party.equals is only defined on the public key so we must check the name as well
assertThat(party.name).isEqualTo(MINI_CORP.name)
assertThat(party.owningKey).isEqualTo(MINI_CORP.publicKey)
}
@Test
fun PublicKey() {
val json = mapper.valueToTree<TextNode>(MINI_CORP.publicKey)
assertThat(json.textValue()).isEqualTo(MINI_CORP.publicKey.toBase58String())
assertThat(mapper.convertValue<PublicKey>(json)).isEqualTo(MINI_CORP.publicKey)
}
@Test
fun `EdDSA public key`() {
val publicKey = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, SEED).public
val json = mapper.valueToTree<TextNode>(publicKey)
assertThat(json.textValue()).isEqualTo(publicKey.toBase58String())
assertThat(mapper.convertValue<PublicKey>(json)).isEqualTo(publicKey)
}
@Test
fun CompositeKey() {
val innerKeys = (1..3).map { i ->
Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, SEED + 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 json = mapper.valueToTree<TextNode>(publicKey)
assertThat(json.textValue()).isEqualTo(publicKey.toBase58String())
assertThat(mapper.convertValue<CompositeKey>(json)).isEqualTo(publicKey)
}
@Test
fun AnonymousParty() {
val anonymousParty = AnonymousParty(ALICE_PUBKEY)
val json = mapper.valueToTree<TextNode>(anonymousParty)
assertThat(json.textValue()).isEqualTo(ALICE_PUBKEY.toBase58String())
assertThat(mapper.convertValue<AnonymousParty>(json)).isEqualTo(anonymousParty)
}
@Test
fun `PartyAndCertificate serialization`() {
val json = mapper.valueToTree<TextNode>(MINI_CORP.identity)
assertThat(json.textValue()).isEqualTo(MINI_CORP.name.toString())
}
@Test
fun `PartyAndCertificate serialization with isFullParty = true`() {
partyObjectMapper.isFullParties = true
val json = mapper.valueToTree<ObjectNode>(MINI_CORP.identity)
println(mapper.writeValueAsString(json))
val (name, certPath) = json.assertHasOnlyFields("name", "certPath")
assertThat(name.valueAs<CordaX500Name>(mapper)).isEqualTo(MINI_CORP.name)
assertThat(certPath.valueAs<CertPath>(mapper)).isEqualTo(MINI_CORP.identity.certPath)
}
@Test
fun `PartyAndCertificate deserialization on cert path`() {
val certPathJson = mapper.valueToTree<JsonNode>(MINI_CORP.identity.certPath)
val partyAndCert = mapper.convertValue<PartyAndCertificate>(mapOf("certPath" to certPathJson))
// PartyAndCertificate.equals is defined on the Party so we must check the certPath directly
assertThat(partyAndCert.certPath).isEqualTo(MINI_CORP.identity.certPath)
}
@Test
fun `NodeInfo serialization`() {
val (nodeInfo) = createNodeInfoAndSigned(ALICE_NAME)
val json = mapper.valueToTree<ObjectNode>(nodeInfo)
val (addresses, legalIdentitiesAndCerts, platformVersion, serial) = json.assertHasOnlyFields(
"addresses",
"legalIdentitiesAndCerts",
"platformVersion",
"serial"
)
addresses.run {
assertThat(this).hasSize(1)
assertThat(this[0].valueAs<NetworkHostAndPort>(mapper)).isEqualTo(nodeInfo.addresses[0])
}
legalIdentitiesAndCerts.run {
assertThat(this).hasSize(1)
assertThat(this[0].valueAs<CordaX500Name>(mapper)).isEqualTo(ALICE_NAME)
}
assertThat(platformVersion.intValue()).isEqualTo(nodeInfo.platformVersion)
assertThat(serial.longValue()).isEqualTo(nodeInfo.serial)
}
@Test
fun `NodeInfo deserialization on name`() {
val (nodeInfo) = createNodeInfoAndSigned(ALICE_NAME)
fun convertToNodeInfo() = mapper.convertValue<NodeInfo>(TextNode(ALICE_NAME.toString()))
assertThatThrownBy { convertToNodeInfo() }
partyObjectMapper.identities += nodeInfo.legalIdentities
partyObjectMapper.nodes += nodeInfo
assertThat(convertToNodeInfo()).isEqualTo(nodeInfo)
}
@Test
fun `NodeInfo deserialization on public key`() {
val (nodeInfo) = createNodeInfoAndSigned(ALICE_NAME)
fun convertToNodeInfo() = mapper.convertValue<NodeInfo>(TextNode(nodeInfo.legalIdentities[0].owningKey.toBase58String()))
assertThatThrownBy { convertToNodeInfo() }
partyObjectMapper.identities += nodeInfo.legalIdentities
partyObjectMapper.nodes += nodeInfo
assertThat(convertToNodeInfo()).isEqualTo(nodeInfo)
}
@Test
fun CertPath() {
val certPath = MINI_CORP.identity.certPath
val json = mapper.valueToTree<ObjectNode>(certPath)
println(mapper.writeValueAsString(json))
val (type, certificates) = json.assertHasOnlyFields("type", "certificates")
assertThat(type.textValue()).isEqualTo(certPath.type)
certificates.run {
val serialNumbers = elements().asSequence().map { it["serialNumber"].bigIntegerValue() }.toList()
assertThat(serialNumbers).isEqualTo(certPath.x509Certificates.map { it.serialNumber })
}
assertThat(mapper.convertValue<CertPath>(json).encoded).isEqualTo(certPath.encoded)
}
@Test
fun `X509Certificate serialization`() {
val cert: X509Certificate = MINI_CORP.identity.certificate
val json = mapper.valueToTree<ObjectNode>(cert)
println(mapper.writeValueAsString(json))
assertThat(json["serialNumber"].bigIntegerValue()).isEqualTo(cert.serialNumber)
assertThat(json["issuer"].valueAs<X500Principal>(mapper)).isEqualTo(cert.issuerX500Principal)
assertThat(json["subject"].valueAs<X500Principal>(mapper)).isEqualTo(cert.subjectX500Principal)
assertThat(json["publicKey"].valueAs<PublicKey>(mapper)).isEqualTo(cert.publicKey)
assertThat(json["notAfter"].valueAs<Date>(mapper)).isEqualTo(cert.notAfter)
assertThat(json["notBefore"].valueAs<Date>(mapper)).isEqualTo(cert.notBefore)
assertThat(json["encoded"].binaryValue()).isEqualTo(cert.encoded)
}
@Test
fun `X509Certificate deserialization`() {
val cert: X509Certificate = MINI_CORP.identity.certificate
assertThat(mapper.convertValue<X509Certificate>(mapOf("encoded" to cert.encoded))).isEqualTo(cert)
assertThat(mapper.convertValue<X509Certificate>(BinaryNode(cert.encoded))).isEqualTo(cert)
}
@Test
fun X500Principal() {
testToStringSerialisation(X500Principal("CN=Common,L=London,O=Org,C=UK"))
}
private inline fun <reified T : Any> testToStringSerialisation(value: T) {
val json = mapper.valueToTree<TextNode>(value)
assertThat(json.textValue()).isEqualTo(value.toString())
assertThat(mapper.convertValue<T>(json)).isEqualTo(value)
}
private fun JsonNode.assertHasOnlyFields(vararg fieldNames: String): List<JsonNode> {
assertThat(fieldNames()).containsOnly(*fieldNames)
return fieldNames.map { this[it] }
}
@CordaSerializable
private data class TestData(val name: CordaX500Name, val summary: String, val subData: SubTestData)
@CordaSerializable
private data class SubTestData(val value: Int)
private class TestPartyObjectMapper : JacksonSupport.PartyObjectMapper {
override var isFullParties: Boolean = false
val identities = ArrayList<Party>()
val nodes = ArrayList<NodeInfo>()
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<Party> {
return identities.filter { query in it.name.toString() }.toSet()
}
override fun nodeInfoFromParty(party: AbstractParty): NodeInfo? {
return nodes.find { party in it.legalIdentities }
}
}
}