diff --git a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt index 4439cb0be5..3fa315bbb6 100644 --- a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt @@ -23,6 +23,7 @@ import net.i2p.crypto.eddsa.EdDSAPublicKey import org.bouncycastle.asn1.x500.X500Name import java.math.BigDecimal import java.security.PublicKey +import java.time.LocalDate import java.util.* /** @@ -76,6 +77,7 @@ object JacksonSupport { addSerializer(SecureHash.SHA256::class.java, SecureHashSerializer) addDeserializer(SecureHash::class.java, SecureHashDeserializer()) addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer()) + addSerializer(BusinessCalendar::class.java, CalendarSerializer) addDeserializer(BusinessCalendar::class.java, CalendarDeserializer) // For ed25519 pubkeys @@ -243,11 +245,31 @@ object JacksonSupport { } } + data class BusinessCalendarWrapper(val holidayDates: List) { + fun toCalendar() = BusinessCalendar(holidayDates) + } + + object CalendarSerializer : JsonSerializer() { + override fun serialize(obj: BusinessCalendar, generator: JsonGenerator, context: SerializerProvider) { + println(obj) + val calendarName = BusinessCalendar.calendars.find { BusinessCalendar.getInstance(it) == obj } + if(calendarName != null) { + generator.writeString(calendarName) + } else { + generator.writeObject(BusinessCalendarWrapper(obj.holidayDates)) + } + } + } + object CalendarDeserializer : JsonDeserializer() { override fun deserialize(parser: JsonParser, context: DeserializationContext): BusinessCalendar { return try { - val array = StringArrayDeserializer.instance.deserialize(parser, context) - BusinessCalendar.getInstance(*array) + try { + val array = StringArrayDeserializer.instance.deserialize(parser, context) + BusinessCalendar.getInstance(*array) + } catch (e: Exception) { + parser.readValueAs(BusinessCalendarWrapper::class.java).toCalendar() + } } catch (e: Exception) { throw JsonParseException(parser, "Invalid calendar(s) ${parser.text}: ${e.message}") } diff --git a/core/src/main/kotlin/net/corda/core/contracts/FinanceTypes.kt b/core/src/main/kotlin/net/corda/core/contracts/FinanceTypes.kt index 9ccff91150..0967b0a644 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/FinanceTypes.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/FinanceTypes.kt @@ -633,7 +633,7 @@ fun LocalDate.isWorkingDay(accordingToCalendar: BusinessCalendar): Boolean = acc * no staff are around to handle problems. */ @CordaSerializable -open class BusinessCalendar private constructor(val holidayDates: List) { +open class BusinessCalendar(val holidayDates: List) { @CordaSerializable class UnknownCalendar(name: String) : Exception("$name not found") diff --git a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt index 29ed213ea3..ffd3fa8642 100644 --- a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt +++ b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt @@ -4,7 +4,9 @@ import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures import net.corda.client.rpc.CordaRPCClient import net.corda.core.getOrThrow +import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.services.ServiceInfo +import net.corda.core.toFuture import net.corda.core.utilities.DUMMY_BANK_A import net.corda.core.utilities.DUMMY_BANK_B import net.corda.core.utilities.DUMMY_NOTARY @@ -22,14 +24,18 @@ import net.corda.testing.http.HttpApi import org.apache.commons.io.IOUtils import org.assertj.core.api.Assertions.assertThat import org.junit.Test +import rx.Observable import rx.observables.BlockingObservable import java.net.URL +import java.time.Duration import java.time.LocalDate +import java.time.temporal.ChronoUnit class IRSDemoTest : IntegrationTestCategory { val rpcUser = User("user", "password", emptySet()) val currentDate: LocalDate = LocalDate.now() val futureDate: LocalDate = currentDate.plusMonths(6) + val maxWaitTime: Duration = Duration.of(60, ChronoUnit.SECONDS) @Test fun `runs IRS demo`() { @@ -50,51 +56,50 @@ class IRSDemoTest : IntegrationTestCategory { println("All webservers started") + val (controllerApi, nodeAApi, nodeBApi) = listOf(controller, nodeA, nodeB).zip(listOf(controllerAddr, nodeAAddr, nodeBAddr)).map { + val mapper = net.corda.jackson.JacksonSupport.createDefaultMapper(it.first.rpc) + HttpApi.fromHostAndPort(it.second, "api/irs", mapper = mapper) + } val nextFixingDates = getFixingDateObservable(nodeA.configuration) - val numADeals = getTradeCount(nodeAAddr) - val numBDeals = getTradeCount(nodeBAddr) + val numADeals = getTradeCount(nodeAApi) + val numBDeals = getTradeCount(nodeBApi) runUploadRates(controllerAddr) - runTrade(nodeAAddr) + runTrade(nodeAApi) - assertThat(getTradeCount(nodeAAddr)).isEqualTo(numADeals + 1) - assertThat(getTradeCount(nodeBAddr)).isEqualTo(numBDeals + 1) + assertThat(getTradeCount(nodeAApi)).isEqualTo(numADeals + 1) + assertThat(getTradeCount(nodeBApi)).isEqualTo(numBDeals + 1) // Wait until the initial trade and all scheduled fixings up to the current date have finished - nextFixingDates.first { it == null || it > currentDate } + nextFixingDates.firstWithTimeout(maxWaitTime){ it == null || it > currentDate } + runDateChange(nodeBApi) + nextFixingDates.firstWithTimeout(maxWaitTime) { it == null || it > futureDate } - runDateChange(nodeBAddr) - nextFixingDates.first { it == null || it > futureDate } - - assertThat(getTrades(nodeAAddr)[0] as InterestRateSwap.State) + assertThat(getTrades(nodeAApi)[0]) } } - fun getFixingDateObservable(config: FullNodeConfiguration): BlockingObservable { + fun getFixingDateObservable(config: FullNodeConfiguration): Observable { val client = CordaRPCClient(config.rpcAddress!!) val proxy = client.start("user", "password").proxy val vaultUpdates = proxy.vaultAndUpdates().second - val fixingDates = vaultUpdates.map { update -> + return vaultUpdates.map { update -> val irsStates = update.produced.map { it.state.data }.filterIsInstance() irsStates.mapNotNull { it.calculation.nextFixingDate() }.max() - }.cache().toBlocking() - - return fixingDates + }.cache() } - private fun runDateChange(nodeAddr: HostAndPort) { - println("Running date change against $nodeAddr") - val url = URL("http://$nodeAddr/api/irs/demodate") - assertThat(putJson(url, "\"$futureDate\"")).isTrue() + private fun runDateChange(nodeApi: HttpApi) { + println("Running date change against ${nodeApi.root}") + assertThat(nodeApi.putJson("demodate", "\"$futureDate\"")).isTrue() } - private fun runTrade(nodeAddr: HostAndPort) { - println("Running trade against $nodeAddr") + private fun runTrade(nodeApi: HttpApi) { + println("Running trade against ${nodeApi.root}") val fileContents = loadResourceFile("net/corda/irs/simulation/example-irs-trade.json") val tradeFile = fileContents.replace("tradeXXX", "trade1") - val url = URL("http://$nodeAddr/api/irs/deals") - assertThat(postJson(url, tradeFile)).isTrue() + assertThat(nodeApi.postJson("deals", tradeFile)).isTrue() } private fun runUploadRates(host: HostAndPort) { @@ -108,17 +113,19 @@ class IRSDemoTest : IntegrationTestCategory { return IOUtils.toString(Thread.currentThread().contextClassLoader.getResourceAsStream(filename), Charsets.UTF_8.name()) } - private fun getTradeCount(nodeAddr: HostAndPort): Int { - println("Getting trade count from $nodeAddr") - val api = HttpApi.fromHostAndPort(nodeAddr, "api/irs") - val deals = api.getJson>("deals") + private fun getTradeCount(nodeApi: HttpApi): Int { + println("Getting trade count from ${nodeApi.root}") + val deals = nodeApi.getJson>("deals") return deals.size } - private fun getTrades(nodeAddr: HostAndPort): Array<*> { - println("Getting trades from $nodeAddr") - val api = HttpApi.fromHostAndPort(nodeAddr, "api/irs") - val deals = api.getJson>("deals") + private fun getTrades(nodeApi: HttpApi): Array { + println("Getting trades from ${nodeApi.root}") + val deals = nodeApi.getJson>("deals") return deals } + + fun Observable.firstWithTimeout(timeout: Duration, pred: (T) -> Boolean) { + first(pred).toFuture().getOrThrow(timeout) + } } diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt index d9a9abe266..f929e2bce0 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt @@ -1,5 +1,8 @@ package net.corda.irs.contract +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty import net.corda.core.contracts.* import net.corda.core.contracts.clauses.* import net.corda.core.crypto.SecureHash @@ -129,6 +132,7 @@ class FixedRatePaymentEvent(date: LocalDate, * If the rate is null returns a zero payment. // TODO: Is this the desired behaviour? */ @CordaSerializable +@JsonIgnoreProperties(ignoreUnknown = true) class FloatingRatePaymentEvent(date: LocalDate, accrualStartDate: LocalDate, accrualEndDate: LocalDate, diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt index 809cb7c05f..9e1829976d 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt @@ -1,5 +1,6 @@ package net.corda.irs.contract +import com.fasterxml.jackson.annotation.JsonIgnore import net.corda.core.contracts.Amount import net.corda.core.contracts.Tenor import net.corda.core.serialization.CordaSerializable @@ -63,6 +64,7 @@ open class Rate(val ratioUnit: RatioUnit? = null) { */ @CordaSerializable class FixedRate(ratioUnit: RatioUnit) : Rate(ratioUnit) { + @JsonIgnore fun isPositive(): Boolean = ratioUnit!!.value > BigDecimal("0.0") override fun equals(other: Any?) = other?.javaClass == javaClass && super.equals(other) diff --git a/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt b/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt index c6f7da5705..42b3221264 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt @@ -1,9 +1,10 @@ package net.corda.testing.http +import com.fasterxml.jackson.databind.ObjectMapper import com.google.common.net.HostAndPort import java.net.URL -class HttpApi(val root: URL) { +class HttpApi(val root: URL, val mapper: ObjectMapper = defaultMapper) { /** * Send a PUT with a payload to the path on the API specified. * @@ -21,12 +22,15 @@ class HttpApi(val root: URL) { /** * Send a GET request to the path on the API specified. */ - inline fun getJson(path: String, params: Map = mapOf()) = HttpUtils.getJson(URL(root, path), params) + inline fun getJson(path: String, params: Map = mapOf()) = HttpUtils.getJson(URL(root, path), params, mapper) private fun toJson(any: Any) = any as? String ?: HttpUtils.defaultMapper.writeValueAsString(any) companion object { - fun fromHostAndPort(hostAndPort: HostAndPort, base: String, protocol: String = "http"): HttpApi - = HttpApi(URL("$protocol://$hostAndPort/$base/")) + fun fromHostAndPort(hostAndPort: HostAndPort, base: String, protocol: String = "http", mapper: ObjectMapper = defaultMapper): HttpApi + = HttpApi(URL("$protocol://$hostAndPort/$base/"), mapper) + private val defaultMapper: ObjectMapper by lazy { + net.corda.jackson.JacksonSupport.createNonRpcMapper() + } } } diff --git a/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt b/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt index 9b2e454f13..94eef77537 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt @@ -35,10 +35,10 @@ object HttpUtils { return makeRequest(Request.Builder().url(url).header("Content-Type", "application/json").post(body).build()) } - inline fun getJson(url: URL, params: Map = mapOf()): T { + inline fun getJson(url: URL, params: Map = mapOf(), mapper: ObjectMapper = defaultMapper): T { val paramString = if (params.isEmpty()) "" else "?" + params.map { "${it.key}=${it.value}" }.joinToString("&") val parameterisedUrl = URL(url.toExternalForm() + paramString) - return defaultMapper.readValue(parameterisedUrl, T::class.java) + return mapper.readValue(parameterisedUrl, T::class.java) } private fun makeRequest(request: Request): Boolean {