IRS Demo - split IRS Demo into two separate applications to showcase … (#1638)

* IRS Demo - split IRS Demo into two separate applications to showcase the separation and usage of RPC client
This commit is contained in:
Maksymilian Pawlak 2017-10-25 16:40:21 +01:00 committed by GitHub
parent ba75146446
commit 44a7d872d8
1035 changed files with 623 additions and 254 deletions

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ tags
.gradle .gradle
local.properties local.properties
.gradletasknamecache
# General build files # General build files
**/build/* **/build/*

8
.idea/compiler.xml generated
View File

@ -2,8 +2,6 @@
<project version="4"> <project version="4">
<component name="CompilerConfiguration"> <component name="CompilerConfiguration">
<bytecodeTargetLevel target="1.8"> <bytecodeTargetLevel target="1.8">
<module name="api-scanner_main" target="1.8" />
<module name="api-scanner_test" target="1.8" />
<module name="attachment-demo_integrationTest" target="1.8" /> <module name="attachment-demo_integrationTest" target="1.8" />
<module name="attachment-demo_main" target="1.8" /> <module name="attachment-demo_main" target="1.8" />
<module name="attachment-demo_test" target="1.8" /> <module name="attachment-demo_test" target="1.8" />
@ -21,6 +19,7 @@
<module name="corda-webserver_integrationTest" target="1.8" /> <module name="corda-webserver_integrationTest" target="1.8" />
<module name="corda-webserver_main" target="1.8" /> <module name="corda-webserver_main" target="1.8" />
<module name="corda-webserver_test" target="1.8" /> <module name="corda-webserver_test" target="1.8" />
<module name="cordapp_integrationTest" target="1.8" />
<module name="cordapp_main" target="1.8" /> <module name="cordapp_main" target="1.8" />
<module name="cordapp_test" target="1.8" /> <module name="cordapp_test" target="1.8" />
<module name="cordform-common_main" target="1.8" /> <module name="cordform-common_main" target="1.8" />
@ -43,11 +42,8 @@
<module name="explorer-capsule_test" target="1.6" /> <module name="explorer-capsule_test" target="1.6" />
<module name="explorer_main" target="1.8" /> <module name="explorer_main" target="1.8" />
<module name="explorer_test" target="1.8" /> <module name="explorer_test" target="1.8" />
<module name="finance_integrationTest" target="1.8" />
<module name="finance_main" target="1.8" /> <module name="finance_main" target="1.8" />
<module name="finance_test" target="1.8" /> <module name="finance_test" target="1.8" />
<module name="gradle-plugins-cordform-common_main" target="1.8" />
<module name="gradle-plugins-cordform-common_test" target="1.8" />
<module name="graphs_main" target="1.8" /> <module name="graphs_main" target="1.8" />
<module name="graphs_test" target="1.8" /> <module name="graphs_test" target="1.8" />
<module name="irs-demo_integrationTest" target="1.8" /> <module name="irs-demo_integrationTest" target="1.8" />
@ -122,6 +118,8 @@
<module name="verifier_integrationTest" target="1.8" /> <module name="verifier_integrationTest" target="1.8" />
<module name="verifier_main" target="1.8" /> <module name="verifier_main" target="1.8" />
<module name="verifier_test" target="1.8" /> <module name="verifier_test" target="1.8" />
<module name="web_main" target="1.8" />
<module name="web_test" target="1.8" />
<module name="webcapsule_main" target="1.6" /> <module name="webcapsule_main" target="1.6" />
<module name="webcapsule_test" target="1.6" /> <module name="webcapsule_test" target="1.6" />
<module name="webserver-webcapsule_main" target="1.6" /> <module name="webserver-webcapsule_main" target="1.6" />

View File

@ -4,25 +4,28 @@ This demo brings up three nodes: Bank A, Bank B and a node that simultaneously r
interest rates oracle. The two banks agree on an interest rate swap, and then do regular fixings of the deal as the interest rates oracle. The two banks agree on an interest rate swap, and then do regular fixings of the deal as the
time on a simulated clock passes. time on a simulated clock passes.
To run from the command line in Unix: Functionality is split into two parts - CordApp which provides actual distributed ledger backend and Spring Boot
webapp which provides REST API and web frontend. Application communicate using Corda RPC protocol.
1. Run ``./gradlew samples:irs-demo:deployNodes`` to install configs and a command line tool under To run from the command line in Unix:
1. Run ``./gradlew samples:irs-demo:cordapp:deployNodes`` to install configs and a command line tool under
``samples/irs-demo/build`` ``samples/irs-demo/build``
2. Run ``./gradlew samples:irs-demo:installDist`` 2. Run ``./gradlew samples:irs-demo:web:deployWebapps`` to install configs and tools for running webservers
3. Move to the ``samples/irs-demo/build`` directory 3. Move to the ``samples/irs-demo/`` directory
4. Run ``./nodes/runnodes`` to open up three new terminals with the three nodes (you may have to install xterm) 4. Run ``./cordapp/build/nodes/runnodes`` to open up three new terminals with the three nodes (you may have to install xterm)
5. Run ``./web/build/webapps/runwebapps`` to open three more terminals for associated webserver
To run from the command line in Windows: To run from the command line in Windows:
1. Run ``gradlew.bat samples:irs-demo:deployNodes`` to install configs and a command line tool under 1. Run ``gradlew.bat samples:irs-demo:cordapp:deployNodes`` to install configs and a command line tool under
``samples\irs-demo\build`` ``samples\irs-demo\build``
2. Run ``gradlew.bat samples:irs-demo:installDist`` 2. Run ``gradlew.bat samples:irs-demo:web:deployWebapps`` to install configs and tools for running webservers
3. Run ``cd samples\irs-demo\build`` to change current working directory 3. Run ``cd samples\irs-demo`` to change current working directory
4. Run ``nodes\runnodes`` to open up several 6 terminals, 2 for each node. First terminal is a web-server associated 4. Run ``cordapp\build\nodes\runnodes`` to open up several 3 terminals for each nodes
with every node and second one is Corda interactive shell for the node 5. Run ``web\build\webapps\webapps`` to open up several 3 terminals for each nodes' webservers
This demo also has a web app. To use this, run nodes and then navigate to http://localhost:10007/web/irsdemo and This demo also has a web app. To use this, run nodes and then navigate to http://localhost:10007/ and
http://localhost:10010/web/irsdemo to see each node's view of the ledger. http://localhost:10010/ to see each node's view of the ledger.
To use the web app, click the "Create Deal" button, fill in the form, then click the "Submit" button. You can then use To use the web app, click the "Create Deal" button, fill in the form, then click the "Submit" button. You can then use
the time controls at the top left of the home page to run the fixings. Click any individual trade in the blotter to the time controls at the top left of the home page to run the fixings. Click any individual trade in the blotter to

View File

@ -1,6 +1,25 @@
buildscript {
ext {
springBootVersion = '1.5.7.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
// 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"
apply plugin: 'java' apply plugin: 'java'
apply plugin: 'kotlin' apply plugin: 'kotlin'
apply plugin: 'idea' apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.quasar-utils'
apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.cordapp'
@ -23,59 +42,25 @@ sourceSets {
configurations { configurations {
integrationTestCompile.extendsFrom testCompile integrationTestCompile.extendsFrom testCompile
integrationTestRuntime.extendsFrom testRuntime integrationTestRuntime.extendsFrom testRuntime
demoArtifacts.extendsFrom testRuntime
} }
dependencies { dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" compile group: 'commons-io', name: 'commons-io', version: '2.5'
compile project(path: ":samples:irs-demo:cordapp", configuration: "demoArtifacts")
// The irs demo CorDapp depends upon Cash CorDapp features compile project(":samples:irs-demo:web")
cordapp project(':finance') compile('org.springframework.boot:spring-boot-starter-web') {
exclude module: "spring-boot-starter-logging"
// Corda integration dependencies exclude module: "logback-classic"
cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') }
cordaCompile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts')
cordaCompile project(':core')
cordaCompile project(':webserver')
// Javax is required for webapis
compile "org.glassfish.jersey.core:jersey-server:${jersey_version}"
// Cordapp dependencies
// Specify your cordapp's dependencies below, including dependent cordapps
compile "com.squareup.okhttp3:okhttp:$okhttp_version"
testCompile project(':node-driver') testCompile project(':node-driver')
testCompile "junit:junit:$junit_version" testCompile "junit:junit:$junit_version"
testCompile "org.assertj:assertj-core:${assertj_version}" testCompile "org.assertj:assertj-core:${assertj_version}"
} }
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { bootRepackage {
directory "./build/nodes" enabled = false
node {
name "O=Notary Service,L=Zurich,C=CH"
notary = [validating : true]
p2pPort 10002
rpcPort 10003
webPort 10004
cordapps = ["$project.group:finance:$corda_release_version"]
useTestClock true
}
node {
name "O=Bank A,L=London,C=GB"
p2pPort 10005
rpcPort 10006
webPort 10007
cordapps = ["$project.group:finance:$corda_release_version"]
useTestClock true
}
node {
name "O=Bank B,L=New York,C=US"
p2pPort 10008
rpcPort 10009
webPort 10010
cordapps = ["$project.group:finance:$corda_release_version"]
useTestClock true
}
} }
task integrationTest(type: Test, dependsOn: []) { task integrationTest(type: Test, dependsOn: []) {
@ -83,41 +68,9 @@ task integrationTest(type: Test, dependsOn: []) {
classpath = sourceSets.integrationTest.runtimeClasspath classpath = sourceSets.integrationTest.runtimeClasspath
} }
// This fixes the "line too long" error when running this demo with windows CLI
// TODO: Automatically apply to all projects via a plugin
tasks.withType(CreateStartScripts).each { task ->
task.doLast {
String text = task.windowsScript.text
// Replaces the per file classpath (which are all jars in "lib") with a wildcard on lib
text = text.replaceFirst(/(set CLASSPATH=%APP_HOME%\\lib\\).*/, { "${it[1]}*" })
task.windowsScript.write text
}
}
idea { idea {
module { module {
downloadJavadoc = true // defaults to false downloadJavadoc = true // defaults to false
downloadSources = true downloadSources = true
} }
} }
publishing {
publications {
jarAndSources(MavenPublication) {
from components.java
artifactId 'irsdemo'
artifact sourceJar
artifact javadocJar
}
}
}
jar {
from sourceSets.test.output
manifest {
attributes(
'Automatic-Module-Name': 'net.corda.samples.demos.irs'
)
}
}

View File

@ -0,0 +1,115 @@
apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'idea'
apply plugin: 'net.corda.plugins.quasar-utils'
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'net.corda.plugins.cordformation'
apply plugin: 'net.corda.plugins.cordapp'
apply plugin: 'maven-publish'
apply plugin: 'application'
mainClassName = 'net.corda.irs.IRSDemo'
sourceSets {
integrationTest {
kotlin {
compileClasspath += main.output + test.output
runtimeClasspath += main.output + test.output
srcDir file('src/integration-test/kotlin')
}
}
}
configurations {
integrationTestCompile.extendsFrom testCompile
integrationTestRuntime.extendsFrom testRuntime
demoArtifacts.extendsFrom integrationTestRuntime
}
dependencies {
// The irs demo CorDapp depends upon Cash CorDapp features
cordapp project(':finance')
// Corda integration dependencies
cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts')
cordaCompile project(':core')
// Cordapp dependencies
// Specify your cordapp's dependencies below, including dependent cordapps
compile group: 'commons-io', name: 'commons-io', version: '2.5'
testCompile project(':node-driver')
testCompile "junit:junit:$junit_version"
testCompile "org.assertj:assertj-core:${assertj_version}"
}
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
ext.rpcUsers = [
['username' : "user",
'password' : "password",
'permissions' : [
"StartFlow.net.corda.irs.flows.AutoOfferFlow\$Requester",
"StartFlow.net.corda.irs.flows.UpdateBusinessDayFlow\$Broadcast",
"StartFlow.net.corda.irs.api.NodeInterestRates\$UploadFixesFlow"
]]
]
directory "./build/nodes"
node {
name "O=Notary Service,L=Zurich,C=CH"
notary = [validating : true]
p2pPort 10002
rpcPort 10003
cordapps = ["net.corda:finance:$corda_release_version"]
rpcUsers = ext.rpcUsers
useTestClock true
}
node {
name "O=Bank A,L=London,C=GB"
p2pPort 10005
rpcPort 10006
cordapps = ["net.corda:finance:$corda_release_version"]
rpcUsers = ext.rpcUsers
useTestClock true
}
node {
name "O=Bank B,L=New York,C=US"
p2pPort 10008
rpcPort 10009
cordapps = ["net.corda:finance:$corda_release_version"]
rpcUsers = ext.rpcUsers
useTestClock true
}
}
task integrationTest(type: Test, dependsOn: []) {
testClassesDirs = sourceSets.integrationTest.output.classesDirs
classpath = sourceSets.integrationTest.runtimeClasspath
}
// This fixes the "line too long" error when running this demo with windows CLI
// TODO: Automatically apply to all projects via a plugin
tasks.withType(CreateStartScripts).each { task ->
task.doLast {
String text = task.windowsScript.text
// Replaces the per file classpath (which are all jars in "lib") with a wildcard on lib
text = text.replaceFirst(/(set CLASSPATH=%APP_HOME%\\lib\\).*/, { "${it[1]}*" })
task.windowsScript.write text
}
}
idea {
module {
downloadJavadoc = true // defaults to false
downloadSources = true
}
}
jar {
from sourceSets.test.output
}
artifacts {
demoArtifacts jar
}

View File

@ -13,23 +13,25 @@ import net.corda.core.contracts.UniqueIdentifier
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.messaging.vaultTrackBy import net.corda.core.messaging.vaultTrackBy
import net.corda.core.toFuture import net.corda.core.toFuture
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.finance.plugin.registerFinanceJSONMappers import net.corda.finance.plugin.registerFinanceJSONMappers
import net.corda.irs.contract.InterestRateSwap import net.corda.irs.contract.InterestRateSwap
import net.corda.irs.utilities.uploadFile import net.corda.irs.web.IrsDemoWebApplication
import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.config.FullNodeConfiguration
import net.corda.nodeapi.User import net.corda.nodeapi.User
import net.corda.testing.* import net.corda.test.spring.springDriver
import net.corda.testing.driver.driver import net.corda.testing.DUMMY_BANK_A
import net.corda.testing.DUMMY_BANK_B
import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.IntegrationTestCategory
import net.corda.testing.chooseIdentity
import net.corda.testing.http.HttpApi 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.Observable
import java.net.URL
import java.time.Duration import java.time.Duration
import java.time.LocalDate import java.time.LocalDate
@ -39,31 +41,34 @@ class IRSDemoTest : IntegrationTestCategory {
val log = loggerFor<IRSDemoTest>() val log = loggerFor<IRSDemoTest>()
} }
private val rpcUser = User("user", "password", setOf("ALL")) val rpcUsers = listOf(User("user", "password",
private val currentDate: LocalDate = LocalDate.now() setOf("StartFlow.net.corda.irs.flows.AutoOfferFlow\$Requester",
private val futureDate: LocalDate = currentDate.plusMonths(6) "StartFlow.net.corda.irs.flows.UpdateBusinessDayFlow\$Broadcast",
private val maxWaitTime: Duration = 60.seconds "StartFlow.net.corda.irs.api.NodeInterestRates\$UploadFixesFlow")))
val currentDate: LocalDate = LocalDate.now()
val futureDate: LocalDate = currentDate.plusMonths(6)
val maxWaitTime: Duration = 60.seconds
@Test @Test
fun `runs IRS demo`() { fun `runs IRS demo`() {
driver(useTestClock = true, isDebug = true) { springDriver(useTestClock = true, isDebug = true, extraCordappPackagesToScan = listOf("net.corda.irs")) {
val (controller, nodeA, nodeB) = listOf( val (controller, nodeA, nodeB) = listOf(
startNotaryNode(DUMMY_NOTARY.name, validating = false), startNotaryNode(DUMMY_NOTARY.name, validating = true, rpcUsers = rpcUsers),
startNode(providedName = DUMMY_BANK_A.name, rpcUsers = listOf(rpcUser)), startNode(providedName = DUMMY_BANK_A.name, rpcUsers = rpcUsers),
startNode(providedName = DUMMY_BANK_B.name)) startNode(providedName = DUMMY_BANK_B.name, rpcUsers = rpcUsers)).map { it.getOrThrow() }
.map { it.getOrThrow() }
log.info("All nodes started") log.info("All nodes started")
val (controllerAddr, nodeAAddr, nodeBAddr) = listOf( val controllerAddrFuture = startSpringBootWebapp(IrsDemoWebApplication::class.java, controller, "/api/irs/demodate")
startWebserver(controller), val nodeAAddrFuture = startSpringBootWebapp(IrsDemoWebApplication::class.java, nodeA, "/api/irs/demodate")
startWebserver(nodeA), val nodeBAddrFuture = startSpringBootWebapp(IrsDemoWebApplication::class.java, nodeB, "/api/irs/demodate")
startWebserver(nodeB)) val (controllerAddr, nodeAAddr, nodeBAddr) =
.map { it.getOrThrow().listenAddress } listOf(controllerAddrFuture, nodeAAddrFuture, nodeBAddrFuture).map { it.getOrThrow().listenAddress }
log.info("All webservers started") log.info("All webservers started")
val (_, nodeAApi, nodeBApi) = listOf(controller, nodeA, nodeB).zip(listOf(controllerAddr, nodeAAddr, nodeBAddr)).map { val (controllerApi, nodeAApi, nodeBApi) = listOf(controller, nodeA, nodeB).zip(listOf(controllerAddr, nodeAAddr, nodeBAddr)).map {
val mapper = net.corda.client.jackson.JacksonSupport.createDefaultMapper(it.first.rpc) val mapper = net.corda.client.jackson.JacksonSupport.createDefaultMapper(it.first.rpc)
registerFinanceJSONMappers(mapper) registerFinanceJSONMappers(mapper)
registerIRSModule(mapper) registerIRSModule(mapper)
@ -73,7 +78,7 @@ class IRSDemoTest : IntegrationTestCategory {
val numADeals = getTradeCount(nodeAApi) val numADeals = getTradeCount(nodeAApi)
val numBDeals = getTradeCount(nodeBApi) val numBDeals = getTradeCount(nodeBApi)
runUploadRates(controllerAddr) runUploadRates(controllerApi)
runTrade(nodeAApi, controller.nodeInfo.chooseIdentity()) runTrade(nodeAApi, controller.nodeInfo.chooseIdentity())
assertThat(getTradeCount(nodeAApi)).isEqualTo(numADeals + 1) assertThat(getTradeCount(nodeAApi)).isEqualTo(numADeals + 1)
@ -89,9 +94,7 @@ class IRSDemoTest : IntegrationTestCategory {
} }
} }
private fun getFloatingLegFixCount(nodeApi: HttpApi): Int { fun getFloatingLegFixCount(nodeApi: HttpApi) = getTrades(nodeApi)[0].calculation.floatingLegPaymentSchedule.count { it.value.rate.ratioUnit != null }
return getTrades(nodeApi)[0].calculation.floatingLegPaymentSchedule.count { it.value.rate.ratioUnit != null }
}
private fun getFixingDateObservable(config: FullNodeConfiguration): Observable<LocalDate?> { private fun getFixingDateObservable(config: FullNodeConfiguration): Observable<LocalDate?> {
val client = CordaRPCClient(config.rpcAddress!!) val client = CordaRPCClient(config.rpcAddress!!)
@ -111,16 +114,15 @@ class IRSDemoTest : IntegrationTestCategory {
private fun runTrade(nodeApi: HttpApi, oracle: Party) { private fun runTrade(nodeApi: HttpApi, oracle: Party) {
log.info("Running trade against ${nodeApi.root}") log.info("Running trade against ${nodeApi.root}")
val fileContents = loadResourceFile("net/corda/irs/simulation/example-irs-trade.json") val fileContents = loadResourceFile("net/corda/irs/web/simulation/example-irs-trade.json")
val tradeFile = fileContents.replace("tradeXXX", "trade1").replace("oracleXXX", oracle.name.toString()) val tradeFile = fileContents.replace("tradeXXX", "trade1").replace("oracleXXX", oracle.name.toString())
assertThat(nodeApi.postJson("deals", tradeFile)).isTrue() assertThat(nodeApi.postJson("deals", tradeFile)).isTrue()
} }
private fun runUploadRates(host: NetworkHostAndPort) { private fun runUploadRates(nodeApi: HttpApi) {
log.info("Running upload rates against $host") log.info("Running upload rates against ${nodeApi.root}")
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/api/irs/fixes") assertThat(nodeApi.postPlain("fixes", fileContents)).isTrue()
assertThat(uploadFile(url, fileContents)).isTrue()
} }
private fun loadResourceFile(filename: String): String { private fun loadResourceFile(filename: String): String {

View File

@ -0,0 +1,124 @@
package net.corda.test.spring
import net.corda.core.concurrent.CordaFuture
import net.corda.core.internal.concurrent.flatMap
import net.corda.core.internal.concurrent.fork
import net.corda.core.internal.concurrent.map
import net.corda.core.utilities.loggerFor
import net.corda.testing.driver.*
import okhttp3.OkHttpClient
import okhttp3.Request
import java.net.ConnectException
import java.net.URL
import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.ExecutorService
import java.util.concurrent.TimeUnit
interface SpringDriverExposedDSLInterface : DriverDSLExposedInterface {
/**
* Starts a Spring Boot application, passes the RPC connection data as parameters the process.
* Returns future which will complete after (and if) the server passes healthcheck.
* @param clazz Class with main method which is expected to run Spring application
* @param handle Corda Node handle this webapp is expected to connect to
* @param checkUrl URL path to use for server readiness check - uses [okhttp3.Response.isSuccessful] as qualifier
*
* TODO: Rather then expecting a given clazz to contain main method which start Spring app our own simple class can do this
*/
fun startSpringBootWebapp(clazz: Class<*>, handle: NodeHandle, checkUrl: String): CordaFuture<WebserverHandle>
}
interface SpringDriverInternalDSLInterface : DriverDSLInternalInterface, SpringDriverExposedDSLInterface
fun <A> springDriver(
defaultParameters: DriverParameters = DriverParameters(),
isDebug: Boolean = defaultParameters.isDebug,
driverDirectory: Path = defaultParameters.driverDirectory,
portAllocation: PortAllocation = defaultParameters.portAllocation,
debugPortAllocation: PortAllocation = defaultParameters.debugPortAllocation,
systemProperties: Map<String, String> = defaultParameters.systemProperties,
useTestClock: Boolean = defaultParameters.useTestClock,
initialiseSerialization: Boolean = defaultParameters.initialiseSerialization,
startNodesInProcess: Boolean = defaultParameters.startNodesInProcess,
extraCordappPackagesToScan: List<String> = defaultParameters.extraCordappPackagesToScan,
dsl: SpringDriverExposedDSLInterface.() -> A
) = genericDriver(
defaultParameters = defaultParameters,
isDebug = isDebug,
driverDirectory = driverDirectory,
portAllocation = portAllocation,
debugPortAllocation = debugPortAllocation,
systemProperties = systemProperties,
useTestClock = useTestClock,
initialiseSerialization = initialiseSerialization,
startNodesInProcess = startNodesInProcess,
extraCordappPackagesToScan = extraCordappPackagesToScan,
driverDslWrapper = { driverDSL:DriverDSL -> SpringBootDriverDSL(driverDSL) },
coerce = { it },
dsl = dsl
)
data class SpringBootDriverDSL(
val driverDSL: DriverDSL
) : DriverDSLInternalInterface by driverDSL, SpringDriverInternalDSLInterface {
val log = loggerFor<SpringBootDriverDSL>()
override fun startSpringBootWebapp(clazz: Class<*>, handle: NodeHandle, checkUrl: String): CordaFuture<WebserverHandle> {
val debugPort = if (driverDSL.isDebug) driverDSL.debugPortAllocation.nextPort() else null
val processFuture = startApplication(driverDSL.executorService, handle, debugPort, clazz)
driverDSL.registerProcess(processFuture)
return processFuture.map { queryWebserver(handle, it, checkUrl) }
}
private fun queryWebserver(handle: NodeHandle, process: Process, checkUrl: String): WebserverHandle {
val protocol = if (handle.configuration.useHTTPS) "https://" else "http://"
val url = URL(URL("$protocol${handle.webAddress}"), checkUrl)
val client = OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build()
var maxRetries = 30
while (process.isAlive && maxRetries > 0) try {
val response = client.newCall(Request.Builder().url(url).build()).execute()
response.use {
if (response.isSuccessful) {
return WebserverHandle(handle.webAddress, process)
}
}
TimeUnit.SECONDS.sleep(2)
maxRetries--
} catch (e: ConnectException) {
log.debug("Retrying webserver info at ${handle.webAddress}")
}
throw IllegalStateException("Webserver at ${handle.webAddress} has died or was not reachable at URL ${url}")
}
private fun startApplication(executorService: ExecutorService, handle: NodeHandle, debugPort: Int?, clazz: Class<*>): CordaFuture<Process> {
return executorService.fork {
val className = clazz.canonicalName
ProcessUtilities.startJavaProcessImpl(
className = className, // cannot directly get class for this, so just use string
jdwpPort = debugPort,
extraJvmArguments = listOf(
"-Dname=node-${handle.configuration.p2pAddress}-webserver",
"-Djava.io.tmpdir=${System.getProperty("java.io.tmpdir")}"
// Inherit from parent process
),
classpath = ProcessUtilities.defaultClassPath,
workingDirectory = handle.configuration.baseDirectory,
errorLogPath = Paths.get("error.$className.log"),
arguments = listOf(
"--base-directory", handle.configuration.baseDirectory.toString(),
"--server.port=${handle.webAddress.port}",
"--corda.host=${handle.configuration.rpcAddress}",
"--corda.user=${handle.configuration.rpcUsers.first().username}",
"--corda.password=${handle.configuration.rpcUsers.first().password}"
),
maximumHeapSize = null
)
}.flatMap { process -> addressMustBeBoundFuture(driverDSL.executorService, handle.webAddress, process).map { process } }
}
}

View File

@ -1,16 +0,0 @@
package net.corda.irs.plugin
import com.fasterxml.jackson.databind.ObjectMapper
import net.corda.finance.plugin.registerFinanceJSONMappers
import net.corda.irs.api.InterestRateSwapAPI
import net.corda.webserver.services.WebServerPluginRegistry
import java.util.function.Function
class IRSPlugin : WebServerPluginRegistry {
override val webApis = listOf(Function(::InterestRateSwapAPI))
override val staticServeDirs: Map<String, String> = mapOf(
"irsdemo" to javaClass.classLoader.getResource("irsweb").toExternalForm()
)
override fun customizeJSONSerialization(om: ObjectMapper): Unit = registerFinanceJSONMappers(om)
}

View File

@ -1,42 +0,0 @@
package net.corda.irs.utilities
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import java.net.URL
import java.util.concurrent.TimeUnit
/**
* A small set of utilities for making HttpCalls, aimed at demos.
*/
private val client by lazy {
OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS).build()
}
fun putJson(url: URL, data: String): Boolean {
val body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), data)
return makeRequest(Request.Builder().url(url).put(body).build())
}
fun postJson(url: URL, data: String): Boolean {
val body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), data)
return makeRequest(Request.Builder().url(url).post(body).build())
}
fun uploadFile(url: URL, file: String): Boolean {
val body = RequestBody.create(MediaType.parse("text/plain; charset=utf-8"), file)
return makeRequest(Request.Builder().url(url).post(body).build())
}
private fun makeRequest(request: Request): Boolean {
val response = client.newCall(request).execute()
if (!response.isSuccessful) {
println("Could not fulfill HTTP request. Status Code: ${response.code()}. Message: ${response.body().string()}")
}
response.close()
return response.isSuccessful
}

View File

@ -1,2 +0,0 @@
# Register a ServiceLoader service extending from net.corda.webserver.services.WebServerPluginRegistry
net.corda.irs.plugin.IRSPlugin

View File

@ -1,9 +0,0 @@
package net.corda.irs.plugin
import net.corda.irs.api.InterestRatesSwapDemoAPI
import net.corda.webserver.services.WebServerPluginRegistry
import java.util.function.Function
class IRSDemoPlugin : WebServerPluginRegistry {
override val webApis = listOf(Function(::InterestRatesSwapDemoAPI))
}

View File

@ -1,2 +0,0 @@
net.corda.irs.plugin.IRSPlugin
net.corda.irs.plugin.IRSDemoPlugin

View File

@ -0,0 +1,55 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlin_version}")
}
}
// 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"
apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'project-report'
apply plugin: 'application'
dependencies {
compile('org.springframework.boot:spring-boot-starter-web') {
exclude module: "spring-boot-starter-logging"
exclude module: "logback-classic"
}
compile("com.fasterxml.jackson.module:jackson-module-kotlin:2.8.9")
compile project(":client:rpc")
compile project(":client:jackson")
compile project(":test-utils")
compile project(path: ":samples:irs-demo:cordapp", configuration: "demoArtifacts")
testCompile('org.springframework.boot:spring-boot-starter-test') {
exclude module: "spring-boot-starter-logging"
exclude module: "logback-classic"
}
}
jar {
from sourceSets.test.output
}
task deployWebapps(type: Copy, dependsOn: ['jar', 'bootRepackage']) {
ext.webappDir = file("build/webapps")
from(jar.outputs)
from("src/test/resources/scripts/") {
filter { it
.replace('#JAR_PATH#', jar.archiveName)
.replace('#DIR#', ext.webappDir.getAbsolutePath())
}
}
into ext.webappDir
}

View File

@ -0,0 +1,52 @@
package net.corda.irs.web
import com.fasterxml.jackson.databind.ObjectMapper
import net.corda.client.jackson.JacksonSupport
import net.corda.client.rpc.CordaRPCClient
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.finance.plugin.registerFinanceJSONMappers
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.context.annotation.Bean
/**
* Simple and sample SpringBoot web application which communicates with Corda node using RPC.
* [CordaRPCOps] instance can be managed simply as plain Spring bean.
* If support for (de)serializatin of Corda classes is required, [ObjectMapper] can be configured using helper
* functions, see [objectMapper]
*/
@SpringBootApplication
class IrsDemoWebApplication {
@Value("\${corda.host}")
lateinit var cordaHost:String
@Value("\${corda.user}")
lateinit var cordaUser:String
@Value("\${corda.password}")
lateinit var cordaPassword:String
@Bean
fun rpcClient(): CordaRPCOps {
return CordaRPCClient(NetworkHostAndPort.parse(cordaHost)).start(cordaUser, cordaPassword).proxy
}
@Bean
fun objectMapper(@Autowired cordaRPCOps: CordaRPCOps): ObjectMapper {
val mapper = JacksonSupport.createDefaultMapper(cordaRPCOps)
registerFinanceJSONMappers(mapper)
return mapper
}
// running as standalone java app
companion object {
@JvmStatic fun main(args: Array<String>) {
SpringApplication.run(IrsDemoWebApplication::class.java, *args)
}
}
}

View File

@ -1,4 +1,4 @@
package net.corda.irs.api package net.corda.irs.web.api
import net.corda.core.contracts.filterStatesOfType import net.corda.core.contracts.filterStatesOfType
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
@ -7,11 +7,12 @@ import net.corda.core.messaging.vaultQueryBy
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.irs.contract.InterestRateSwap import net.corda.irs.contract.InterestRateSwap
import net.corda.irs.flows.AutoOfferFlow import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.*
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import java.net.URI import java.net.URI
import javax.ws.rs.* import net.corda.irs.flows.AutoOfferFlow
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response
/** /**
* This provides a simplified API, currently for demonstration use only. * This provides a simplified API, currently for demonstration use only.
@ -28,8 +29,10 @@ import javax.ws.rs.core.Response
* *
* TODO: replace simulated date advancement with business event based implementation * TODO: replace simulated date advancement with business event based implementation
*/ */
@Path("irs")
class InterestRateSwapAPI(val rpc: CordaRPCOps) { @RestController
@RequestMapping("/api/irs")
class InterestRateSwapAPI {
private val logger = loggerFor<InterestRateSwapAPI>() private val logger = loggerFor<InterestRateSwapAPI>()
@ -44,6 +47,10 @@ class InterestRateSwapAPI(val rpc: CordaRPCOps) {
} }
} }
@Autowired
lateinit var rpc: CordaRPCOps
private fun getAllDeals(): Array<InterestRateSwap.State> { private fun getAllDeals(): Array<InterestRateSwap.State> {
val vault = rpc.vaultQueryBy<InterestRateSwap.State>().states val vault = rpc.vaultQueryBy<InterestRateSwap.State>().states
val states = vault.filterStatesOfType<InterestRateSwap.State>() val states = vault.filterStatesOfType<InterestRateSwap.State>()
@ -51,33 +58,30 @@ class InterestRateSwapAPI(val rpc: CordaRPCOps) {
return swaps return swaps
} }
@GET @GetMapping("/deals")
@Path("deals")
@Produces(MediaType.APPLICATION_JSON)
fun fetchDeals(): Array<InterestRateSwap.State> = getAllDeals() fun fetchDeals(): Array<InterestRateSwap.State> = getAllDeals()
@POST @PostMapping("/deals")
@Path("deals") fun storeDeal(@RequestBody newDeal: InterestRateSwap.State): ResponseEntity<Any?> {
@Consumes(MediaType.APPLICATION_JSON)
fun storeDeal(newDeal: InterestRateSwap.State): Response {
return try { return try {
rpc.startFlow(AutoOfferFlow::Requester, newDeal).returnValue.getOrThrow() rpc.startFlow(AutoOfferFlow::Requester, newDeal).returnValue.getOrThrow()
Response.created(URI.create(generateDealLink(newDeal))).build() ResponseEntity.created(URI.create(generateDealLink(newDeal))).build()
} catch (ex: Throwable) { } catch (ex: Throwable) {
logger.info("Exception when creating deal: $ex") logger.info("Exception when creating deal: $ex", ex)
Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(ex.toString()).build() ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.toString())
} }
} }
@GET @GetMapping("/deals/{ref:.+}")
@Path("deals/{ref}") fun fetchDeal(@PathVariable ref: String?): ResponseEntity<Any?> {
@Produces(MediaType.APPLICATION_JSON) val deal = getDealByRef(ref!!)
fun fetchDeal(@PathParam("ref") ref: String): Response { return if (deal == null) {
val deal = getDealByRef(ref) ResponseEntity.notFound().build()
if (deal == null) {
return Response.status(Response.Status.NOT_FOUND).build()
} else { } else {
return Response.ok().entity(deal).build() ResponseEntity.ok(deal)
} }
} }
@GetMapping("/deals/networksnapshot")
fun fetchDeal() = rpc.networkMapSnapshot().toString()
} }

View File

@ -0,0 +1,2 @@
corda.host=localhost:10006
server.port=10007

View File

@ -0,0 +1,2 @@
corda.host=localhost:10009
server.port=10010

View File

@ -0,0 +1,2 @@
corda.host=localhost:10003
server.port=10004

View File

@ -0,0 +1,2 @@
corda.user=user
corda.password=password

Some files were not shown because too many files have changed in this diff Show More