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 af64d56aa4..30f762f4f2 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 @@ -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>() { 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) } else { val wrapper = parser.readValueAs() diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt index 606df580e4..5062974a25 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt @@ -2,13 +2,17 @@ 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.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.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.node.ObjectNode 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.utilities.ByteSequence import net.corda.core.utilities.NetworkHostAndPort +import java.math.BigDecimal import java.security.PublicKey import java.security.cert.CertPath +import java.util.* class CordaModule : SimpleModule("corda-core") { override fun setupModule(context: SetupContext) { super.setupModule(context) + context.addBeanDeserializerModifier(AmountBeanDeserializerModifier()) + context.setMixInAnnotations(PartyAndCertificate::class.java, PartyAndCertificateMixin::class.java) context.setMixInAnnotations(NetworkHostAndPort::class.java, NetworkHostAndPortMixin::class.java) context.setMixInAnnotations(CordaX500Name::class.java, CordaX500NameMixin::class.java) @@ -134,9 +142,78 @@ private interface SecureHashSHA256Mixin @JsonDeserialize(using = JacksonSupport.PublicKeyDeserializer::class) private interface PublicKeyMixin +@Suppress("unused_parameter") @ToStringSerialize -@JsonDeserialize(using = JacksonSupport.AmountDeserializer::class) -private interface AmountMixin +private abstract class AmountMixin @JsonCreator(mode = DISABLED) constructor( + 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(), 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, 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) @JsonSerialize(using = ByteSequenceSerializer::class) diff --git a/client/jackson/src/test/kotlin/net/corda/client/jackson/AmountTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/AmountTest.kt new file mode 100644 index 0000000000..967ef28df6 --- /dev/null +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/AmountTest.kt @@ -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>(str, object : TypeReference>() {}) + 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>(str, object : TypeReference>() {}) + 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>(str, object : TypeReference>() {}) + 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>(str, object : TypeReference>() {}) + 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>(str, object : TypeReference>() {}) + 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>(str, object : TypeReference>() {}) + assertThat(amount.quantity).isEqualTo(100) + assertThat(amount.token).isEqualTo(Currency.getInstance("USD")) + } + + @Test + fun `Amount(Currency) YAML serialization`() { + assertThat(yamlMapper.valueToTree(Amount.parseCurrency("£25000000"))).isEqualTo(TextNode("25000000.00 GBP")) + assertThat(yamlMapper.valueToTree(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) +} \ No newline at end of file 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 8cb5e3823f..d61e0179dd 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 @@ -87,10 +87,14 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: @Test fun `Amount(Currency) deserialization`() { val old = mapOf( - "quantity" to 2500000000, - "token" to "USD" + "quantity" to 2500000000, + "token" to "USD" ) assertThat(mapper.convertValue>(old)).isEqualTo(Amount(2_500_000_000, USD)) + } + + @Test + fun `Amount(Currency) Text deserialization`() { assertThat(mapper.convertValue>(TextNode("$25000000"))).isEqualTo(Amount(2_500_000_000, USD)) } diff --git a/samples/irs-demo/build.gradle b/samples/irs-demo/build.gradle index 0f3959d29c..737d4f2082 100644 --- a/samples/irs-demo/build.gradle +++ b/samples/irs-demo/build.gradle @@ -13,9 +13,10 @@ buildscript { // 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 // 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['hibernate.version'] = "$hibernate_version" -ext['selenium.version'] = "$selenium_version" +ext['artemis.version'] = artemis_version +ext['hibernate.version'] = hibernate_version +ext['selenium.version'] = selenium_version +ext['jackson.version'] = jackson_version apply plugin: 'java' apply plugin: 'kotlin' diff --git a/samples/irs-demo/web/build.gradle b/samples/irs-demo/web/build.gradle index 273b835310..d540f64f28 100644 --- a/samples/irs-demo/web/build.gradle +++ b/samples/irs-demo/web/build.gradle @@ -64,7 +64,7 @@ dependencies { exclude module: "spring-boot-starter-logging" 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:jackson") compile project(":test-utils") diff --git a/samples/network-visualiser/build.gradle b/samples/network-visualiser/build.gradle index 9767030c79..46b50e36a7 100644 --- a/samples/network-visualiser/build.gradle +++ b/samples/network-visualiser/build.gradle @@ -14,8 +14,9 @@ buildscript { // 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 // This has to be repeated here as otherwise the order of files does matter -ext['artemis.version'] = "$artemis_version" -ext['hibernate.version'] = "$hibernate_version" +ext['artemis.version'] = artemis_version +ext['hibernate.version'] = hibernate_version +ext['jackson.version'] = jackson_version apply plugin: 'java' @@ -36,8 +37,8 @@ dependencies { testCompile "junit:junit:$junit_version" // Corda integration dependencies - compile project(path: ":node:capsule", configuration: 'runtimeArtifacts') - compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') + runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts') + runtime project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') compile project(':core') compile project(':finance') compile project(':node-driver')