Merge pull request #834 from corda/clint-irswebfix

IRS web demo now shows fixings + general IRS fixes
This commit is contained in:
Clinton 2017-06-15 17:49:43 +01:00 committed by GitHub
commit a6853be035
15 changed files with 167 additions and 52 deletions

View File

@ -23,6 +23,7 @@ import net.i2p.crypto.eddsa.EdDSAPublicKey
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import java.math.BigDecimal import java.math.BigDecimal
import java.security.PublicKey import java.security.PublicKey
import java.time.LocalDate
import java.util.* import java.util.*
/** /**
@ -80,6 +81,7 @@ object JacksonSupport {
addSerializer(SecureHash.SHA256::class.java, SecureHashSerializer) addSerializer(SecureHash.SHA256::class.java, SecureHashSerializer)
addDeserializer(SecureHash::class.java, SecureHashDeserializer()) addDeserializer(SecureHash::class.java, SecureHashDeserializer())
addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer()) addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer())
addSerializer(BusinessCalendar::class.java, CalendarSerializer)
addDeserializer(BusinessCalendar::class.java, CalendarDeserializer) addDeserializer(BusinessCalendar::class.java, CalendarDeserializer)
// For ed25519 pubkeys // For ed25519 pubkeys
@ -268,11 +270,30 @@ object JacksonSupport {
} }
} }
data class BusinessCalendarWrapper(val holidayDates: List<LocalDate>) {
fun toCalendar() = BusinessCalendar(holidayDates)
}
object CalendarSerializer : JsonSerializer<BusinessCalendar>() {
override fun serialize(obj: BusinessCalendar, generator: JsonGenerator, context: SerializerProvider) {
val calendarName = BusinessCalendar.calendars.find { BusinessCalendar.getInstance(it) == obj }
if(calendarName != null) {
generator.writeString(calendarName)
} else {
generator.writeObject(BusinessCalendarWrapper(obj.holidayDates))
}
}
}
object CalendarDeserializer : JsonDeserializer<BusinessCalendar>() { object CalendarDeserializer : JsonDeserializer<BusinessCalendar>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): BusinessCalendar { override fun deserialize(parser: JsonParser, context: DeserializationContext): BusinessCalendar {
return try { return try {
val array = StringArrayDeserializer.instance.deserialize(parser, context) try {
BusinessCalendar.getInstance(*array) val array = StringArrayDeserializer.instance.deserialize(parser, context)
BusinessCalendar.getInstance(*array)
} catch (e: Exception) {
parser.readValueAs(BusinessCalendarWrapper::class.java).toCalendar()
}
} catch (e: Exception) { } catch (e: Exception) {
throw JsonParseException(parser, "Invalid calendar(s) ${parser.text}: ${e.message}") throw JsonParseException(parser, "Invalid calendar(s) ${parser.text}: ${e.message}")
} }

View File

@ -633,7 +633,7 @@ fun LocalDate.isWorkingDay(accordingToCalendar: BusinessCalendar): Boolean = acc
* no staff are around to handle problems. * no staff are around to handle problems.
*/ */
@CordaSerializable @CordaSerializable
open class BusinessCalendar private constructor(val holidayDates: List<LocalDate>) { open class BusinessCalendar(val holidayDates: List<LocalDate>) {
@CordaSerializable @CordaSerializable
class UnknownCalendar(name: String) : Exception("$name not found") class UnknownCalendar(name: String) : Exception("$name not found")

View File

@ -4,7 +4,9 @@ import com.google.common.net.HostAndPort
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClient
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.node.services.ServiceInfo 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_A
import net.corda.core.utilities.DUMMY_BANK_B import net.corda.core.utilities.DUMMY_BANK_B
import net.corda.core.utilities.DUMMY_NOTARY 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.apache.commons.io.IOUtils
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.Test import org.junit.Test
import rx.Observable
import rx.observables.BlockingObservable import rx.observables.BlockingObservable
import java.net.URL import java.net.URL
import java.time.Duration
import java.time.LocalDate import java.time.LocalDate
import java.time.temporal.ChronoUnit
class IRSDemoTest : IntegrationTestCategory { class IRSDemoTest : IntegrationTestCategory {
val rpcUser = User("user", "password", emptySet()) val rpcUser = User("user", "password", emptySet())
val currentDate: LocalDate = LocalDate.now() val currentDate: LocalDate = LocalDate.now()
val futureDate: LocalDate = currentDate.plusMonths(6) val futureDate: LocalDate = currentDate.plusMonths(6)
val maxWaitTime: Duration = Duration.of(60, ChronoUnit.SECONDS)
@Test @Test
fun `runs IRS demo`() { fun `runs IRS demo`() {
@ -40,56 +46,67 @@ class IRSDemoTest : IntegrationTestCategory {
startNode(DUMMY_BANK_B.name) startNode(DUMMY_BANK_B.name)
).getOrThrow() ).getOrThrow()
println("All nodes started")
val (controllerAddr, nodeAAddr, nodeBAddr) = Futures.allAsList( val (controllerAddr, nodeAAddr, nodeBAddr) = Futures.allAsList(
startWebserver(controller), startWebserver(controller),
startWebserver(nodeA), startWebserver(nodeA),
startWebserver(nodeB) startWebserver(nodeB)
).getOrThrow().map { it.listenAddress } ).getOrThrow().map { it.listenAddress }
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 nextFixingDates = getFixingDateObservable(nodeA.configuration)
val numADeals = getTradeCount(nodeAAddr) val numADeals = getTradeCount(nodeAApi)
val numBDeals = getTradeCount(nodeBAddr) val numBDeals = getTradeCount(nodeBApi)
runUploadRates(controllerAddr) runUploadRates(controllerAddr)
runTrade(nodeAAddr) runTrade(nodeAApi)
assertThat(getTradeCount(nodeAAddr)).isEqualTo(numADeals + 1) assertThat(getTradeCount(nodeAApi)).isEqualTo(numADeals + 1)
assertThat(getTradeCount(nodeBAddr)).isEqualTo(numBDeals + 1) assertThat(getTradeCount(nodeBApi)).isEqualTo(numBDeals + 1)
assertThat(getFloatingLegFixCount(nodeAApi) == 0)
// Wait until the initial trade and all scheduled fixings up to the current date have finished // 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) assertThat(getFloatingLegFixCount(nodeAApi) > 0)
nextFixingDates.first { it == null || it > futureDate }
} }
} }
fun getFixingDateObservable(config: FullNodeConfiguration): BlockingObservable<LocalDate?> { fun getFloatingLegFixCount(nodeApi: HttpApi) = getTrades(nodeApi)[0].calculation.floatingLegPaymentSchedule.count { it.value.rate.ratioUnit != null }
fun getFixingDateObservable(config: FullNodeConfiguration): Observable<LocalDate?> {
val client = CordaRPCClient(config.rpcAddress!!) val client = CordaRPCClient(config.rpcAddress!!)
val proxy = client.start("user", "password").proxy val proxy = client.start("user", "password").proxy
val vaultUpdates = proxy.vaultAndUpdates().second val vaultUpdates = proxy.vaultAndUpdates().second
val fixingDates = vaultUpdates.map { update -> return vaultUpdates.map { update ->
val irsStates = update.produced.map { it.state.data }.filterIsInstance<InterestRateSwap.State>() val irsStates = update.produced.map { it.state.data }.filterIsInstance<InterestRateSwap.State>()
irsStates.mapNotNull { it.calculation.nextFixingDate() }.max() irsStates.mapNotNull { it.calculation.nextFixingDate() }.max()
}.cache().toBlocking() }.cache()
return fixingDates
} }
private fun runDateChange(nodeAddr: HostAndPort) { private fun runDateChange(nodeApi: HttpApi) {
val url = URL("http://$nodeAddr/api/irs/demodate") println("Running date change against ${nodeApi.root}")
assertThat(putJson(url, "\"$futureDate\"")).isTrue() assertThat(nodeApi.putJson("demodate", "\"$futureDate\"")).isTrue()
} }
private fun runTrade(nodeAddr: HostAndPort) { private fun runTrade(nodeApi: HttpApi) {
println("Running trade against ${nodeApi.root}")
val fileContents = loadResourceFile("net/corda/irs/simulation/example-irs-trade.json") val fileContents = loadResourceFile("net/corda/irs/simulation/example-irs-trade.json")
val tradeFile = fileContents.replace("tradeXXX", "trade1") val tradeFile = fileContents.replace("tradeXXX", "trade1")
val url = URL("http://$nodeAddr/api/irs/deals") assertThat(nodeApi.postJson("deals", tradeFile)).isTrue()
assertThat(postJson(url, tradeFile)).isTrue()
} }
private fun runUploadRates(host: HostAndPort) { private fun runUploadRates(host: HostAndPort) {
println("Running upload rates against $host")
val fileContents = loadResourceFile("net/corda/irs/simulation/example.rates.txt") val fileContents = loadResourceFile("net/corda/irs/simulation/example.rates.txt")
val url = URL("http://$host/upload/interest-rates") val url = URL("http://$host/upload/interest-rates")
assertThat(uploadFile(url, fileContents)).isTrue() assertThat(uploadFile(url, fileContents)).isTrue()
@ -99,9 +116,19 @@ class IRSDemoTest : IntegrationTestCategory {
return IOUtils.toString(Thread.currentThread().contextClassLoader.getResourceAsStream(filename), Charsets.UTF_8.name()) return IOUtils.toString(Thread.currentThread().contextClassLoader.getResourceAsStream(filename), Charsets.UTF_8.name())
} }
private fun getTradeCount(nodeAddr: HostAndPort): Int { private fun getTradeCount(nodeApi: HttpApi): Int {
val api = HttpApi.fromHostAndPort(nodeAddr, "api/irs") println("Getting trade count from ${nodeApi.root}")
val deals = api.getJson<Array<*>>("deals") val deals = nodeApi.getJson<Array<*>>("deals")
return deals.size return deals.size
} }
private fun getTrades(nodeApi: HttpApi): Array<InterestRateSwap.State> {
println("Getting trades from ${nodeApi.root}")
val deals = nodeApi.getJson<Array<InterestRateSwap.State>>("deals")
return deals
}
fun<T> Observable<T>.firstWithTimeout(timeout: Duration, pred: (T) -> Boolean) {
first(pred).toFuture().getOrThrow(timeout)
}
} }

View File

@ -1,5 +1,8 @@
package net.corda.irs.contract 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.*
import net.corda.core.contracts.clauses.* import net.corda.core.contracts.clauses.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
@ -106,6 +109,7 @@ abstract class RatePaymentEvent(date: LocalDate,
* Assumes that the rate is valid. * Assumes that the rate is valid.
*/ */
@CordaSerializable @CordaSerializable
@JsonIgnoreProperties(ignoreUnknown = true)
class FixedRatePaymentEvent(date: LocalDate, class FixedRatePaymentEvent(date: LocalDate,
accrualStartDate: LocalDate, accrualStartDate: LocalDate,
accrualEndDate: LocalDate, accrualEndDate: LocalDate,
@ -129,6 +133,7 @@ class FixedRatePaymentEvent(date: LocalDate,
* If the rate is null returns a zero payment. // TODO: Is this the desired behaviour? * If the rate is null returns a zero payment. // TODO: Is this the desired behaviour?
*/ */
@CordaSerializable @CordaSerializable
@JsonIgnoreProperties(ignoreUnknown = true)
class FloatingRatePaymentEvent(date: LocalDate, class FloatingRatePaymentEvent(date: LocalDate,
accrualStartDate: LocalDate, accrualStartDate: LocalDate,
accrualEndDate: LocalDate, accrualEndDate: LocalDate,
@ -651,6 +656,7 @@ class InterestRateSwap : Contract {
/** /**
* The state class contains the 4 major data classes. * The state class contains the 4 major data classes.
*/ */
@JsonIgnoreProperties("parties", "participants", ignoreUnknown = true)
data class State( data class State(
val fixedLeg: FixedLeg, val fixedLeg: FixedLeg,
val floatingLeg: FloatingLeg, val floatingLeg: FloatingLeg,

View File

@ -1,5 +1,7 @@
package net.corda.irs.contract package net.corda.irs.contract
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import net.corda.core.contracts.Tenor import net.corda.core.contracts.Tenor
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
@ -36,6 +38,7 @@ val String.percent: PercentageRatioUnit get() = PercentageRatioUnit(this)
/** /**
* Parent of the Rate family. Used to denote fixed rates, floating rates, reference rates etc. * Parent of the Rate family. Used to denote fixed rates, floating rates, reference rates etc.
*/ */
@JsonIgnoreProperties(ignoreUnknown = true)
open class Rate(val ratioUnit: RatioUnit? = null) { open class Rate(val ratioUnit: RatioUnit? = null) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@ -63,6 +66,7 @@ open class Rate(val ratioUnit: RatioUnit? = null) {
*/ */
@CordaSerializable @CordaSerializable
class FixedRate(ratioUnit: RatioUnit) : Rate(ratioUnit) { class FixedRate(ratioUnit: RatioUnit) : Rate(ratioUnit) {
@JsonIgnore
fun isPositive(): Boolean = ratioUnit!!.value > BigDecimal("0.0") fun isPositive(): Boolean = ratioUnit!!.value > BigDecimal("0.0")
override fun equals(other: Any?) = other?.javaClass == javaClass && super.equals(other) override fun equals(other: Any?) = other?.javaClass == javaClass && super.equals(other)

View File

@ -6,10 +6,12 @@ define([
'utils/semantic', 'utils/semantic',
'utils/dayCountBasisLookup', 'utils/dayCountBasisLookup',
'services/NodeApi', 'services/NodeApi',
'Deal' 'Deal',
'services/HttpErrorHandler'
], (angular, maskedInput, semantic, dayCountBasisLookup, nodeApi, Deal) => { ], (angular, maskedInput, semantic, dayCountBasisLookup, nodeApi, Deal) => {
angular.module('irsViewer').controller('CreateDealController', function CreateDealController($http, $scope, $location, nodeService) { angular.module('irsViewer').controller('CreateDealController', function CreateDealController($http, $scope, $location, nodeService, httpErrorHandler) {
semantic.init($scope, nodeService.isLoading); semantic.init($scope, nodeService.isLoading);
let handleHttpFail = httpErrorHandler.createErrorHandler($scope);
$scope.dayCountBasisLookup = dayCountBasisLookup; $scope.dayCountBasisLookup = dayCountBasisLookup;
$scope.deal = nodeService.newDeal(); $scope.deal = nodeService.newDeal();
@ -17,7 +19,7 @@ define([
nodeService.createDeal(new Deal($scope.deal)) nodeService.createDeal(new Deal($scope.deal))
.then((tradeId) => $location.path('#/deal/' + tradeId), (resp) => { .then((tradeId) => $location.path('#/deal/' + tradeId), (resp) => {
$scope.formError = resp.data; $scope.formError = resp.data;
}); }, handleHttpFail);
}; };
$('input.percent').mask("9.999999%", {placeholder: "", autoclear: false}); $('input.percent').mask("9.999999%", {placeholder: "", autoclear: false});
$('#swapirscolumns').click(() => { $('#swapirscolumns').click(() => {

View File

@ -1,9 +1,19 @@
'use strict'; 'use strict';
define(['angular', 'utils/semantic', 'services/NodeApi'], (angular, semantic, nodeApi) => { define(['angular', 'utils/semantic', 'services/NodeApi', 'services/HttpErrorHandler'], (angular, semantic) => {
angular.module('irsViewer').controller('DealController', function DealController($http, $scope, $routeParams, nodeService) { angular.module('irsViewer').controller('DealController', function DealController($http, $scope, $routeParams, nodeService, httpErrorHandler) {
semantic.init($scope, nodeService.isLoading); semantic.init($scope, nodeService.isLoading);
let handleHttpFail = httpErrorHandler.createErrorHandler($scope);
let decorateDeal = (deal) => {
let paymentSchedule = deal.calculation.floatingLegPaymentSchedule;
Object.keys(paymentSchedule).map((key, index) => {
const sign = paymentSchedule[key].rate.positive ? 1 : -1;
paymentSchedule[key].ratePercent = paymentSchedule[key].rate.ratioUnit ? (paymentSchedule[key].rate.ratioUnit.value * 100 * sign).toFixed(5) + "%": "";
});
nodeService.getDeal($routeParams.dealId).then((deal) => $scope.deal = deal); return deal;
};
nodeService.getDeal($routeParams.dealId).then((deal) => $scope.deal = decorateDeal(deal), handleHttpFail);
}); });
}); });

View File

@ -1,12 +1,10 @@
'use strict'; 'use strict';
define(['angular', 'utils/semantic', 'services/NodeApi'], (angular, semantic, nodeApi) => { define(['angular', 'utils/semantic', 'services/NodeApi', 'services/HttpErrorHandler'], (angular, semantic) => {
angular.module('irsViewer').controller('HomeController', function HomeController($http, $scope, nodeService) { angular.module('irsViewer').controller('HomeController', function HomeController($http, $scope, nodeService, httpErrorHandler) {
semantic.addLoadingModal($scope, nodeService.isLoading); semantic.addLoadingModal($scope, nodeService.isLoading);
let handleHttpFail = (resp) => { let handleHttpFail = httpErrorHandler.createErrorHandler($scope);
$scope.httpError = resp.data
};
$scope.infoMsg = ""; $scope.infoMsg = "";
$scope.errorText = ""; $scope.errorText = "";
@ -28,7 +26,7 @@ define(['angular', 'utils/semantic', 'services/NodeApi'], (angular, semantic, no
return name; return name;
}; };
nodeService.getDate().then((date) => $scope.date = date); nodeService.getDate().then((date) => $scope.date = date, handleHttpFail);
nodeService.getDeals().then((deals) => $scope.deals = deals); nodeService.getDeals().then((deals) => $scope.deals = deals, handleHttpFail);
}); });
}); });

View File

@ -0,0 +1,17 @@
'use strict';
define(['angular', 'lodash', 'viewmodel/Deal'], (angular, _) => {
angular.module('irsViewer').factory('httpErrorHandler', () => {
return {
createErrorHandler: (scope) => {
return (resp) => {
if(resp.status == -1) {
scope.httpError = "Could not connect to node web server";
} else {
scope.httpError = resp.data;
}
};
}
};
});
});

View File

@ -5,6 +5,7 @@ define(['angular', 'lodash', 'viewmodel/Deal'], (angular, _, dealViewModel) => {
return new (function() { return new (function() {
let date = new Date(2016, 0, 1, 0, 0, 0); let date = new Date(2016, 0, 1, 0, 0, 0);
let curLoading = {}; let curLoading = {};
let serverAddr = ''; // Leave empty to target the same host this page is served from
let load = (type, promise) => { let load = (type, promise) => {
curLoading[type] = true; curLoading[type] = true;
@ -17,19 +18,20 @@ define(['angular', 'lodash', 'viewmodel/Deal'], (angular, _, dealViewModel) => {
}); });
}; };
let endpoint = (target) => serverAddr + target;
let changeDateOnNode = (newDate) => { let changeDateOnNode = (newDate) => {
const dateStr = formatDateForNode(newDate); const dateStr = formatDateForNode(newDate);
let endpoint = '/api/irs/demodate'; return load('date', $http.put(endpoint('/api/irs/demodate'), "\"" + dateStr + "\"")).then((resp) => {
return load('date', $http.put(endpoint, "\"" + dateStr + "\"")).then((resp) => {
date = newDate; date = newDate;
return this.getDateModel(date); return this.getDateModel(date);
}); });
}; };
this.getDate = () => { this.getDate = () => {
return load('date', $http.get('/api/irs/demodate')).then((resp) => { return load('date', $http.get(endpoint('/api/irs/demodate'))).then((resp) => {
const parts = resp.data.split("-"); const dateParts = resp.data;
date = new Date(parts[0], parts[1] - 1, parts[2]); // JS uses 0 based months date = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]); // JS uses 0 based months
return this.getDateModel(date); return this.getDateModel(date);
}); });
}; };
@ -54,13 +56,13 @@ define(['angular', 'lodash', 'viewmodel/Deal'], (angular, _, dealViewModel) => {
}; };
this.getDeals = () => { this.getDeals = () => {
return load('deals', $http.get('/api/irs/deals')).then((resp) => { return load('deals', $http.get(endpoint('/api/irs/deals'))).then((resp) => {
return resp.data.reverse(); return resp.data.reverse();
}); });
}; };
this.getDeal = (dealId) => { this.getDeal = (dealId) => {
return load('deal' + dealId, $http.get('/api/irs/deals/' + dealId)).then((resp) => { return load('deal' + dealId, $http.get(endpoint('/api/irs/deals/' + dealId))).then((resp) => {
// Do some data modification to simplify the model // Do some data modification to simplify the model
let deal = resp.data; let deal = resp.data;
deal.fixedLeg.fixedRate.value = (deal.fixedLeg.fixedRate.ratioUnit.value * 100).toString().slice(0, 6); deal.fixedLeg.fixedRate.value = (deal.fixedLeg.fixedRate.ratioUnit.value * 100).toString().slice(0, 6);
@ -87,7 +89,7 @@ define(['angular', 'lodash', 'viewmodel/Deal'], (angular, _, dealViewModel) => {
}; };
this.createDeal = (deal) => { this.createDeal = (deal) => {
return load('create-deal', $http.post('/api/irs/deals', deal.toJson())) return load('create-deal', $http.post(endpoint('/api/irs/deals'), deal.toJson()))
.then((resp) => { .then((resp) => {
return deal.tradeId; return deal.tradeId;
}, (resp) => { }, (resp) => {

View File

@ -1,5 +1,7 @@
<div class="ui container"> <div class="ui container">
<div class="ui hidden divider"></div>
<div class="ui negative message" id="form-error" ng-show="formError">{{formError}}</div> <div class="ui negative message" id="form-error" ng-show="formError">{{formError}}</div>
<div class="ui negative message" id="http-error" ng-show="httpError">{{httpError}}</div>
<h3 class="ui horizontal divider header"> <h3 class="ui horizontal divider header">
<i class="list icon"></i> <i class="list icon"></i>
New Deal New Deal

View File

@ -1,5 +1,6 @@
<div class="ui container"> <div class="ui container">
<div class="ui hidden divider"></div> <div class="ui hidden divider"></div>
<div class="ui negative message" id="http-error" ng-show="httpError">{{httpError}}</div>
<div class="ui grid"> <div class="ui grid">
<div class="sixteen wide column" id="common"> <div class="sixteen wide column" id="common">
<table class="ui striped table"> <table class="ui striped table">
@ -199,6 +200,26 @@
</div> </div>
</td> </td>
</tr> </tr>
<tr class="center aligned">
<td colspan="2">
<div class="ui accordion">
<div class="title">
<i class="dropdown icon"></i>
Fixings
</div>
<div class="content">
<table class="ui celled small table">
<tbody>
<tr class="center aligned" ng-repeat="fixing in deal.calculation.floatingLegPaymentSchedule">
<td>{{fixing.fixingDate[0]}}-{{fixing.fixingDate[1]}}-{{fixing.fixingDate[2]}}</td>
<td>{{fixing.ratePercent}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -1,4 +1,5 @@
<div class="ui container"> <div class="ui container">
<div class="ui hidden divider"></div>
<div class="ui negative message" id="http-error" ng-show="httpError">{{httpError}}</div> <div class="ui negative message" id="http-error" ng-show="httpError">{{httpError}}</div>
<div class="ui info message" id="info-message" ng-show="infoMsg">{{infoMsg}}</div> <div class="ui info message" id="info-message" ng-show="infoMsg">{{infoMsg}}</div>
<div class="ui active dimmer" ng-show="isLoading()"> <div class="ui active dimmer" ng-show="isLoading()">

View File

@ -1,9 +1,10 @@
package net.corda.testing.http package net.corda.testing.http
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import java.net.URL 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. * 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. * Send a GET request to the path on the API specified.
*/ */
inline fun <reified T : Any> getJson(path: String, params: Map<String, String> = mapOf()) = HttpUtils.getJson<T>(URL(root, path), params) inline fun <reified T : Any> getJson(path: String, params: Map<String, String> = mapOf()) = HttpUtils.getJson<T>(URL(root, path), params, mapper)
private fun toJson(any: Any) = any as? String ?: HttpUtils.defaultMapper.writeValueAsString(any) private fun toJson(any: Any) = any as? String ?: HttpUtils.defaultMapper.writeValueAsString(any)
companion object { companion object {
fun fromHostAndPort(hostAndPort: HostAndPort, base: String, protocol: String = "http"): HttpApi fun fromHostAndPort(hostAndPort: HostAndPort, base: String, protocol: String = "http", mapper: ObjectMapper = defaultMapper): HttpApi
= HttpApi(URL("$protocol://$hostAndPort/$base/")) = HttpApi(URL("$protocol://$hostAndPort/$base/"), mapper)
private val defaultMapper: ObjectMapper by lazy {
net.corda.jackson.JacksonSupport.createNonRpcMapper()
}
} }
} }

View File

@ -35,10 +35,10 @@ object HttpUtils {
return makeRequest(Request.Builder().url(url).header("Content-Type", "application/json").post(body).build()) return makeRequest(Request.Builder().url(url).header("Content-Type", "application/json").post(body).build())
} }
inline fun <reified T : Any> getJson(url: URL, params: Map<String, String> = mapOf()): T { inline fun <reified T : Any> getJson(url: URL, params: Map<String, String> = mapOf(), mapper: ObjectMapper = defaultMapper): T {
val paramString = if (params.isEmpty()) "" else "?" + params.map { "${it.key}=${it.value}" }.joinToString("&") val paramString = if (params.isEmpty()) "" else "?" + params.map { "${it.key}=${it.value}" }.joinToString("&")
val parameterisedUrl = URL(url.toExternalForm() + paramString) 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 { private fun makeRequest(request: Request): Boolean {