mirror of
https://github.com/corda/corda.git
synced 2025-02-10 04:41:35 +00:00
CORDA-1905: Extend JSON deserialisation to handle Amount<T> for any T (#3819)
* CORDA-1905: Extend JSON deserialisation to handle Amount<T> for any T. (#3790) * Extend JSON deserialisation to handle Amount<T> for any T. * Rewrite message for @Deprecated. Conflicts: client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt * CORDA-1905: Configure Spring Boot to use same version of Jackson as Corda.
This commit is contained in:
parent
e301977b17
commit
81d9e4f382
@ -400,10 +400,10 @@ object JacksonSupport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("This is an internal class, do not use")
|
@Deprecated("Do not use - Replaced by Corda's internal AmountDeserializer and TokenDeserializer classes")
|
||||||
object AmountDeserializer : JsonDeserializer<Amount<*>>() {
|
object AmountDeserializer : JsonDeserializer<Amount<*>>() {
|
||||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): Amount<*> {
|
override fun deserialize(parser: JsonParser, context: DeserializationContext): Amount<*> {
|
||||||
return if (parser.currentToken == JsonToken.VALUE_STRING) {
|
return if (parser.currentToken() == JsonToken.VALUE_STRING) {
|
||||||
Amount.parseCurrency(parser.text)
|
Amount.parseCurrency(parser.text)
|
||||||
} else {
|
} else {
|
||||||
val wrapper = parser.readValueAs<CurrencyAmountWrapper>()
|
val wrapper = parser.readValueAs<CurrencyAmountWrapper>()
|
||||||
|
@ -2,13 +2,17 @@
|
|||||||
|
|
||||||
package net.corda.client.jackson.internal
|
package net.corda.client.jackson.internal
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
import com.fasterxml.jackson.annotation.*
|
||||||
|
import com.fasterxml.jackson.annotation.JsonCreator.Mode.*
|
||||||
import com.fasterxml.jackson.core.JsonGenerator
|
import com.fasterxml.jackson.core.JsonGenerator
|
||||||
import com.fasterxml.jackson.core.JsonParser
|
import com.fasterxml.jackson.core.JsonParser
|
||||||
import com.fasterxml.jackson.core.JsonToken
|
import com.fasterxml.jackson.core.JsonToken
|
||||||
import com.fasterxml.jackson.databind.*
|
import com.fasterxml.jackson.databind.*
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||||
|
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier
|
||||||
|
import com.fasterxml.jackson.databind.deser.ContextualDeserializer
|
||||||
|
import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer
|
||||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||||
import net.corda.client.jackson.JacksonSupport
|
import net.corda.client.jackson.JacksonSupport
|
||||||
@ -26,13 +30,17 @@ import net.corda.core.transactions.SignedTransaction
|
|||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
import net.corda.core.utilities.ByteSequence
|
import net.corda.core.utilities.ByteSequence
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
|
import java.math.BigDecimal
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.security.cert.CertPath
|
import java.security.cert.CertPath
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class CordaModule : SimpleModule("corda-core") {
|
class CordaModule : SimpleModule("corda-core") {
|
||||||
override fun setupModule(context: SetupContext) {
|
override fun setupModule(context: SetupContext) {
|
||||||
super.setupModule(context)
|
super.setupModule(context)
|
||||||
|
|
||||||
|
context.addBeanDeserializerModifier(AmountBeanDeserializerModifier())
|
||||||
|
|
||||||
context.setMixInAnnotations(PartyAndCertificate::class.java, PartyAndCertificateMixin::class.java)
|
context.setMixInAnnotations(PartyAndCertificate::class.java, PartyAndCertificateMixin::class.java)
|
||||||
context.setMixInAnnotations(NetworkHostAndPort::class.java, NetworkHostAndPortMixin::class.java)
|
context.setMixInAnnotations(NetworkHostAndPort::class.java, NetworkHostAndPortMixin::class.java)
|
||||||
context.setMixInAnnotations(CordaX500Name::class.java, CordaX500NameMixin::class.java)
|
context.setMixInAnnotations(CordaX500Name::class.java, CordaX500NameMixin::class.java)
|
||||||
@ -134,9 +142,78 @@ private interface SecureHashSHA256Mixin
|
|||||||
@JsonDeserialize(using = JacksonSupport.PublicKeyDeserializer::class)
|
@JsonDeserialize(using = JacksonSupport.PublicKeyDeserializer::class)
|
||||||
private interface PublicKeyMixin
|
private interface PublicKeyMixin
|
||||||
|
|
||||||
|
@Suppress("unused_parameter")
|
||||||
@ToStringSerialize
|
@ToStringSerialize
|
||||||
@JsonDeserialize(using = JacksonSupport.AmountDeserializer::class)
|
private abstract class AmountMixin @JsonCreator(mode = DISABLED) constructor(
|
||||||
private interface AmountMixin
|
quantity: Long,
|
||||||
|
displayTokenSize: BigDecimal,
|
||||||
|
token: Any
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* This mirrors the [Amount] constructor that we want Jackson to use, and
|
||||||
|
* requires that we also tell Jackson NOT to use [Amount]'s primary constructor.
|
||||||
|
*/
|
||||||
|
@JsonCreator constructor(
|
||||||
|
@JsonProperty("quantity")
|
||||||
|
quantity: Long,
|
||||||
|
|
||||||
|
@JsonDeserialize(using = TokenDeserializer::class)
|
||||||
|
@JsonProperty("token")
|
||||||
|
token: Any
|
||||||
|
) : this(quantity, Amount.getDisplayTokenSize(token), token)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements polymorphic deserialization for [Amount.token]. Kotlin must
|
||||||
|
* be able to determine the concrete [Amount] type at runtime, or it will
|
||||||
|
* fall back to using [Currency].
|
||||||
|
*/
|
||||||
|
private class TokenDeserializer(private val tokenType: Class<*>) : JsonDeserializer<Any>(), ContextualDeserializer {
|
||||||
|
@Suppress("unused")
|
||||||
|
constructor() : this(Currency::class.java)
|
||||||
|
|
||||||
|
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): Any = parser.readValueAs(tokenType)
|
||||||
|
|
||||||
|
override fun createContextual(ctxt: DeserializationContext, property: BeanProperty?): TokenDeserializer {
|
||||||
|
if (property == null) return this
|
||||||
|
return TokenDeserializer(property.type.rawClass.let { type ->
|
||||||
|
if (type == Any::class.java) Currency::class.java else type
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intercepts bean-based deserialization for the generic [Amount] type.
|
||||||
|
*/
|
||||||
|
private class AmountBeanDeserializerModifier : BeanDeserializerModifier() {
|
||||||
|
override fun modifyDeserializer(config: DeserializationConfig, description: BeanDescription, deserializer: JsonDeserializer<*>): JsonDeserializer<*> {
|
||||||
|
val modified = super.modifyDeserializer(config, description, deserializer)
|
||||||
|
return if (Amount::class.java.isAssignableFrom(description.beanClass)) {
|
||||||
|
AmountDeserializer(modified)
|
||||||
|
} else {
|
||||||
|
modified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AmountDeserializer(delegate: JsonDeserializer<*>) : DelegatingDeserializer(delegate) {
|
||||||
|
override fun newDelegatingInstance(newDelegatee: JsonDeserializer<*>) = AmountDeserializer(newDelegatee)
|
||||||
|
|
||||||
|
override fun deserialize(parser: JsonParser, context: DeserializationContext?): Any {
|
||||||
|
return if (parser.currentToken() == JsonToken.VALUE_STRING) {
|
||||||
|
/*
|
||||||
|
* This is obviously specific to Amount<Currency>, and is here to
|
||||||
|
* preserve the original deserializing behaviour for this case.
|
||||||
|
*/
|
||||||
|
Amount.parseCurrency(parser.text)
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
* Otherwise continue deserializing our Bean as usual.
|
||||||
|
*/
|
||||||
|
_delegatee.deserialize(parser, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JsonDeserialize(using = JacksonSupport.OpaqueBytesDeserializer::class)
|
@JsonDeserialize(using = JacksonSupport.OpaqueBytesDeserializer::class)
|
||||||
@JsonSerialize(using = ByteSequenceSerializer::class)
|
@JsonSerialize(using = ByteSequenceSerializer::class)
|
||||||
|
@ -0,0 +1,88 @@
|
|||||||
|
package net.corda.client.jackson
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.databind.node.TextNode
|
||||||
|
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
|
||||||
|
import net.corda.client.jackson.internal.CordaModule
|
||||||
|
import net.corda.core.contracts.Amount
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class AmountTest {
|
||||||
|
private companion object {
|
||||||
|
private val CO2 = CarbonCredit("CO2")
|
||||||
|
private val jsonMapper: ObjectMapper = ObjectMapper().registerModule(CordaModule())
|
||||||
|
private val yamlMapper: ObjectMapper = ObjectMapper(YAMLFactory()).registerModule(CordaModule())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Amount(Currency) JSON deserialization`() {
|
||||||
|
val str = """{ "quantity": 100, "token": "USD" }"""
|
||||||
|
val amount = jsonMapper.readValue<Amount<Currency>>(str, object : TypeReference<Amount<Currency>>() {})
|
||||||
|
assertThat(amount.quantity).isEqualTo(100)
|
||||||
|
assertThat(amount.token).isEqualTo(Currency.getInstance("USD"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Amount(Currency) YAML deserialization`() {
|
||||||
|
val str = """{ quantity: 100, token: USD }"""
|
||||||
|
val amount = yamlMapper.readValue<Amount<Currency>>(str, object : TypeReference<Amount<Currency>>() {})
|
||||||
|
assertThat(amount.quantity).isEqualTo(100)
|
||||||
|
assertThat(amount.token).isEqualTo(Currency.getInstance("USD"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Amount(CarbonCredit) JSON deserialization`() {
|
||||||
|
val str = """{ "quantity": 200, "token": { "type": "CO2" } }"""
|
||||||
|
val amount = jsonMapper.readValue<Amount<CarbonCredit>>(str, object : TypeReference<Amount<CarbonCredit>>() {})
|
||||||
|
assertThat(amount.quantity).isEqualTo(200)
|
||||||
|
assertThat(amount.token).isEqualTo(CO2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Amount(CarbonCredit) YAML deserialization`() {
|
||||||
|
val str = """{ quantity: 250, token: { type: CO2 } }"""
|
||||||
|
val amount = yamlMapper.readValue<Amount<CarbonCredit>>(str, object : TypeReference<Amount<CarbonCredit>>() {})
|
||||||
|
assertThat(amount.quantity).isEqualTo(250)
|
||||||
|
assertThat(amount.token).isEqualTo(CO2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Amount(Unknown) JSON deserialization`() {
|
||||||
|
val str = """{ "quantity": 100, "token": "USD" }"""
|
||||||
|
val amount = jsonMapper.readValue<Amount<*>>(str, object : TypeReference<Amount<*>>() {})
|
||||||
|
assertThat(amount.quantity).isEqualTo(100)
|
||||||
|
assertThat(amount.token).isEqualTo(Currency.getInstance("USD"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Amount(Unknown) YAML deserialization`() {
|
||||||
|
val str = """{ quantity: 100, token: USD }"""
|
||||||
|
val amount = yamlMapper.readValue<Amount<*>>(str, object : TypeReference<Amount<*>>() {})
|
||||||
|
assertThat(amount.quantity).isEqualTo(100)
|
||||||
|
assertThat(amount.token).isEqualTo(Currency.getInstance("USD"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Amount(Currency) YAML serialization`() {
|
||||||
|
assertThat(yamlMapper.valueToTree<TextNode>(Amount.parseCurrency("£25000000"))).isEqualTo(TextNode("25000000.00 GBP"))
|
||||||
|
assertThat(yamlMapper.valueToTree<TextNode>(Amount.parseCurrency("$250000"))).isEqualTo(TextNode("250000.00 USD"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Amount(CarbonCredit) JSON serialization`() {
|
||||||
|
assertThat(jsonMapper.writeValueAsString(Amount(123456, CO2)).trim())
|
||||||
|
.isEqualTo(""""123456 CarbonCredit(type=CO2)"""")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Amount(CarbonCredit) YAML serialization`() {
|
||||||
|
assertThat(yamlMapper.writeValueAsString(Amount(123456, CO2)).trim())
|
||||||
|
.isEqualTo("""--- "123456 CarbonCredit(type=CO2)"""")
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CarbonCredit(@JsonProperty("type") val type: String)
|
||||||
|
}
|
@ -91,6 +91,10 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
|||||||
"token" to "USD"
|
"token" to "USD"
|
||||||
)
|
)
|
||||||
assertThat(mapper.convertValue<Amount<Currency>>(old)).isEqualTo(Amount(2_500_000_000, USD))
|
assertThat(mapper.convertValue<Amount<Currency>>(old)).isEqualTo(Amount(2_500_000_000, USD))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Amount(Currency) Text deserialization`() {
|
||||||
assertThat(mapper.convertValue<Amount<Currency>>(TextNode("$25000000"))).isEqualTo(Amount(2_500_000_000, USD))
|
assertThat(mapper.convertValue<Amount<Currency>>(TextNode("$25000000"))).isEqualTo(Amount(2_500_000_000, USD))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,9 +13,10 @@ buildscript {
|
|||||||
// Spring Boot plugin adds a numerous hardcoded dependencies in the version much lower then Corda expects
|
// Spring Boot plugin adds a numerous hardcoded dependencies in the version much lower then Corda expects
|
||||||
// causing the problems in runtime. Those can be changed by manipulating above properties
|
// causing the problems in runtime. Those can be changed by manipulating above properties
|
||||||
// See https://github.com/spring-gradle-plugins/dependency-management-plugin/blob/master/README.md#changing-the-value-of-a-version-property
|
// See https://github.com/spring-gradle-plugins/dependency-management-plugin/blob/master/README.md#changing-the-value-of-a-version-property
|
||||||
ext['artemis.version'] = "$artemis_version"
|
ext['artemis.version'] = artemis_version
|
||||||
ext['hibernate.version'] = "$hibernate_version"
|
ext['hibernate.version'] = hibernate_version
|
||||||
ext['selenium.version'] = "$selenium_version"
|
ext['selenium.version'] = selenium_version
|
||||||
|
ext['jackson.version'] = jackson_version
|
||||||
|
|
||||||
apply plugin: 'java'
|
apply plugin: 'java'
|
||||||
apply plugin: 'kotlin'
|
apply plugin: 'kotlin'
|
||||||
|
@ -64,7 +64,7 @@ dependencies {
|
|||||||
exclude module: "spring-boot-starter-logging"
|
exclude module: "spring-boot-starter-logging"
|
||||||
exclude module: "logback-classic"
|
exclude module: "logback-classic"
|
||||||
}
|
}
|
||||||
compile("com.fasterxml.jackson.module:jackson-module-kotlin:2.8.9")
|
compile "com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version"
|
||||||
compile project(":client:rpc")
|
compile project(":client:rpc")
|
||||||
compile project(":client:jackson")
|
compile project(":client:jackson")
|
||||||
compile project(":test-utils")
|
compile project(":test-utils")
|
||||||
|
@ -14,8 +14,9 @@ buildscript {
|
|||||||
// causing the problems in runtime. Those can be changed by manipulating above properties
|
// causing the problems in runtime. Those can be changed by manipulating above properties
|
||||||
// See https://github.com/spring-gradle-plugins/dependency-management-plugin/blob/master/README.md#changing-the-value-of-a-version-property
|
// See https://github.com/spring-gradle-plugins/dependency-management-plugin/blob/master/README.md#changing-the-value-of-a-version-property
|
||||||
// This has to be repeated here as otherwise the order of files does matter
|
// This has to be repeated here as otherwise the order of files does matter
|
||||||
ext['artemis.version'] = "$artemis_version"
|
ext['artemis.version'] = artemis_version
|
||||||
ext['hibernate.version'] = "$hibernate_version"
|
ext['hibernate.version'] = hibernate_version
|
||||||
|
ext['jackson.version'] = jackson_version
|
||||||
|
|
||||||
|
|
||||||
apply plugin: 'java'
|
apply plugin: 'java'
|
||||||
@ -36,8 +37,8 @@ dependencies {
|
|||||||
testCompile "junit:junit:$junit_version"
|
testCompile "junit:junit:$junit_version"
|
||||||
|
|
||||||
// Corda integration dependencies
|
// Corda integration dependencies
|
||||||
compile project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
||||||
compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts')
|
runtime project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts')
|
||||||
compile project(':core')
|
compile project(':core')
|
||||||
compile project(':finance')
|
compile project(':finance')
|
||||||
compile project(':node-driver')
|
compile project(':node-driver')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user