diff --git a/.gitignore b/.gitignore
index 2fe98442b7..f3ae176bcc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,7 +14,6 @@ local.properties
# General build files
**/build/*
-!docs/build/*
lib/dokka.jar
@@ -34,6 +33,9 @@ lib/dokka.jar
.idea/shelf
.idea/dataSources
+# Include the -parameters compiler option by default in IntelliJ required for serialization.
+!.idea/compiler.xml
+
# if you remove the above rule, at least ignore the following:
# User-specific stuff:
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000000..d639c80f07
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Attachment_Demo__Run_Nodes.xml b/.idea/runConfigurations/Attachment_Demo__Run_Nodes.xml
deleted file mode 100644
index ecad50d56c..0000000000
--- a/.idea/runConfigurations/Attachment_Demo__Run_Nodes.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.idea/runConfigurations/Attachment_Demo__Run_Recipient.xml b/.idea/runConfigurations/Attachment_Demo__Run_Recipient.xml
deleted file mode 100644
index 2d3762ecd0..0000000000
--- a/.idea/runConfigurations/Attachment_Demo__Run_Recipient.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations/Attachment_Demo__Run_Sender.xml b/.idea/runConfigurations/Attachment_Demo__Run_Sender.xml
deleted file mode 100644
index 419abc2218..0000000000
--- a/.idea/runConfigurations/Attachment_Demo__Run_Sender.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations/Bank_of_Corda_Demo__Run_Issuer.xml b/.idea/runConfigurations/Bank_of_Corda_Demo__Run_Issuer.xml
deleted file mode 100644
index 314089c670..0000000000
--- a/.idea/runConfigurations/Bank_of_Corda_Demo__Run_Issuer.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations/Bank_of_Corda_Demo__Run_RPC_Cash_Issue.xml b/.idea/runConfigurations/Bank_of_Corda_Demo__Run_RPC_Cash_Issue.xml
deleted file mode 100644
index dfe14546a3..0000000000
--- a/.idea/runConfigurations/Bank_of_Corda_Demo__Run_RPC_Cash_Issue.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations/Bank_of_Corda_Demo__Run_Web_Cash_Issue.xml b/.idea/runConfigurations/Bank_of_Corda_Demo__Run_Web_Cash_Issue.xml
deleted file mode 100644
index ce474a1406..0000000000
--- a/.idea/runConfigurations/Bank_of_Corda_Demo__Run_Web_Cash_Issue.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations/IRS_Demo__Run_Date_Change.xml b/.idea/runConfigurations/IRS_Demo__Run_Date_Change.xml
deleted file mode 100644
index 7491348805..0000000000
--- a/.idea/runConfigurations/IRS_Demo__Run_Date_Change.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations/IRS_Demo__Run_Nodes.xml b/.idea/runConfigurations/IRS_Demo__Run_Nodes.xml
deleted file mode 100644
index 78a7bf26a6..0000000000
--- a/.idea/runConfigurations/IRS_Demo__Run_Nodes.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.idea/runConfigurations/IRS_Demo__Run_Trade.xml b/.idea/runConfigurations/IRS_Demo__Run_Trade.xml
deleted file mode 100644
index 831ceca67c..0000000000
--- a/.idea/runConfigurations/IRS_Demo__Run_Trade.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations/IRS_Demo__Upload_Rates.xml b/.idea/runConfigurations/IRS_Demo__Upload_Rates.xml
deleted file mode 100644
index 22fc908198..0000000000
--- a/.idea/runConfigurations/IRS_Demo__Upload_Rates.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations/Raft_Notary_Demo__Run_Nodes.xml b/.idea/runConfigurations/Raft_Notary_Demo__Run_Nodes.xml
deleted file mode 100644
index a94708b378..0000000000
--- a/.idea/runConfigurations/Raft_Notary_Demo__Run_Nodes.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations/Raft_Notary_Demo__Run_Notarisation.xml b/.idea/runConfigurations/Raft_Notary_Demo__Run_Notarisation.xml
deleted file mode 100644
index 23512d7425..0000000000
--- a/.idea/runConfigurations/Raft_Notary_Demo__Run_Notarisation.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations/SIMM_Valuation_Demo.xml b/.idea/runConfigurations/SIMM_Valuation_Demo.xml
deleted file mode 100644
index 8b74d24497..0000000000
--- a/.idea/runConfigurations/SIMM_Valuation_Demo.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations/Trader_Demo__Run_Buyer.xml b/.idea/runConfigurations/Trader_Demo__Run_Buyer.xml
deleted file mode 100644
index dbfbf49a48..0000000000
--- a/.idea/runConfigurations/Trader_Demo__Run_Buyer.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations/Trader_Demo__Run_Nodes.xml b/.idea/runConfigurations/Trader_Demo__Run_Nodes.xml
deleted file mode 100644
index bf383f618f..0000000000
--- a/.idea/runConfigurations/Trader_Demo__Run_Nodes.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.idea/runConfigurations/Trader_Demo__Run_Seller.xml b/.idea/runConfigurations/Trader_Demo__Run_Seller.xml
deleted file mode 100644
index 5202c95ad1..0000000000
--- a/.idea/runConfigurations/Trader_Demo__Run_Seller.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index 601bf3bd71..e592d86a21 100644
--- a/README.md
+++ b/README.md
@@ -61,8 +61,9 @@ To look at the Corda source and run some sample applications:
## Development State
-Corda is currently in very early development and should not be used in production systems. Breaking
-changes will happen on minor versions until 1.0. Experimentation with Corda is recommended.
+Corda is under active development and is maturing rapidly. We are targeting
+production-readiness in 2017. The API will continue to evolve throughout 2017;
+backwards compatibility not assured until version 1.0.
Pull requests, experiments, and contributions are encouraged and welcomed.
diff --git a/build.gradle b/build.gradle
index 85211e7cff..bf86531ff6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,16 +1,20 @@
+
buildscript {
// For sharing constants between builds
- Properties props = new Properties()
- file("publish.properties").withInputStream { props.load(it) }
+ Properties constants = new Properties()
+ file("$projectDir/constants.properties").withInputStream { constants.load(it) }
// Our version: bump this on release.
- ext.corda_version = "0.10-SNAPSHOT"
- ext.gradle_plugins_version = props.getProperty("gradlePluginsVersion")
+ ext.corda_release_version = "0.12-SNAPSHOT"
+ // Increment this on any release that changes public APIs anywhere in the Corda platform
+ // TODO This is going to be difficult until we have a clear separation throughout the code of what is public and what is internal
+ ext.corda_platform_version = 1
+ ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
// Dependency versions. Can run 'gradle dependencyUpdates' to find new versions of things.
//
// TODO: Sort this alphabetically.
- ext.kotlin_version = '1.0.7'
+ ext.kotlin_version = constants.getProperty("kotlinVersion")
ext.quasar_version = '0.7.6' // TODO: Upgrade to 0.7.7+ when Quasar bug 238 is resolved.
ext.asm_version = '0.5.3'
ext.artemis_version = '1.5.3'
@@ -19,24 +23,27 @@ buildscript {
ext.jersey_version = '2.25'
ext.jolokia_version = '2.0.0-M3'
ext.assertj_version = '3.6.1'
- ext.slf4j_version = '1.7.24'
+ ext.slf4j_version = '1.7.25'
ext.log4j_version = '2.7'
- ext.bouncycastle_version = '1.56'
- ext.guava_version = '19.0'
+ ext.bouncycastle_version = constants.getProperty("bouncycastleVersion")
+ ext.guava_version = constants.getProperty("guavaVersion")
ext.quickcheck_version = '0.7'
ext.okhttp_version = '3.5.0'
ext.netty_version = '4.1.5.Final'
- ext.typesafe_config_version = '1.3.1'
+ ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion")
ext.fileupload_version = '1.3.2'
ext.junit_version = '4.12'
+ ext.mockito_version = '1.10.19'
ext.jopt_simple_version = '5.0.2'
ext.jansi_version = '1.14'
ext.hibernate_version = '5.2.6.Final'
ext.h2_version = '1.4.194'
ext.rxjava_version = '1.2.4'
- ext.requery_version = '1.1.1'
+ ext.requery_version = '1.2.1'
ext.dokka_version = '0.9.13'
- ext.crash_version = '1.3.2'
+
+ // Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
+ ext.java8_minUpdateVersion = '131'
repositories {
mavenLocal()
@@ -66,13 +73,10 @@ ext {
corda_revision = org.ajoberstar.grgit.Grgit.open(file('.')).head().id
}
-apply plugin: 'kotlin'
apply plugin: 'project-report'
apply plugin: 'com.github.ben-manes.versions'
apply plugin: 'net.corda.plugins.publish-utils'
-apply plugin: 'net.corda.plugins.quasar-utils'
apply plugin: 'net.corda.plugins.cordformation'
-apply plugin: 'org.jetbrains.dokka'
// We need the following three lines even though they're inside an allprojects {} block below because otherwise
// IntelliJ gets confused when importing the project and ends up erasing and recreating the .idea directory, along
@@ -84,22 +88,65 @@ targetCompatibility = 1.8
allprojects {
+ apply plugin: 'kotlin'
apply plugin: 'java'
apply plugin: 'jacoco'
sourceCompatibility = 1.8
targetCompatibility = 1.8
+ // Use manual resource copying of log4j2.xml rather than source sets.
+ // This prevents problems in IntelliJ with regard to duplicate source roots.
+ processTestResources {
+ from file("$rootDir/config/test/log4j2.xml")
+ }
+
tasks.withType(JavaCompile) {
- options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" << "-Xlint:-options"
+ options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" << "-Xlint:-options" << "-parameters"
+ }
+
+ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
+ kotlinOptions {
+ languageVersion = "1.1"
+ apiVersion = "1.1"
+ jvmTarget = "1.8"
+ javaParameters = true // Useful for reflection.
+ }
+ }
+
+ tasks.withType(Jar) { // Includes War and Ear
+ manifest {
+ attributes('Corda-Release-Version': corda_release_version)
+ attributes('Corda-Platform-Version': corda_platform_version)
+ attributes('Corda-Revision': corda_revision)
+ attributes('Corda-Vendor': 'Corda Open Source')
+ }
+ }
+
+ tasks.withType(Test) {
+ // Prevent the project from creating temporary files outside of the build directory.
+ systemProperties['java.io.tmpdir'] = buildDir
}
group 'net.corda'
- version "$corda_version"
+ version "$corda_release_version"
repositories {
+ mavenLocal()
+ mavenCentral()
+ jcenter()
+ // TODO: remove this once we eliminate Exposed
+ maven {
+ url 'https://dl.bintray.com/kotlin/exposed'
+ }
maven { url 'https://jitpack.io' }
}
+
+ configurations.compile {
+ // We want to use SLF4J's version of these bindings: jcl-over-slf4j
+ // Remove any transitive dependency on Apache's version.
+ exclude group: 'commons-logging', module: 'commons-logging'
+ }
}
// Check that we are running on a Java 8 JDK. The source/targetCompatibility values above aren't sufficient to
@@ -108,7 +155,7 @@ allprojects {
// We recommend a specific minor version (unfortunately, not checkable directly) because JavaFX adds APIs in
// minor releases, so we can't work with just any Java 8, it has to be a recent one.
if (!JavaVersion.current().java8Compatible)
- throw new GradleException("Corda requires Java 8, please upgrade to at least 1.8.0_112")
+ throw new GradleException("Corda requires Java 8, please upgrade to at least 1.8.0_$java8_minUpdateVersion")
repositories {
mavenCentral()
@@ -123,8 +170,9 @@ dependencies {
compile project(':node')
compile "com.google.guava:guava:$guava_version"
- runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts')
- runtime project(path: ":node:webserver:webcapsule", configuration: 'runtimeArtifacts')
+ // Set to compile to ensure it exists now deploy nodes no longer relies on build
+ compile project(path: ":node:capsule", configuration: 'runtimeArtifacts')
+ compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts')
// For the buildCordappDependenciesJar task
runtime project(':client:jfx')
@@ -132,7 +180,7 @@ dependencies {
runtime project(':client:rpc')
runtime project(':core')
runtime project(':finance')
- runtime project(':node:webserver')
+ runtime project(':webserver')
testCompile project(':test-utils')
}
@@ -161,18 +209,18 @@ tasks.withType(Test) {
reports.html.destination = file("${reporting.baseDir}/${name}")
}
-task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
+task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
directory "./build/nodes"
- networkMap "Controller"
+ networkMap "CN=Controller,O=R3,OU=corda,L=London,C=UK"
node {
- name "Controller"
+ name "CN=Controller,O=R3,OU=corda,L=London,C=UK"
nearestCity "London"
advertisedServices = ["corda.notary.validating"]
p2pPort 10002
cordapps = []
}
node {
- name "Bank A"
+ name "CN=Bank A,O=R3,OU=corda,L=London,C=UK"
nearestCity "London"
advertisedServices = []
p2pPort 10012
@@ -181,7 +229,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
cordapps = []
}
node {
- name "Bank B"
+ name "CN=Bank B,O=R3,OU=corda,L=London,C=UK"
nearestCity "New York"
advertisedServices = []
p2pPort 10007
@@ -201,7 +249,7 @@ bintrayConfig {
projectUrl = 'https://github.com/corda/corda'
gpgSign = true
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
- publications = ['jfx', 'mock', 'rpc', 'core', 'corda', 'corda-webserver', 'finance', 'node', 'node-api', 'node-schemas', 'test-utils', 'jackson', 'webserver']
+ publications = ['jfx', 'mock', 'rpc', 'core', 'corda', 'corda-webserver', 'finance', 'node', 'node-api', 'node-schemas', 'test-utils', 'jackson', 'verifier', 'webserver']
license {
name = 'Apache-2.0'
url = 'https://www.apache.org/licenses/LICENSE-2.0'
@@ -214,25 +262,6 @@ bintrayConfig {
}
}
-// API docs
-
-dokka {
- moduleName = 'corda'
- outputDirectory = 'docs/build/html/api/kotlin'
- processConfigurations = ['compile']
- sourceDirs = files('core/src/main/kotlin', 'client/jfx/src/main/kotlin', 'client/mock/src/main/kotlin', 'client/rpc/src/main/kotlin', 'node/src/main/kotlin', 'finance/src/main/kotlin', 'client/jackson/src/main/kotlin')
-}
-
-task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
- moduleName = 'corda'
- outputFormat = "javadoc"
- outputDirectory = 'docs/build/html/api/javadoc'
- processConfigurations = ['compile']
- sourceDirs = files('core/src/main/kotlin', 'client/jfx/src/main/kotlin', 'client/mock/src/main/kotlin', 'client/rpc/src/main/kotlin', 'node/src/main/kotlin', 'finance/src/main/kotlin', 'client/jackson/src/main/kotlin')
-}
-
-task apidocs(dependsOn: ['dokka', 'dokkaJavadoc'])
-
// Build a ZIP of all JARs required to compile the Cordapp template
// Note: corda.jar is used at runtime so no runtime ZIP is necessary.
// Resulting ZIP can be found in "build/distributions"
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index fb34bb0911..bd4f497c0b 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -1,3 +1,10 @@
+buildscript {
+ Properties constants = new Properties()
+ file("../constants.properties").withInputStream { constants.load(it) }
+
+ ext.guava_version = constants.getProperty("guavaVersion")
+}
+
apply plugin: 'maven'
repositories {
@@ -5,6 +12,5 @@ repositories {
}
dependencies {
- // Cannot use ext.guava_version here :(
- compile "com.google.guava:guava:20.0"
+ compile "com.google.guava:guava:$guava_version"
}
diff --git a/client/jackson/build.gradle b/client/jackson/build.gradle
index f961a88581..9a7f6a6306 100644
--- a/client/jackson/build.gradle
+++ b/client/jackson/build.gradle
@@ -2,18 +2,9 @@ apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'net.corda.plugins.publish-utils'
-repositories {
- mavenLocal()
- mavenCentral()
- jcenter()
- maven {
- url 'https://dl.bintray.com/kotlin/exposed'
- }
-}
-
dependencies {
compile project(':core')
- compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
// Jackson and its plugins: parsing to/from JSON and other textual formats.
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 d9cdd9a0ce..361313dbe3 100644
--- a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt
+++ b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt
@@ -10,6 +10,8 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule
import net.corda.core.contracts.Amount
import net.corda.core.contracts.BusinessCalendar
import net.corda.core.crypto.*
+import net.corda.core.identity.AnonymousParty
+import net.corda.core.identity.Party
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.IdentityService
@@ -17,7 +19,10 @@ import net.corda.core.serialization.OpaqueBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.i2p.crypto.eddsa.EdDSAPublicKey
+import org.bouncycastle.asn1.ASN1InputStream
+import org.bouncycastle.asn1.x500.X500Name
import java.math.BigDecimal
+import java.security.PublicKey
import java.util.*
/**
@@ -31,21 +36,28 @@ object JacksonSupport {
// If you change this API please update the docs in the docsite (json.rst)
interface PartyObjectMapper {
+ @Deprecated("Use partyFromX500Name instead")
fun partyFromName(partyName: String): Party?
- fun partyFromKey(owningKey: CompositeKey): Party?
+ fun partyFromPrincipal(principal: X500Name): Party?
+ fun partyFromKey(owningKey: PublicKey): Party?
}
class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
override fun partyFromName(partyName: String): Party? = rpc.partyFromName(partyName)
- override fun partyFromKey(owningKey: CompositeKey): Party? = rpc.partyFromKey(owningKey)
+ override fun partyFromPrincipal(principal: X500Name): Party? = rpc.partyFromX500Name(principal)
+ override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey)
}
+
class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
override fun partyFromName(partyName: String): Party? = identityService.partyFromName(partyName)
- override fun partyFromKey(owningKey: CompositeKey): Party? = identityService.partyFromKey(owningKey)
+ override fun partyFromPrincipal(principal: X500Name): Party? = identityService.partyFromX500Name(principal)
+ override fun partyFromKey(owningKey: PublicKey): Party? = identityService.partyFromKey(owningKey)
}
- class NoPartyObjectMapper(factory: JsonFactory): PartyObjectMapper, ObjectMapper(factory) {
+
+ class NoPartyObjectMapper(factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
override fun partyFromName(partyName: String): Party? = throw UnsupportedOperationException()
- override fun partyFromKey(owningKey: CompositeKey): Party? = throw UnsupportedOperationException()
+ override fun partyFromPrincipal(principal: X500Name): Party? = throw UnsupportedOperationException()
+ override fun partyFromKey(owningKey: PublicKey): Party? = throw UnsupportedOperationException()
}
val cordaModule: Module by lazy {
@@ -82,6 +94,10 @@ object JacksonSupport {
// For OpaqueBytes
addDeserializer(OpaqueBytes::class.java, OpaqueBytesDeserializer)
addSerializer(OpaqueBytes::class.java, OpaqueBytesSerializer)
+
+ // For X.500 distinguished names
+ addDeserializer(X500Name::class.java, X500NameDeserializer)
+ addSerializer(X500Name::class.java, X500NameSerializer)
}
}
@@ -126,14 +142,14 @@ object JacksonSupport {
}
// TODO this needs to use some industry identifier(s) instead of these keys
- val key = CompositeKey.parseFromBase58(parser.text)
+ val key = parsePublicKeyBase58(parser.text)
return AnonymousParty(key)
}
}
object PartySerializer : JsonSerializer() {
override fun serialize(obj: Party, generator: JsonGenerator, provider: SerializerProvider) {
- generator.writeString(obj.name)
+ generator.writeString(obj.name.toString())
}
}
@@ -144,8 +160,28 @@ object JacksonSupport {
}
val mapper = parser.codec as PartyObjectMapper
- // TODO this needs to use some industry identifier(s) not just these human readable names
- return mapper.partyFromName(parser.text) ?: throw JsonParseException(parser, "Could not find a Party with name ${parser.text}")
+ val principal = X500Name(parser.text)
+ return mapper.partyFromPrincipal(principal) ?: throw JsonParseException(parser, "Could not find a Party with name ${principal}")
+ }
+ }
+
+ object X500NameSerializer : JsonSerializer() {
+ override fun serialize(obj: X500Name, generator: JsonGenerator, provider: SerializerProvider) {
+ generator.writeString(obj.toString())
+ }
+ }
+
+ object X500NameDeserializer : JsonDeserializer() {
+ override fun deserialize(parser: JsonParser, context: DeserializationContext): X500Name {
+ if (parser.currentToken == JsonToken.FIELD_NAME) {
+ parser.nextToken()
+ }
+
+ return try {
+ X500Name(parser.text)
+ } catch(ex: IllegalArgumentException) {
+ throw JsonParseException(parser, "Invalid X.500 name ${parser.text}: ${ex.message}", ex)
+ }
}
}
@@ -204,7 +240,7 @@ object JacksonSupport {
object PublicKeySerializer : JsonSerializer() {
override fun serialize(obj: EdDSAPublicKey, generator: JsonGenerator, provider: SerializerProvider) {
- check(obj.params == ed25519Curve)
+ check(obj.params == Crypto.EDDSA_ED25519_SHA512.algSpec)
generator.writeString(obj.toBase58String())
}
}
@@ -212,7 +248,7 @@ object JacksonSupport {
object PublicKeyDeserializer : JsonDeserializer() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): EdDSAPublicKey {
return try {
- parsePublicKeyBase58(parser.text)
+ parsePublicKeyBase58(parser.text) as EdDSAPublicKey
} catch (e: Exception) {
throw JsonParseException(parser, "Invalid public key ${parser.text}: ${e.message}")
}
@@ -228,7 +264,7 @@ object JacksonSupport {
object CompositeKeyDeserializer : JsonDeserializer() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): CompositeKey {
return try {
- CompositeKey.parseFromBase58(parser.text)
+ parsePublicKeyBase58(parser.text) as CompositeKey
} catch (e: Exception) {
throw JsonParseException(parser, "Invalid composite key ${parser.text}: ${e.message}")
}
diff --git a/client/jackson/src/main/kotlin/net/corda/jackson/StringToMethodCallParser.kt b/client/jackson/src/main/kotlin/net/corda/jackson/StringToMethodCallParser.kt
index edc4767343..225bb9ec59 100644
--- a/client/jackson/src/main/kotlin/net/corda/jackson/StringToMethodCallParser.kt
+++ b/client/jackson/src/main/kotlin/net/corda/jackson/StringToMethodCallParser.kt
@@ -5,17 +5,15 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.google.common.collect.HashMultimap
import com.google.common.collect.Multimap
-import com.google.common.collect.MultimapBuilder
import net.corda.jackson.StringToMethodCallParser.ParsedMethodCall
import org.slf4j.LoggerFactory
import java.lang.reflect.Constructor
import java.lang.reflect.Method
-import java.util.*
import java.util.concurrent.Callable
import javax.annotation.concurrent.ThreadSafe
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
-import kotlin.reflect.KotlinReflectionInternalError
+import kotlin.reflect.jvm.internal.KotlinReflectionInternalError
import kotlin.reflect.jvm.kotlinFunction
/**
@@ -75,14 +73,14 @@ import kotlin.reflect.jvm.kotlinFunction
@ThreadSafe
open class StringToMethodCallParser @JvmOverloads constructor(
targetType: Class,
- private val om: ObjectMapper = JacksonSupport.createNonRpcMapper(YAMLFactory()))
-{
+ private val om: ObjectMapper = JacksonSupport.createNonRpcMapper(YAMLFactory())) {
/** Same as the regular constructor but takes a Kotlin reflection [KClass] instead of a Java [Class]. */
constructor(targetType: KClass) : this(targetType.java)
companion object {
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
private val ignoredNames = Object::class.java.methods.map { it.name }
+
private fun methodsFromType(clazz: Class<*>): Multimap {
val result = HashMultimap.create()
for ((key, value) in clazz.methods.filterNot { it.isSynthetic && it.name !in ignoredNames }.map { it.name to it }) {
@@ -90,6 +88,7 @@ open class StringToMethodCallParser @JvmOverloads constructor(
}
return result
}
+
private val log = LoggerFactory.getLogger(StringToMethodCallParser::class.java)!!
}
@@ -126,7 +125,7 @@ open class StringToMethodCallParser @JvmOverloads constructor(
return method.parameters.mapIndexed { index, param ->
when {
param.isNamePresent -> param.name
- // index + 1 because the first Kotlin reflection param is 'this', but that doesn't match Java reflection.
+ // index + 1 because the first Kotlin reflection param is 'this', but that doesn't match Java reflection.
kf != null -> kf.parameters[index + 1].name ?: throw UnparseableCallException.ReflectionDataMissing(method.name, index)
else -> throw UnparseableCallException.ReflectionDataMissing(method.name, index)
}
@@ -147,11 +146,12 @@ open class StringToMethodCallParser @JvmOverloads constructor(
}
}
- open class UnparseableCallException(command: String) : Exception("Could not parse as a command: $command") {
+ open class UnparseableCallException(command: String, cause: Throwable? = null) : Exception("Could not parse as a command: $command", cause) {
class UnknownMethod(val methodName: String) : UnparseableCallException("Unknown command name: $methodName")
class MissingParameter(methodName: String, val paramName: String, command: String) : UnparseableCallException("Parameter $paramName missing from attempt to invoke $methodName in command: $command")
class TooManyParameters(methodName: String, command: String) : UnparseableCallException("Too many parameters provided for $methodName: $command")
class ReflectionDataMissing(methodName: String, argIndex: Int) : UnparseableCallException("Method $methodName missing parameter name at index $argIndex")
+ class FailedParse(e: Exception) : UnparseableCallException(e.message ?: e.toString(), e)
}
/**
@@ -193,10 +193,14 @@ open class StringToMethodCallParser @JvmOverloads constructor(
val parameterString = "{ $args }"
val tree: JsonNode = om.readTree(parameterString) ?: throw UnparseableCallException(args)
if (tree.size() > parameters.size) throw UnparseableCallException.TooManyParameters(methodNameHint, args)
- val inOrderParams: List = parameters.mapIndexed { index, param ->
+ val inOrderParams: List = parameters.mapIndexed { _, param ->
val (argName, argType) = param
val entry = tree[argName] ?: throw UnparseableCallException.MissingParameter(methodNameHint, argName, args)
- om.readValue(entry.traverse(om), argType)
+ try {
+ om.readValue(entry.traverse(om), argType)
+ } catch(e: Exception) {
+ throw UnparseableCallException.FailedParse(e)
+ }
}
if (log.isDebugEnabled) {
inOrderParams.forEachIndexed { i, param ->
diff --git a/client/jackson/src/test/kotlin/net/corda/jackson/StringToMethodCallParserTest.kt b/client/jackson/src/test/kotlin/net/corda/jackson/StringToMethodCallParserTest.kt
index 2944427ac6..7c46e4919b 100644
--- a/client/jackson/src/test/kotlin/net/corda/jackson/StringToMethodCallParserTest.kt
+++ b/client/jackson/src/test/kotlin/net/corda/jackson/StringToMethodCallParserTest.kt
@@ -10,7 +10,7 @@ class StringToMethodCallParserTest {
fun simple() = "simple"
fun string(note: String) = note
fun twoStrings(a: String, b: String) = a + b
- fun simpleObject(hash: SecureHash.SHA256) = hash.toString()!!
+ fun simpleObject(hash: SecureHash.SHA256) = hash.toString()
fun complexObject(pair: Pair) = pair
fun overload(a: String) = a
diff --git a/client/jfx/build.gradle b/client/jfx/build.gradle
index 6279c489c1..ba6ae045c5 100644
--- a/client/jfx/build.gradle
+++ b/client/jfx/build.gradle
@@ -4,18 +4,6 @@ apply plugin: 'net.corda.plugins.publish-utils'
description 'Corda client JavaFX modules'
-repositories {
- mavenLocal()
- mavenCentral()
- maven {
- url 'http://oss.sonatype.org/content/repositories/snapshots'
- }
- jcenter()
- maven {
- url 'https://dl.bintray.com/kotlin/exposed'
- }
-}
-
//noinspection GroovyAssignabilityCheck
configurations {
// we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment
@@ -33,11 +21,6 @@ sourceSets {
srcDir file('src/integration-test/kotlin')
}
}
- test {
- resources {
- srcDir "../../config/test"
- }
- }
}
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
@@ -48,6 +31,7 @@ dependencies {
compile project(':finance')
compile project(':client:rpc')
+ compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
compile "com.google.guava:guava:$guava_version"
// ReactFX: Functional reactive UI programming.
diff --git a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/CordaRPCClientTest.kt b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/CordaRPCClientTest.kt
deleted file mode 100644
index b6dc1572c3..0000000000
--- a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/CordaRPCClientTest.kt
+++ /dev/null
@@ -1,118 +0,0 @@
-package net.corda.client.jfx
-
-import net.corda.client.rpc.CordaRPCClient
-import net.corda.core.contracts.DOLLARS
-import net.corda.core.flows.FlowException
-import net.corda.core.getOrThrow
-import net.corda.core.messaging.startFlow
-import net.corda.core.node.services.ServiceInfo
-import net.corda.core.random63BitValue
-import net.corda.core.serialization.OpaqueBytes
-import net.corda.flows.CashIssueFlow
-import net.corda.flows.CashPaymentFlow
-import net.corda.node.internal.Node
-import net.corda.node.services.startFlowPermission
-import net.corda.node.services.transactions.ValidatingNotaryService
-import net.corda.nodeapi.User
-import net.corda.testing.node.NodeBasedTest
-import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
-import org.assertj.core.api.Assertions.assertThatExceptionOfType
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import java.util.*
-import kotlin.test.assertEquals
-import kotlin.test.assertTrue
-
-class CordaRPCClientTest : NodeBasedTest() {
- private val rpcUser = User("user1", "test", permissions = setOf(
- startFlowPermission(),
- startFlowPermission()
- ))
- private lateinit var node: Node
- private lateinit var client: CordaRPCClient
-
- @Before
- fun setUp() {
- node = startNode("Alice", rpcUsers = listOf(rpcUser), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))).getOrThrow()
- client = CordaRPCClient(node.configuration.rpcAddress!!)
- }
-
- @After
- fun done() {
- client.close()
- }
-
- @Test
- fun `log in with valid username and password`() {
- client.start(rpcUser.username, rpcUser.password)
- }
-
- @Test
- fun `log in with unknown user`() {
- assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
- client.start(random63BitValue().toString(), rpcUser.password)
- }
- }
-
- @Test
- fun `log in with incorrect password`() {
- assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
- client.start(rpcUser.username, random63BitValue().toString())
- }
- }
-
- @Test
- fun `close-send deadlock and premature shutdown on empty observable`() {
- println("Starting client")
- client.start(rpcUser.username, rpcUser.password)
- println("Creating proxy")
- val proxy = client.proxy()
- println("Starting flow")
- val flowHandle = proxy.startFlow(
- ::CashIssueFlow,
- 20.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity, node.info.legalIdentity)
- println("Started flow, waiting on result")
- flowHandle.progress.subscribe {
- println("PROGRESS $it")
- }
- println("Result: ${flowHandle.returnValue.getOrThrow()}")
- }
-
- @Test
- fun `FlowException thrown by flow`() {
- client.start(rpcUser.username, rpcUser.password)
- val proxy = client.proxy()
- val handle = proxy.startFlow(::CashPaymentFlow, 100.DOLLARS, node.info.legalIdentity)
- // TODO Restrict this to CashException once RPC serialisation has been fixed
- assertThatExceptionOfType(FlowException::class.java).isThrownBy {
- handle.returnValue.getOrThrow()
- }
- }
-
- @Test
- fun `get cash balances`() {
- println("Starting client")
- client.start(rpcUser.username, rpcUser.password)
- println("Creating proxy")
- val proxy = client.proxy()
-
- val startCash = proxy.getCashBalances()
- assertTrue(startCash.isEmpty(), "Should not start with any cash")
-
- val flowHandle = proxy.startFlow(::CashIssueFlow,
- 123.DOLLARS, OpaqueBytes.of(0),
- node.info.legalIdentity, node.info.legalIdentity
- )
- println("Started issuing cash, waiting on result")
- flowHandle.progress.subscribe {
- println("CashIssue PROGRESS $it")
- }
-
- val finishCash = proxy.getCashBalances()
- println("Cash Balances: $finishCash")
- assertEquals(1, finishCash.size)
- assertEquals(123.DOLLARS, finishCash.get(Currency.getInstance("USD")))
- }
-
-}
diff --git a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt
index 48df7516b1..5f22eadbd4 100644
--- a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt
+++ b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt
@@ -6,6 +6,9 @@ import net.corda.core.bufferUntilSubscribed
import net.corda.core.contracts.Amount
import net.corda.core.contracts.DOLLARS
import net.corda.core.contracts.USD
+import net.corda.core.crypto.isFulfilledBy
+import net.corda.core.crypto.keys
+import net.corda.core.flows.FlowInitiator
import net.corda.core.flows.StateMachineRunId
import net.corda.core.getOrThrow
import net.corda.core.messaging.CordaRPCOps
@@ -18,6 +21,10 @@ import net.corda.core.node.services.StateMachineTransactionMapping
import net.corda.core.node.services.Vault
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.SignedTransaction
+import net.corda.core.utilities.ALICE
+import net.corda.core.utilities.BOB
+import net.corda.core.utilities.CHARLIE
+import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.flows.CashExitFlow
import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow
@@ -30,21 +37,25 @@ import net.corda.testing.expect
import net.corda.testing.expectEvents
import net.corda.testing.node.DriverBasedTest
import net.corda.testing.sequence
+import org.bouncycastle.asn1.x500.X500Name
import org.junit.Test
import rx.Observable
class NodeMonitorModelTest : DriverBasedTest() {
lateinit var aliceNode: NodeInfo
+ lateinit var bobNode: NodeInfo
lateinit var notaryNode: NodeInfo
lateinit var rpc: CordaRPCOps
+ lateinit var rpcBob: CordaRPCOps
lateinit var stateMachineTransactionMapping: Observable
lateinit var stateMachineUpdates: Observable
+ lateinit var stateMachineUpdatesBob: Observable
lateinit var progressTracking: Observable
lateinit var transactions: Observable
lateinit var vaultUpdates: Observable
lateinit var networkMapUpdates: Observable
- lateinit var newNode: (String) -> NodeInfo
+ lateinit var newNode: (X500Name) -> NodeInfo
override fun setup() = driver {
val cashUser = User("user1", "test", permissions = setOf(
@@ -52,15 +63,14 @@ class NodeMonitorModelTest : DriverBasedTest() {
startFlowPermission(),
startFlowPermission())
)
- val aliceNodeFuture = startNode("Alice", rpcUsers = listOf(cashUser))
- val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
+ val aliceNodeFuture = startNode(ALICE.name, rpcUsers = listOf(cashUser))
+ val notaryNodeFuture = startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
val aliceNodeHandle = aliceNodeFuture.getOrThrow()
val notaryNodeHandle = notaryNodeFuture.getOrThrow()
aliceNode = aliceNodeHandle.nodeInfo
notaryNode = notaryNodeHandle.nodeInfo
newNode = { nodeName -> startNode(nodeName).getOrThrow().nodeInfo }
val monitor = NodeMonitorModel()
-
stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed()
stateMachineUpdates = monitor.stateMachineUpdates.bufferUntilSubscribed()
progressTracking = monitor.progressTracking.bufferUntilSubscribed()
@@ -70,26 +80,32 @@ class NodeMonitorModelTest : DriverBasedTest() {
monitor.register(aliceNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password)
rpc = monitor.proxyObservable.value!!
+
+ val bobNodeHandle = startNode(BOB.name, rpcUsers = listOf(cashUser)).getOrThrow()
+ bobNode = bobNodeHandle.nodeInfo
+ val monitorBob = NodeMonitorModel()
+ stateMachineUpdatesBob = monitorBob.stateMachineUpdates.bufferUntilSubscribed()
+ monitorBob.register(bobNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password)
+ rpcBob = monitorBob.proxyObservable.value!!
runTest()
}
@Test
fun `network map update`() {
- newNode("Bob")
- newNode("Charlie")
+ newNode(CHARLIE.name)
networkMapUpdates.filter { !it.node.advertisedServices.any { it.info.type.isNotary() } }
.filter { !it.node.advertisedServices.any { it.info.type == NetworkMapService.type } }
.expectEvents(isStrict = false) {
sequence(
// TODO : Add test for remove when driver DSL support individual node shutdown.
expect { output: NetworkMapCache.MapChange ->
- require(output.node.legalIdentity.name == "Alice") { "Expecting : Alice, Actual : ${output.node.legalIdentity.name}" }
+ require(output.node.legalIdentity.name == ALICE.name) { "Expecting : ${ALICE.name}, Actual : ${output.node.legalIdentity.name}" }
},
expect { output: NetworkMapCache.MapChange ->
- require(output.node.legalIdentity.name == "Bob") { "Expecting : Bob, Actual : ${output.node.legalIdentity.name}" }
+ require(output.node.legalIdentity.name == BOB.name) { "Expecting : ${BOB.name}, Actual : ${output.node.legalIdentity.name}" }
},
expect { output: NetworkMapCache.MapChange ->
- require(output.node.legalIdentity.name == "Charlie") { "Expecting : Charlie, Actual : ${output.node.legalIdentity.name}" }
+ require(output.node.legalIdentity.name == CHARLIE.name) { "Expecting : ${CHARLIE.name}, Actual : ${output.node.legalIdentity.name}" }
}
)
}
@@ -108,12 +124,12 @@ class NodeMonitorModelTest : DriverBasedTest() {
sequence(
// SNAPSHOT
expect { output: Vault.Update ->
- require(output.consumed.size == 0) { output.consumed.size }
- require(output.produced.size == 0) { output.produced.size }
+ require(output.consumed.isEmpty()) { output.consumed.size }
+ require(output.produced.isEmpty()) { output.produced.size }
},
// ISSUE
expect { output: Vault.Update ->
- require(output.consumed.size == 0) { output.consumed.size }
+ require(output.consumed.isEmpty()) { output.consumed.size }
require(output.produced.size == 1) { output.produced.size }
}
)
@@ -123,7 +139,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
@Test
fun `cash issue and move`() {
rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), aliceNode.legalIdentity, notaryNode.notaryIdentity).returnValue.getOrThrow()
- rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, aliceNode.legalIdentity).returnValue.getOrThrow()
+ rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity).returnValue.getOrThrow()
var issueSmId: StateMachineRunId? = null
var moveSmId: StateMachineRunId? = null
@@ -134,6 +150,8 @@ class NodeMonitorModelTest : DriverBasedTest() {
// ISSUE
expect { add: StateMachineUpdate.Added ->
issueSmId = add.id
+ val initiator = add.stateMachineInfo.initiator
+ require(initiator is FlowInitiator.RPC && initiator.username == "user1")
},
expect { remove: StateMachineUpdate.Removed ->
require(remove.id == issueSmId)
@@ -141,6 +159,8 @@ class NodeMonitorModelTest : DriverBasedTest() {
// MOVE
expect { add: StateMachineUpdate.Added ->
moveSmId = add.id
+ val initiator = add.stateMachineInfo.initiator
+ require(initiator is FlowInitiator.RPC && initiator.username == "user1")
},
expect { remove: StateMachineUpdate.Removed ->
require(remove.id == moveSmId)
@@ -148,28 +168,38 @@ class NodeMonitorModelTest : DriverBasedTest() {
)
}
+ stateMachineUpdatesBob.expectEvents {
+ sequence(
+ // MOVE
+ expect { add: StateMachineUpdate.Added ->
+ val initiator = add.stateMachineInfo.initiator
+ require(initiator is FlowInitiator.Peer && initiator.party.name == aliceNode.legalIdentity.name)
+ }
+ )
+ }
+
transactions.expectEvents {
sequence(
// ISSUE
- expect { tx ->
- require(tx.tx.inputs.isEmpty())
- require(tx.tx.outputs.size == 1)
- val signaturePubKeys = tx.sigs.map { it.by }.toSet()
+ expect { stx ->
+ require(stx.tx.inputs.isEmpty())
+ require(stx.tx.outputs.size == 1)
+ val signaturePubKeys = stx.sigs.map { it.by }.toSet()
// Only Alice signed
val aliceKey = aliceNode.legalIdentity.owningKey
require(signaturePubKeys.size <= aliceKey.keys.size)
require(aliceKey.isFulfilledBy(signaturePubKeys))
- issueTx = tx
+ issueTx = stx
},
// MOVE
- expect { tx ->
- require(tx.tx.inputs.size == 1)
- require(tx.tx.outputs.size == 1)
- val signaturePubKeys = tx.sigs.map { it.by }.toSet()
+ expect { stx ->
+ require(stx.tx.inputs.size == 1)
+ require(stx.tx.outputs.size == 1)
+ val signaturePubKeys = stx.sigs.map { it.by }.toSet()
// Alice and Notary signed
require(aliceNode.legalIdentity.owningKey.isFulfilledBy(signaturePubKeys))
require(notaryNode.notaryIdentity.owningKey.isFulfilledBy(signaturePubKeys))
- moveTx = tx
+ moveTx = stx
}
)
}
@@ -178,18 +208,18 @@ class NodeMonitorModelTest : DriverBasedTest() {
sequence(
// SNAPSHOT
expect { output: Vault.Update ->
- require(output.consumed.size == 0) { output.consumed.size }
- require(output.produced.size == 0) { output.produced.size }
+ require(output.consumed.isEmpty()) { output.consumed.size }
+ require(output.produced.isEmpty()) { output.produced.size }
},
// ISSUE
expect { update ->
- require(update.consumed.size == 0) { update.consumed.size }
+ require(update.consumed.isEmpty()) { update.consumed.size }
require(update.produced.size == 1) { update.produced.size }
},
// MOVE
expect { update ->
require(update.consumed.size == 1) { update.consumed.size }
- require(update.produced.size == 1) { update.produced.size }
+ require(update.produced.isEmpty()) { update.produced.size }
}
)
}
diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ContractStateModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ContractStateModel.kt
index 4ed8c99ca5..bbe3736c22 100644
--- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ContractStateModel.kt
+++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ContractStateModel.kt
@@ -2,7 +2,6 @@ package net.corda.client.jfx.model
import javafx.collections.FXCollections
import javafx.collections.ObservableList
-import kotlinx.support.jdk8.collections.removeIf
import net.corda.client.jfx.utils.fold
import net.corda.client.jfx.utils.map
import net.corda.contracts.asset.Cash
@@ -28,7 +27,7 @@ class ContractStateModel {
private val cashStatesDiff: Observable> = contractStatesDiff.map {
Diff(it.added.filterCashStateAndRefs(), it.removed.filterCashStateAndRefs())
}
- val cashStates: ObservableList> = cashStatesDiff.fold(FXCollections.observableArrayList()) { list, statesDiff ->
+ val cashStates: ObservableList> = cashStatesDiff.fold(FXCollections.observableArrayList()) { list: MutableList>, statesDiff ->
list.removeIf { it in statesDiff.removed }
list.addAll(statesDiff.added)
}
diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt
index bbf7a2f42d..ee47ed9947 100644
--- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt
+++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt
@@ -3,12 +3,11 @@ package net.corda.client.jfx.model
import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections
import javafx.collections.ObservableList
-import kotlinx.support.jdk8.collections.removeIf
import net.corda.client.jfx.utils.firstOrDefault
import net.corda.client.jfx.utils.firstOrNullObservable
import net.corda.client.jfx.utils.fold
import net.corda.client.jfx.utils.map
-import net.corda.core.crypto.CompositeKey
+import net.corda.core.crypto.keys
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.NetworkMapCache.MapChange
import java.security.PublicKey
@@ -40,10 +39,6 @@ class NetworkIdentityModel {
}
// TODO: Use Identity Service in service hub instead?
- fun lookup(compositeKey: CompositeKey): ObservableValue = parties.firstOrDefault(notaries.firstOrNullObservable { it.notaryIdentity.owningKey == compositeKey }) {
- it.legalIdentity.owningKey == compositeKey
- }
-
fun lookup(publicKey: PublicKey): ObservableValue = parties.firstOrDefault(notaries.firstOrNullObservable { it.notaryIdentity.owningKey.keys.any { it == publicKey } }) {
it.legalIdentity.owningKey.keys.any { it == publicKey }
}
diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt
index a6ed4a7cf7..946815f91c 100644
--- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt
+++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt
@@ -3,6 +3,7 @@ package net.corda.client.jfx.model
import com.google.common.net.HostAndPort
import javafx.beans.property.SimpleObjectProperty
import net.corda.client.rpc.CordaRPCClient
+import net.corda.client.rpc.CordaRPCClientConfiguration
import net.corda.core.flows.StateMachineRunId
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.StateMachineInfo
@@ -52,11 +53,14 @@ class NodeMonitorModel {
* TODO provide an unsubscribe mechanism
*/
fun register(nodeHostAndPort: HostAndPort, username: String, password: String) {
- val client = CordaRPCClient(nodeHostAndPort){
- maxRetryInterval = 10.seconds.toMillis()
- }
- client.start(username, password)
- val proxy = client.proxy()
+ val client = CordaRPCClient(
+ hostAndPort = nodeHostAndPort,
+ configuration = CordaRPCClientConfiguration.default.copy(
+ connectionMaxRetryInterval = 10.seconds
+ )
+ )
+ val connection = client.start(username, password)
+ val proxy = connection.proxy
val (stateMachines, stateMachineUpdates) = proxy.stateMachinesAndUpdates()
// Extract the flow tracking stream
diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/TransactionDataModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/TransactionDataModel.kt
index 4a709616f1..9e0cc1e6e9 100644
--- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/TransactionDataModel.kt
+++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/TransactionDataModel.kt
@@ -30,9 +30,13 @@ data class PartiallyResolvedTransaction(
val inputs: List>) {
val id = transaction.id
- sealed class InputResolution(val stateRef: StateRef) {
- class Unresolved(stateRef: StateRef) : InputResolution(stateRef)
- class Resolved(val stateAndRef: StateAndRef) : InputResolution(stateAndRef.ref)
+ sealed class InputResolution {
+ abstract val stateRef: StateRef
+
+ data class Unresolved(override val stateRef: StateRef) : InputResolution()
+ data class Resolved(val stateAndRef: StateAndRef) : InputResolution() {
+ override val stateRef: StateRef get() = stateAndRef.ref
+ }
}
companion object {
@@ -54,22 +58,13 @@ data class PartiallyResolvedTransaction(
}
}
-sealed class TransactionCreateStatus(val message: String?) {
- class Started(message: String?) : TransactionCreateStatus(message)
- class Failed(message: String?) : TransactionCreateStatus(message)
+data class FlowStatus(val status: String)
- override fun toString(): String = message ?: javaClass.simpleName
-}
+sealed class StateMachineStatus {
+ abstract val stateMachineName: String
-data class FlowStatus(
- val status: String
-)
-
-sealed class StateMachineStatus(val stateMachineName: String) {
- class Added(stateMachineName: String) : StateMachineStatus(stateMachineName)
- class Removed(stateMachineName: String) : StateMachineStatus(stateMachineName)
-
- override fun toString(): String = "${javaClass.simpleName}($stateMachineName)"
+ data class Added(override val stateMachineName: String) : StateMachineStatus()
+ data class Removed(override val stateMachineName: String) : StateMachineStatus()
}
data class StateMachineData(
diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/AggregatedList.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/AggregatedList.kt
index 801def3027..820d9102b7 100644
--- a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/AggregatedList.kt
+++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/AggregatedList.kt
@@ -4,7 +4,6 @@ import javafx.collections.FXCollections
import javafx.collections.ListChangeListener
import javafx.collections.ObservableList
import javafx.collections.transformation.TransformationList
-import kotlin.comparisons.compareValues
/**
* Given an [ObservableList]<[E]> and a grouping key [K], [AggregatedList] groups the elements by the key into a fresh
diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/AmountBindings.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/AmountBindings.kt
index 5b88591d9e..31cb4bf3e6 100644
--- a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/AmountBindings.kt
+++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/AmountBindings.kt
@@ -3,7 +3,6 @@ package net.corda.client.jfx.utils
import javafx.beans.binding.Bindings
import javafx.beans.value.ObservableValue
import javafx.collections.ObservableList
-import kotlinx.support.jdk8.collections.stream
import net.corda.client.jfx.model.ExchangeRate
import net.corda.core.contracts.Amount
import org.fxmisc.easybind.EasyBind
@@ -14,7 +13,7 @@ import java.util.stream.Collectors
* Utility bindings for the [Amount] type, similar in spirit to [Bindings]
*/
object AmountBindings {
- fun sum(amounts: ObservableList>, token: T) = EasyBind.map(
+ fun sum(amounts: ObservableList>, token: T) = EasyBind.map(
Bindings.createLongBinding({
amounts.stream().collect(Collectors.summingLong {
require(it.token == token)
diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ChosenList.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ChosenList.kt
index 77447feb58..61f60e53aa 100644
--- a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ChosenList.kt
+++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ChosenList.kt
@@ -31,7 +31,7 @@ class ChosenList(
}
init {
- chosenListObservable.addListener { observable: Observable -> rechoose() }
+ chosenListObservable.addListener { _: Observable -> rechoose() }
currentList.addListener(listener)
beginChange()
nextAdd(0, currentList.size)
diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ConcatenatedList.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ConcatenatedList.kt
index d6f665d397..9b8a5f45e5 100644
--- a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ConcatenatedList.kt
+++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ConcatenatedList.kt
@@ -5,7 +5,6 @@ import javafx.collections.ListChangeListener
import javafx.collections.ObservableList
import javafx.collections.transformation.TransformationList
import java.util.*
-import kotlin.comparisons.compareValues
/**
* [ConcatenatedList] takes a list of lists and concatenates them. Any change to the underlying lists or the outer list
diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/FlattenedList.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/FlattenedList.kt
index 4e2153c905..3b349ce67d 100644
--- a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/FlattenedList.kt
+++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/FlattenedList.kt
@@ -38,7 +38,7 @@ class FlattenedList(val sourceList: ObservableList
}
private fun createListener(wrapped: WrappedObservableValue): ChangeListener {
- val listener = ChangeListener { _observableValue, oldValue, newValue ->
+ val listener = ChangeListener { _, oldValue, _ ->
val currentIndex = indexMap[wrapped]!!.first
beginChange()
nextReplace(currentIndex, currentIndex + 1, listOf(oldValue))
@@ -55,7 +55,7 @@ class FlattenedList(val sourceList: ObservableList
val from = c.from
val to = c.to
val permutation = IntArray(to, { c.getPermutation(it) })
- indexMap.replaceAll { _observableValue, pair -> Pair(permutation[pair.first], pair.second) }
+ indexMap.replaceAll { _, pair -> Pair(permutation[pair.first], pair.second) }
nextPermutation(from, to, permutation)
} else if (c.wasUpdated()) {
throw UnsupportedOperationException("FlattenedList doesn't support Update changes")
diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/MapValuesList.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/MapValuesList.kt
index 37c0963f0d..1f621cd901 100644
--- a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/MapValuesList.kt
+++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/MapValuesList.kt
@@ -4,7 +4,6 @@ import javafx.collections.FXCollections
import javafx.collections.MapChangeListener
import javafx.collections.ObservableList
import javafx.collections.ObservableMap
-import kotlin.comparisons.compareValues
/**
* [MapValuesList] takes an [ObservableMap] and returns its values as an [ObservableList].
diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableFold.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableFold.kt
index aa5187aa3e..aefd494aff 100644
--- a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableFold.kt
+++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableFold.kt
@@ -67,7 +67,7 @@ fun Observable.recordInSequence(): ObservableList {
* @param toKey Function retrieving the key to associate with.
* @param merge The function to be called if there is an existing element at the key.
*/
-fun Observable.recordAsAssociation(toKey: (A) -> K, merge: (K, oldValue: A, newValue: A) -> A = { _key, _oldValue, newValue -> newValue }): ObservableMap {
+fun Observable.recordAsAssociation(toKey: (A) -> K, merge: (K, oldValue: A, newValue: A) -> A = { _, _, newValue -> newValue }): ObservableMap {
return fold(FXCollections.observableHashMap()) { map, item ->
val key = toKey(item)
map[key] = map[key]?.let { merge(key, it, item) } ?: item
diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableUtilities.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableUtilities.kt
index b39cc8a8d0..5ccc04a7c4 100644
--- a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableUtilities.kt
+++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableUtilities.kt
@@ -111,8 +111,14 @@ fun ObservableList.filter(predicate: ObservableValue<(A) -> Boolean>)
* val owners: ObservableList = dogs.map(Dog::owner).filterNotNull()
*/
fun ObservableList.filterNotNull(): ObservableList {
+ //TODO This is a tactical work round for an issue with SAM conversion (https://youtrack.jetbrains.com/issue/ALL-1552) so that the M10 explorer works.
@Suppress("UNCHECKED_CAST")
- return filtered { it != null } as ObservableList
+ return (this as ObservableList).filtered(object : Predicate {
+ override fun test(t: A?): Boolean {
+ return t != null
+
+ }
+ }) as ObservableList
}
/**
@@ -158,7 +164,7 @@ fun ObservableList.associateBy(toKey: (A) -> K, assemble: (K, A
* val nameToPerson: ObservableMap = people.associateBy(Person::name)
*/
fun ObservableList.associateBy(toKey: (A) -> K): ObservableMap {
- return associateBy(toKey) { key, value -> value }
+ return associateBy(toKey) { _, value -> value }
}
/**
@@ -176,7 +182,7 @@ fun ObservableList.associateByAggregation(toKey: (A
* val heightToPeople: ObservableMap> = people.associateByAggregation(Person::height)
*/
fun ObservableList.associateByAggregation(toKey: (A) -> K): ObservableMap> {
- return associateByAggregation(toKey) { key, value -> value }
+ return associateByAggregation(toKey) { _, value -> value }
}
/**
@@ -260,7 +266,7 @@ fun ObservableList.leftOuterJoin(
val leftTableMap = associateByAggregation(leftToJoinKey)
val rightTableMap = rightTable.associateByAggregation(rightToJoinKey)
val joinedMap: ObservableMap, ObservableList>> =
- LeftOuterJoinedMap(leftTableMap, rightTableMap) { _key, left, rightValue ->
+ LeftOuterJoinedMap(leftTableMap, rightTableMap) { _, left, rightValue ->
Pair(left, ChosenList(rightValue.map { it ?: FXCollections.emptyObservableList() }))
}
return joinedMap
@@ -285,7 +291,7 @@ fun ObservableList.last(): ObservableValue {
}
fun ObservableList.unique(): ObservableList {
- return AggregatedList(this, { it }, { key, _list -> key })
+ return AggregatedList(this, { it }, { key, _ -> key })
}
fun ObservableValue<*>.isNotNull(): BooleanBinding {
diff --git a/client/jfx/src/test/kotlin/net/corda/client/jfx/utils/AssociatedListTest.kt b/client/jfx/src/test/kotlin/net/corda/client/jfx/utils/AssociatedListTest.kt
index 052b422c01..139d8a7b20 100644
--- a/client/jfx/src/test/kotlin/net/corda/client/jfx/utils/AssociatedListTest.kt
+++ b/client/jfx/src/test/kotlin/net/corda/client/jfx/utils/AssociatedListTest.kt
@@ -16,7 +16,7 @@ class AssociatedListTest {
@Before
fun setup() {
sourceList = FXCollections.observableArrayList(0)
- associatedList = AssociatedList(sourceList, { it % 3 }) { mod3, number -> number }
+ associatedList = AssociatedList(sourceList, { it % 3 }) { _, number -> number }
replayedMap = ReplayedMap(associatedList)
}
diff --git a/client/mock/build.gradle b/client/mock/build.gradle
index 1a08e05b38..adac270b39 100644
--- a/client/mock/build.gradle
+++ b/client/mock/build.gradle
@@ -4,32 +4,12 @@ apply plugin: 'net.corda.plugins.publish-utils'
description 'Corda client mock modules'
-repositories {
- mavenLocal()
- mavenCentral()
- maven {
- url 'http://oss.sonatype.org/content/repositories/snapshots'
- }
- jcenter()
- maven {
- url 'https://dl.bintray.com/kotlin/exposed'
- }
-}
-
//noinspection GroovyAssignabilityCheck
configurations {
// we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment
runtime.exclude module: 'isolated'
}
-sourceSets {
- test {
- resources {
- srcDir "../../config/test"
- }
- }
-}
-
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
// build/reports/project/dependencies/index.html for green highlighted parts of the tree.
diff --git a/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt b/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt
index 99eed3a4e0..f8effdfa8e 100644
--- a/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt
+++ b/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt
@@ -1,10 +1,8 @@
package net.corda.client.mock
-import net.corda.contracts.asset.Cash
-import net.corda.core.contracts.*
-import net.corda.core.crypto.Party
+import net.corda.core.contracts.Amount
+import net.corda.core.identity.Party
import net.corda.core.serialization.OpaqueBytes
-import net.corda.core.transactions.TransactionBuilder
import net.corda.flows.CashFlowCommand
import java.util.*
@@ -12,91 +10,27 @@ import java.util.*
* [Generator]s for incoming/outgoing events to/from the [WalletMonitorService]. Internally it keeps track of owned
* state/ref pairs, but it doesn't necessarily generate "correct" events!
*/
-class EventGenerator(
- val parties: List,
- val notary: Party,
- val currencies: List = listOf(USD, GBP, CHF),
- val issuers: List = parties
-) {
- private var vault = listOf>()
- val issuerGenerator =
- Generator.pickOne(issuers).combine(Generator.intRange(0, 1)) { party, ref -> party.ref(ref.toByte()) }
+class EventGenerator(val parties: List, val currencies: List, val notary: Party) {
+ private val partyGenerator = Generator.pickOne(parties)
+ private val issueRefGenerator = Generator.intRange(0, 1).map { number -> OpaqueBytes(ByteArray(1, { number.toByte() })) }
+ private val amountGenerator = Generator.longRange(10000, 1000000)
+ private val currencyGenerator = Generator.pickOne(currencies)
- val currencyGenerator = Generator.pickOne(currencies)
-
- val issuedGenerator = issuerGenerator.combine(currencyGenerator) { issuer, currency -> Issued(issuer, currency) }
- val amountIssuedGenerator = generateAmount(1, 10000, issuedGenerator)
-
- val publicKeyGenerator = Generator.pickOne(parties.map { it.owningKey })
- val partyGenerator = Generator.pickOne(parties)
-
- val cashStateGenerator = amountIssuedGenerator.combine(publicKeyGenerator) { amount, from ->
- val builder = TransactionBuilder(notary = notary)
- builder.addOutputState(Cash.State(amount, from))
- builder.addCommand(Command(Cash.Commands.Issue(), amount.token.issuer.party.owningKey))
- builder.toWireTransaction().outRef(0)
+ private val issueCashGenerator = amountGenerator.combine(partyGenerator, issueRefGenerator, currencyGenerator) { amount, to, issueRef, ccy ->
+ CashFlowCommand.IssueCash(Amount(amount, ccy), issueRef, to, notary)
}
- val consumedGenerator: Generator> = Generator.frequency(
- 0.7 to Generator.pure(setOf()),
- 0.3 to Generator.impure { vault }.bind { states ->
- Generator.sampleBernoulli(states, 0.2).map { someStates ->
- val consumedSet = someStates.map { it.ref }.toSet()
- vault = vault.filter { it.ref !in consumedSet }
- consumedSet
- }
- }
- )
- val producedGenerator: Generator>> = Generator.frequency(
- // 0.1 to Generator.pure(setOf())
- 0.9 to Generator.impure { vault }.bind { states ->
- Generator.replicate(2, cashStateGenerator).map {
- vault = states + it
- it.toSet()
- }
- }
- )
+ private val exitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator) { amount, issueRef, ccy ->
+ CashFlowCommand.ExitCash(Amount(amount, ccy), issueRef)
+ }
- val issueRefGenerator = Generator.intRange(0, 1).map { number -> OpaqueBytes(ByteArray(1, { number.toByte() })) }
+ val moveCashGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency ->
+ CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient)
+ }
- val amountGenerator = Generator.intRange(0, 10000).combine(currencyGenerator) { quantity, currency -> Amount(quantity.toLong(), currency) }
-
- val issueCashGenerator =
- amountGenerator.combine(partyGenerator, issueRefGenerator) { amount, to, issueRef ->
- CashFlowCommand.IssueCash(
- amount,
- issueRef,
- to,
- notary
- )
- }
-
- val moveCashGenerator =
- amountIssuedGenerator.combine(partyGenerator) { amountIssued, recipient ->
- CashFlowCommand.PayCash(
- amount = amountIssued.withoutIssuer(),
- recipient = recipient
- )
- }
-
- val exitCashGenerator =
- amountIssuedGenerator.map {
- CashFlowCommand.ExitCash(
- it.withoutIssuer(),
- it.token.issuer.reference
- )
- }
-
- val clientCommandGenerator = Generator.frequency(
- 1.0 to moveCashGenerator
- )
-
- val bankOfCordaExitGenerator = Generator.frequency(
- 0.4 to exitCashGenerator
- )
-
- val bankOfCordaIssueGenerator = Generator.frequency(
- 0.6 to issueCashGenerator
- )
+ val issuerGenerator = Generator.frequency(listOf(
+ 0.1 to exitCashGenerator,
+ 0.9 to issueCashGenerator
+ ))
}
diff --git a/client/mock/src/main/kotlin/net/corda/client/mock/Generator.kt b/client/mock/src/main/kotlin/net/corda/client/mock/Generator.kt
index bf737bc898..74c052e723 100644
--- a/client/mock/src/main/kotlin/net/corda/client/mock/Generator.kt
+++ b/client/mock/src/main/kotlin/net/corda/client/mock/Generator.kt
@@ -144,6 +144,23 @@ fun Generator.Companion.doubleRange(from: Double, to: Double): Generator
from + it.nextDouble() * (to - from)
}
+fun Generator.Companion.char() = Generator {
+ val codePoint = Math.abs(it.nextInt()) % (17 * (1 shl 16))
+ if (Character.isValidCodePoint(codePoint)) {
+ return@Generator ErrorOr(codePoint.toChar())
+ } else {
+ ErrorOr.of(IllegalStateException("Could not generate valid codepoint"))
+ }
+}
+
+fun Generator.Companion.string(meanSize: Double = 16.0) = replicatePoisson(meanSize, char()).map {
+ val builder = StringBuilder()
+ it.forEach {
+ builder.append(it)
+ }
+ builder.toString()
+}
+
fun Generator.Companion.replicate(number: Int, generator: Generator): Generator> {
val generators = mutableListOf>()
for (i in 1..number) {
diff --git a/client/rpc/build.gradle b/client/rpc/build.gradle
index eaeb7a81ef..ec52494023 100644
--- a/client/rpc/build.gradle
+++ b/client/rpc/build.gradle
@@ -4,18 +4,6 @@ apply plugin: 'net.corda.plugins.publish-utils'
description 'Corda client RPC modules'
-repositories {
- mavenLocal()
- mavenCentral()
- maven {
- url 'http://oss.sonatype.org/content/repositories/snapshots'
- }
- jcenter()
- maven {
- url 'https://dl.bintray.com/kotlin/exposed'
- }
-}
-
//noinspection GroovyAssignabilityCheck
configurations {
// we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment
@@ -33,11 +21,6 @@ sourceSets {
srcDir file('src/integration-test/kotlin')
}
}
- test {
- resources {
- srcDir "../../config/test"
- }
- }
}
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
@@ -53,6 +36,7 @@ dependencies {
testCompile "org.assertj:assertj-core:${assertj_version}"
testCompile project(':test-utils')
+ testCompile project(':client:mock')
// Integration test helpers
integrationTestCompile "junit:junit:$junit_version"
diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt
new file mode 100644
index 0000000000..72f3ef6316
--- /dev/null
+++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt
@@ -0,0 +1,154 @@
+package net.corda.client.rpc
+
+import net.corda.core.contracts.DOLLARS
+import net.corda.core.flows.FlowInitiator
+import net.corda.core.getOrThrow
+import net.corda.core.messaging.*
+import net.corda.core.node.services.ServiceInfo
+import net.corda.core.random63BitValue
+import net.corda.core.serialization.OpaqueBytes
+import net.corda.core.utilities.ALICE
+import net.corda.flows.CashException
+import net.corda.flows.CashIssueFlow
+import net.corda.flows.CashPaymentFlow
+import net.corda.node.internal.Node
+import net.corda.node.services.startFlowPermission
+import net.corda.node.services.transactions.ValidatingNotaryService
+import net.corda.nodeapi.User
+import net.corda.testing.node.NodeBasedTest
+import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
+import org.assertj.core.api.Assertions.assertThatExceptionOfType
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import java.util.*
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class CordaRPCClientTest : NodeBasedTest() {
+ private val rpcUser = User("user1", "test", permissions = setOf(
+ startFlowPermission(),
+ startFlowPermission()
+ ))
+ private lateinit var node: Node
+ private lateinit var client: CordaRPCClient
+ private var connection: CordaRPCConnection? = null
+
+ private fun login(username: String, password: String) {
+ connection = client.start(username, password)
+ }
+
+ @Before
+ fun setUp() {
+ node = startNode(ALICE.name, rpcUsers = listOf(rpcUser), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))).getOrThrow()
+ client = CordaRPCClient(node.configuration.rpcAddress!!)
+ }
+
+ @After
+ fun done() {
+ connection?.close()
+ }
+
+ @Test
+ fun `log in with valid username and password`() {
+ login(rpcUser.username, rpcUser.password)
+ }
+
+ @Test
+ fun `log in with unknown user`() {
+ assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
+ login(random63BitValue().toString(), rpcUser.password)
+ }
+ }
+
+ @Test
+ fun `log in with incorrect password`() {
+ assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
+ login(rpcUser.username, random63BitValue().toString())
+ }
+ }
+
+ @Test
+ fun `close-send deadlock and premature shutdown on empty observable`() {
+ println("Starting client")
+ login(rpcUser.username, rpcUser.password)
+ println("Creating proxy")
+ println("Starting flow")
+ val flowHandle = connection!!.proxy.startTrackedFlow(
+ ::CashIssueFlow,
+ 20.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity, node.info.legalIdentity)
+ println("Started flow, waiting on result")
+ flowHandle.progress.subscribe {
+ println("PROGRESS $it")
+ }
+ println("Result: ${flowHandle.returnValue.getOrThrow()}")
+ }
+
+ @Test
+ fun `sub-type of FlowException thrown by flow`() {
+ login(rpcUser.username, rpcUser.password)
+ val handle = connection!!.proxy.startFlow(::CashPaymentFlow, 100.DOLLARS, node.info.legalIdentity)
+ assertThatExceptionOfType(CashException::class.java).isThrownBy {
+ handle.returnValue.getOrThrow()
+ }
+ }
+
+ @Test
+ fun `check basic flow has no progress`() {
+ login(rpcUser.username, rpcUser.password)
+ connection!!.proxy.startFlow(::CashPaymentFlow, 100.DOLLARS, node.info.legalIdentity).use {
+ assertFalse(it is FlowProgressHandle<*>)
+ assertTrue(it is FlowHandle<*>)
+ }
+ }
+
+ @Test
+ fun `get cash balances`() {
+ login(rpcUser.username, rpcUser.password)
+ val proxy = connection!!.proxy
+ val startCash = proxy.getCashBalances()
+ assertTrue(startCash.isEmpty(), "Should not start with any cash")
+
+ val flowHandle = proxy.startFlow(::CashIssueFlow,
+ 123.DOLLARS, OpaqueBytes.of(0),
+ node.info.legalIdentity, node.info.legalIdentity
+ )
+ println("Started issuing cash, waiting on result")
+ flowHandle.returnValue.get()
+
+ val finishCash = proxy.getCashBalances()
+ println("Cash Balances: $finishCash")
+ assertEquals(1, finishCash.size)
+ assertEquals(123.DOLLARS, finishCash.get(Currency.getInstance("USD")))
+ }
+
+ @Test
+ fun `flow initiator via RPC`() {
+ login(rpcUser.username, rpcUser.password)
+ val proxy = connection!!.proxy
+ val smUpdates = proxy.stateMachinesAndUpdates()
+ var countRpcFlows = 0
+ var countShellFlows = 0
+ smUpdates.second.subscribe {
+ if (it is StateMachineUpdate.Added) {
+ val initiator = it.stateMachineInfo.initiator
+ if (initiator is FlowInitiator.RPC)
+ countRpcFlows++
+ if (initiator is FlowInitiator.Shell)
+ countShellFlows++
+ }
+ }
+ val nodeIdentity = node.info.legalIdentity
+ node.services.startFlow(CashIssueFlow(2000.DOLLARS, OpaqueBytes.of(0), nodeIdentity, nodeIdentity), FlowInitiator.Shell).resultFuture.getOrThrow()
+ proxy.startFlow(::CashIssueFlow,
+ 123.DOLLARS, OpaqueBytes.of(0),
+ nodeIdentity, nodeIdentity
+ ).returnValue.getOrThrow()
+ proxy.startFlowDynamic(CashIssueFlow::class.java,
+ 1000.DOLLARS, OpaqueBytes.of(0),
+ nodeIdentity, nodeIdentity).returnValue.getOrThrow()
+ assertEquals(2, countRpcFlows)
+ assertEquals(1, countShellFlows)
+ }
+}
diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt
new file mode 100644
index 0000000000..5287a0de4f
--- /dev/null
+++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt
@@ -0,0 +1,169 @@
+package net.corda.client.rpc
+
+import com.esotericsoftware.kryo.Kryo
+import com.esotericsoftware.kryo.Serializer
+import com.esotericsoftware.kryo.io.Input
+import com.esotericsoftware.kryo.io.Output
+import com.esotericsoftware.kryo.pool.KryoPool
+import com.google.common.util.concurrent.Futures
+import net.corda.core.messaging.RPCOps
+import net.corda.core.millis
+import net.corda.core.random63BitValue
+import net.corda.node.services.messaging.RPCServerConfiguration
+import net.corda.nodeapi.RPCApi
+import net.corda.nodeapi.RPCKryo
+import net.corda.testing.*
+import org.apache.activemq.artemis.api.core.SimpleString
+import org.junit.Test
+import rx.Observable
+import rx.subjects.PublishSubject
+import rx.subjects.UnicastSubject
+import java.time.Duration
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+
+
+class RPCStabilityTests {
+
+ interface LeakObservableOps: RPCOps {
+ fun leakObservable(): Observable
+ }
+
+ @Test
+ fun `client cleans up leaked observables`() {
+ rpcDriver {
+ val leakObservableOpsImpl = object : LeakObservableOps {
+ val leakedUnsubscribedCount = AtomicInteger(0)
+ override val protocolVersion = 0
+ override fun leakObservable(): Observable {
+ return PublishSubject.create().doOnUnsubscribe {
+ leakedUnsubscribedCount.incrementAndGet()
+ }
+ }
+ }
+ val server = startRpcServer(ops = leakObservableOpsImpl)
+ val proxy = startRpcClient(server.get().hostAndPort).get()
+ // Leak many observables
+ val N = 200
+ (1..N).toList().parallelStream().forEach {
+ proxy.leakObservable()
+ }
+ // In a loop force GC and check whether the server is notified
+ while (true) {
+ System.gc()
+ if (leakObservableOpsImpl.leakedUnsubscribedCount.get() == N) break
+ Thread.sleep(100)
+ }
+ }
+ }
+
+ interface TrackSubscriberOps : RPCOps {
+ fun subscribe(): Observable
+ }
+
+ /**
+ * In this test we create a number of out of process RPC clients that call [TrackSubscriberOps.subscribe] in a loop.
+ */
+ @Test
+ fun `server cleans up queues after disconnected clients`() {
+ rpcDriver {
+ val trackSubscriberOpsImpl = object : TrackSubscriberOps {
+ override val protocolVersion = 0
+ val subscriberCount = AtomicInteger(0)
+ val trackSubscriberCountObservable = UnicastSubject.create().share().
+ doOnSubscribe { subscriberCount.incrementAndGet() }.
+ doOnUnsubscribe { subscriberCount.decrementAndGet() }
+ override fun subscribe(): Observable {
+ return trackSubscriberCountObservable
+ }
+ }
+ val server = startRpcServer(
+ configuration = RPCServerConfiguration.default.copy(
+ reapInterval = 100.millis
+ ),
+ ops = trackSubscriberOpsImpl
+ ).get()
+
+ val numberOfClients = 4
+ val clients = Futures.allAsList((1 .. numberOfClients).map {
+ startRandomRpcClient(server.hostAndPort)
+ }).get()
+
+ // Poll until all clients connect
+ pollUntilClientNumber(server, numberOfClients)
+ pollUntilTrue("number of times subscribe() has been called") { trackSubscriberOpsImpl.subscriberCount.get() >= 100 }.get()
+ // Kill one client
+ clients[0].destroyForcibly()
+ pollUntilClientNumber(server, numberOfClients - 1)
+ // Kill the rest
+ (1 .. numberOfClients - 1).forEach {
+ clients[it].destroyForcibly()
+ }
+ pollUntilClientNumber(server, 0)
+ // Now poll until the server detects the disconnects and unsubscribes from all obserables.
+ pollUntilTrue("number of times subscribe() has been called") { trackSubscriberOpsImpl.subscriberCount.get() == 0 }.get()
+ }
+ }
+
+ interface SlowConsumerRPCOps : RPCOps {
+ fun streamAtInterval(interval: Duration, size: Int): Observable
+ }
+ class SlowConsumerRPCOpsImpl : SlowConsumerRPCOps {
+ override val protocolVersion = 0
+
+ override fun streamAtInterval(interval: Duration, size: Int): Observable {
+ val chunk = ByteArray(size)
+ return Observable.interval(interval.toMillis(), TimeUnit.MILLISECONDS).map { chunk }
+ }
+ }
+ val dummyObservableSerialiser = object : Serializer>() {
+ override fun write(kryo: Kryo?, output: Output?, `object`: Observable?) {
+ }
+ override fun read(kryo: Kryo?, input: Input?, type: Class>?): Observable {
+ return Observable.empty()
+ }
+ }
+ @Test
+ fun `slow consumers are kicked`() {
+ val kryoPool = KryoPool.Builder { RPCKryo(dummyObservableSerialiser) }.build()
+ rpcDriver {
+ val server = startRpcServer(maxBufferedBytesPerClient = 10 * 1024 * 1024, ops = SlowConsumerRPCOpsImpl()).get()
+
+ // Construct an RPC session manually so that we can hang in the message handler
+ val myQueue = "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.test.${random63BitValue()}"
+ val session = startArtemisSession(server.hostAndPort)
+ session.createTemporaryQueue(myQueue, myQueue)
+ val consumer = session.createConsumer(myQueue, null, -1, -1, false)
+ consumer.setMessageHandler {
+ Thread.sleep(50) // 5x slower than the server producer
+ it.acknowledge()
+ }
+ val producer = session.createProducer(RPCApi.RPC_SERVER_QUEUE_NAME)
+ session.start()
+
+ pollUntilClientNumber(server, 1)
+
+ val message = session.createMessage(false)
+ val request = RPCApi.ClientToServer.RpcRequest(
+ clientAddress = SimpleString(myQueue),
+ id = RPCApi.RpcRequestId(random63BitValue()),
+ methodName = SlowConsumerRPCOps::streamAtInterval.name,
+ arguments = listOf(10.millis, 123456)
+ )
+ request.writeToClientMessage(kryoPool, message)
+ producer.send(message)
+ session.commit()
+
+ // We are consuming slower than the server is producing, so we should be kicked after a while
+ pollUntilClientNumber(server, 0)
+ }
+ }
+
+}
+
+fun RPCDriverExposedDSLInterface.pollUntilClientNumber(server: RpcServerHandle, expected: Int) {
+ pollUntilTrue("number of RPC clients to become $expected") {
+ val clientAddresses = server.serverControl.addressNames.filter { it.startsWith(RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX) }
+ clientAddresses.size == expected
+ }.get()
+}
\ No newline at end of file
diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt
index 15c290400f..a6c97c3e3a 100644
--- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt
+++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt
@@ -1,164 +1,49 @@
package net.corda.client.rpc
import com.google.common.net.HostAndPort
-import net.corda.nodeapi.config.SSLConfiguration
-import net.corda.core.ThreadBox
-import net.corda.core.logElapsedTime
+import net.corda.client.rpc.internal.RPCClient
+import net.corda.client.rpc.internal.RPCClientConfiguration
import net.corda.core.messaging.CordaRPCOps
-import net.corda.core.minutes
-import net.corda.core.seconds
-import net.corda.core.utilities.loggerFor
-import net.corda.nodeapi.ArtemisMessagingComponent
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
import net.corda.nodeapi.ConnectionDirection
-import net.corda.nodeapi.RPCException
-import net.corda.nodeapi.rpcLog
-import org.apache.activemq.artemis.api.core.ActiveMQException
-import org.apache.activemq.artemis.api.core.client.ActiveMQClient
-import org.apache.activemq.artemis.api.core.client.ClientSession
-import org.apache.activemq.artemis.api.core.client.ClientSessionFactory
-import org.apache.activemq.artemis.api.core.client.ServerLocator
-import rx.Observable
-import java.io.Closeable
+import net.corda.nodeapi.config.SSLConfiguration
import java.time.Duration
-import javax.annotation.concurrent.ThreadSafe
-/**
- * An RPC client connects to the specified server and allows you to make calls to the server that perform various
- * useful tasks. See the documentation for [proxy] or review the docsite to learn more about how this API works.
- *
- * @param host The hostname and messaging port of the node.
- * @param config If specified, the SSL configuration to use. If not specified, SSL will be disabled and the node will only be authenticated on non-SSL RPC port, the RPC traffic with not be encrypted when SSL is disabled.
- */
-@ThreadSafe
-class CordaRPCClient(val host: HostAndPort, override val config: SSLConfiguration? = null, val serviceConfigurationOverride: (ServerLocator.() -> Unit)? = null) : Closeable, ArtemisMessagingComponent() {
- private companion object {
- val log = loggerFor()
+class CordaRPCConnection internal constructor(
+ connection: RPCClient.RPCConnection
+) : RPCClient.RPCConnection by connection
+
+data class CordaRPCClientConfiguration(
+ val connectionMaxRetryInterval: Duration
+) {
+ internal fun toRpcClientConfiguration(): RPCClientConfiguration {
+ return RPCClientConfiguration.default.copy(
+ connectionMaxRetryInterval = connectionMaxRetryInterval
+ )
+ }
+ companion object {
+ @JvmStatic
+ val default = CordaRPCClientConfiguration(
+ connectionMaxRetryInterval = RPCClientConfiguration.default.connectionMaxRetryInterval
+ )
+ }
+}
+
+class CordaRPCClient(
+ hostAndPort: HostAndPort,
+ sslConfiguration: SSLConfiguration? = null,
+ configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default
+) {
+ private val rpcClient = RPCClient(
+ tcpTransport(ConnectionDirection.Outbound(), hostAndPort, sslConfiguration),
+ configuration.toRpcClientConfiguration()
+ )
+
+ fun start(username: String, password: String): CordaRPCConnection {
+ return CordaRPCConnection(rpcClient.start(CordaRPCOps::class.java, username, password))
}
- // TODO: Certificate handling for clients needs more work.
- private inner class State {
- var running = false
- lateinit var sessionFactory: ClientSessionFactory
- lateinit var session: ClientSession
- lateinit var clientImpl: CordaRPCClientImpl
- }
-
- private val state = ThreadBox(State())
-
- /**
- * Opens the connection to the server with the given username and password, then returns itself.
- * Registers a JVM shutdown hook to cleanly disconnect.
- */
- @Throws(ActiveMQException::class)
- fun start(username: String, password: String): CordaRPCClient {
- state.locked {
- check(!running)
- log.logElapsedTime("Startup") {
- checkStorePasswords()
- val serverLocator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport(ConnectionDirection.Outbound(), host, config, enableSSL = config != null)).apply {
- // TODO: Put these in config file or make it user configurable?
- threadPoolMaxSize = 1
- confirmationWindowSize = 100000 // a guess
- retryInterval = 5.seconds.toMillis()
- retryIntervalMultiplier = 1.5 // Exponential backoff
- maxRetryInterval = 3.minutes.toMillis()
- serviceConfigurationOverride?.invoke(this)
- }
- sessionFactory = serverLocator.createSessionFactory()
- session = sessionFactory.createSession(username, password, false, true, true, serverLocator.isPreAcknowledge, serverLocator.ackBatchSize)
- session.start()
- clientImpl = CordaRPCClientImpl(session, state.lock, username)
- running = true
- }
- }
-
- Runtime.getRuntime().addShutdownHook(Thread {
- close()
- })
-
- return this
- }
-
- /**
- * A convenience function that opens a connection with the given credentials, executes the given code block with all
- * available RPCs in scope and shuts down the RPC connection again. It's meant for quick prototyping and demos. For
- * more control you probably want to control the lifecycle of the client and proxies independently, as well as
- * configuring a timeout and other such features via the [proxy] method.
- *
- * After this method returns the client is closed and can't be restarted.
- */
- @Throws(ActiveMQException::class)
- fun use(username: String, password: String, block: CordaRPCOps.() -> T): T {
- require(!state.locked { running })
- start(username, password)
- (this as Closeable).use {
- return proxy().block()
- }
- }
-
- /** Shuts down the client and lets the server know it can free the used resources (in a nice way). */
- override fun close() {
- state.locked {
- if (!running) return
- session.close()
- sessionFactory.close()
- running = false
- }
- }
-
- /**
- * Returns a fresh proxy that lets you invoke RPCs on the server. Calls on it block, and if the server throws an
- * exception then it will be rethrown on the client. Proxies are thread safe but only one RPC can be in flight at
- * once. If you'd like to perform multiple RPCs in parallel, use this function multiple times to get multiple
- * proxies.
- *
- * Creation of a proxy is a somewhat expensive operation that involves calls to the server, so if you want to do
- * calls from many threads at once you should cache one proxy per thread and reuse them. This function itself is
- * thread safe though so requires no extra synchronisation.
- *
- * RPC sends and receives are logged on the net.corda.rpc logger.
- *
- * By default there are no timeouts on calls. This is deliberate, RPCs without timeouts can survive restarts,
- * maintenance downtime and moves of the server. RPCs can survive temporary losses or changes in client connectivity,
- * like switching between wifi networks. You can specify a timeout on the level of a proxy. If a call times
- * out it will throw [RPCException.Deadline].
- *
- * The [CordaRPCOps] defines what client RPCs are available. If an RPC returns an [Observable] anywhere in the
- * object graph returned then the server-side observable is transparently linked to a messaging queue, and that
- * queue linked to another observable on the client side here. *You are expected to use it*. The server will begin
- * buffering messages immediately that it will expect you to drain by subscribing to the returned observer. You can
- * opt-out of this by simply casting the [Observable] to [Closeable] or [AutoCloseable] and then calling the close
- * method on it. You don't have to explicitly close the observable if you actually subscribe to it: it will close
- * itself and free up the server-side resources either when the client or JVM itself is shutdown, or when there are
- * no more subscribers to it. Once all the subscribers to a returned observable are unsubscribed, the observable is
- * closed and you can't then re-subscribe again: you'll have to re-request a fresh observable with another RPC.
- *
- * The proxy and linked observables consume some small amount of resources on the server. It's OK to just exit your
- * process and let the server clean up, but in a long running process where you only need something for a short
- * amount of time it is polite to cast the objects to [Closeable] or [AutoCloseable] and close it when you are done.
- * Finalizers are in place to warn you if you lose a reference to an unclosed proxy or observable.
- *
- * @throws RPCException if the server version is too low or if the server isn't reachable within the given time.
- */
- @JvmOverloads
- @Throws(RPCException::class)
- fun proxy(timeout: Duration? = null, minVersion: Int = 0): CordaRPCOps {
- return state.locked {
- check(running) { "Client must have been started first" }
- log.logElapsedTime("Proxy build") {
- clientImpl.proxyFor(CordaRPCOps::class.java, timeout, minVersion)
- }
- }
- }
-
- @Suppress("UNUSED")
- private fun finalize() {
- state.locked {
- if (running) {
- rpcLog.warn("A CordaMQClient is being finalised whilst still running, did you forget to call close?")
- close()
- }
- }
+ inline fun use(username: String, password: String, block: (CordaRPCConnection) -> A): A {
+ return start(username, password).use(block)
}
}
\ No newline at end of file
diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClientImpl.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClientImpl.kt
deleted file mode 100644
index e5771beb54..0000000000
--- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClientImpl.kt
+++ /dev/null
@@ -1,402 +0,0 @@
-package net.corda.client.rpc
-
-import com.esotericsoftware.kryo.Kryo
-import com.esotericsoftware.kryo.KryoException
-import com.esotericsoftware.kryo.Serializer
-import com.esotericsoftware.kryo.io.Input
-import com.esotericsoftware.kryo.io.Output
-import com.esotericsoftware.kryo.pool.KryoPool
-import com.google.common.cache.CacheBuilder
-import net.corda.core.ErrorOr
-import net.corda.core.bufferUntilSubscribed
-import net.corda.core.messaging.RPCOps
-import net.corda.core.messaging.RPCReturnsObservables
-import net.corda.core.random63BitValue
-import net.corda.core.serialization.deserialize
-import net.corda.core.serialization.serialize
-import net.corda.core.utilities.debug
-import net.corda.nodeapi.*
-import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException
-import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID
-import org.apache.activemq.artemis.api.core.SimpleString
-import org.apache.activemq.artemis.api.core.client.ClientConsumer
-import org.apache.activemq.artemis.api.core.client.ClientMessage
-import org.apache.activemq.artemis.api.core.client.ClientProducer
-import org.apache.activemq.artemis.api.core.client.ClientSession
-import rx.Observable
-import rx.subjects.PublishSubject
-import java.io.Closeable
-import java.lang.ref.WeakReference
-import java.lang.reflect.InvocationHandler
-import java.lang.reflect.Method
-import java.lang.reflect.Proxy
-import java.time.Duration
-import java.util.*
-import java.util.concurrent.atomic.AtomicInteger
-import java.util.concurrent.locks.ReentrantLock
-import javax.annotation.concurrent.GuardedBy
-import javax.annotation.concurrent.ThreadSafe
-import kotlin.concurrent.withLock
-import kotlin.reflect.jvm.javaMethod
-
-/**
- * Core RPC engine implementation, to learn how to use RPC you should be looking at [CordaRPCClient].
- *
- * # Design notes
- *
- * The way RPCs are handled is fairly standard except for the handling of observables. When an RPC might return
- * an [Observable] it is specially tagged. This causes the client to create a new transient queue for the
- * receiving of observables and their observations with a random ID in the name. This ID is sent to the server in
- * a message header. All observations are sent via this single queue.
- *
- * The reason for doing it this way and not the more obvious approach of one-queue-per-observable is that we want
- * the queues to be *transient*, meaning their lifetime in the broker is tied to the session that created them.
- * A server side observable and its associated queue is not a cost-free thing, let alone the memory and resources
- * needed to actually generate the observations themselves, therefore we want to ensure these cannot leak. A
- * transient queue will be deleted automatically if the client session terminates, which by default happens on
- * disconnect but can also be configured to happen after a short delay (this allows clients to e.g. switch IP
- * address). On the server the deletion of the observations queue triggers unsubscription from the associated
- * observables, which in turn may then be garbage collected.
- *
- * Creating a transient queue requires a roundtrip to the broker and thus doing an RPC that could return
- * observables takes two server roundtrips instead of one. That's why we require RPCs to be marked with
- * [RPCReturnsObservables] as needing this special treatment instead of always doing it.
- *
- * If the Artemis/JMS APIs allowed us to create transient queues assigned to someone else then we could
- * potentially use a different design in which the node creates new transient queues (one per observable) on the
- * fly. The client would then have to watch out for this and start consuming those queues as they were created.
- *
- * We use one queue per RPC because we don't know ahead of time how many observables the server might return and
- * often the server doesn't know either, which pushes towards a single queue design, but at the same time the
- * processing of observations returned by an RPC might be striped across multiple threads and we'd like
- * backpressure management to not be scoped per client process but with more granularity. So we end up with
- * a compromise where the unit of backpressure management is the response to a single RPC.
- *
- * TODO: Backpressure isn't propagated all the way through the MQ broker at the moment.
- */
-class CordaRPCClientImpl(private val session: ClientSession,
- private val sessionLock: ReentrantLock,
- private val username: String) {
- companion object {
- private val closeableCloseMethod = Closeable::close.javaMethod
- private val autocloseableCloseMethod = AutoCloseable::close.javaMethod
- }
-
- /**
- * Builds a proxy for the given type, which must descend from [RPCOps].
- *
- * @see CordaRPCClient.proxy for more information about how to use the proxies.
- */
- fun proxyFor(rpcInterface: Class, timeout: Duration? = null, minVersion: Int = 0): T {
- sessionLock.withLock {
- if (producer == null)
- producer = session.createProducer()
- }
- val proxyImpl = RPCProxyHandler(timeout)
- @Suppress("UNCHECKED_CAST")
- val proxy = Proxy.newProxyInstance(rpcInterface.classLoader, arrayOf(rpcInterface, Closeable::class.java), proxyImpl) as T
- proxyImpl.serverProtocolVersion = proxy.protocolVersion
- if (minVersion > proxyImpl.serverProtocolVersion)
- throw RPCException("Requested minimum protocol version $minVersion is higher than the server's supported protocol version (${proxyImpl.serverProtocolVersion})")
- return proxy
- }
-
- //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- //
- //region RPC engine
- //
- // You can find docs on all this in the api doc for the proxyFor method, and in the docsite.
-
- // Utility to quickly suck out the contents of an Artemis message. There's probably a more efficient way to
- // do this.
- private fun ClientMessage.deserialize(kryo: Kryo): T = ByteArray(bodySize).apply { bodyBuffer.readBytes(this) }.deserialize(kryo)
-
- // We by default use a weak reference so GC can happen, otherwise they persist for the life of the client.
- @GuardedBy("sessionLock")
- private val addressToQueuedObservables = CacheBuilder.newBuilder().weakValues().build()
- // This is used to hold a reference counted hard reference when we know there are subscribers.
- private val hardReferencesToQueuedObservables = Collections.synchronizedSet(mutableSetOf())
-
- private var producer: ClientProducer? = null
-
- class ObservableDeserializer() : Serializer>() {
- override fun read(kryo: Kryo, input: Input, type: Class>): Observable {
- val qName = kryo.context[RPCKryoQNameKey] as String
- val rpcName = kryo.context[RPCKryoMethodNameKey] as String
- val rpcLocation = kryo.context[RPCKryoLocationKey] as Throwable
- val rpcClient = kryo.context[RPCKryoClientKey] as CordaRPCClientImpl
- val handle = input.readInt(true)
- val ob = rpcClient.sessionLock.withLock {
- rpcClient.addressToQueuedObservables.getIfPresent(qName) ?: rpcClient.QueuedObservable(qName, rpcName, rpcLocation).apply {
- rpcClient.addressToQueuedObservables.put(qName, this)
- }
- }
- val result = ob.getForHandle(handle)
- rpcLog.debug { "Deserializing and connecting a new observable for $rpcName on $qName: $result" }
- return result
- }
-
- override fun write(kryo: Kryo, output: Output, `object`: Observable) {
- throw UnsupportedOperationException("not implemented")
- }
- }
-
- /**
- * The proxy class returned to the client is auto-generated on the fly by the java.lang.reflect Proxy
- * infrastructure. The JDK Proxy class writes bytecode into memory for a class that implements the requested
- * interfaces and then routes all method calls to the invoke method below in a conveniently reified form.
- * We can then easily take the data about the method call and turn it into an RPC. This avoids the need
- * for the compile-time code generation which is so common in RPC systems.
- */
- @ThreadSafe
- private inner class RPCProxyHandler(private val timeout: Duration?) : InvocationHandler, Closeable {
- private val proxyId = random63BitValue()
- private val consumer: ClientConsumer
-
- var serverProtocolVersion = 0
-
- init {
- val proxyAddress = constructAddress(proxyId)
- consumer = sessionLock.withLock {
- session.createTemporaryQueue(proxyAddress, proxyAddress)
- session.createConsumer(proxyAddress)
- }
- }
-
- private fun constructAddress(addressId: Long) = "${ArtemisMessagingComponent.CLIENTS_PREFIX}$username.rpc.$addressId"
-
- @Synchronized
- override fun invoke(proxy: Any, method: Method, args: Array?): Any? {
- if (isCloseInvocation(method)) {
- close()
- return null
- }
- if (method.name == "toString" && args == null)
- return "Client RPC proxy"
-
- if (consumer.isClosed)
- throw RPCException("RPC Proxy is closed")
-
- // All invoked methods on the proxy end up here.
- val location = Throwable()
- rpcLog.debug {
- val argStr = args?.joinToString() ?: ""
- "-> RPC -> ${method.name}($argStr): ${method.returnType}"
- }
-
- checkMethodVersion(method)
-
- val msg: ClientMessage = createMessage(method)
- // We could of course also check the return type of the method to see if it's Observable, but I'd
- // rather haved the annotation be used consistently.
- val returnsObservables = method.isAnnotationPresent(RPCReturnsObservables::class.java)
- val kryo = if (returnsObservables) maybePrepareForObservables(location, method, msg) else createRPCKryoForDeserialization(this@CordaRPCClientImpl)
- val next: ErrorOr<*> = try {
- sendRequest(args, msg)
- receiveResponse(kryo, method, timeout)
- } finally {
- releaseRPCKryoForDeserialization(kryo)
- }
- rpcLog.debug { "<- RPC <- ${method.name} = $next" }
- return unwrapOrThrow(next)
- }
-
- @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
- private fun unwrapOrThrow(next: ErrorOr<*>): Any? {
- val ex = next.error
- if (ex != null) {
- // Replace the stack trace because that's an implementation detail of the server that isn't so
- // helpful to the user who wants to see where the error was on their side, and serialising stack
- // frame objects is a bit annoying. We slice it here to avoid the invoke() machinery being exposed.
- // The resulting exception looks like it was thrown from inside the called method.
- (ex as java.lang.Throwable).stackTrace = java.lang.Throwable().stackTrace.let { it.sliceArray(1..it.size - 1) }
- throw ex
- } else {
- return next.value
- }
- }
-
- private fun receiveResponse(kryo: Kryo, method: Method, timeout: Duration?): ErrorOr<*> {
- val artemisMessage: ClientMessage =
- if (timeout == null)
- consumer.receive() ?: throw ActiveMQObjectClosedException()
- else
- consumer.receive(timeout.toMillis()) ?: throw RPCException.DeadlineExceeded(method.name)
- artemisMessage.acknowledge()
- val next = artemisMessage.deserialize>(kryo)
- return next
- }
-
- private fun sendRequest(args: Array?, msg: ClientMessage) {
- sessionLock.withLock {
- val argsKryo = createRPCKryoForDeserialization(this@CordaRPCClientImpl)
- val serializedArgs = try {
- (args ?: emptyArray()).serialize(argsKryo)
- } catch (e: KryoException) {
- throw RPCException("Could not serialize RPC arguments", e)
- } finally {
- releaseRPCKryoForDeserialization(argsKryo)
- }
- msg.writeBodyBufferBytes(serializedArgs.bytes)
- producer!!.send(ArtemisMessagingComponent.RPC_REQUESTS_QUEUE, msg)
- }
- }
-
- private fun maybePrepareForObservables(location: Throwable, method: Method, msg: ClientMessage): Kryo {
- // Create a temporary queue just for the emissions on any observables that are returned.
- val observationsId = random63BitValue()
- val observationsQueueName = constructAddress(observationsId)
- session.createTemporaryQueue(observationsQueueName, observationsQueueName)
- msg.putLongProperty(ClientRPCRequestMessage.OBSERVATIONS_TO, observationsId)
- // And make sure that we deserialise observable handles so that they're linked to the right
- // queue. Also record a bit of metadata for debugging purposes.
- return createRPCKryoForDeserialization(this@CordaRPCClientImpl, observationsQueueName, method.name, location)
- }
-
- private fun createMessage(method: Method): ClientMessage {
- return session.createMessage(false).apply {
- putStringProperty(ClientRPCRequestMessage.METHOD_NAME, method.name)
- putLongProperty(ClientRPCRequestMessage.REPLY_TO, proxyId)
- // Use the magic deduplication property built into Artemis as our message identity too
- putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
- }
- }
-
- private fun checkMethodVersion(method: Method) {
- val methodVersion = method.getAnnotation(RPCSinceVersion::class.java)?.version ?: 0
- if (methodVersion > serverProtocolVersion)
- throw UnsupportedOperationException("Method ${method.name} was added in RPC protocol version $methodVersion but the server is running $serverProtocolVersion")
- }
-
- private fun isCloseInvocation(method: Method) = method == closeableCloseMethod || method == autocloseableCloseMethod
-
- override fun close() {
- consumer.close()
- sessionLock.withLock { session.deleteQueue(constructAddress(proxyId)) }
- }
-
- override fun toString() = "Corda RPC Proxy listening on queue ${constructAddress(proxyId)}"
- }
-
- /**
- * When subscribed to, starts consuming from the given queue name and demultiplexing the observables being
- * sent to it. The server queue is moved into in-memory buffers (one per attached server-side observable)
- * until drained through a subscription. When the subscriptions are all gone, the server-side queue is deleted.
- */
- @ThreadSafe
- private inner class QueuedObservable(private val qName: String,
- private val rpcName: String,
- private val rpcLocation: Throwable) {
- private val root = PublishSubject.create()
- private val rootShared = root.doOnUnsubscribe { close() }.share()
-
- // This could be made more efficient by using a specialised IntMap
- // When handling this map we don't synchronise on [this], otherwise there is a race condition between close() and deliver()
- private val observables = Collections.synchronizedMap(HashMap>())
-
- private var consumer: ClientConsumer? = null
-
- private val referenceCount = AtomicInteger(0)
-
- // We have to create a weak reference, otherwise we cannot be GC'd.
- init {
- val weakThis = WeakReference(this)
- consumer = sessionLock.withLock { session.createConsumer(qName) }.setMessageHandler { weakThis.get()?.deliver(it) }
- }
-
- /**
- * We have to reference count subscriptions to the returned [Observable]s to prevent early GC because we are
- * weak referenced.
- *
- * Derived [Observables] (e.g. filtered etc) hold a strong reference to the original, but for example, if
- * the pattern as follows is used, the original passes out of scope and the direction of reference is from the
- * original to the [Observer]. We use the reference counting to allow for this pattern.
- *
- * val observationsSubject = PublishSubject.create()
- * originalObservable.subscribe(observationsSubject)
- * return observationsSubject
- */
- private fun refCountUp() {
- if(referenceCount.andIncrement == 0) {
- hardReferencesToQueuedObservables.add(this)
- }
- }
-
- private fun refCountDown() {
- if(referenceCount.decrementAndGet() == 0) {
- hardReferencesToQueuedObservables.remove(this)
- }
- }
-
- fun getForHandle(handle: Int): Observable {
- synchronized(observables) {
- return observables.getOrPut(handle) {
- /**
- * Note that the order of bufferUntilSubscribed() -> dematerialize() is very important here.
- *
- * In particular doing it the other way around may result in the following edge case:
- * The RPC returns two (or more) Observables. The first Observable unsubscribes *during serialisation*,
- * before the second one is hit, causing the [rootShared] to unsubscribe and consequently closing
- * the underlying artemis queue, even though the second Observable was not even registered.
- *
- * The buffer -> dematerialize order ensures that the Observable may not unsubscribe until the caller
- * subscribes, which must be after full deserialisation and registering of all top level Observables.
- *
- * In addition, when subscribe and unsubscribe is called on the [Observable] returned here, we
- * reference count a hard reference to this [QueuedObservable] to prevent premature GC.
- */
- rootShared.filter { it.forHandle == handle }.map { it.what }.bufferUntilSubscribed().dematerialize().doOnSubscribe { refCountUp() }.doOnUnsubscribe { refCountDown() }.share()
- }
- }
- }
-
- private fun deliver(msg: ClientMessage) {
- msg.acknowledge()
- val kryo = createRPCKryoForDeserialization(this@CordaRPCClientImpl, qName, rpcName, rpcLocation)
- val received: MarshalledObservation = try { msg.deserialize(kryo) } finally {
- releaseRPCKryoForDeserialization(kryo)
- }
- rpcLog.debug { "<- Observable [$rpcName] <- Received $received" }
- synchronized(observables) {
- // Force creation of the buffer if it doesn't already exist.
- getForHandle(received.forHandle)
- root.onNext(received)
- }
- }
-
- @Synchronized
- fun close() {
- rpcLog.debug("Closing queue observable for call to $rpcName : $qName")
- consumer?.close()
- consumer = null
- sessionLock.withLock { session.deleteQueue(qName) }
- }
-
- @Suppress("UNUSED")
- fun finalize() {
- val c = synchronized(this) { consumer }
- if (c != null) {
- rpcLog.warn("A hot observable returned from an RPC ($rpcName) was never subscribed to. " +
- "This wastes server-side resources because it was queueing observations for retrieval. " +
- "It is being closed now, but please adjust your code to subscribe and unsubscribe from the observable to close it explicitly.", rpcLocation)
- c.close()
- }
- }
- }
- //endregion
-}
-
-private val rpcDesKryoPool = KryoPool.Builder { RPCKryo(CordaRPCClientImpl.ObservableDeserializer()) }.build()
-
-fun createRPCKryoForDeserialization(rpcClient: CordaRPCClientImpl, qName: String? = null, rpcName: String? = null, rpcLocation: Throwable? = null): Kryo {
- val kryo = rpcDesKryoPool.borrow()
- kryo.context.put(RPCKryoClientKey, rpcClient)
- kryo.context.put(RPCKryoQNameKey, qName)
- kryo.context.put(RPCKryoMethodNameKey, rpcName)
- kryo.context.put(RPCKryoLocationKey, rpcLocation)
- return kryo
-}
-
-fun releaseRPCKryoForDeserialization(kryo: Kryo) {
- rpcDesKryoPool.release(kryo)
-}
diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/Utils.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/Utils.kt
new file mode 100644
index 0000000000..891a57202a
--- /dev/null
+++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/Utils.kt
@@ -0,0 +1,19 @@
+package net.corda.client.rpc
+
+import rx.Observable
+
+/**
+ * This function should be invoked on any unwanted Observables returned from RPC to release the server resources.
+ *
+ * subscribe({}, {}) was used instead of simply calling subscribe()
+ * because if an {@code onError} emission arrives (eg. due to an non-correct transaction, such as 'Not sufficient funds')
+ * then {@link OnErrorNotImplementedException} is thrown. As we won't handle exceptions from unused Observables,
+ * empty inputs are used to subscribe({}, {}).
+ */
+fun Observable.notUsed() {
+ try {
+ this.subscribe({}, {}).unsubscribe()
+ } catch (e: Exception) {
+ // Swallow any other exceptions as well.
+ }
+}
diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt
new file mode 100644
index 0000000000..60d50928bd
--- /dev/null
+++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt
@@ -0,0 +1,169 @@
+package net.corda.client.rpc.internal
+
+import com.google.common.net.HostAndPort
+import net.corda.core.logElapsedTime
+import net.corda.core.messaging.RPCOps
+import net.corda.core.minutes
+import net.corda.core.random63BitValue
+import net.corda.core.seconds
+import net.corda.core.utilities.loggerFor
+import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
+import net.corda.nodeapi.ConnectionDirection
+import net.corda.nodeapi.RPCApi
+import net.corda.nodeapi.RPCException
+import net.corda.nodeapi.config.SSLConfiguration
+import org.apache.activemq.artemis.api.core.SimpleString
+import org.apache.activemq.artemis.api.core.TransportConfiguration
+import org.apache.activemq.artemis.api.core.client.ActiveMQClient
+import java.io.Closeable
+import java.lang.reflect.Proxy
+import java.time.Duration
+
+/**
+ * This configuration may be used to tweak the internals of the RPC client.
+ */
+data class RPCClientConfiguration(
+ /** The minimum protocol version required from the server */
+ val minimumServerProtocolVersion: Int,
+ /**
+ * If set to true the client will track RPC call sites. If an error occurs subsequently during the RPC or in a
+ * returned Observable stream the stack trace of the originating RPC will be shown as well. Note that
+ * constructing call stacks is a moderately expensive operation.
+ */
+ val trackRpcCallSites: Boolean,
+ /**
+ * The interval of unused observable reaping. Leaked Observables (unused ones) are detected using weak references
+ * and are cleaned up in batches in this interval. If set too large it will waste server side resources for this
+ * duration. If set too low it wastes client side cycles.
+ */
+ val reapInterval: Duration,
+ /** The number of threads to use for observations (for executing [Observable.onNext]) */
+ val observationExecutorPoolSize: Int,
+ /** The maximum number of producers to create to handle outgoing messages */
+ val producerPoolBound: Int,
+ /**
+ * Determines the concurrency level of the Observable Cache. This is exposed because it implicitly determines
+ * the limit on the number of leaked observables reaped because of garbage collection per reaping.
+ * See the implementation of [com.google.common.cache.LocalCache] for details.
+ */
+ val cacheConcurrencyLevel: Int,
+ /** The retry interval of artemis connections in milliseconds */
+ val connectionRetryInterval: Duration,
+ /** The retry interval multiplier for exponential backoff */
+ val connectionRetryIntervalMultiplier: Double,
+ /** Maximum retry interval */
+ val connectionMaxRetryInterval: Duration,
+ /** Maximum file size */
+ val maxFileSize: Int
+) {
+ companion object {
+ @JvmStatic
+ val default = RPCClientConfiguration(
+ minimumServerProtocolVersion = 0,
+ trackRpcCallSites = false,
+ reapInterval = 1.seconds,
+ observationExecutorPoolSize = 4,
+ producerPoolBound = 1,
+ cacheConcurrencyLevel = 8,
+ connectionRetryInterval = 5.seconds,
+ connectionRetryIntervalMultiplier = 1.5,
+ connectionMaxRetryInterval = 3.minutes,
+ /** 10 MiB maximum allowed file size for attachments, including message headers. TODO: acquire this value from Network Map when supported. */
+ maxFileSize = 10485760
+ )
+ }
+}
+
+/**
+ * An RPC client that may be used to create connections to an RPC server.
+ *
+ * @param transport The Artemis transport to use to connect to the server.
+ * @param rpcConfiguration Configuration used to tweak client behaviour.
+ */
+class RPCClient(
+ val transport: TransportConfiguration,
+ val rpcConfiguration: RPCClientConfiguration = RPCClientConfiguration.default
+) {
+ constructor(
+ hostAndPort: HostAndPort,
+ sslConfiguration: SSLConfiguration? = null,
+ configuration: RPCClientConfiguration = RPCClientConfiguration.default
+ ) : this(tcpTransport(ConnectionDirection.Outbound(), hostAndPort, sslConfiguration), configuration)
+
+ companion object {
+ private val log = loggerFor>()
+ }
+
+ /**
+ * Holds a proxy object implementing [I] that forwards requests to the RPC server.
+ *
+ * [Closeable.close] may be used to shut down the connection and release associated resources.
+ */
+ interface RPCConnection : Closeable {
+ val proxy: I
+ /** The RPC protocol version reported by the server */
+ val serverProtocolVersion: Int
+ }
+
+ /**
+ * Returns an [RPCConnection] containing a proxy that lets you invoke RPCs on the server. Calls on it block, and if
+ * the server throws an exception then it will be rethrown on the client. Proxies are thread safe and may be used to
+ * invoke multiple RPCs in parallel.
+ *
+ * RPC sends and receives are logged on the net.corda.rpc logger.
+ *
+ * The [RPCOps] defines what client RPCs are available. If an RPC returns an [Observable] anywhere in the object
+ * graph returned then the server-side observable is transparently forwarded to the client side here.
+ * *You are expected to use it*. The server will begin buffering messages immediately that it will expect you to
+ * drain by subscribing to the returned observer. You can opt-out of this by simply calling the
+ * [net.corda.client.rpc.notUsed] method on it. You don't have to explicitly close the observable if you actually
+ * subscribe to it: it will close itself and free up the server-side resources either when the client or JVM itself
+ * is shutdown, or when there are no more subscribers to it. Once all the subscribers to a returned observable are
+ * unsubscribed or the observable completes successfully or with an error, the observable is closed and you can't
+ * then re-subscribe again: you'll have to re-request a fresh observable with another RPC.
+ *
+ * @param rpcOpsClass The [Class] of the RPC interface.
+ * @param username The username to authenticate with.
+ * @param password The password to authenticate with.
+ * @throws RPCException if the server version is too low or if the server isn't reachable within the given time.
+ */
+ fun start(
+ rpcOpsClass: Class,
+ username: String,
+ password: String
+ ): RPCConnection {
+ return log.logElapsedTime("Startup") {
+ val clientAddress = SimpleString("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username.${random63BitValue()}")
+
+ val serverLocator = ActiveMQClient.createServerLocatorWithoutHA(transport).apply {
+ retryInterval = rpcConfiguration.connectionRetryInterval.toMillis()
+ retryIntervalMultiplier = rpcConfiguration.connectionRetryIntervalMultiplier
+ maxRetryInterval = rpcConfiguration.connectionMaxRetryInterval.toMillis()
+ minLargeMessageSize = rpcConfiguration.maxFileSize
+ }
+
+ val proxyHandler = RPCClientProxyHandler(rpcConfiguration, username, password, serverLocator, clientAddress, rpcOpsClass)
+ proxyHandler.start()
+
+ @Suppress("UNCHECKED_CAST")
+ val ops = Proxy.newProxyInstance(rpcOpsClass.classLoader, arrayOf(rpcOpsClass), proxyHandler) as I
+
+ val serverProtocolVersion = ops.protocolVersion
+ if (serverProtocolVersion < rpcConfiguration.minimumServerProtocolVersion) {
+ throw RPCException("Requested minimum protocol version (${rpcConfiguration.minimumServerProtocolVersion}) is higher" +
+ " than the server's supported protocol version ($serverProtocolVersion)")
+ }
+ proxyHandler.setServerProtocolVersion(serverProtocolVersion)
+
+ log.debug("RPC connected, returning proxy")
+ object : RPCConnection {
+ override val proxy = ops
+ override val serverProtocolVersion = serverProtocolVersion
+ override fun close() {
+ proxyHandler.close()
+ serverLocator.close()
+ }
+ }
+ }
+ }
+}
diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt
new file mode 100644
index 0000000000..da95f01b4d
--- /dev/null
+++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt
@@ -0,0 +1,422 @@
+package net.corda.client.rpc.internal
+
+import com.esotericsoftware.kryo.Kryo
+import com.esotericsoftware.kryo.Serializer
+import com.esotericsoftware.kryo.io.Input
+import com.esotericsoftware.kryo.io.Output
+import com.esotericsoftware.kryo.pool.KryoPool
+import com.google.common.cache.Cache
+import com.google.common.cache.CacheBuilder
+import com.google.common.cache.RemovalCause
+import com.google.common.cache.RemovalListener
+import com.google.common.util.concurrent.SettableFuture
+import com.google.common.util.concurrent.ThreadFactoryBuilder
+import net.corda.core.ThreadBox
+import net.corda.core.getOrThrow
+import net.corda.core.messaging.RPCOps
+import net.corda.core.random63BitValue
+import net.corda.core.serialization.KryoPoolWithContext
+import net.corda.core.utilities.*
+import net.corda.nodeapi.*
+import org.apache.activemq.artemis.api.core.SimpleString
+import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
+import org.apache.activemq.artemis.api.core.client.ClientMessage
+import org.apache.activemq.artemis.api.core.client.ServerLocator
+import rx.Notification
+import rx.Observable
+import rx.subjects.UnicastSubject
+import sun.reflect.CallerSensitive
+import java.lang.reflect.InvocationHandler
+import java.lang.reflect.Method
+import java.util.*
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.Executors
+import java.util.concurrent.ScheduledFuture
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.collections.ArrayList
+import kotlin.reflect.jvm.javaMethod
+
+/**
+ * This class provides a proxy implementation of an RPC interface for RPC clients. It translates API calls to lower-level
+ * RPC protocol messages. For this protocol see [RPCApi].
+ *
+ * When a method is called on the interface the arguments are serialised and the request is forwarded to the server. The
+ * server then executes the code that implements the RPC and sends a reply.
+ *
+ * An RPC reply may contain [Observable]s, which are serialised simply as unique IDs. On the client side we create a
+ * [UnicastSubject] for each such ID. Subsequently the server may send observations attached to this ID, which are
+ * forwarded to the [UnicastSubject]. Note that the observations themselves may contain further [Observable]s, which are
+ * handled in the same way.
+ *
+ * To do the above we take advantage of Kryo's datastructure traversal. When the client is deserialising a message from
+ * the server that may contain Observables it is supplied with an [ObservableContext] that exposes the map used to demux
+ * the observations. When an [Observable] is encountered during traversal a new [UnicastSubject] is added to the map and
+ * we carry on. Each observation later contains the corresponding Observable ID, and we just forward that to the
+ * associated [UnicastSubject].
+ *
+ * The client may signal that it no longer consumes a particular [Observable]. This may be done explicitly by
+ * unsubscribing from the [Observable], or if the [Observable] is garbage collected the client will eventually
+ * automatically signal the server. This is done using a cache that holds weak references to the [UnicastSubject]s.
+ * The cleanup happens in batches using a dedicated reaper, scheduled on [reaperExecutor].
+ */
+class RPCClientProxyHandler(
+ private val rpcConfiguration: RPCClientConfiguration,
+ private val rpcUsername: String,
+ private val rpcPassword: String,
+ private val serverLocator: ServerLocator,
+ private val clientAddress: SimpleString,
+ private val rpcOpsClass: Class
+) : InvocationHandler {
+
+ private enum class State {
+ UNSTARTED,
+ SERVER_VERSION_NOT_SET,
+ STARTED,
+ FINISHED
+ }
+ private val lifeCycle = LifeCycle(State.UNSTARTED)
+
+ private companion object {
+ val log = loggerFor()
+ // Note that this KryoPool is not yet capable of deserialising Observables, it requires Proxy-specific context
+ // to do that. However it may still be used for serialisation of RPC requests and related messages.
+ val kryoPool = KryoPool.Builder { RPCKryo(RpcClientObservableSerializer) }.build()
+ // To check whether toString() is being invoked
+ val toStringMethod: Method = Object::toString.javaMethod!!
+ }
+
+ // Used for reaping
+ private val reaperExecutor = Executors.newScheduledThreadPool(
+ 1,
+ ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").build()
+ )
+
+ // A sticky pool for running Observable.onNext()s. We need the stickiness to preserve the observation ordering.
+ private val observationExecutorThreadFactory = ThreadFactoryBuilder().setNameFormat("rpc-client-observation-pool-%d").build()
+ private val observationExecutorPool = LazyStickyPool(rpcConfiguration.observationExecutorPoolSize) {
+ Executors.newFixedThreadPool(1, observationExecutorThreadFactory)
+ }
+
+ // Holds the RPC reply futures.
+ private val rpcReplyMap = RpcReplyMap()
+ // Optionally holds RPC call site stack traces to be shown on errors/warnings.
+ private val callSiteMap = if (rpcConfiguration.trackRpcCallSites) CallSiteMap() else null
+ // Holds the Observables and a reference store to keep Observables alive when subscribed to.
+ private val observableContext = ObservableContext(
+ callSiteMap = callSiteMap,
+ observableMap = createRpcObservableMap(),
+ hardReferenceStore = Collections.synchronizedSet(mutableSetOf>())
+ )
+ // Holds a reference to the scheduled reaper.
+ private lateinit var reaperScheduledFuture: ScheduledFuture<*>
+ // The protocol version of the server, to be initialised to the value of [RPCOps.protocolVersion]
+ private var serverProtocolVersion: Int? = null
+
+ // Stores the Observable IDs that are already removed from the map but are not yet sent to the server.
+ private val observablesToReap = ThreadBox(object {
+ var observables = ArrayList()
+ })
+ // A Kryo pool that automatically adds the observable context when an instance is requested.
+ private val kryoPoolWithObservableContext = RpcClientObservableSerializer.createPoolWithContext(kryoPool, observableContext)
+
+ private fun createRpcObservableMap(): RpcObservableMap {
+ val onObservableRemove = RemovalListener>> {
+ val rpcCallSite = callSiteMap?.remove(it.key.toLong)
+ if (it.cause == RemovalCause.COLLECTED) {
+ log.warn(listOf(
+ "A hot observable returned from an RPC was never subscribed to.",
+ "This wastes server-side resources because it was queueing observations for retrieval.",
+ "It is being closed now, but please adjust your code to call .notUsed() on the observable",
+ "to close it explicitly. (Java users: subscribe to it then unsubscribe). This warning",
+ "will appear less frequently in future versions of the platform and you can ignore it",
+ "if you want to.").joinToString(" "), rpcCallSite)
+ }
+ observablesToReap.locked { observables.add(it.key) }
+ }
+ return CacheBuilder.newBuilder().
+ weakValues().
+ removalListener(onObservableRemove).
+ concurrencyLevel(rpcConfiguration.cacheConcurrencyLevel).
+ build()
+ }
+
+ // We cannot pool consumers as we need to preserve the original muxed message order.
+ // TODO We may need to pool these somehow anyway, otherwise if the server sends many big messages in parallel a
+ // single consumer may be starved for flow control credits. Recheck this once Artemis's large message streaming is
+ // integrated properly.
+ private lateinit var sessionAndConsumer: ArtemisConsumer
+ // Pool producers to reduce contention on the client side.
+ private val sessionAndProducerPool = LazyPool(bound = rpcConfiguration.producerPoolBound) {
+ // Note how we create new sessions *and* session factories per producer.
+ // We cannot simply pool producers on one session because sessions are single threaded.
+ // We cannot simply pool sessions on one session factory because flow control credits are tied to factories, so
+ // sessions tend to starve each other when used concurrently.
+ val sessionFactory = serverLocator.createSessionFactory()
+ val session = sessionFactory.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)
+ session.start()
+ ArtemisProducer(sessionFactory, session, session.createProducer(RPCApi.RPC_SERVER_QUEUE_NAME))
+ }
+
+ /**
+ * Start the client. This creates the per-client queue, starts the consumer session and the reaper.
+ */
+ fun start() {
+ reaperScheduledFuture = reaperExecutor.scheduleAtFixedRate(
+ this::reapObservables,
+ rpcConfiguration.reapInterval.toMillis(),
+ rpcConfiguration.reapInterval.toMillis(),
+ TimeUnit.MILLISECONDS
+ )
+ sessionAndProducerPool.run {
+ it.session.createTemporaryQueue(clientAddress, clientAddress)
+ }
+ val sessionFactory = serverLocator.createSessionFactory()
+ val session = sessionFactory.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)
+ val consumer = session.createConsumer(clientAddress)
+ consumer.setMessageHandler(this@RPCClientProxyHandler::artemisMessageHandler)
+ sessionAndConsumer = ArtemisConsumer(sessionFactory, session, consumer)
+ lifeCycle.transition(State.UNSTARTED, State.SERVER_VERSION_NOT_SET)
+ session.start()
+ }
+
+ // This is the general function that transforms a client side RPC to internal Artemis messages.
+ override fun invoke(proxy: Any, method: Method, arguments: Array?): Any? {
+ lifeCycle.requireState { it == State.STARTED || it == State.SERVER_VERSION_NOT_SET }
+ checkProtocolVersion(method)
+ if (method == toStringMethod) {
+ return "Client RPC proxy for $rpcOpsClass"
+ }
+ if (sessionAndConsumer.session.isClosed) {
+ throw RPCException("RPC Proxy is closed")
+ }
+ val rpcId = RPCApi.RpcRequestId(random63BitValue())
+ callSiteMap?.set(rpcId.toLong, Throwable(""))
+ try {
+ val request = RPCApi.ClientToServer.RpcRequest(clientAddress, rpcId, method.name, arguments?.toList() ?: emptyList())
+ val replyFuture = SettableFuture.create()
+ sessionAndProducerPool.run {
+ val message = it.session.createMessage(false)
+ request.writeToClientMessage(kryoPool, message)
+
+ log.debug {
+ val argumentsString = arguments?.joinToString() ?: ""
+ "-> RPC($rpcId) -> ${method.name}($argumentsString): ${method.returnType}"
+ }
+
+ require(rpcReplyMap.put(rpcId, replyFuture) == null) {
+ "Generated several RPC requests with same ID $rpcId"
+ }
+ it.producer.send(message)
+ it.session.commit()
+ }
+ return replyFuture.getOrThrow()
+ } finally {
+ callSiteMap?.remove(rpcId.toLong)
+ }
+ }
+
+ // The handler for Artemis messages.
+ private fun artemisMessageHandler(message: ClientMessage) {
+ val serverToClient = RPCApi.ServerToClient.fromClientMessage(kryoPoolWithObservableContext, message)
+ log.debug { "Got message from RPC server $serverToClient" }
+ when (serverToClient) {
+ is RPCApi.ServerToClient.RpcReply -> {
+ val replyFuture = rpcReplyMap.remove(serverToClient.id)
+ if (replyFuture == null) {
+ log.error("RPC reply arrived to unknown RPC ID ${serverToClient.id}, this indicates an internal RPC error.")
+ } else {
+ val rpcCallSite = callSiteMap?.get(serverToClient.id.toLong)
+ serverToClient.result.match(
+ onError = {
+ if (rpcCallSite != null) addRpcCallSiteToThrowable(it, rpcCallSite)
+ replyFuture.setException(it)
+ },
+ onValue = { replyFuture.set(it) }
+ )
+ }
+ }
+ is RPCApi.ServerToClient.Observation -> {
+ val observable = observableContext.observableMap.getIfPresent(serverToClient.id)
+ if (observable == null) {
+ log.debug("Observation ${serverToClient.content} arrived to unknown Observable with ID ${serverToClient.id}. " +
+ "This may be due to an observation arriving before the server was " +
+ "notified of observable shutdown")
+ } else {
+ // We schedule the onNext() on an executor sticky-pooled based on the Observable ID.
+ observationExecutorPool.run(serverToClient.id) { executor ->
+ executor.submit {
+ val content = serverToClient.content
+ if (content.isOnCompleted || content.isOnError) {
+ observableContext.observableMap.invalidate(serverToClient.id)
+ }
+ // Add call site information on error
+ if (content.isOnError) {
+ val rpcCallSite = callSiteMap?.get(serverToClient.id.toLong)
+ if (rpcCallSite != null) addRpcCallSiteToThrowable(content.throwable, rpcCallSite)
+ }
+ observable.onNext(content)
+ }
+ }
+ }
+ }
+ }
+ message.acknowledge()
+ }
+
+ /**
+ * Closes the RPC proxy. Reaps all observables, shuts down the reaper, closes all sessions and executors.
+ */
+ fun close() {
+ sessionAndConsumer.consumer.close()
+ sessionAndConsumer.session.close()
+ sessionAndConsumer.sessionFactory.close()
+ reaperScheduledFuture.cancel(false)
+ observableContext.observableMap.invalidateAll()
+ reapObservables()
+ reaperExecutor.shutdownNow()
+ sessionAndProducerPool.close().forEach {
+ it.producer.close()
+ it.session.close()
+ it.sessionFactory.close()
+ }
+ // Note the ordering is important, we shut down the consumer *before* the observation executor, otherwise we may
+ // leak borrowed executors.
+ val observationExecutors = observationExecutorPool.close()
+ observationExecutors.forEach { it.shutdownNow() }
+ observationExecutors.forEach { it.awaitTermination(100, TimeUnit.MILLISECONDS) }
+ lifeCycle.transition(State.STARTED, State.FINISHED)
+ }
+
+ /**
+ * Check the [RPCSinceVersion] of the passed in [calledMethod] against the server's protocol version.
+ */
+ private fun checkProtocolVersion(calledMethod: Method) {
+ val serverProtocolVersion = serverProtocolVersion
+ if (serverProtocolVersion == null) {
+ lifeCycle.requireState(State.SERVER_VERSION_NOT_SET)
+ } else {
+ lifeCycle.requireState(State.STARTED)
+ val sinceVersion = calledMethod.getAnnotation(RPCSinceVersion::class.java)?.version ?: 0
+ if (sinceVersion > serverProtocolVersion) {
+ throw UnsupportedOperationException("Method $calledMethod was added in RPC protocol version $sinceVersion but the server is running $serverProtocolVersion")
+ }
+ }
+ }
+
+ /**
+ * Set the server's protocol version. Note that before doing so the client is not considered fully started, although
+ * RPCs already may be called with it.
+ */
+ internal fun setServerProtocolVersion(version: Int) {
+ if (serverProtocolVersion == null) {
+ serverProtocolVersion = version
+ } else {
+ throw IllegalStateException("setServerProtocolVersion called, but the protocol version was already set!")
+ }
+ lifeCycle.transition(State.SERVER_VERSION_NOT_SET, State.STARTED)
+ }
+
+ private fun reapObservables() {
+ observableContext.observableMap.cleanUp()
+ val observableIds = observablesToReap.locked {
+ if (observables.isNotEmpty()) {
+ val temporary = observables
+ observables = ArrayList()
+ temporary
+ } else {
+ null
+ }
+ }
+ if (observableIds != null) {
+ log.debug { "Reaping ${observableIds.size} observables" }
+ sessionAndProducerPool.run {
+ val message = it.session.createMessage(false)
+ RPCApi.ClientToServer.ObservablesClosed(observableIds).writeToClientMessage(message)
+ it.producer.send(message)
+ }
+ }
+ }
+}
+
+private typealias RpcObservableMap = Cache>>
+private typealias RpcReplyMap = ConcurrentHashMap>
+private typealias CallSiteMap = ConcurrentHashMap
+
+/**
+ * Holds a context available during Kryo deserialisation of messages that are expected to contain Observables.
+ *
+ * @param observableMap holds the Observables that are ultimately exposed to the user.
+ * @param hardReferenceStore holds references to Observables we want to keep alive while they are subscribed to.
+ */
+private data class ObservableContext(
+ val callSiteMap: CallSiteMap?,
+ val observableMap: RpcObservableMap,
+ val hardReferenceStore: MutableSet>
+)
+
+/**
+ * A [Serializer] to deserialise Observables once the corresponding Kryo instance has been provided with an [ObservableContext].
+ */
+private object RpcClientObservableSerializer : Serializer>() {
+ private object RpcObservableContextKey
+ fun createPoolWithContext(kryoPool: KryoPool, observableContext: ObservableContext): KryoPool {
+ return KryoPoolWithContext(kryoPool, RpcObservableContextKey, observableContext)
+ }
+
+ override fun read(kryo: Kryo, input: Input, type: Class>): Observable {
+ @Suppress("UNCHECKED_CAST")
+ val observableContext = kryo.context[RpcObservableContextKey] as ObservableContext
+ val observableId = RPCApi.ObservableId(input.readLong(true))
+ val observable = UnicastSubject.create>()
+ require(observableContext.observableMap.getIfPresent(observableId) == null) {
+ "Multiple Observables arrived with the same ID $observableId"
+ }
+ val rpcCallSite = getRpcCallSite(kryo, observableContext)
+ observableContext.observableMap.put(observableId, observable)
+ observableContext.callSiteMap?.put(observableId.toLong, rpcCallSite)
+ // We pin all Observables into a hard reference store (rooted in the RPC proxy) on subscription so that users
+ // don't need to store a reference to the Observables themselves.
+ return observable.pinInSubscriptions(observableContext.hardReferenceStore).doOnUnsubscribe {
+ // This causes Future completions to give warnings because the corresponding OnComplete sent from the server
+ // will arrive after the client unsubscribes from the observable and consequently invalidates the mapping.
+ // The unsubscribe is due to [ObservableToFuture]'s use of first().
+ observableContext.observableMap.invalidate(observableId)
+ }.dematerialize()
+ }
+
+ override fun write(kryo: Kryo, output: Output, observable: Observable) {
+ throw UnsupportedOperationException("Cannot serialise Observables on the client side")
+ }
+
+ private fun getRpcCallSite(kryo: Kryo, observableContext: ObservableContext): Throwable? {
+ val rpcRequestOrObservableId = kryo.context[RPCApi.RpcRequestOrObservableIdKey] as Long
+ return observableContext.callSiteMap?.get(rpcRequestOrObservableId)
+ }
+}
+
+private fun addRpcCallSiteToThrowable(throwable: Throwable, callSite: Throwable) {
+ var currentThrowable = throwable
+ while (true) {
+ val cause = currentThrowable.cause
+ if (cause == null) {
+ currentThrowable.initCause(callSite)
+ break
+ } else {
+ currentThrowable = cause
+ }
+ }
+}
+
+private fun Observable.pinInSubscriptions(hardReferenceStore: MutableSet>): Observable {
+ val refCount = AtomicInteger(0)
+ return this.doOnSubscribe {
+ if (refCount.getAndIncrement() == 0) {
+ require(hardReferenceStore.add(this)) { "Reference store already contained reference $this on add" }
+ }
+ }.doOnUnsubscribe {
+ if (refCount.decrementAndGet() == 0) {
+ require(hardReferenceStore.remove(this)) { "Reference store did not contain reference $this on remove" }
+ }
+ }
+}
diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt
new file mode 100644
index 0000000000..6139ad79fb
--- /dev/null
+++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt
@@ -0,0 +1,56 @@
+package net.corda.client.rpc
+
+import net.corda.client.rpc.internal.RPCClientConfiguration
+import net.corda.core.flatMap
+import net.corda.core.map
+import net.corda.core.messaging.RPCOps
+import net.corda.node.services.messaging.RPCServerConfiguration
+import net.corda.nodeapi.User
+import net.corda.testing.RPCDriverExposedDSLInterface
+import net.corda.testing.rpcTestUser
+import net.corda.testing.startInVmRpcClient
+import net.corda.testing.startRpcClient
+import org.apache.activemq.artemis.api.core.client.ClientSession
+import org.junit.runners.Parameterized
+
+open class AbstractRPCTest {
+ enum class RPCTestMode {
+ InVm,
+ Netty
+ }
+
+ companion object {
+ @JvmStatic @Parameterized.Parameters(name = "Mode = {0}")
+ fun defaultModes() = modes(RPCTestMode.InVm, RPCTestMode.Netty)
+ fun modes(vararg modes: RPCTestMode) = listOf(*modes).map { arrayOf(it) }
+ }
+ @Parameterized.Parameter
+ lateinit var mode: RPCTestMode
+
+ data class TestProxy(
+ val ops: I,
+ val createSession: () -> ClientSession
+ )
+
+ inline fun RPCDriverExposedDSLInterface.testProxy(
+ ops: I,
+ rpcUser: User = rpcTestUser,
+ clientConfiguration: RPCClientConfiguration = RPCClientConfiguration.default,
+ serverConfiguration: RPCServerConfiguration = RPCServerConfiguration.default
+ ): TestProxy {
+ return when (mode) {
+ RPCTestMode.InVm ->
+ startInVmRpcServer(ops = ops, rpcUser = rpcUser, configuration = serverConfiguration).flatMap {
+ startInVmRpcClient(rpcUser.username, rpcUser.password, clientConfiguration).map {
+ TestProxy(it, { startInVmArtemisSession(rpcUser.username, rpcUser.password) })
+ }
+ }.get()
+ RPCTestMode.Netty ->
+ startRpcServer(ops = ops, rpcUser = rpcUser, configuration = serverConfiguration).flatMap { server ->
+ startRpcClient(server.hostAndPort, rpcUser.username, rpcUser.password, clientConfiguration).map {
+ TestProxy(it, { startArtemisSession(server.hostAndPort, rpcUser.username, rpcUser.password) })
+ }
+ }.get()
+ }
+ }
+}
diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt
new file mode 100644
index 0000000000..c9d3c65879
--- /dev/null
+++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt
@@ -0,0 +1,192 @@
+package net.corda.client.rpc
+
+import com.google.common.util.concurrent.Futures
+import com.google.common.util.concurrent.ListenableFuture
+import com.google.common.util.concurrent.SettableFuture
+import net.corda.core.getOrThrow
+import net.corda.core.messaging.RPCOps
+import net.corda.core.success
+import net.corda.node.services.messaging.getRpcContext
+import net.corda.nodeapi.RPCSinceVersion
+import net.corda.testing.RPCDriverExposedDSLInterface
+import net.corda.testing.rpcDriver
+import net.corda.testing.rpcTestUser
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import rx.Observable
+import rx.subjects.PublishSubject
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.LinkedBlockingQueue
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
+
+@RunWith(Parameterized::class)
+class ClientRPCInfrastructureTests : AbstractRPCTest() {
+ // TODO: Test that timeouts work
+
+ private fun RPCDriverExposedDSLInterface.testProxy() = testProxy(TestOpsImpl()).ops
+
+ interface TestOps : RPCOps {
+ @Throws(IllegalArgumentException::class)
+ fun barf()
+
+ fun void()
+
+ fun someCalculation(str: String, num: Int): String
+
+ fun makeObservable(): Observable
+
+ fun makeComplicatedObservable(): Observable>>
+
+ fun makeListenableFuture(): ListenableFuture
+
+ fun makeComplicatedListenableFuture(): ListenableFuture>>
+
+ @RPCSinceVersion(2)
+ fun addedLater()
+
+ fun captureUser(): String
+ }
+
+ private lateinit var complicatedObservable: Observable>>
+ private lateinit var complicatedListenableFuturee: ListenableFuture>>
+
+ inner class TestOpsImpl : TestOps {
+ override val protocolVersion = 1
+ override fun barf(): Unit = throw IllegalArgumentException("Barf!")
+ override fun void() {}
+ override fun someCalculation(str: String, num: Int) = "$str $num"
+ override fun makeObservable(): Observable = Observable.just(1, 2, 3, 4)
+ override fun makeListenableFuture(): ListenableFuture = Futures.immediateFuture(1)
+ override fun makeComplicatedObservable() = complicatedObservable
+ override fun makeComplicatedListenableFuture(): ListenableFuture>> = complicatedListenableFuturee
+ override fun addedLater(): Unit = throw IllegalStateException()
+ override fun captureUser(): String = getRpcContext().currentUser.username
+ }
+
+ @Test
+ fun `simple RPCs`() {
+ rpcDriver {
+ val proxy = testProxy()
+ // Does nothing, doesn't throw.
+ proxy.void()
+
+ assertEquals("Barf!", assertFailsWith {
+ proxy.barf()
+ }.message)
+
+ assertEquals("hi 5", proxy.someCalculation("hi", 5))
+ }
+ }
+
+ @Test
+ fun `simple observable`() {
+ rpcDriver {
+ val proxy = testProxy()
+ // This tests that the observations are transmitted correctly, also completion is transmitted.
+ val observations = proxy.makeObservable().toBlocking().toIterable().toList()
+ assertEquals(listOf(1, 2, 3, 4), observations)
+ }
+ }
+
+ @Test
+ fun `complex observables`() {
+ rpcDriver {
+ val proxy = testProxy()
+ // This checks that we can return an object graph with complex usage of observables, like an observable
+ // that emits objects that contain more observables.
+ val serverQuotes = PublishSubject.create>>()
+ val unsubscribeLatch = CountDownLatch(1)
+ complicatedObservable = serverQuotes.asObservable().doOnUnsubscribe { unsubscribeLatch.countDown() }
+
+ val twainQuotes = "Mark Twain" to Observable.just(
+ "I have never let my schooling interfere with my education.",
+ "Clothes make the man. Naked people have little or no influence on society."
+ )
+ val wildeQuotes = "Oscar Wilde" to Observable.just(
+ "I can resist everything except temptation.",
+ "Always forgive your enemies - nothing annoys them so much."
+ )
+
+ val clientQuotes = LinkedBlockingQueue()
+ val clientObs = proxy.makeComplicatedObservable()
+
+ val subscription = clientObs.subscribe {
+ val name = it.first
+ it.second.subscribe {
+ clientQuotes += "Quote by $name: $it"
+ }
+ }
+
+ assertThat(clientQuotes).isEmpty()
+
+ serverQuotes.onNext(twainQuotes)
+ assertEquals("Quote by Mark Twain: I have never let my schooling interfere with my education.", clientQuotes.take())
+ assertEquals("Quote by Mark Twain: Clothes make the man. Naked people have little or no influence on society.", clientQuotes.take())
+
+ serverQuotes.onNext(wildeQuotes)
+ assertEquals("Quote by Oscar Wilde: I can resist everything except temptation.", clientQuotes.take())
+ assertEquals("Quote by Oscar Wilde: Always forgive your enemies - nothing annoys them so much.", clientQuotes.take())
+
+ assertTrue(serverQuotes.hasObservers())
+ subscription.unsubscribe()
+ unsubscribeLatch.await()
+ }
+ }
+
+ @Test
+ fun `simple ListenableFuture`() {
+ rpcDriver {
+ val proxy = testProxy()
+ val value = proxy.makeListenableFuture().getOrThrow()
+ assertThat(value).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun `complex ListenableFuture`() {
+ rpcDriver {
+ val proxy = testProxy()
+ val serverQuote = SettableFuture.create>>()
+ complicatedListenableFuturee = serverQuote
+
+ val twainQuote = "Mark Twain" to Futures.immediateFuture("I have never let my schooling interfere with my education.")
+
+ val clientQuotes = LinkedBlockingQueue()
+ val clientFuture = proxy.makeComplicatedListenableFuture()
+
+ clientFuture.success {
+ val name = it.first
+ it.second.success {
+ clientQuotes += "Quote by $name: $it"
+ }
+ }
+
+ assertThat(clientQuotes).isEmpty()
+
+ serverQuote.set(twainQuote)
+ assertThat(clientQuotes.take()).isEqualTo("Quote by Mark Twain: I have never let my schooling interfere with my education.")
+
+ // TODO This final assert sometimes fails because the relevant queue hasn't been removed yet
+ }
+ }
+
+ @Test
+ fun versioning() {
+ rpcDriver {
+ val proxy = testProxy()
+ assertFailsWith { proxy.addedLater() }
+ }
+ }
+
+ @Test
+ fun `authenticated user is available to RPC`() {
+ rpcDriver {
+ val proxy = testProxy()
+ assertThat(proxy.captureUser()).isEqualTo(rpcTestUser.username)
+ }
+ }
+}
diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt
new file mode 100644
index 0000000000..2ffe065832
--- /dev/null
+++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt
@@ -0,0 +1,182 @@
+package net.corda.client.rpc
+
+import net.corda.client.rpc.internal.RPCClientConfiguration
+import net.corda.core.future
+import net.corda.core.messaging.RPCOps
+import net.corda.core.millis
+import net.corda.core.random63BitValue
+import net.corda.core.serialization.CordaSerializable
+import net.corda.node.services.messaging.RPCServerConfiguration
+import net.corda.testing.RPCDriverExposedDSLInterface
+import net.corda.testing.rpcDriver
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import rx.Observable
+import rx.subjects.UnicastSubject
+import java.util.*
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.CountDownLatch
+
+@RunWith(Parameterized::class)
+class RPCConcurrencyTests : AbstractRPCTest() {
+
+ /**
+ * Holds a "rose"-tree of [Observable]s which allows us to test arbitrary [Observable] nesting in RPC replies.
+ */
+ @CordaSerializable
+ data class ObservableRose(val value: A, val branches: Observable>)
+
+ private interface TestOps : RPCOps {
+ fun newLatch(numberOfDowns: Int): Long
+ fun waitLatch(id: Long)
+ fun downLatch(id: Long)
+ fun getImmediateObservableTree(depth: Int, branchingFactor: Int): ObservableRose
+ fun getParallelObservableTree(depth: Int, branchingFactor: Int): ObservableRose
+ }
+
+ class TestOpsImpl : TestOps {
+ private val latches = ConcurrentHashMap()
+ override val protocolVersion = 0
+
+ override fun newLatch(numberOfDowns: Int): Long {
+ val id = random63BitValue()
+ val latch = CountDownLatch(numberOfDowns)
+ latches.put(id, latch)
+ return id
+ }
+
+ override fun waitLatch(id: Long) {
+ latches[id]!!.await()
+ }
+
+ override fun downLatch(id: Long) {
+ latches[id]!!.countDown()
+ }
+
+ override fun getImmediateObservableTree(depth: Int, branchingFactor: Int): ObservableRose {
+ val branches = if (depth == 0) {
+ Observable.empty>()
+ } else {
+ Observable.just(getImmediateObservableTree(depth - 1, branchingFactor)).repeat(branchingFactor.toLong())
+ }
+ return ObservableRose(depth, branches)
+ }
+
+ override fun getParallelObservableTree(depth: Int, branchingFactor: Int): ObservableRose {
+ val branches = if (depth == 0) {
+ Observable.empty>()
+ } else {
+ val publish = UnicastSubject.create>()
+ future {
+ (1..branchingFactor).toList().parallelStream().forEach {
+ publish.onNext(getParallelObservableTree(depth - 1, branchingFactor))
+ }
+ publish.onCompleted()
+ }
+ publish
+ }
+ return ObservableRose(depth, branches)
+ }
+ }
+
+ private lateinit var testOpsImpl: TestOpsImpl
+ private fun RPCDriverExposedDSLInterface.testProxy(): TestProxy {
+ testOpsImpl = TestOpsImpl()
+ return testProxy(
+ testOpsImpl,
+ clientConfiguration = RPCClientConfiguration.default.copy(
+ reapInterval = 100.millis,
+ cacheConcurrencyLevel = 16
+ ),
+ serverConfiguration = RPCServerConfiguration.default.copy(
+ rpcThreadPoolSize = 4
+ )
+ )
+ }
+
+ @Test
+ fun `call multiple RPCs in parallel`() {
+ rpcDriver {
+ val proxy = testProxy()
+ val numberOfBlockedCalls = 2
+ val numberOfDownsRequired = 100
+ val id = proxy.ops.newLatch(numberOfDownsRequired)
+ val done = CountDownLatch(numberOfBlockedCalls)
+ // Start a couple of blocking RPC calls
+ (1..numberOfBlockedCalls).forEach {
+ future {
+ proxy.ops.waitLatch(id)
+ done.countDown()
+ }
+ }
+ // Down the latch that the others are waiting for concurrently
+ (1..numberOfDownsRequired).toList().parallelStream().forEach {
+ proxy.ops.downLatch(id)
+ }
+ done.await()
+ }
+ }
+
+ private fun intPower(base: Int, power: Int): Int {
+ return when (power) {
+ 0 -> 1
+ 1 -> base
+ else -> {
+ val a = intPower(base, power / 2)
+ if (power and 1 == 0) {
+ a * a
+ } else {
+ a * a * base
+ }
+ }
+ }
+ }
+
+ @Test
+ fun `nested immediate observables sequence correctly`() {
+ rpcDriver {
+ // We construct a rose tree of immediate Observables and check that parent observations arrive before children.
+ val proxy = testProxy()
+ val treeDepth = 6
+ val treeBranchingFactor = 3
+ val remainingLatch = CountDownLatch((intPower(treeBranchingFactor, treeDepth + 1) - 1) / (treeBranchingFactor - 1))
+ val depthsSeen = Collections.synchronizedSet(HashSet())
+ fun ObservableRose.subscribeToAll() {
+ remainingLatch.countDown()
+ this.branches.subscribe { tree ->
+ (tree.value + 1..treeDepth - 1).forEach {
+ require(it in depthsSeen) { "Got ${tree.value} before $it" }
+ }
+ depthsSeen.add(tree.value)
+ tree.subscribeToAll()
+ }
+ }
+ proxy.ops.getImmediateObservableTree(treeDepth, treeBranchingFactor).subscribeToAll()
+ remainingLatch.await()
+ }
+ }
+
+ @Test
+ fun `parallel nested observables`() {
+ rpcDriver {
+ val proxy = testProxy()
+ val treeDepth = 2
+ val treeBranchingFactor = 10
+ val remainingLatch = CountDownLatch((intPower(treeBranchingFactor, treeDepth + 1) - 1) / (treeBranchingFactor - 1))
+ val depthsSeen = Collections.synchronizedSet(HashSet())
+ fun ObservableRose.subscribeToAll() {
+ remainingLatch.countDown()
+ branches.subscribe { tree ->
+ (tree.value + 1..treeDepth - 1).forEach {
+ require(it in depthsSeen) { "Got ${tree.value} before $it" }
+ }
+ depthsSeen.add(tree.value)
+ tree.subscribeToAll()
+ }
+ }
+ proxy.ops.getParallelObservableTree(treeDepth, treeBranchingFactor).subscribeToAll()
+ remainingLatch.await()
+ }
+ }
+}
\ No newline at end of file
diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt
new file mode 100644
index 0000000000..8d1fdcb65b
--- /dev/null
+++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt
@@ -0,0 +1,315 @@
+package net.corda.client.rpc
+
+import com.codahale.metrics.ConsoleReporter
+import com.codahale.metrics.Gauge
+import com.codahale.metrics.JmxReporter
+import com.codahale.metrics.MetricRegistry
+import com.google.common.base.Stopwatch
+import net.corda.client.rpc.internal.RPCClientConfiguration
+import net.corda.core.messaging.RPCOps
+import net.corda.core.minutes
+import net.corda.core.seconds
+import net.corda.core.utilities.Rate
+import net.corda.core.utilities.div
+import net.corda.node.driver.ShutdownManager
+import net.corda.node.services.messaging.RPCServerConfiguration
+import net.corda.testing.RPCDriverExposedDSLInterface
+import net.corda.testing.measure
+import net.corda.testing.rpcDriver
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.time.Duration
+import java.util.*
+import java.util.concurrent.*
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicInteger
+import java.util.concurrent.locks.ReentrantLock
+import javax.management.ObjectName
+import kotlin.concurrent.thread
+import kotlin.concurrent.withLock
+
+@Ignore("Only use this locally for profiling")
+@RunWith(Parameterized::class)
+class RPCPerformanceTests : AbstractRPCTest() {
+ companion object {
+ @JvmStatic @Parameterized.Parameters(name = "Mode = {0}")
+ fun modes() = modes(RPCTestMode.Netty)
+ }
+ private interface TestOps : RPCOps {
+ fun simpleReply(input: ByteArray, sizeOfReply: Int): ByteArray
+ }
+
+ class TestOpsImpl : TestOps {
+ override val protocolVersion = 0
+ override fun simpleReply(input: ByteArray, sizeOfReply: Int): ByteArray {
+ return ByteArray(sizeOfReply)
+ }
+ }
+
+ private fun RPCDriverExposedDSLInterface.testProxy(
+ clientConfiguration: RPCClientConfiguration,
+ serverConfiguration: RPCServerConfiguration
+ ): TestProxy {
+ return testProxy(
+ TestOpsImpl(),
+ clientConfiguration = clientConfiguration,
+ serverConfiguration = serverConfiguration
+ )
+ }
+
+ private fun warmup() {
+ rpcDriver {
+ val proxy = testProxy(
+ RPCClientConfiguration.default,
+ RPCServerConfiguration.default
+ )
+ val executor = Executors.newFixedThreadPool(4)
+ val N = 10000
+ val latch = CountDownLatch(N)
+ for (i in 1 .. N) {
+ executor.submit {
+ proxy.ops.simpleReply(ByteArray(1024), 1024)
+ latch.countDown()
+ }
+ }
+ latch.await()
+ }
+ }
+
+ data class SimpleRPCResult(
+ val requestPerSecond: Double,
+ val averageIndividualMs: Double,
+ val Mbps: Double
+ )
+ @Test
+ fun `measure Megabytes per second for simple RPCs`() {
+ warmup()
+ val inputOutputSizes = listOf(1024, 4096, 100 * 1024)
+ val overallTraffic = 512 * 1024 * 1024L
+ measure(inputOutputSizes, (1..5)) { inputOutputSize, N ->
+ rpcDriver {
+ val proxy = testProxy(
+ RPCClientConfiguration.default.copy(
+ cacheConcurrencyLevel = 16,
+ observationExecutorPoolSize = 2,
+ producerPoolBound = 2
+ ),
+ RPCServerConfiguration.default.copy(
+ rpcThreadPoolSize = 8,
+ consumerPoolSize = 2,
+ producerPoolBound = 8
+ )
+ )
+
+ val numberOfRequests = overallTraffic / (2 * inputOutputSize)
+ val timings = Collections.synchronizedList(ArrayList())
+ val executor = Executors.newFixedThreadPool(8)
+ val totalElapsed = Stopwatch.createStarted().apply {
+ startInjectorWithBoundedQueue(
+ executor = executor,
+ numberOfInjections = numberOfRequests.toInt(),
+ queueBound = 100
+ ) {
+ val elapsed = Stopwatch.createStarted().apply {
+ proxy.ops.simpleReply(ByteArray(inputOutputSize), inputOutputSize)
+ }.stop().elapsed(TimeUnit.MICROSECONDS)
+ timings.add(elapsed)
+ }
+ }.stop().elapsed(TimeUnit.MICROSECONDS)
+ executor.shutdownNow()
+ SimpleRPCResult(
+ requestPerSecond = 1000000.0 * numberOfRequests.toDouble() / totalElapsed.toDouble(),
+ averageIndividualMs = timings.average() / 1000.0,
+ Mbps = (overallTraffic.toDouble() / totalElapsed.toDouble()) * (1000000.0 / (1024.0 * 1024.0))
+ )
+ }
+ }.forEach(::println)
+ }
+
+ /**
+ * Runs 20k RPCs per second for two minutes and publishes relevant stats to JMX.
+ */
+ @Test
+ fun `consumption rate`() {
+ rpcDriver {
+ val metricRegistry = startReporter()
+ val proxy = testProxy(
+ RPCClientConfiguration.default.copy(
+ reapInterval = 1.seconds,
+ cacheConcurrencyLevel = 16,
+ producerPoolBound = 8
+ ),
+ RPCServerConfiguration.default.copy(
+ rpcThreadPoolSize = 8,
+ consumerPoolSize = 1,
+ producerPoolBound = 8
+ )
+ )
+ measurePerformancePublishMetrics(
+ metricRegistry = metricRegistry,
+ parallelism = 8,
+ overallDuration = 5.minutes,
+ injectionRate = 20000L / TimeUnit.SECONDS,
+ queueSizeMetricName = "$mode.QueueSize",
+ workDurationMetricName = "$mode.WorkDuration",
+ shutdownManager = this.shutdownManager,
+ work = {
+ proxy.ops.simpleReply(ByteArray(4096), 4096)
+ }
+ )
+ }
+ }
+
+ data class BigMessagesResult(
+ val Mbps: Double
+ )
+ @Test
+ fun `big messages`() {
+ warmup()
+ measure(listOf(1)) { clientParallelism -> // TODO this hangs with more parallelism
+ rpcDriver {
+ val proxy = testProxy(
+ RPCClientConfiguration.default,
+ RPCServerConfiguration.default.copy(
+ consumerPoolSize = 1
+ )
+ )
+ val executor = Executors.newFixedThreadPool(clientParallelism)
+ val numberOfMessages = 1000
+ val bigSize = 10_000_000
+ val elapsed = Stopwatch.createStarted().apply {
+ startInjectorWithBoundedQueue(
+ executor = executor,
+ numberOfInjections = numberOfMessages,
+ queueBound = 4
+ ) {
+ proxy.ops.simpleReply(ByteArray(bigSize), 0)
+ }
+ }.stop().elapsed(TimeUnit.MICROSECONDS)
+ executor.shutdownNow()
+ BigMessagesResult(
+ Mbps = bigSize.toDouble() * numberOfMessages.toDouble() / elapsed * (1000000.0 / (1024.0 * 1024.0))
+ )
+ }
+ }.forEach(::println)
+ }
+}
+
+fun measurePerformancePublishMetrics(
+ metricRegistry: MetricRegistry,
+ parallelism: Int,
+ overallDuration: Duration,
+ injectionRate: Rate,
+ queueSizeMetricName: String,
+ workDurationMetricName: String,
+ shutdownManager: ShutdownManager,
+ work: () -> Unit
+) {
+ val workSemaphore = Semaphore(0)
+ metricRegistry.register(queueSizeMetricName, Gauge { workSemaphore.availablePermits() })
+ val workDurationTimer = metricRegistry.timer(workDurationMetricName)
+ val executor = Executors.newSingleThreadScheduledExecutor()
+ val workExecutor = Executors.newFixedThreadPool(parallelism)
+ val timings = Collections.synchronizedList(ArrayList())
+ for (i in 1 .. parallelism) {
+ workExecutor.submit {
+ try {
+ while (true) {
+ workSemaphore.acquire()
+ workDurationTimer.time {
+ timings.add(
+ Stopwatch.createStarted().apply {
+ work()
+ }.stop().elapsed(TimeUnit.MICROSECONDS)
+ )
+ }
+ }
+ } catch (throwable: Throwable) {
+ throwable.printStackTrace()
+ }
+ }
+ }
+ val injector = executor.scheduleAtFixedRate(
+ {
+ workSemaphore.release((injectionRate * TimeUnit.SECONDS).toInt())
+ },
+ 0,
+ 1,
+ TimeUnit.SECONDS
+ )
+ shutdownManager.registerShutdown {
+ injector.cancel(true)
+ workExecutor.shutdownNow()
+ executor.shutdownNow()
+ workExecutor.awaitTermination(1, TimeUnit.SECONDS)
+ executor.awaitTermination(1, TimeUnit.SECONDS)
+ }
+ Thread.sleep(overallDuration.toMillis())
+}
+
+fun startInjectorWithBoundedQueue(
+ executor: ExecutorService,
+ numberOfInjections: Int,
+ queueBound: Int,
+ work: () -> Unit
+) {
+ val remainingLatch = CountDownLatch(numberOfInjections)
+ val queuedCount = AtomicInteger(0)
+ val lock = ReentrantLock()
+ val canQueueAgain = lock.newCondition()
+ val injectorShutdown = AtomicBoolean(false)
+ val injector = thread(name = "injector") {
+ while (true) {
+ if (injectorShutdown.get()) break
+ executor.submit {
+ work()
+ if (queuedCount.decrementAndGet() < queueBound / 2) {
+ lock.withLock {
+ canQueueAgain.signal()
+ }
+ }
+ remainingLatch.countDown()
+ }
+ if (queuedCount.incrementAndGet() > queueBound) {
+ lock.withLock {
+ canQueueAgain.await()
+ }
+ }
+ }
+ }
+ remainingLatch.await()
+ injectorShutdown.set(true)
+ injector.join()
+}
+
+fun RPCDriverExposedDSLInterface.startReporter(): MetricRegistry {
+ val metricRegistry = MetricRegistry()
+ val jmxReporter = thread {
+ JmxReporter.
+ forRegistry(metricRegistry).
+ inDomain("net.corda").
+ createsObjectNamesWith { _, domain, name ->
+ // Make the JMX hierarchy a bit better organised.
+ val category = name.substringBefore('.')
+ val subName = name.substringAfter('.', "")
+ if (subName == "")
+ ObjectName("$domain:name=$category")
+ else
+ ObjectName("$domain:type=$category,name=$subName")
+ }.
+ build().
+ start()
+ }
+ val consoleReporter = thread {
+ ConsoleReporter.forRegistry(metricRegistry).build().start(1, TimeUnit.SECONDS)
+ }
+ shutdownManager.registerShutdown {
+ jmxReporter.interrupt()
+ consoleReporter.interrupt()
+ jmxReporter.join()
+ consoleReporter.join()
+ }
+ return metricRegistry
+}
diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt
new file mode 100644
index 0000000000..ebc9cef461
--- /dev/null
+++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt
@@ -0,0 +1,93 @@
+package net.corda.client.rpc
+
+import net.corda.core.messaging.RPCOps
+import net.corda.node.services.messaging.requirePermission
+import net.corda.node.services.messaging.getRpcContext
+import net.corda.nodeapi.PermissionException
+import net.corda.nodeapi.User
+import net.corda.testing.RPCDriverExposedDSLInterface
+import net.corda.testing.rpcDriver
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import kotlin.test.assertFailsWith
+
+@RunWith(Parameterized::class)
+class RPCPermissionsTests : AbstractRPCTest() {
+ companion object {
+ const val DUMMY_FLOW = "StartFlow.net.corda.flows.DummyFlow"
+ const val OTHER_FLOW = "StartFlow.net.corda.flows.OtherFlow"
+ const val ALL_ALLOWED = "ALL"
+ }
+
+ /*
+ * RPC operation.
+ */
+ interface TestOps : RPCOps {
+ fun validatePermission(str: String)
+ }
+
+ class TestOpsImpl : TestOps {
+ override val protocolVersion = 1
+ override fun validatePermission(str: String) = getRpcContext().requirePermission(str)
+ }
+
+ /**
+ * Create an RPC proxy for the given user.
+ */
+ private fun RPCDriverExposedDSLInterface.testProxyFor(rpcUser: User) = testProxy(TestOpsImpl(), rpcUser).ops
+
+ private fun userOf(name: String, permissions: Set) = User(name, "password", permissions)
+
+ @Test
+ fun `empty user cannot use any flows`() {
+ rpcDriver {
+ val emptyUser = userOf("empty", emptySet())
+ val proxy = testProxyFor(emptyUser)
+ assertFailsWith(PermissionException::class,
+ "User ${emptyUser.username} should not be allowed to use $DUMMY_FLOW.",
+ { proxy.validatePermission(DUMMY_FLOW) })
+ }
+ }
+
+ @Test
+ fun `admin user can use any flow`() {
+ rpcDriver {
+ val adminUser = userOf("admin", setOf(ALL_ALLOWED))
+ val proxy = testProxyFor(adminUser)
+ proxy.validatePermission(DUMMY_FLOW)
+ }
+ }
+
+ @Test
+ fun `joe user is allowed to use DummyFlow`() {
+ rpcDriver {
+ val joeUser = userOf("joe", setOf(DUMMY_FLOW))
+ val proxy = testProxyFor(joeUser)
+ proxy.validatePermission(DUMMY_FLOW)
+ }
+ }
+
+ @Test
+ fun `joe user is not allowed to use OtherFlow`() {
+ rpcDriver {
+ val joeUser = userOf("joe", setOf(DUMMY_FLOW))
+ val proxy = testProxyFor(joeUser)
+ assertFailsWith(PermissionException::class,
+ "User ${joeUser.username} should not be allowed to use $OTHER_FLOW",
+ { proxy.validatePermission(OTHER_FLOW) })
+ }
+ }
+
+ @Test
+ fun `check ALL is implemented the correct way round` () {
+ rpcDriver {
+ val joeUser = userOf("joe", setOf(DUMMY_FLOW))
+ val proxy = testProxyFor(joeUser)
+ assertFailsWith(PermissionException::class,
+ "Permission $ALL_ALLOWED should not do anything for User ${joeUser.username}",
+ { proxy.validatePermission(ALL_ALLOWED) })
+ }
+ }
+
+}
diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RepeatingBytesInputStream.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RepeatingBytesInputStream.kt
new file mode 100644
index 0000000000..06ed23f1bb
--- /dev/null
+++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RepeatingBytesInputStream.kt
@@ -0,0 +1,25 @@
+package net.corda.client.rpc
+
+import java.io.InputStream
+
+class RepeatingBytesInputStream(val bytesToRepeat: ByteArray, val numberOfBytes: Int) : InputStream() {
+ private var bytesLeft = numberOfBytes
+ override fun available() = bytesLeft
+ override fun read(): Int {
+ if (bytesLeft == 0) {
+ return -1
+ } else {
+ bytesLeft--
+ return bytesToRepeat[(numberOfBytes - bytesLeft) % bytesToRepeat.size].toInt()
+ }
+ }
+ override fun read(byteArray: ByteArray, offset: Int, length: Int): Int {
+ val until = Math.min(Math.min(offset + length, byteArray.size), offset + bytesLeft)
+ for (i in offset .. until - 1) {
+ byteArray[i] = bytesToRepeat[(numberOfBytes - bytesLeft + i - offset) % bytesToRepeat.size]
+ }
+ val bytesRead = until - offset
+ bytesLeft -= bytesRead
+ return if (bytesRead == 0 && bytesLeft == 0) -1 else bytesRead
+ }
+}
\ No newline at end of file
diff --git a/client/rpc/src/test/net/corda/client/rpc/AbstractClientRPC.kt b/client/rpc/src/test/net/corda/client/rpc/AbstractClientRPC.kt
deleted file mode 100644
index 7431ce3e1f..0000000000
--- a/client/rpc/src/test/net/corda/client/rpc/AbstractClientRPC.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-package net.corda.client.rpc
-
-import net.corda.core.messaging.RPCOps
-import net.corda.core.serialization.SerializedBytes
-import net.corda.core.utilities.LogHelper
-import net.corda.node.services.RPCUserService
-import net.corda.node.services.User
-import net.corda.node.services.messaging.ArtemisMessagingComponent
-import net.corda.node.services.messaging.RPCDispatcher
-import net.corda.node.utilities.AffinityExecutor
-import org.apache.activemq.artemis.api.core.Message
-import org.apache.activemq.artemis.api.core.SimpleString
-import org.apache.activemq.artemis.api.core.TransportConfiguration
-import org.apache.activemq.artemis.api.core.client.ActiveMQClient
-import org.apache.activemq.artemis.api.core.client.ClientMessage
-import org.apache.activemq.artemis.api.core.client.ClientProducer
-import org.apache.activemq.artemis.api.core.client.ClientSession
-import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl
-import org.apache.activemq.artemis.core.remoting.impl.invm.InVMAcceptorFactory
-import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnectorFactory
-import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ
-import org.junit.After
-import org.junit.Before
-import java.util.*
-import java.util.concurrent.locks.ReentrantLock
-
-abstract class AbstractClientRPC {
-
- lateinit var artemis: EmbeddedActiveMQ
- lateinit var serverSession: ClientSession
- lateinit var clientSession: ClientSession
- lateinit var producer: ClientProducer
- lateinit var serverThread: AffinityExecutor.ServiceAffinityExecutor
-
- @Before
- fun rpcSetup() {
- // Set up an in-memory Artemis with an RPC requests queue.
- artemis = EmbeddedActiveMQ()
- artemis.setConfiguration(ConfigurationImpl().apply {
- acceptorConfigurations = setOf(TransportConfiguration(InVMAcceptorFactory::class.java.name))
- isSecurityEnabled = false
- isPersistenceEnabled = false
- })
- artemis.start()
-
- val serverLocator = ActiveMQClient.createServerLocatorWithoutHA(TransportConfiguration(InVMConnectorFactory::class.java.name))
- val sessionFactory = serverLocator.createSessionFactory()
- serverSession = sessionFactory.createSession()
- serverSession.start()
-
- serverSession.createTemporaryQueue(ArtemisMessagingComponent.RPC_REQUESTS_QUEUE, ArtemisMessagingComponent.RPC_REQUESTS_QUEUE)
- producer = serverSession.createProducer()
- serverThread = AffinityExecutor.ServiceAffinityExecutor("unit-tests-rpc-dispatch-thread", 1)
- serverSession.createTemporaryQueue("activemq.notifications", "rpc.qremovals", "_AMQ_NotifType = 'BINDING_REMOVED'")
-
- clientSession = sessionFactory.createSession()
- clientSession.start()
-
- LogHelper.setLevel("+net.corda.rpc")
- }
-
- @After
- fun rpcShutdown() {
- safeClose(producer)
- clientSession.stop()
- serverSession.stop()
- artemis.stop()
- serverThread.shutdownNow()
- }
-
- fun rpcProxyFor(rpcUser: User, rpcImpl: T, type: Class): T {
- val userService = object : RPCUserService {
- override fun getUser(username: String): User? = if (username == rpcUser.username) rpcUser else null
- override val users: List get() = listOf(rpcUser)
- }
-
- val dispatcher = object : RPCDispatcher(rpcImpl, userService, "SomeName") {
- override fun send(data: SerializedBytes<*>, toAddress: String) {
- val msg = serverSession.createMessage(false).apply {
- writeBodyBufferBytes(data.bytes)
- // Use the magic deduplication property built into Artemis as our message identity too
- putStringProperty(Message.HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
- }
- producer.send(toAddress, msg)
- }
-
- override fun getUser(message: ClientMessage): User = rpcUser
- }
-
- val serverNotifConsumer = serverSession.createConsumer("rpc.qremovals")
- val serverConsumer = serverSession.createConsumer(ArtemisMessagingComponent.RPC_REQUESTS_QUEUE)
- dispatcher.start(serverConsumer, serverNotifConsumer, serverThread)
- return CordaRPCClientImpl(clientSession, ReentrantLock(), rpcUser.username).proxyFor(type)
- }
-
- fun safeClose(obj: Any) = try { (obj as AutoCloseable).close() } catch (e: Exception) {}
-}
\ No newline at end of file
diff --git a/client/rpc/src/test/net/corda/client/rpc/ClientRPCInfrastructureTests.kt b/client/rpc/src/test/net/corda/client/rpc/ClientRPCInfrastructureTests.kt
deleted file mode 100644
index 9eb28a474d..0000000000
--- a/client/rpc/src/test/net/corda/client/rpc/ClientRPCInfrastructureTests.kt
+++ /dev/null
@@ -1,194 +0,0 @@
-package net.corda.client.rpc
-
-import com.google.common.util.concurrent.Futures
-import com.google.common.util.concurrent.ListenableFuture
-import com.google.common.util.concurrent.SettableFuture
-import net.corda.core.getOrThrow
-import net.corda.core.messaging.RPCOps
-import net.corda.core.messaging.RPCReturnsObservables
-import net.corda.core.success
-import net.corda.node.services.User
-import net.corda.node.services.messaging.CURRENT_RPC_USER
-import net.corda.node.services.messaging.RPCSinceVersion
-import org.apache.activemq.artemis.api.core.SimpleString
-import org.assertj.core.api.Assertions.assertThat
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import rx.Observable
-import rx.subjects.PublishSubject
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.LinkedBlockingQueue
-import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
-import kotlin.test.assertTrue
-
-class ClientRPCInfrastructureTests : AbstractClientRPC() {
- // TODO: Test that timeouts work
-
- lateinit var proxy: TestOps
-
- private val authenticatedUser = User("test", "password", permissions = setOf())
-
- @Before
- fun setup() {
- proxy = rpcProxyFor(authenticatedUser, TestOpsImpl(), TestOps::class.java)
- }
-
- @After
- fun shutdown() {
- safeClose(proxy)
- }
-
- interface TestOps : RPCOps {
- @Throws(IllegalArgumentException::class)
- fun barf()
-
- fun void()
-
- fun someCalculation(str: String, num: Int): String
-
- @RPCReturnsObservables
- fun makeObservable(): Observable
-
- @RPCReturnsObservables
- fun makeComplicatedObservable(): Observable>>
-
- @RPCReturnsObservables
- fun makeListenableFuture(): ListenableFuture
-
- @RPCReturnsObservables
- fun makeComplicatedListenableFuture(): ListenableFuture>>
-
- @RPCSinceVersion(2)
- fun addedLater()
-
- fun captureUser(): String
- }
-
- private lateinit var complicatedObservable: Observable>>
- private lateinit var complicatedListenableFuturee: ListenableFuture>>
-
- inner class TestOpsImpl : TestOps {
- override val protocolVersion = 1
- override fun barf(): Unit = throw IllegalArgumentException("Barf!")
- override fun void() {}
- override fun someCalculation(str: String, num: Int) = "$str $num"
- override fun makeObservable(): Observable = Observable.just(1, 2, 3, 4)
- override fun makeListenableFuture(): ListenableFuture = Futures.immediateFuture(1)
- override fun makeComplicatedObservable() = complicatedObservable
- override fun makeComplicatedListenableFuture(): ListenableFuture>> = complicatedListenableFuturee
- override fun addedLater(): Unit = throw UnsupportedOperationException("not implemented")
- override fun captureUser(): String = CURRENT_RPC_USER.get().username
- }
-
- @Test
- fun `simple RPCs`() {
- // Does nothing, doesn't throw.
- proxy.void()
-
- assertEquals("Barf!", assertFailsWith {
- proxy.barf()
- }.message)
-
- assertEquals("hi 5", proxy.someCalculation("hi", 5))
- }
-
- @Test
- fun `simple observable`() {
- // This tests that the observations are transmitted correctly, also completion is transmitted.
- val observations = proxy.makeObservable().toBlocking().toIterable().toList()
- assertEquals(listOf(1, 2, 3, 4), observations)
- }
-
- @Test
- fun `complex observables`() {
- // This checks that we can return an object graph with complex usage of observables, like an observable
- // that emits objects that contain more observables.
- val serverQuotes = PublishSubject.create>>()
- val unsubscribeLatch = CountDownLatch(1)
- complicatedObservable = serverQuotes.asObservable().doOnUnsubscribe { unsubscribeLatch.countDown() }
-
- val twainQuotes = "Mark Twain" to Observable.just(
- "I have never let my schooling interfere with my education.",
- "Clothes make the man. Naked people have little or no influence on society."
- )
- val wildeQuotes = "Oscar Wilde" to Observable.just(
- "I can resist everything except temptation.",
- "Always forgive your enemies - nothing annoys them so much."
- )
-
- val clientQuotes = LinkedBlockingQueue()
- val clientObs = proxy.makeComplicatedObservable()
-
- val subscription = clientObs.subscribe {
- val name = it.first
- it.second.subscribe {
- clientQuotes += "Quote by $name: $it"
- }
- }
-
- val rpcQueuesQuery = SimpleString("clients.${authenticatedUser.username}.rpc.*")
- assertEquals(2, clientSession.addressQuery(rpcQueuesQuery).queueNames.size)
-
- assertThat(clientQuotes).isEmpty()
-
- serverQuotes.onNext(twainQuotes)
- assertEquals("Quote by Mark Twain: I have never let my schooling interfere with my education.", clientQuotes.take())
- assertEquals("Quote by Mark Twain: Clothes make the man. Naked people have little or no influence on society.", clientQuotes.take())
-
- serverQuotes.onNext(wildeQuotes)
- assertEquals("Quote by Oscar Wilde: I can resist everything except temptation.", clientQuotes.take())
- assertEquals("Quote by Oscar Wilde: Always forgive your enemies - nothing annoys them so much.", clientQuotes.take())
-
- assertTrue(serverQuotes.hasObservers())
- subscription.unsubscribe()
- unsubscribeLatch.await()
- assertEquals(1, clientSession.addressQuery(rpcQueuesQuery).queueNames.size)
- }
-
- @Test
- fun `simple ListenableFuture`() {
- val value = proxy.makeListenableFuture().getOrThrow()
- assertThat(value).isEqualTo(1)
- }
-
- @Test
- fun `complex ListenableFuture`() {
- val serverQuote = SettableFuture.create>>()
- complicatedListenableFuturee = serverQuote
-
- val twainQuote = "Mark Twain" to Futures.immediateFuture("I have never let my schooling interfere with my education.")
-
- val clientQuotes = LinkedBlockingQueue()
- val clientFuture = proxy.makeComplicatedListenableFuture()
-
- clientFuture.success {
- val name = it.first
- it.second.success {
- clientQuotes += "Quote by $name: $it"
- }
- }
-
- val rpcQueuesQuery = SimpleString("clients.${authenticatedUser.username}.rpc.*")
- assertEquals(2, clientSession.addressQuery(rpcQueuesQuery).queueNames.size)
-
- assertThat(clientQuotes).isEmpty()
-
- serverQuote.set(twainQuote)
- assertThat(clientQuotes.take()).isEqualTo("Quote by Mark Twain: I have never let my schooling interfere with my education.")
-
- // TODO This final assert sometimes fails because the relevant queue hasn't been removed yet
-// assertEquals(1, clientSession.addressQuery(rpcQueuesQuery).queueNames.size)
- }
-
- @Test
- fun versioning() {
- assertFailsWith { proxy.addedLater() }
- }
-
- @Test
- fun `authenticated user is available to RPC`() {
- assertThat(proxy.captureUser()).isEqualTo(authenticatedUser.username)
- }
-}
diff --git a/client/rpc/src/test/net/corda/client/rpc/RPCPermissionsTest.kt b/client/rpc/src/test/net/corda/client/rpc/RPCPermissionsTest.kt
deleted file mode 100644
index 2729ae8f86..0000000000
--- a/client/rpc/src/test/net/corda/client/rpc/RPCPermissionsTest.kt
+++ /dev/null
@@ -1,84 +0,0 @@
-package net.corda.client.rpc
-
-import net.corda.core.messaging.RPCOps
-import net.corda.node.services.User
-import net.corda.node.services.messaging.*
-import org.junit.After
-import org.junit.Test
-import kotlin.test.*
-
-class RPCPermissionsTest : AbstractClientRPC() {
- companion object {
- const val DUMMY_FLOW = "StartFlow.net.corda.flows.DummyFlow"
- const val OTHER_FLOW = "StartFlow.net.corda.flows.OtherFlow"
- const val ALL_ALLOWED = "ALL"
- }
-
- lateinit var proxy: TestOps
-
- @After
- fun shutdown() {
- safeClose(proxy)
- }
-
- /*
- * RPC operation.
- */
- interface TestOps : RPCOps {
- fun validatePermission(str: String)
- }
-
- class TestOpsImpl : TestOps {
- override val protocolVersion = 1
- override fun validatePermission(str: String) = requirePermission(str)
- }
-
- /**
- * Create an RPC proxy for the given user.
- */
- private fun proxyFor(rpcUser: User): TestOps = rpcProxyFor(rpcUser, TestOpsImpl(), TestOps::class.java)
-
- private fun userOf(name: String, permissions: Set) = User(name, "password", permissions)
-
- @Test
- fun `empty user cannot use any flows`() {
- val emptyUser = userOf("empty", emptySet())
- proxy = proxyFor(emptyUser)
- assertFailsWith(PermissionException::class,
- "User ${emptyUser.username} should not be allowed to use $DUMMY_FLOW.",
- { proxy.validatePermission(DUMMY_FLOW) })
- }
-
- @Test
- fun `admin user can use any flow`() {
- val adminUser = userOf("admin", setOf(ALL_ALLOWED))
- proxy = proxyFor(adminUser)
- proxy.validatePermission(DUMMY_FLOW)
- }
-
- @Test
- fun `joe user is allowed to use DummyFlow`() {
- val joeUser = userOf("joe", setOf(DUMMY_FLOW))
- proxy = proxyFor(joeUser)
- proxy.validatePermission(DUMMY_FLOW)
- }
-
- @Test
- fun `joe user is not allowed to use OtherFlow`() {
- val joeUser = userOf("joe", setOf(DUMMY_FLOW))
- proxy = proxyFor(joeUser)
- assertFailsWith(PermissionException::class,
- "User ${joeUser.username} should not be allowed to use $OTHER_FLOW",
- { proxy.validatePermission(OTHER_FLOW) })
- }
-
- @Test
- fun `check ALL is implemented the correct way round` () {
- val joeUser = userOf("joe", setOf(DUMMY_FLOW))
- proxy = proxyFor(joeUser)
- assertFailsWith(PermissionException::class,
- "Permission $ALL_ALLOWED should not do anything for User ${joeUser.username}",
- { proxy.validatePermission(ALL_ALLOWED) })
- }
-
-}
diff --git a/config/dev/generalnodea.conf b/config/dev/generalnodea.conf
index 366c5bbfc1..1ad2dea3d2 100644
--- a/config/dev/generalnodea.conf
+++ b/config/dev/generalnodea.conf
@@ -1,4 +1,4 @@
-myLegalName : "Bank A"
+myLegalName : "CN=Bank A,O=Bank A,L=London,C=UK"
nearestCity : "London"
keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass"
@@ -8,6 +8,6 @@ webAddress : "localhost:10004"
extraAdvertisedServiceIds : [ "corda.interest_rates" ]
networkMapService : {
address : "localhost:10000"
- legalName : "Network Map Service"
+ legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=UK"
}
useHTTPS : false
diff --git a/config/dev/generalnodeb.conf b/config/dev/generalnodeb.conf
index a6c4aa3361..510b3e9b70 100644
--- a/config/dev/generalnodeb.conf
+++ b/config/dev/generalnodeb.conf
@@ -1,4 +1,4 @@
-myLegalName : "Bank B"
+myLegalName : "CN=Bank B,O=Bank A,L=London,C=UK"
nearestCity : "London"
keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass"
@@ -8,6 +8,6 @@ webAddress : "localhost:10007"
extraAdvertisedServiceIds : [ "corda.interest_rates" ]
networkMapService : {
address : "localhost:10000"
- legalName : "Network Map Service"
+ legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=UK"
}
useHTTPS : false
diff --git a/config/dev/log4j2.xml b/config/dev/log4j2.xml
index 820d5d73f1..f85a22fbee 100644
--- a/config/dev/log4j2.xml
+++ b/config/dev/log4j2.xml
@@ -57,5 +57,8 @@
+
+
+
-
\ No newline at end of file
+
diff --git a/config/dev/nameservernode.conf b/config/dev/nameservernode.conf
index feab919ab2..73591fa46d 100644
--- a/config/dev/nameservernode.conf
+++ b/config/dev/nameservernode.conf
@@ -1,4 +1,4 @@
-myLegalName : "Notary Service"
+myLegalName : "CN=Notary Service,O=R3,OU=corda,L=London,C=UK"
nearestCity : "London"
keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass"
diff --git a/constants.properties b/constants.properties
new file mode 100644
index 0000000000..6312235f73
--- /dev/null
+++ b/constants.properties
@@ -0,0 +1,5 @@
+gradlePluginsVersion=0.12.0
+kotlinVersion=1.1.2
+guavaVersion=21.0
+bouncycastleVersion=1.56
+typesafeConfigVersion=1.3.1
diff --git a/core/build.gradle b/core/build.gradle
index 82322e8f0e..0e24a0eb52 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -10,26 +10,6 @@ buildscript {
}
}
-repositories {
- mavenLocal()
- mavenCentral()
- jcenter()
- maven {
- url 'http://oss.sonatype.org/content/repositories/snapshots'
- }
- maven {
- url 'https://dl.bintray.com/kotlin/exposed'
- }
-}
-
-sourceSets {
- test {
- resources {
- srcDir "../config/test"
- }
- }
-}
-
dependencies {
testCompile "junit:junit:$junit_version"
@@ -42,9 +22,8 @@ dependencies {
testCompile project(":node")
testCompile project(":test-utils")
- compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
- compile "org.jetbrains.kotlinx:kotlinx-support-jdk8:0.3"
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
// Thread safety annotations
@@ -84,7 +63,7 @@ dependencies {
compile "com.fasterxml.jackson.core:jackson-databind:${jackson_version}"
// Java ed25519 implementation. See https://github.com/str4d/ed25519-java/
- compile 'net.i2p.crypto:eddsa:0.1.0'
+ compile 'net.i2p.crypto:eddsa:0.2.0'
// Bouncy castle support needed for X509 certificate manipulation
compile "org.bouncycastle:bcprov-jdk15on:${bouncycastle_version}"
@@ -93,20 +72,14 @@ dependencies {
// JPA 2.1 annotations.
compile "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final"
- // RS API: Response type and codes for ApiUtils.
- compile "javax.ws.rs:javax.ws.rs-api:2.0.1"
-
// Requery: SQL based query & persistence for Kotlin
compile "io.requery:requery-kotlin:$requery_version"
+
+ // For AMQP serialisation.
+ compile "org.apache.qpid:proton-j:0.18.0"
}
configurations {
- compile {
- // We want to use SLF4J's version of these binding: jcl-over-slf4j
- // Remove any transitive dependency on Apache's version.
- exclude group: 'commons-logging', module: 'commons-logging'
- }
-
testArtifacts.extendsFrom testRuntime
}
diff --git a/core/src/main/kotlin/net/corda/core/Utils.kt b/core/src/main/kotlin/net/corda/core/Utils.kt
index ca39b64fcd..6bdb45d83e 100644
--- a/core/src/main/kotlin/net/corda/core/Utils.kt
+++ b/core/src/main/kotlin/net/corda/core/Utils.kt
@@ -3,21 +3,19 @@
package net.corda.core
-import com.google.common.base.Function
import com.google.common.base.Throwables
import com.google.common.io.ByteStreams
import com.google.common.util.concurrent.*
-import kotlinx.support.jdk7.use
+import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.newSecureRandom
+import net.corda.core.crypto.sha256
import net.corda.core.serialization.CordaSerializable
import org.slf4j.Logger
import rx.Observable
import rx.Observer
import rx.subjects.PublishSubject
import rx.subjects.UnicastSubject
-import java.io.BufferedInputStream
-import java.io.InputStream
-import java.io.OutputStream
+import java.io.*
import java.math.BigDecimal
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets.UTF_8
@@ -25,11 +23,22 @@ import java.nio.file.*
import java.nio.file.attribute.FileAttribute
import java.time.Duration
import java.time.temporal.Temporal
+import java.util.HashMap
import java.util.concurrent.*
import java.util.concurrent.locks.ReentrantLock
import java.util.function.BiConsumer
import java.util.stream.Stream
+import java.util.zip.Deflater
+import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
+import java.util.zip.ZipOutputStream
+import kotlin.collections.Iterable
+import kotlin.collections.LinkedHashMap
+import kotlin.collections.List
+import kotlin.collections.filter
+import kotlin.collections.firstOrNull
+import kotlin.collections.fold
+import kotlin.collections.forEach
import kotlin.concurrent.withLock
import kotlin.reflect.KProperty
@@ -75,7 +84,7 @@ fun future(block: () -> T): ListenableFuture = CompletableToListenable(Co
private class CompletableToListenable(private val base: CompletableFuture) : Future by base, ListenableFuture {
override fun addListener(listener: Runnable, executor: Executor) {
- base.whenCompleteAsync(BiConsumer { result, exception -> listener.run() }, executor)
+ base.whenCompleteAsync(BiConsumer { _, _ -> listener.run() }, executor)
}
}
@@ -102,7 +111,9 @@ fun ListenableFuture.failure(executor: Executor, body: (Throwable) -> Uni
infix fun ListenableFuture.then(body: () -> Unit): ListenableFuture = apply { then(RunOnCallerThread, body) }
infix fun ListenableFuture.success(body: (T) -> Unit): ListenableFuture = apply { success(RunOnCallerThread, body) }
infix fun ListenableFuture.failure(body: (Throwable) -> Unit): ListenableFuture = apply { failure(RunOnCallerThread, body) }
-infix fun ListenableFuture.map(mapper: (F) -> T): ListenableFuture = Futures.transform(this, Function { mapper(it!!) })
+@Suppress("UNCHECKED_CAST") // We need the awkward cast because otherwise F cannot be nullable, even though it's safe.
+infix fun ListenableFuture.map(mapper: (F) -> T): ListenableFuture = Futures.transform(this, { (mapper as (F?) -> T)(it) })
+
infix fun ListenableFuture.flatMap(mapper: (F) -> ListenableFuture): ListenableFuture = Futures.transformAsync(this) { mapper(it!!) }
/** Executes the given block and sets the future to either the result, or any exception that was thrown. */
inline fun SettableFuture.catch(block: () -> T) {
@@ -152,7 +163,7 @@ fun Path.writeLines(lines: Iterable