mirror of
https://github.com/corda/corda.git
synced 2025-06-22 17:09:00 +00:00
JacksonSupport: add support for Amount<Currency> and OpaqueBytes
This commit is contained in:
@ -10,15 +10,18 @@ import com.fasterxml.jackson.databind.deser.std.StringArrayDeserializer
|
|||||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||||
|
import net.corda.core.contracts.Amount
|
||||||
import net.corda.core.contracts.BusinessCalendar
|
import net.corda.core.contracts.BusinessCalendar
|
||||||
import net.corda.core.crypto.*
|
import net.corda.core.crypto.*
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.services.IdentityService
|
import net.corda.core.node.services.IdentityService
|
||||||
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utilities and serialisers for working with JSON representations of basic types. This adds Jackson support for
|
* Utilities and serialisers for working with JSON representations of basic types. This adds Jackson support for
|
||||||
@ -71,6 +74,14 @@ object JacksonSupport {
|
|||||||
// TODO this tunnels the Kryo representation as a Base58 encoded string. Replace when RPC supports this.
|
// TODO this tunnels the Kryo representation as a Base58 encoded string. Replace when RPC supports this.
|
||||||
addSerializer(NodeInfo::class.java, NodeInfoSerializer)
|
addSerializer(NodeInfo::class.java, NodeInfoSerializer)
|
||||||
addDeserializer(NodeInfo::class.java, NodeInfoDeserializer)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,5 +235,43 @@ object JacksonSupport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object OpaqueBytesSerializer : JsonSerializer<OpaqueBytes>() {
|
||||||
|
override fun serialize(value: OpaqueBytes, gen: JsonGenerator, serializers: SerializerProvider) {
|
||||||
|
gen.writeBinary(value.bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
package net.corda.jackson
|
package net.corda.jackson
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature
|
||||||
import com.pholser.junit.quickcheck.From
|
import com.pholser.junit.quickcheck.From
|
||||||
import com.pholser.junit.quickcheck.Property
|
import com.pholser.junit.quickcheck.Property
|
||||||
import com.pholser.junit.quickcheck.runner.JUnitQuickcheck
|
import com.pholser.junit.quickcheck.runner.JUnitQuickcheck
|
||||||
|
import net.corda.core.contracts.Amount
|
||||||
|
import net.corda.core.contracts.USD
|
||||||
import net.corda.core.testing.PublicKeyGenerator
|
import net.corda.core.testing.PublicKeyGenerator
|
||||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||||
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
import java.util.*
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
@RunWith(JUnitQuickcheck::class)
|
@RunWith(JUnitQuickcheck::class)
|
||||||
@ -21,4 +26,28 @@ class JacksonSupportTest {
|
|||||||
val parsedKey = mapper.readValue(serialized, EdDSAPublicKey::class.java)
|
val parsedKey = mapper.readValue(serialized, EdDSAPublicKey::class.java)
|
||||||
assertEquals(publicKey, parsedKey)
|
assertEquals(publicKey, parsedKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class Dummy(val notional: Amount<Currency>)
|
||||||
|
|
||||||
|
@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"))))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,17 @@ Changelog
|
|||||||
|
|
||||||
Here are brief summaries of what's changed between each snapshot release.
|
Here are brief summaries of what's changed between each snapshot release.
|
||||||
|
|
||||||
|
UNRELEASED
|
||||||
|
----------
|
||||||
|
|
||||||
|
API changes:
|
||||||
|
|
||||||
|
* The new Jackson module provides JSON/YAML serialisers for common Corda datatypes. If you have previously been
|
||||||
|
using the JSON support in the standalone web server, please be aware that amounts are now serialised as strings
|
||||||
|
instead of { quantity, token } pairs as before. The old format is still accepted, but new JSON will be produced
|
||||||
|
using strings like "1000.00 USD" when writing. You can use any format supported by ``Amount.parseCurrency``
|
||||||
|
as input.
|
||||||
|
|
||||||
Milestone 9.1
|
Milestone 9.1
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
{
|
{
|
||||||
"fixedLeg": {
|
"fixedLeg": {
|
||||||
"fixedRatePayer": "fixedRatePayerKey",
|
"fixedRatePayer": "fixedRatePayerKey",
|
||||||
"notional": {
|
"notional": "€25000000",
|
||||||
"quantity": 2500000000,
|
|
||||||
"token": "EUR"
|
|
||||||
},
|
|
||||||
"paymentFrequency": "SemiAnnual",
|
"paymentFrequency": "SemiAnnual",
|
||||||
"effectiveDate": "2016-03-11",
|
"effectiveDate": "2016-03-11",
|
||||||
"effectiveDateAdjustment": null,
|
"effectiveDateAdjustment": null,
|
||||||
@ -26,10 +23,7 @@
|
|||||||
},
|
},
|
||||||
"floatingLeg": {
|
"floatingLeg": {
|
||||||
"floatingRatePayer": "floatingRatePayerKey",
|
"floatingRatePayer": "floatingRatePayerKey",
|
||||||
"notional": {
|
"notional": "€25000000",
|
||||||
"quantity": 2500000000,
|
|
||||||
"token": "EUR"
|
|
||||||
},
|
|
||||||
"paymentFrequency": "Quarterly",
|
"paymentFrequency": "Quarterly",
|
||||||
"effectiveDate": "2016-03-11",
|
"effectiveDate": "2016-03-11",
|
||||||
"effectiveDateAdjustment": null,
|
"effectiveDateAdjustment": null,
|
||||||
@ -66,22 +60,10 @@
|
|||||||
"baseCurrency": "EUR",
|
"baseCurrency": "EUR",
|
||||||
"eligibleCurrency": "EUR",
|
"eligibleCurrency": "EUR",
|
||||||
"eligibleCreditSupport": "Cash in an Eligible Currency",
|
"eligibleCreditSupport": "Cash in an Eligible Currency",
|
||||||
"independentAmounts": {
|
"independentAmounts": "0 EUR",
|
||||||
"quantity": 0,
|
"threshold": "0 EUR",
|
||||||
"token": "EUR"
|
"minimumTransferAmount": "250000 EUR",
|
||||||
},
|
"rounding": "10000 EUR",
|
||||||
"threshold": {
|
|
||||||
"quantity": 0,
|
|
||||||
"token": "EUR"
|
|
||||||
},
|
|
||||||
"minimumTransferAmount": {
|
|
||||||
"quantity": 25000000,
|
|
||||||
"token": "EUR"
|
|
||||||
},
|
|
||||||
"rounding": {
|
|
||||||
"quantity": 1000000,
|
|
||||||
"token": "EUR"
|
|
||||||
},
|
|
||||||
"valuationDateDescription": "Every Local Business Day",
|
"valuationDateDescription": "Every Local Business Day",
|
||||||
"notificationTime": "2:00pm London",
|
"notificationTime": "2:00pm London",
|
||||||
"resolutionTime": "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
|
"resolutionTime": "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
{
|
{
|
||||||
"fixedLeg": {
|
"fixedLeg": {
|
||||||
"fixedRatePayer": "3ThWzJauCq7qLrcX4KuKHxKnxZ6HoxnxU7pFL1HwfCkCJLUfTJ9zN92oxRLxnw",
|
"fixedRatePayer": "3ThWzJauCq7qLrcX4KuKHxKnxZ6HoxnxU7pFL1HwfCkCJLUfTJ9zN92oxRLxnw",
|
||||||
"notional": {
|
"notional": "$25000000",
|
||||||
"quantity": 2500000000,
|
|
||||||
"token": "USD"
|
|
||||||
},
|
|
||||||
"paymentFrequency": "SemiAnnual",
|
"paymentFrequency": "SemiAnnual",
|
||||||
"effectiveDate": "2016-03-11",
|
"effectiveDate": "2016-03-11",
|
||||||
"effectiveDateAdjustment": null,
|
"effectiveDateAdjustment": null,
|
||||||
@ -66,22 +63,10 @@
|
|||||||
"baseCurrency": "EUR",
|
"baseCurrency": "EUR",
|
||||||
"eligibleCurrency": "EUR",
|
"eligibleCurrency": "EUR",
|
||||||
"eligibleCreditSupport": "Cash in an Eligible Currency",
|
"eligibleCreditSupport": "Cash in an Eligible Currency",
|
||||||
"independentAmounts": {
|
"independentAmounts": "€0",
|
||||||
"quantity": 0,
|
"threshold": "€0",
|
||||||
"token": "EUR"
|
"minimumTransferAmount": "250000 EUR",
|
||||||
},
|
"rounding": "10000 EUR",
|
||||||
"threshold": {
|
|
||||||
"quantity": 0,
|
|
||||||
"token": "EUR"
|
|
||||||
},
|
|
||||||
"minimumTransferAmount": {
|
|
||||||
"quantity": 25000000,
|
|
||||||
"token": "EUR"
|
|
||||||
},
|
|
||||||
"rounding": {
|
|
||||||
"quantity": 1000000,
|
|
||||||
"token": "EUR"
|
|
||||||
},
|
|
||||||
"valuationDateDescription": "Every Local Business Day",
|
"valuationDateDescription": "Every Local Business Day",
|
||||||
"notificationTime": "2:00pm London",
|
"notificationTime": "2:00pm London",
|
||||||
"resolutionTime": "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
|
"resolutionTime": "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
|
||||||
|
Reference in New Issue
Block a user