CORDA-556: Added Cordapp Config and a sample (#2469)

* Added per-cordapp configuration 
* Added new API for Cordformation cordapp declarations to support per-cordapp configuration
* Added a cordapp configuration sample
This commit is contained in:
Clinton 2018-02-14 14:49:59 +00:00 committed by GitHub
parent 3802066bf6
commit 174ed3c64b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 1063 additions and 206 deletions

View File

@ -635,7 +635,7 @@ public static final class net.corda.core.contracts.UniqueIdentifier$Companion ex
@org.jetbrains.annotations.NotNull public abstract List getServices()
##
public final class net.corda.core.cordapp.CordappContext extends java.lang.Object
public <init>(net.corda.core.cordapp.Cordapp, net.corda.core.crypto.SecureHash, ClassLoader)
public <init>(net.corda.core.cordapp.Cordapp, net.corda.core.crypto.SecureHash, ClassLoader, net.corda.core.cordapp.CordappConfig)
@org.jetbrains.annotations.Nullable public final net.corda.core.crypto.SecureHash getAttachmentId()
@org.jetbrains.annotations.NotNull public final ClassLoader getClassLoader()
@org.jetbrains.annotations.NotNull public final net.corda.core.cordapp.Cordapp getCordapp()

2
.idea/compiler.xml generated
View File

@ -29,6 +29,8 @@
<module name="corda-webserver_integrationTest" target="1.8" />
<module name="corda-webserver_main" target="1.8" />
<module name="corda-webserver_test" target="1.8" />
<module name="cordapp-configuration_main" target="1.8" />
<module name="cordapp-configuration_test" target="1.8" />
<module name="cordapp_integrationTest" target="1.8" />
<module name="cordapp_main" target="1.8" />
<module name="cordapp_test" target="1.8" />

View File

@ -1,4 +1,4 @@
gradlePluginsVersion=3.0.5
gradlePluginsVersion=3.0.6
kotlinVersion=1.2.20
platformVersion=2
guavaVersion=21.0
@ -6,4 +6,4 @@ bouncycastleVersion=1.57
typesafeConfigVersion=1.3.1
jsr305Version=3.0.2
artifactoryPluginVersion=4.4.18
snakeYamlVersion=1.19
snakeYamlVersion=1.19

View File

@ -0,0 +1,6 @@
package net.corda.core.cordapp
/**
* Thrown if an exception occurs in accessing or parsing cordapp configuration
*/
class CordappConfigException(msg: String, e: Throwable) : Exception(msg, e)

View File

@ -0,0 +1,70 @@
package net.corda.core.cordapp
import net.corda.core.DoNotImplement
/**
* Provides access to cordapp configuration independent of the configuration provider.
*/
@DoNotImplement
interface CordappConfig {
/**
* Check if a config exists at path
*/
fun exists(path: String): Boolean
/**
* Get the value of the configuration at "path".
*
* @throws CordappConfigException If the configuration fails to load, parse, or find a value.
*/
fun get(path: String): Any
/**
* Get the int value of the configuration at "path".
*
* @throws CordappConfigException If the configuration fails to load, parse, or find a value.
*/
fun getInt(path: String): Int
/**
* Get the long value of the configuration at "path".
*
* @throws CordappConfigException If the configuration fails to load, parse, or find a value.
*/
fun getLong(path: String): Long
/**
* Get the float value of the configuration at "path".
*
* @throws CordappConfigException If the configuration fails to load, parse, or find a value.
*/
fun getFloat(path: String): Float
/**
* Get the double value of the configuration at "path".
*
* @throws CordappConfigException If the configuration fails to load, parse, or find a value.
*/
fun getDouble(path: String): Double
/**
* Get the number value of the configuration at "path".
*
* @throws CordappConfigException If the configuration fails to load, parse, or find a value.
*/
fun getNumber(path: String): Number
/**
* Get the string value of the configuration at "path".
*
* @throws CordappConfigException If the configuration fails to load, parse, or find a value.
*/
fun getString(path: String): String
/**
* Get the boolean value of the configuration at "path".
*
* @throws CordappConfigException If the configuration fails to load, parse, or find a value.
*/
fun getBoolean(path: String): Boolean
}

View File

@ -2,8 +2,6 @@ package net.corda.core.cordapp
import net.corda.core.crypto.SecureHash
// TODO: Add per app config
/**
* An app context provides information about where an app was loaded from, access to its classloader,
* and (in the included [Cordapp] object) lists of annotated classes discovered via scanning the JAR.
@ -15,5 +13,11 @@ import net.corda.core.crypto.SecureHash
* @property attachmentId For CorDapps containing [Contract] or [UpgradedContract] implementations this will be populated
* with the attachment containing those class files
* @property classLoader the classloader used to load this cordapp's classes
* @property config Configuration for this CorDapp
*/
class CordappContext(val cordapp: Cordapp, val attachmentId: SecureHash?, val classLoader: ClassLoader)
class CordappContext internal constructor(
val cordapp: Cordapp,
val attachmentId: SecureHash?,
val classLoader: ClassLoader,
val config: CordappConfig
)

View File

@ -2,6 +2,9 @@
package net.corda.core.internal
import net.corda.core.cordapp.Cordapp
import net.corda.core.cordapp.CordappConfig
import net.corda.core.cordapp.CordappContext
import net.corda.core.cordapp.CordappProvider
import net.corda.core.crypto.*
import net.corda.core.identity.CordaX500Name
@ -375,3 +378,7 @@ inline fun <T : Any> SerializedBytes<T>.sign(keyPair: KeyPair): SignedData<T> {
}
fun ByteBuffer.copyBytes() = ByteArray(remaining()).also { get(it) }
fun createCordappContext(cordapp: Cordapp, attachmentId: SecureHash?, classLoader: ClassLoader, config: CordappConfig): CordappContext {
return CordappContext(cordapp, attachmentId, classLoader, config)
}

View File

@ -2,6 +2,7 @@ package net.corda.core.node
import net.corda.core.DoNotImplement
import net.corda.core.contracts.*
import net.corda.core.cordapp.CordappContext
import net.corda.core.cordapp.CordappProvider
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SignableData
@ -369,4 +370,9 @@ interface ServiceHub : ServicesForResolution {
* node starts.
*/
fun registerUnloadHandler(runOnStop: () -> Unit)
/**
* See [CordappProvider.getAppContext]
*/
fun getAppContext(): CordappContext = cordappProvider.getAppContext()
}

View File

@ -7,6 +7,10 @@ from the previous milestone release.
UNRELEASED
----------
* Per CorDapp configuration is now exposed. ``CordappContext`` now exposes a ``CordappConfig`` object that is populated
at CorDapp context creation time from a file source during runtime.
* Introduced Flow Draining mode, in which a node continues executing existing flows, but does not start new. This is to support graceful node shutdown/restarts.
In particular, when this mode is on, new flows through RPC will be rejected, scheduled flows will be ignored, and initial session messages will not be consumed.
This will ensure that the number of checkpoints will strictly diminish with time, allowing for a clean shutdown.
@ -188,18 +192,9 @@ UNRELEASED
* Marked ``stateMachine`` on ``FlowLogic`` as ``CordaInternal`` to make clear that is it not part of the public api and is
only for internal use
* Provided experimental support for specifying your own webserver to be used instead of the default development
webserver in ``Cordform`` using the ``webserverJar`` argument
* Created new ``StartedMockNode`` and ``UnstartedMockNode`` classes which are wrappers around our MockNode implementation
that expose relevant methods for testing without exposing internals, create these using a ``MockNetwork``.
* The test utils in ``Expect.kt``, ``SerializationTestHelpers.kt``, ``TestConstants.kt`` and ``TestUtils.kt`` have moved
from the ``net.corda.testing`` package to the ``net.corda.testing.core`` package, and ``FlowStackSnapshot.kt`` has moved to the
``net.corda.testing.services`` package. Moving items out of the ``net.corda.testing.*`` package will help make it clearer which
parts of the api are stable. The bash script ``tools\scripts\update-test-packages.sh`` can be used to smooth the upgrade
process for existing projects.
.. _changelog_v1:
Release 1.0

View File

@ -159,3 +159,20 @@ Installing the CorDapp JAR
At runtime, nodes will load any CorDapps present in their ``cordapps`` folder. Therefore in order to install a CorDapp on
a node, the CorDapp JAR must be added to the ``<node_dir>/cordapps/`` folder, where ``node_dir`` is the folder in which
the node's JAR and configuration files are stored.
CorDapp configuration files
---------------------------
CorDapp configuration files should be placed in ``<node_dir>/cordapps/config``. The name of the file should match the
name of the JAR of the CorDapp (eg; if your CorDapp is called ``hello-0.1.jar`` the config should be ``config/hello-0.1.conf``).
Config files are currently only available in the `Typesafe/Lightbend <https://github.com/lightbend/config>`_ config format.
These files are loaded when a CorDapp context is created and so can change during runtime.
CorDapp configuration can be accessed from ``CordappContext::config`` whenever a ``CordappContext`` is available.
There is an example project that demonstrates in ``samples` called ``cordapp-configuration`` and API documentation in
<api/kotlin/corda/net.corda.core.cordapp/index.html>`_.

View File

@ -1,4 +1,5 @@
apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'maven-publish'
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
@ -7,11 +8,9 @@ repositories {
mavenCentral()
}
// This tracks the gradle plugins version and not Corda
version gradle_plugins_version
group 'net.corda.plugins'
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
// JSR 305: Nullability annotations
compile "com.google.code.findbugs:jsr305:$jsr305_version"

View File

@ -10,7 +10,12 @@ import java.util.function.Consumer;
public abstract class CordformDefinition {
private Path nodesDirectory = Paths.get("build", "nodes");
private final List<Consumer<CordformNode>> nodeConfigurers = new ArrayList<>();
private final List<String> cordappPackages = new ArrayList<>();
/**
* A list of Cordapp maven coordinates and project name
*
* If maven coordinates are set project name is ignored
*/
private final List<CordappDependency> cordappDeps = new ArrayList<>();
public Path getNodesDirectory() {
return nodesDirectory;
@ -28,8 +33,11 @@ public abstract class CordformDefinition {
nodeConfigurers.add(configurer);
}
public List<String> getCordappPackages() {
return cordappPackages;
/**
* Cordapp maven coordinates or project names (ie; net.corda:finance:0.1 or ":finance") to scan for when resolving cordapp JARs
*/
public List<CordappDependency> getCordappDependencies() {
return cordappDeps;
}
/**

View File

@ -0,0 +1,10 @@
package net.corda.cordform
data class CordappDependency(
val mavenCoordinates: String? = null,
val projectName: String? = null
) {
init {
require((mavenCoordinates != null) != (projectName != null), { "Only one of maven coordinates or project name must be set" })
}
}

View File

@ -9,6 +9,7 @@ buildscript {
}
apply plugin: 'kotlin'
apply plugin: 'java-gradle-plugin'
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
@ -33,18 +34,22 @@ sourceSets {
}
dependencies {
compile gradleApi()
gradleApi()
compile project(":cordapp")
compile project(':cordform-common')
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
compile "commons-io:commons-io:2.6"
noderunner "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
compile project(':cordform-common')
testCompile "junit:junit:4.12" // TODO: Unify with core
testCompile "org.assertj:assertj-core:3.8.0"
// Docker-compose file generation
compile "org.yaml:snakeyaml:$snake_yaml_version"
}
task createNodeRunner(type: Jar, dependsOn: [classes]) {
task createNodeRunner(type: Jar) {
manifest {
attributes('Main-Class': 'net.corda.plugins.NodeRunnerKt')
}
@ -53,12 +58,12 @@ task createNodeRunner(type: Jar, dependsOn: [classes]) {
from sourceSets.runnodes.output
}
jar {
publish {
name project.name
}
processResources {
from(createNodeRunner) {
rename { 'net/corda/plugins/runnodes.jar' }
}
}
publish {
name project.name
}

View File

@ -2,11 +2,9 @@ package net.corda.plugins
import groovy.lang.Closure
import net.corda.cordform.CordformDefinition
import org.apache.tools.ant.filters.FixCrLfFilter
import org.gradle.api.DefaultTask
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.lang.reflect.InvocationTargetException
import java.net.URLClassLoader
@ -111,20 +109,27 @@ open class Baseform : DefaultTask() {
}
}
protected fun initializeConfiguration() {
internal fun initializeConfiguration() {
if (definitionClass != null) {
val cd = loadCordformDefinition()
// If the user has specified their own directory (even if it's the same default path) then let them know
// it's not used and should just rely on the one in CordformDefinition
require(directory === defaultDirectory) {
project.logger.info("User has used '$directory', default directory is '${defaultDirectory}'")
"'directory' cannot be used when 'definitionClass' is specified. Use CordformDefinition.nodesDirectory instead."
}
directory = cd.nodesDirectory
val cordapps = cd.getMatchingCordapps()
val cordapps = cd.cordappDependencies
cd.nodeConfigurers.forEach {
val node = node { }
it.accept(node)
node.additionalCordapps.addAll(cordapps)
cordapps.forEach {
if (it.mavenCoordinates != null) {
node.cordapp(project.project(it.mavenCoordinates!!))
} else {
node.cordapp(it.projectName!!)
}
}
node.rootDir(directory)
}
cd.setup { nodeName -> project.projectDir.toPath().resolve(getNodeByName(nodeName)!!.nodeDir.toPath()) }
@ -134,7 +139,6 @@ open class Baseform : DefaultTask() {
}
}
}
protected fun bootstrapNetwork() {
val networkBootstrapperClass = loadNetworkBootstrapperClass()
val networkBootstrapper = networkBootstrapperClass.newInstance()
@ -148,18 +152,6 @@ open class Baseform : DefaultTask() {
}
}
private fun CordformDefinition.getMatchingCordapps(): List<File> {
val cordappJars = project.configuration("cordapp").files
return cordappPackages.map { `package` ->
val cordappsWithPackage = cordappJars.filter { it.containsPackage(`package`) }
when (cordappsWithPackage.size) {
0 -> throw IllegalArgumentException("There are no cordapp dependencies containing the package $`package`")
1 -> cordappsWithPackage[0]
else -> throw IllegalArgumentException("More than one cordapp dependency contains the package $`package`: $cordappsWithPackage")
}
}
}
private fun File.containsPackage(`package`: String): Boolean {
JarInputStream(inputStream()).use {
while (true) {

View File

@ -0,0 +1,26 @@
package net.corda.plugins
import org.gradle.api.Project
import java.io.File
open class Cordapp private constructor(val coordinates: String?, val project: Project?) {
constructor(coordinates: String) : this(coordinates, null)
constructor(cordappProject: Project) : this(null, cordappProject)
// The configuration text that will be written
internal var config: String? = null
/**
* Set the configuration text that will be written to the cordapp's configuration file
*/
fun config(config: String) {
this.config = config
}
/**
* Reads config from the file and later writes it to the cordapp's configuration file
*/
fun config(configFile: File) {
this.config = configFile.readText()
}
}

View File

@ -1,9 +1,6 @@
package net.corda.plugins
import org.apache.tools.ant.filters.FixCrLfFilter
import org.gradle.api.DefaultTask
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
import org.gradle.api.tasks.TaskAction
import java.nio.file.Path
import java.nio.file.Paths
@ -15,18 +12,25 @@ import java.nio.file.Paths
*/
@Suppress("unused")
open class Cordform : Baseform() {
private companion object {
internal companion object {
val nodeJarName = "corda.jar"
private val defaultDirectory: Path = Paths.get("build", "nodes")
}
/**
* Returns a node by name.
*
* @param name The name of the node as specified in the node configuration DSL.
* @return A node instance.
*/
private fun getNodeByName(name: String): Node? = nodes.firstOrNull { it.name == name }
/**
* Installs the run script into the nodes directory.
*/
private fun installRunScript() {
project.copy {
it.apply {
from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes.jar"))
from(Cordformation.getPluginFile(project, "runnodes.jar"))
fileMode = Cordformation.executableFileMode
into("$directory/")
}
@ -34,7 +38,7 @@ open class Cordform : Baseform() {
project.copy {
it.apply {
from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes"))
from(Cordformation.getPluginFile(project, "runnodes"))
// Replaces end of line with lf to avoid issues with the bash interpreter and Windows style line endings.
filter(mapOf("eol" to FixCrLfFilter.CrLf.newInstance("lf")), FixCrLfFilter::class.java)
fileMode = Cordformation.executableFileMode
@ -44,7 +48,7 @@ open class Cordform : Baseform() {
project.copy {
it.apply {
from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes.bat"))
from(Cordformation.getPluginFile(project, "runnodes.bat"))
into("$directory/")
}
}
@ -63,4 +67,5 @@ open class Cordform : Baseform() {
bootstrapNetwork()
nodes.forEach(Node::build)
}
}

View File

@ -3,6 +3,7 @@ package net.corda.plugins
import org.gradle.api.Plugin
import org.gradle.api.Project
import java.io.File
import java.io.InputStream
/**
* The Cordformation plugin deploys nodes to a directory in a state ready to be used by a developer for experimentation,
@ -13,19 +14,19 @@ class Cordformation : Plugin<Project> {
const val CORDFORMATION_TYPE = "cordformationInternal"
/**
* Gets a resource file from this plugin's JAR file.
* Gets a resource file from this plugin's JAR file by creating an intermediate tmp dir
*
* @param project The project environment this plugin executes in.
* @param filePathInJar The file in the JAR, relative to root, you wish to access.
* @return A file handle to the file in the JAR.
*/
fun getPluginFile(project: Project, filePathInJar: String): File {
val archive = project.rootProject.buildscript.configurations
.single { it.name == "classpath" }
.first { it.name.contains("cordformation") }
return project.rootProject.resources.text
.fromArchiveEntry(archive, filePathInJar)
.asFile()
val tmpDir = File(project.buildDir, "tmp")
val outputFile = File(tmpDir, filePathInJar)
tmpDir.mkdir()
outputFile.outputStream().use {
Cordformation::class.java.getResourceAsStream(filePathInJar).copyTo(it)
}
return outputFile
}
/**
@ -38,7 +39,7 @@ class Cordformation : Plugin<Project> {
fun verifyAndGetRuntimeJar(project: Project, jarName: String): File {
val releaseVersion = project.rootProject.ext<String>("corda_release_version")
val maybeJar = project.configuration("runtime").filter {
"$jarName-$releaseVersion.jar" in it.toString() || "$jarName-enterprise-$releaseVersion.jar" in it.toString()
"$jarName-$releaseVersion.jar" in it.toString() || "$jarName-r3-$releaseVersion.jar" in it.toString()
}
if (maybeJar.isEmpty) {
throw IllegalStateException("No $jarName JAR found. Have you deployed the Corda project to Maven? Looked for \"$jarName-$releaseVersion.jar\"")

View File

@ -1,22 +1,28 @@
package net.corda.plugins
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigObject
import com.typesafe.config.ConfigRenderOptions
import com.typesafe.config.ConfigValueFactory
import com.typesafe.config.ConfigObject
import groovy.lang.Closure
import net.corda.cordform.CordformNode
import net.corda.cordform.RpcSettings
import org.apache.commons.io.FilenameUtils
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.artifacts.ProjectDependency
import java.io.File
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import javax.inject.Inject
/**
* Represents a node that will be installed.
*/
class Node(private val project: Project) : CordformNode() {
open class Node @Inject constructor(private val project: Project) : CordformNode() {
private data class ResolvedCordapp(val jarFile: File, val config: String?)
companion object {
@JvmStatic
val webJarName = "corda-webserver.jar"
@ -30,8 +36,17 @@ class Node(private val project: Project) : CordformNode() {
* @note Your app will be installed by default and does not need to be included here.
* @note Type is any due to gradle's use of "GStrings" - each value will have "toString" called on it
*/
var cordapps = mutableListOf<Any>()
internal var additionalCordapps = mutableListOf<File>()
var cordapps: MutableList<Any>
get() = internalCordapps as MutableList<Any>
@Deprecated("Use cordapp instead - setter will be removed by Corda V4.0")
set(value) {
value.forEach {
cordapp(it.toString())
}
}
private val internalCordapps = mutableListOf<Cordapp>()
private val builtCordapp = Cordapp(project)
internal lateinit var nodeDir: File
private set
internal lateinit var rootDir: File
@ -76,8 +91,83 @@ class Node(private val project: Project) : CordformNode() {
*
* @param sshdPort The port for SSH server to listen on
*/
fun sshdPort(sshdPort: Int?) {
config = config.withValue("sshd.port", ConfigValueFactory.fromAnyRef(sshdPort))
fun sshdPort(sshdPort: Int) {
config = config.withValue("sshdAddress",
ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$sshdPort"))
}
/**
* Install a cordapp to this node
*
* @param coordinates The coordinates of the [Cordapp]
* @param configureClosure A groovy closure to configure a [Cordapp] object
* @return The created and inserted [Cordapp]
*/
fun cordapp(coordinates: String, configureClosure: Closure<in Cordapp>): Cordapp {
val cordapp = project.configure(Cordapp(coordinates), configureClosure) as Cordapp
internalCordapps += cordapp
return cordapp
}
/**
* Install a cordapp to this node
*
* @param cordappProject A project that produces a cordapp JAR
* @param configureClosure A groovy closure to configure a [Cordapp] object
* @return The created and inserted [Cordapp]
*/
fun cordapp(cordappProject: Project, configureClosure: Closure<in Cordapp>): Cordapp {
val cordapp = project.configure(Cordapp(cordappProject), configureClosure) as Cordapp
internalCordapps += cordapp
return cordapp
}
/**
* Install a cordapp to this node
*
* @param cordappProject A project that produces a cordapp JAR
* @return The created and inserted [Cordapp]
*/
fun cordapp(cordappProject: Project): Cordapp {
return Cordapp(cordappProject).apply {
internalCordapps += this
}
}
/**
* Install a cordapp to this node
*
* @param coordinates The coordinates of the [Cordapp]
* @return The created and inserted [Cordapp]
*/
fun cordapp(coordinates: String): Cordapp {
return Cordapp(coordinates).apply {
internalCordapps += this
}
}
/**
* Install a cordapp to this node
*
* @param configureFunc A lambda to configure a [Cordapp] object
* @return The created and inserted [Cordapp]
*/
fun cordapp(coordinates: String, configureFunc: Cordapp.() -> Unit): Cordapp {
return Cordapp(coordinates).apply {
configureFunc()
internalCordapps += this
}
}
/**
* Configures the default cordapp automatically added to this node from this project
*
* @param configureClosure A groovy closure to configure a [Cordapp] object
* @return The created and inserted [Cordapp]
*/
fun projectCordapp(configureClosure: Closure<in Cordapp>): Cordapp {
project.configure(builtCordapp, configureClosure) as Cordapp
return builtCordapp
}
/**
@ -96,8 +186,8 @@ class Node(private val project: Project) : CordformNode() {
installWebserverJar()
}
installAgentJar()
installBuiltCordapp()
installCordapps()
installConfig()
}
internal fun buildDocker() {
@ -109,7 +199,6 @@ class Node(private val project: Project) : CordformNode() {
}
}
installAgentJar()
installBuiltCordapp()
installCordapps()
}
@ -160,19 +249,6 @@ class Node(private val project: Project) : CordformNode() {
}
}
/**
* Installs this project's cordapp to this directory.
*/
private fun installBuiltCordapp() {
val cordappsDir = File(nodeDir, "cordapps")
project.copy {
it.apply {
from(project.tasks.getByName("jar"))
into(cordappsDir)
}
}
}
/**
* Installs the jolokia monitoring agent JAR to the node/drivers directory
*/
@ -197,6 +273,14 @@ class Node(private val project: Project) : CordformNode() {
}
}
private fun installCordappConfigs(cordapps: Collection<ResolvedCordapp>) {
val cordappsDir = project.file(File(nodeDir, "cordapps"))
cordappsDir.mkdirs()
cordapps.filter { it.config != null }
.map { Pair<String, String>("${FilenameUtils.removeExtension(it.jarFile.name)}.conf", it.config!!) }
.forEach { project.file(File(cordappsDir, it.first)).writeText(it.second) }
}
private fun createTempConfigFile(configObject: ConfigObject): File {
val options = ConfigRenderOptions
.defaults()
@ -217,7 +301,7 @@ class Node(private val project: Project) : CordformNode() {
/**
* Installs the configuration file to the root directory and detokenises it.
*/
internal fun installConfig() {
fun installConfig() {
configureProperties()
val tmpConfFile = createTempConfigFile(config.root())
appendOptionalConfig(tmpConfFile)
@ -269,31 +353,57 @@ class Node(private val project: Project) : CordformNode() {
}
}
/**
* Installs other cordapps to this node's cordapps directory.
* Installs the jolokia monitoring agent JAR to the node/drivers directory
*/
internal fun installCordapps() {
additionalCordapps.addAll(getCordappList())
private fun installCordapps() {
val cordapps = getCordappList()
val cordappsDir = File(nodeDir, "cordapps")
project.copy {
it.apply {
from(additionalCordapps)
into(cordappsDir)
from(cordapps.map { it.jarFile })
into(project.file(cordappsDir))
}
}
installCordappConfigs(cordapps)
}
/**
* Gets a list of cordapps based on what dependent cordapps were specified.
*
* @return List of this node's cordapps.
*/
private fun getCordappList(): Collection<File> {
// Cordapps can sometimes contain a GString instance which fails the equality test with the Java string
@Suppress("RemoveRedundantCallsOfConversionMethods")
val cordapps: List<String> = cordapps.map { it.toString() }
return project.configuration("cordapp").files {
cordapps.contains(it.group + ":" + it.name + ":" + it.version)
private fun getCordappList(): Collection<ResolvedCordapp> =
internalCordapps.map { cordapp -> resolveCordapp(cordapp) } + resolveBuiltCordapp()
private fun resolveCordapp(cordapp: Cordapp): ResolvedCordapp {
val cordappConfiguration = project.configuration("cordapp")
val cordappName = if (cordapp.project != null) cordapp.project.name else cordapp.coordinates
val cordappFile = cordappConfiguration.files {
when {
(it is ProjectDependency) && (cordapp.project != null) -> it.dependencyProject == cordapp.project
cordapp.coordinates != null -> {
// Cordapps can sometimes contain a GString instance which fails the equality test with the Java string
@Suppress("RemoveRedundantCallsOfConversionMethods")
val coordinates = cordapp.coordinates.toString()
coordinates == (it.group + ":" + it.name + ":" + it.version)
}
else -> false
}
}
return when {
cordappFile.size == 0 -> throw GradleException("Cordapp $cordappName not found in cordapps configuration.")
cordappFile.size > 1 -> throw GradleException("Multiple files found for $cordappName")
else -> ResolvedCordapp(cordappFile.single(), cordapp.config)
}
}
private fun resolveBuiltCordapp(): ResolvedCordapp {
val projectCordappFile = project.tasks.getByName("jar").outputs.files.singleFile
return ResolvedCordapp(projectCordappFile, builtCordapp.config)
}
}

View File

@ -109,35 +109,43 @@ private abstract class JavaCommand(
private class HeadlessJavaCommand(jarName: String, dir: File, debugPort: Int?, monitoringPort: Int?, args: List<String>, jvmArgs: List<String>)
: JavaCommand(jarName, dir, debugPort, monitoringPort, dir.name, { add("--no-local-shell") }, args, jvmArgs) {
override fun processBuilder() = ProcessBuilder(command).redirectError(File("error.$nodeName.log")).inheritIO()
override fun processBuilder(): ProcessBuilder {
println("Running command: ${command.joinToString(" ")}")
return ProcessBuilder(command).redirectError(File("error.$nodeName.log")).inheritIO()
}
override fun getJavaPath() = File(File(System.getProperty("java.home"), "bin"), "java").path
}
private class TerminalWindowJavaCommand(jarName: String, dir: File, debugPort: Int?, monitoringPort: Int?, args: List<String>, jvmArgs: List<String>)
: JavaCommand(jarName, dir, debugPort, monitoringPort, "${dir.name}-$jarName", {}, args, jvmArgs) {
override fun processBuilder() = ProcessBuilder(when (os) {
OS.MACOS -> {
listOf("osascript", "-e", """tell app "Terminal"
override fun processBuilder(): ProcessBuilder {
val params = when (os) {
OS.MACOS -> {
listOf("osascript", "-e", """tell app "Terminal"
activate
delay 0.5
tell app "System Events" to tell process "Terminal" to keystroke "t" using command down
delay 0.5
do script "bash -c 'cd \"$dir\" ; \"${command.joinToString("""\" \"""")}\" && exit'" in selected tab of the front window
end tell""")
}
OS.WINDOWS -> {
listOf("cmd", "/C", "start ${command.joinToString(" ") { windowsSpaceEscape(it) }}")
}
OS.LINUX -> {
// Start shell to keep window open unless java terminated normally or due to SIGTERM:
val command = "${unixCommand()}; [ $? -eq 0 -o $? -eq 143 ] || sh"
if (isTmux()) {
listOf("tmux", "new-window", "-n", nodeName, command)
} else {
listOf("xterm", "-T", nodeName, "-e", command)
}
OS.WINDOWS -> {
listOf("cmd", "/C", "start ${command.joinToString(" ") { windowsSpaceEscape(it) }}")
}
OS.LINUX -> {
// Start shell to keep window open unless java terminated normally or due to SIGTERM:
val command = "${unixCommand()}; [ $? -eq 0 -o $? -eq 143 ] || sh"
if (isTmux()) {
listOf("tmux", "new-window", "-n", nodeName, command)
} else {
listOf("xterm", "-T", nodeName, "-e", command)
}
}
}
})
println("Running command: ${params.joinToString(" ")}")
return ProcessBuilder(params)
}
private fun unixCommand() = command.map(::quotedFormOf).joinToString(" ")
override fun getJavaPath(): String = File(File(System.getProperty("java.home"), "bin"), "java").path

View File

@ -0,0 +1,77 @@
package net.corda.plugins
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.assertj.core.api.Assertions.*
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome
class CordformTest {
@Rule
@JvmField
val testProjectDir = TemporaryFolder()
private var buildFile: File? = null
private companion object {
val cordaFinanceJarName = "corda-finance-3.0-SNAPSHOT"
val localCordappJarName = "locally-built-cordapp"
val notaryNodeName = "Notary Service"
}
@Before
fun setup() {
buildFile = testProjectDir.newFile("build.gradle")
}
@Test
fun `a node with cordapp dependency`() {
val runner = getStandardGradleRunnerFor("DeploySingleNodeWithCordapp.gradle")
val result = runner.build()
assertThat(result.task(":deployNodes")!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
assertThat(getNodeCordappJar(notaryNodeName, cordaFinanceJarName)).exists()
}
@Test
fun `deploy a node with cordapp config`() {
val runner = getStandardGradleRunnerFor("DeploySingleNodeWithCordappConfig.gradle")
val result = runner.build()
assertThat(result.task(":deployNodes")!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
assertThat(getNodeCordappJar(notaryNodeName, cordaFinanceJarName)).exists()
assertThat(getNodeCordappConfig(notaryNodeName, cordaFinanceJarName)).exists()
}
@Test
fun `deploy the locally built cordapp with cordapp config`() {
val runner = getStandardGradleRunnerFor("DeploySingleNodeWithLocallyBuildCordappAndConfig.gradle")
val result = runner.build()
assertThat(result.task(":deployNodes")!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
assertThat(getNodeCordappJar(notaryNodeName, localCordappJarName)).exists()
assertThat(getNodeCordappConfig(notaryNodeName, localCordappJarName)).exists()
}
private fun getStandardGradleRunnerFor(buildFileResourceName: String): GradleRunner {
createBuildFile(buildFileResourceName)
return GradleRunner.create()
.withProjectDir(testProjectDir.root)
.withArguments("deployNodes", "-s")
.withPluginClasspath()
}
private fun createBuildFile(buildFileResourceName: String) = IOUtils.copy(javaClass.getResourceAsStream(buildFileResourceName), buildFile!!.outputStream())
private fun getNodeCordappJar(nodeName: String, cordappJarName: String) = File(testProjectDir.root, "build/nodes/$nodeName/cordapps/$cordappJarName.jar")
private fun getNodeCordappConfig(nodeName: String, cordappJarName: String) = File(testProjectDir.root, "build/nodes/$nodeName/cordapps/$cordappJarName.conf")
}

View File

@ -0,0 +1,33 @@
buildscript {
ext {
corda_group = 'net.corda'
corda_release_version = '3.0-SNAPSHOT' // TODO: Set to 3.0.0 when Corda 3 is released
jolokia_version = '1.3.7'
}
}
plugins {
id 'java'
id 'net.corda.plugins.cordformation'
}
repositories {
mavenCentral()
maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-dev' }
}
dependencies {
runtime "$corda_group:corda:$corda_release_version"
runtime "$corda_group:corda-node-api:$corda_release_version"
cordapp "$corda_group:corda-finance:$corda_release_version"
}
task deployNodes(type: net.corda.plugins.Cordform) {
node {
name 'O=Notary Service,L=Zurich,C=CH'
notary = [validating : true]
p2pPort 10002
rpcPort 10003
cordapps = ["$corda_group:corda-finance:$corda_release_version"]
}
}

View File

@ -0,0 +1,35 @@
buildscript {
ext {
corda_group = 'net.corda'
corda_release_version = '3.0-SNAPSHOT' // TODO: Set to 3.0.0 when Corda 3 is released
jolokia_version = '1.3.7'
}
}
plugins {
id 'java'
id 'net.corda.plugins.cordformation'
}
repositories {
mavenCentral()
maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-dev' }
}
dependencies {
runtime "$corda_group:corda:$corda_release_version"
runtime "$corda_group:corda-node-api:$corda_release_version"
cordapp "$corda_group:corda-finance:$corda_release_version"
}
task deployNodes(type: net.corda.plugins.Cordform) {
node {
name 'O=Notary Service,L=Zurich,C=CH'
notary = [validating : true]
p2pPort 10002
rpcPort 10003
cordapp "$corda_group:corda-finance:$corda_release_version", {
config "a=b"
}
}
}

View File

@ -0,0 +1,39 @@
buildscript {
ext {
corda_group = 'net.corda'
corda_release_version = '3.0-SNAPSHOT' // TODO: Set to 3.0.0 when Corda 3 is released
jolokia_version = '1.3.7'
}
}
plugins {
id 'java'
id 'net.corda.plugins.cordformation'
}
repositories {
mavenCentral()
maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-dev' }
}
dependencies {
runtime "$corda_group:corda:$corda_release_version"
runtime "$corda_group:corda-node-api:$corda_release_version"
cordapp "$corda_group:corda-finance:$corda_release_version"
}
jar {
baseName 'locally-built-cordapp'
}
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: [jar]) {
node {
name 'O=Notary Service,L=Zurich,C=CH'
notary = [validating : true]
p2pPort 10002
rpcPort 10003
cordapp {
config "a=b"
}
}
}

View File

@ -17,6 +17,7 @@ import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockCordappConfigProvider
import net.corda.testing.services.MockAttachmentStorage
import org.junit.Assert.*
import org.junit.Rule
@ -58,7 +59,7 @@ class AttachmentsClassLoaderStaticContractTests {
}
private val serviceHub = rigorousMock<ServicesForResolution>().also {
doReturn(CordappProviderImpl(CordappLoader.createWithTestPackages(listOf("net.corda.nodeapi.internal")), MockAttachmentStorage())).whenever(it).cordappProvider
doReturn(CordappProviderImpl(CordappLoader.createWithTestPackages(listOf("net.corda.nodeapi.internal")), MockCordappConfigProvider(), MockAttachmentStorage())).whenever(it).cordappProvider
}
@Test

View File

@ -23,6 +23,7 @@ import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.internal.kryoSpecific
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockCordappConfigProvider
import net.corda.testing.services.MockAttachmentStorage
import org.apache.commons.io.IOUtils
import org.junit.Assert.*
@ -57,7 +58,7 @@ class AttachmentsClassLoaderTests {
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val attachments = MockAttachmentStorage()
private val cordappProvider = CordappProviderImpl(CordappLoader.createDevMode(listOf(ISOLATED_CONTRACTS_JAR_PATH)), attachments)
private val cordappProvider = CordappProviderImpl(CordappLoader.createDevMode(listOf(ISOLATED_CONTRACTS_JAR_PATH)), MockCordappConfigProvider(), attachments)
private val cordapp get() = cordappProvider.cordapps.first()
private val attachmentId get() = cordappProvider.getCordappAttachmentId(cordapp)!!
private val appContext get() = cordappProvider.getAppContext(cordapp)

View File

@ -59,11 +59,6 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) {
// If you change these flags, please also update Driver.kt
jvmArgs = ['-Xmx200m', '-XX:+UseG1GC']
}
// Make the resulting JAR file directly executable on UNIX by prepending a shell script to it.
// This lets you run the file like so: ./corda.jar
// Other than being slightly less typing, this has one big advantage: Ctrl-C works properly in the terminal.
reallyExecutable { trampolining() }
}
artifacts {

View File

@ -0,0 +1,60 @@
package net.corda.node
import com.typesafe.config.Config
import com.typesafe.config.ConfigException
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions
import net.corda.node.internal.cordapp.CordappConfigFileProvider
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
class CordappConfigFileProviderTests {
private companion object {
val cordappConfDir = File("build/tmp/cordapps/config")
val cordappName = "test"
val cordappConfFile = File(cordappConfDir, cordappName + ".conf").toPath()
val validConfig = ConfigFactory.parseString("key=value")
val alternateValidConfig = ConfigFactory.parseString("key=alternateValue")
val invalidConfig = "Invalid"
}
val provider = CordappConfigFileProvider(cordappConfDir)
@Test
fun `test that config can be loaded`() {
writeConfig(validConfig, cordappConfFile)
assertThat(provider.getConfigByName(cordappName)).isEqualTo(validConfig)
}
@Test
fun `config is idempotent if the underlying file is not changed`() {
writeConfig(validConfig, cordappConfFile)
assertThat(provider.getConfigByName(cordappName)).isEqualTo(validConfig)
assertThat(provider.getConfigByName(cordappName)).isEqualTo(validConfig)
}
@Test
fun `config is not idempotent if the underlying file is changed`() {
writeConfig(validConfig, cordappConfFile)
assertThat(provider.getConfigByName(cordappName)).isEqualTo(validConfig)
writeConfig(alternateValidConfig, cordappConfFile)
assertThat(provider.getConfigByName(cordappName)).isEqualTo(alternateValidConfig)
}
@Test(expected = ConfigException.Parse::class)
fun `an invalid config throws an exception`() {
Files.write(cordappConfFile, invalidConfig.toByteArray())
provider.getConfigByName(cordappName)
}
/**
* Writes the config to the path provided - will (and must) overwrite any existing config
*/
private fun writeConfig(config: Config, to: Path) = Files.write(cordappConfFile, config.root().render(ConfigRenderOptions.concise()).toByteArray())
}

View File

@ -29,6 +29,7 @@ import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.driver
import net.corda.testing.internal.rigorousMock
import net.corda.testing.internal.withoutTestSerialization
import net.corda.testing.node.MockCordappConfigProvider
import net.corda.testing.services.MockAttachmentStorage
import org.junit.Assert.assertEquals
import org.junit.Rule
@ -42,7 +43,7 @@ class AttachmentLoadingTests {
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val attachments = MockAttachmentStorage()
private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR)), attachments)
private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR)), MockCordappConfigProvider(), attachments)
private val cordapp get() = provider.cordapps.first()
private val attachmentId get() = provider.getCordappAttachmentId(cordapp)!!
private val appContext get() = provider.getAppContext(cordapp)

View File

@ -32,6 +32,7 @@ import net.corda.core.utilities.debug
import net.corda.core.utilities.getOrThrow
import net.corda.node.VersionInfo
import net.corda.node.internal.classloading.requireAnnotation
import net.corda.node.internal.cordapp.CordappConfigFileProvider
import net.corda.node.internal.cordapp.CordappLoader
import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.node.internal.cordapp.CordappProviderInternal
@ -539,7 +540,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
checkpointStorage = DBCheckpointStorage()
val metrics = MetricRegistry()
attachments = NodeAttachmentService(metrics, configuration.attachmentContentCacheSizeBytes, configuration.attachmentCacheBound)
val cordappProvider = CordappProviderImpl(cordappLoader, attachments)
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments)
val keyManagementService = makeKeyManagementService(identityService, keyPairs)
_services = ServiceHubInternalImpl(
identityService,

View File

@ -0,0 +1,36 @@
package net.corda.node.internal.cordapp
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import net.corda.core.internal.cordapp.CordappConfigProvider
import net.corda.core.utilities.loggerFor
import sun.plugin.dom.exception.InvalidStateException
import java.io.File
class CordappConfigFileProvider(val configDir: File = DEFAULT_CORDAPP_CONFIG_DIR) : CordappConfigProvider {
companion object {
val DEFAULT_CORDAPP_CONFIG_DIR = File("cordapps/config")
val CONFIG_EXT = ".conf"
val logger = loggerFor<CordappConfigFileProvider>()
}
init {
configDir.mkdirs()
}
override fun getConfigByName(name: String): Config {
val configFile = File(configDir, name + CONFIG_EXT)
return if (configFile.exists()) {
if (configFile.isDirectory) {
throw InvalidStateException("ile at ${configFile.absolutePath} is a directory, expected a config file")
} else {
logger.info("Found config for cordapp $name in ${configFile.absolutePath}")
ConfigFactory.parseFile(configFile)
}
} else {
logger.info("No config found for cordapp $name in ${configFile.absolutePath}")
ConfigFactory.empty()
}
}
}

View File

@ -0,0 +1,7 @@
package net.corda.core.internal.cordapp
import com.typesafe.config.Config
interface CordappConfigProvider {
fun getConfigByName(name: String): Config
}

View File

@ -5,21 +5,27 @@ import net.corda.core.contracts.ContractClassName
import net.corda.core.cordapp.Cordapp
import net.corda.core.cordapp.CordappContext
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.cordapp.CordappConfigProvider
import net.corda.core.internal.createCordappContext
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.loggerFor
import java.net.URL
import java.util.concurrent.ConcurrentHashMap
/**
* Cordapp provider and store. For querying CorDapps for their attachment and vice versa.
*/
open class CordappProviderImpl(private val cordappLoader: CordappLoader, attachmentStorage: AttachmentStorage) : SingletonSerializeAsToken(), CordappProviderInternal {
open class CordappProviderImpl(private val cordappLoader: CordappLoader, private val cordappConfigProvider: CordappConfigProvider, attachmentStorage: AttachmentStorage) : SingletonSerializeAsToken(), CordappProviderInternal {
companion object {
private val log = loggerFor<CordappProviderImpl>()
}
private val contextCache = ConcurrentHashMap<Cordapp, CordappContext>()
override fun getAppContext(): CordappContext {
// TODO: Use better supported APIs in Java 9
Exception().stackTrace.forEach { stackFrame ->
@ -51,7 +57,7 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader, attachm
private fun loadContractsIntoAttachmentStore(attachmentStorage: AttachmentStorage): Map<SecureHash, URL> {
val cordappsWithAttachments = cordapps.filter { !it.contractClassNames.isEmpty() }.map { it.jarPath }
val attachmentIds = cordappsWithAttachments.map { it.openStream().use { attachmentStorage.importOrGetAttachment(it) }}
val attachmentIds = cordappsWithAttachments.map { it.openStream().use { attachmentStorage.importOrGetAttachment(it) } }
return attachmentIds.zip(cordappsWithAttachments).toMap()
}
@ -62,7 +68,14 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader, attachm
* @return A cordapp context for the given CorDapp
*/
fun getAppContext(cordapp: Cordapp): CordappContext {
return CordappContext(cordapp, getCordappAttachmentId(cordapp), cordappLoader.appClassLoader)
return contextCache.computeIfAbsent(cordapp, {
createCordappContext(
cordapp,
getCordappAttachmentId(cordapp),
cordappLoader.appClassLoader,
TypesafeCordappConfig(cordappConfigProvider.getConfigByName(cordapp.name))
)
})
}
/**

View File

@ -0,0 +1,79 @@
package net.corda.node.internal.cordapp
import com.typesafe.config.Config
import com.typesafe.config.ConfigException
import net.corda.core.cordapp.CordappConfig
import net.corda.core.cordapp.CordappConfigException
/**
* Provides configuration from a typesafe config source
*/
class TypesafeCordappConfig(private val cordappConfig: Config) : CordappConfig {
override fun exists(path: String): Boolean {
return cordappConfig.hasPath(path)
}
override fun get(path: String): Any {
try {
return cordappConfig.getAnyRef(path)
} catch (e: ConfigException) {
throw CordappConfigException("Cordapp configuration is incorrect due to exception", e)
}
}
override fun getInt(path: String): Int {
try {
return cordappConfig.getInt(path)
} catch (e: ConfigException) {
throw CordappConfigException("Cordapp configuration is incorrect due to exception", e)
}
}
override fun getLong(path: String): Long {
try {
return cordappConfig.getLong(path)
} catch (e: ConfigException) {
throw CordappConfigException("Cordapp configuration is incorrect due to exception", e)
}
}
override fun getFloat(path: String): Float {
try {
return cordappConfig.getDouble(path).toFloat()
} catch (e: ConfigException) {
throw CordappConfigException("Cordapp configuration is incorrect due to exception", e)
}
}
override fun getDouble(path: String): Double {
try {
return cordappConfig.getDouble(path)
} catch (e: ConfigException) {
throw CordappConfigException("Cordapp configuration is incorrect due to exception", e)
}
}
override fun getNumber(path: String): Number {
try {
return cordappConfig.getNumber(path)
} catch (e: ConfigException) {
throw CordappConfigException("Cordapp configuration is incorrect due to exception", e)
}
}
override fun getString(path: String): String {
try {
return cordappConfig.getString(path)
} catch (e: ConfigException) {
throw CordappConfigException("Cordapp configuration is incorrect due to exception", e)
}
}
override fun getBoolean(path: String): Boolean {
try {
return cordappConfig.getBoolean(path)
} catch (e: ConfigException) {
throw CordappConfigException("Cordapp configuration is incorrect due to exception", e)
}
}
}

View File

@ -1,15 +1,27 @@
package net.corda.node.internal.cordapp
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import net.corda.core.internal.cordapp.CordappConfigProvider
import net.corda.core.node.services.AttachmentStorage
import net.corda.testing.node.MockCordappConfigProvider
import net.corda.testing.services.MockAttachmentStorage
import org.assertj.core.api.Assertions.assertThat
import org.junit.Assert
import org.junit.Before
import org.junit.Test
class CordappProviderImplTests {
companion object {
private val isolatedJAR = this::class.java.getResource("isolated.jar")!!
private val emptyJAR = this::class.java.getResource("empty.jar")!!
private companion object {
val isolatedJAR = this::class.java.getResource("isolated.jar")!!
// TODO: Cordapp name should differ from the JAR name
val isolatedCordappName = "isolated"
val emptyJAR = this::class.java.getResource("empty.jar")!!
val validConfig = ConfigFactory.parseString("key=value")
val stubConfigProvider = object : CordappConfigProvider {
override fun getConfigByName(name: String): Config = ConfigFactory.empty()
}
}
private lateinit var attachmentStore: AttachmentStorage
@ -22,7 +34,7 @@ class CordappProviderImplTests {
@Test
fun `isolated jar is loaded into the attachment store`() {
val loader = CordappLoader.createDevMode(listOf(isolatedJAR))
val provider = CordappProviderImpl(loader, attachmentStore)
val provider = CordappProviderImpl(loader, stubConfigProvider, attachmentStore)
val maybeAttachmentId = provider.getCordappAttachmentId(provider.cordapps.first())
Assert.assertNotNull(maybeAttachmentId)
@ -32,14 +44,14 @@ class CordappProviderImplTests {
@Test
fun `empty jar is not loaded into the attachment store`() {
val loader = CordappLoader.createDevMode(listOf(emptyJAR))
val provider = CordappProviderImpl(loader, attachmentStore)
val provider = CordappProviderImpl(loader, stubConfigProvider, attachmentStore)
Assert.assertNull(provider.getCordappAttachmentId(provider.cordapps.first()))
}
@Test
fun `test that we find a cordapp class that is loaded into the store`() {
val loader = CordappLoader.createDevMode(listOf(isolatedJAR))
val provider = CordappProviderImpl(loader, attachmentStore)
val provider = CordappProviderImpl(loader, stubConfigProvider, attachmentStore)
val className = "net.corda.finance.contracts.isolated.AnotherDummyContract"
val expected = provider.cordapps.first()
@ -50,9 +62,9 @@ class CordappProviderImplTests {
}
@Test
fun `test that we find an attachment for a cordapp contrat class`() {
fun `test that we find an attachment for a cordapp contract class`() {
val loader = CordappLoader.createDevMode(listOf(isolatedJAR))
val provider = CordappProviderImpl(loader, attachmentStore)
val provider = CordappProviderImpl(loader, stubConfigProvider, attachmentStore)
val className = "net.corda.finance.contracts.isolated.AnotherDummyContract"
val expected = provider.getAppContext(provider.cordapps.first()).attachmentId
val actual = provider.getContractAttachmentID(className)
@ -60,4 +72,16 @@ class CordappProviderImplTests {
Assert.assertNotNull(actual)
Assert.assertEquals(actual!!, expected)
}
@Test
fun `test cordapp configuration`() {
val configProvider = MockCordappConfigProvider()
configProvider.cordappConfigs.put(isolatedCordappName, validConfig)
val loader = CordappLoader.createDevMode(listOf(isolatedJAR))
val provider = CordappProviderImpl(loader, configProvider, attachmentStore)
val expected = provider.getAppContext(provider.cordapps.first()).config
assertThat(expected.getString("key")).isEqualTo("value")
}
}

View File

@ -0,0 +1,47 @@
package net.corda.node.internal.cordapp
import com.typesafe.config.ConfigFactory
import net.corda.core.cordapp.CordappConfigException
import org.junit.Test
import org.assertj.core.api.Assertions.assertThat
class TypesafeCordappConfigTests {
@Test
fun `test that all value types can be retrieved`() {
val config = ConfigFactory.parseString("string=string\nint=1\nfloat=1.0\ndouble=1.0\nnumber=2\ndouble=1.01\nbool=false")
val cordappConf = TypesafeCordappConfig(config)
assertThat(cordappConf.get("string")).isEqualTo("string")
assertThat(cordappConf.getString("string")).isEqualTo("string")
assertThat(cordappConf.getInt("int")).isEqualTo(1)
assertThat(cordappConf.getFloat("float")).isEqualTo(1.0F)
assertThat(cordappConf.getDouble("double")).isEqualTo(1.01)
assertThat(cordappConf.getNumber("number")).isEqualTo(2)
assertThat(cordappConf.getBoolean("bool")).isEqualTo(false)
}
@Test
fun `test a nested path`() {
val config = ConfigFactory.parseString("outer: { inner: string }")
val cordappConf = TypesafeCordappConfig(config)
assertThat(cordappConf.getString("outer.inner")).isEqualTo("string")
}
@Test
fun `test exists determines existence and lack of existence correctly`() {
val config = ConfigFactory.parseString("exists=exists")
val cordappConf = TypesafeCordappConfig(config)
assertThat(cordappConf.exists("exists")).isTrue()
assertThat(cordappConf.exists("notexists")).isFalse()
}
@Test(expected = CordappConfigException::class)
fun `test that an exception is thrown when trying to access a non-extant field`() {
val config = ConfigFactory.empty()
val cordappConf = TypesafeCordappConfig(config)
cordappConf.get("anything")
}
}

View File

@ -2,13 +2,13 @@ package net.corda.bank
import net.corda.finance.DOLLARS
import net.corda.finance.POUNDS
import net.corda.testing.node.internal.demorun.deployNodesThen
import net.corda.testing.node.internal.demorun.nodeRunner
import org.junit.Test
class BankOfCordaCordformTest {
@Test
fun `run demo`() {
BankOfCordaCordform().deployNodesThen {
BankOfCordaCordform().nodeRunner().scanPackages(listOf("net.corda.finance")).deployAndRunNodesThen {
IssueCash.requestWebIssue(30000.POUNDS)
IssueCash.requestRpcIssue(20000.DOLLARS)
}

View File

@ -25,8 +25,8 @@ private const val BOC_RPC_ADMIN_PORT = 10015
private const val BOC_WEB_PORT = 10007
class BankOfCordaCordform : CordformDefinition() {
// TODO: Readd finance dependency - will fail without it
init {
cordappPackages += "net.corda.finance"
node {
name(NOTARY_NAME)
notary(NotaryConfig(validating = true))
@ -65,7 +65,7 @@ class BankOfCordaCordform : CordformDefinition() {
object DeployNodes {
@JvmStatic
fun main(args: Array<String>) {
BankOfCordaCordform().deployNodes()
BankOfCordaCordform().nodeRunner().scanPackages(listOf("net.corda.finance")).deployAndRunNodes()
}
}

View File

@ -0,0 +1,23 @@
# Cordapp Configuration Sample
This sample shows a simple example of how to use per-cordapp configuration. It includes;
* A configuration file
* Gradle build file to show how to install your Cordapp configuration
* A flow that consumes the Cordapp configuration
## Usage
To run the sample you must first build it from the project root with;
./gradlew deployNodes
This will deploy the node with the configuration installed.
The relevant section is the ``deployNodes`` task.
## Running
* Windows: `build\nodes\runnodes`
* Mac/Linux: `./build/nodes/runnodes`
Once the nodes have started up and show a prompt you can now run your flow.

View File

@ -0,0 +1,54 @@
apply plugin: 'kotlin'
apply plugin: 'java'
apply plugin: 'net.corda.plugins.cordapp'
apply plugin: 'net.corda.plugins.cordformation'
dependencies {
cordaCompile project(":core")
cordaCompile project(":node-api")
cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts')
cordaCompile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts')
}
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
ext.rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]]
directory "./build/nodes"
node {
name "O=Notary Service,L=Zurich,C=CH"
notary = [validating : true]
p2pPort 10002
rpcSettings {
port 10003
adminPort 10004
}
}
node {
name "O=Bank A,L=London,C=GB"
p2pPort 10005
cordapps = []
rpcUsers = ext.rpcUsers
// This configures the default cordapp for this node
projectCordapp {
config "someStringValue=test"
}
rpcSettings {
port 10007
adminPort 10008
}
}
node {
name "O=Bank B,L=New York,C=US"
p2pPort 10009
cordapps = []
rpcUsers = ext.rpcUsers
// This configures the default cordapp for this node
projectCordapp {
config project.file("src/config.conf")
}
rpcSettings {
port 10011
adminPort 10012
}
}
}

View File

@ -0,0 +1,5 @@
someStringValue=hello world
someIntValue=1
nested: {
value: a string
}

View File

@ -0,0 +1,10 @@
package net.corda.configsample
import net.corda.core.flows.FlowLogic
class ConfigSampleFlow : FlowLogic<String>() {
override fun call(): String {
val config = serviceHub.getAppContext().config
return config.getString("someStringValue")
}
}

View File

@ -14,7 +14,7 @@ import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import java.nio.file.Paths
fun main(args: Array<String>) = BFTNotaryCordform().deployNodes()
fun main(args: Array<String>) = BFTNotaryCordform().nodeRunner().deployAndRunNodes()
private val clusterSize = 4 // Minimum size that tolerates a faulty replica.
private val notaryNames = createNotaryNames(clusterSize)

View File

@ -1,9 +1,9 @@
package net.corda.notarydemo
import net.corda.testing.node.internal.demorun.clean
import net.corda.testing.node.internal.demorun.nodeRunner
fun main(args: Array<String>) {
listOf(SingleNotaryCordform(), RaftNotaryCordform(), BFTNotaryCordform()).forEach {
listOf(SingleNotaryCordform(), RaftNotaryCordform(), BFTNotaryCordform()).map { it.nodeRunner() }.forEach {
it.clean()
}
}

View File

@ -9,7 +9,7 @@ import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import java.nio.file.Paths
fun main(args: Array<String>) = CustomNotaryCordform().deployNodes()
fun main(args: Array<String>) = CustomNotaryCordform().nodeRunner().deployAndRunNodes()
class CustomNotaryCordform : CordformDefinition() {
init {

View File

@ -13,7 +13,7 @@ import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import java.nio.file.Paths
fun main(args: Array<String>) = RaftNotaryCordform().deployNodes()
fun main(args: Array<String>) = RaftNotaryCordform().nodeRunner().deployAndRunNodes()
internal fun createNotaryNames(clusterSize: Int) = (0 until clusterSize).map { CordaX500Name("Notary Service $it", "Zurich", "CH") }

View File

@ -11,7 +11,7 @@ import net.corda.testing.node.User
import net.corda.testing.node.internal.demorun.*
import java.nio.file.Paths
fun main(args: Array<String>) = SingleNotaryCordform().deployNodes()
fun main(args: Array<String>) = SingleNotaryCordform().nodeRunner().deployAndRunNodes()
val notaryDemoUser = User("demou", "demop", setOf(all()))

View File

@ -68,7 +68,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
name "O=Notary Service,L=Zurich,C=CH"
notary = [validating : true]
p2pPort 10002
cordapps = ["$project.group:finance:$corda_release_version"]
cordapp project(':finance')
extraConfig = [
jvmArgs : [ "-Xmx1g"]
]
@ -81,7 +81,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
address("localhost:10016")
adminAddress("localhost:10017")
}
cordapps = ["$project.group:finance:$corda_release_version"]
cordapp project(':finance')
rpcUsers = ext.rpcUsers
extraConfig = [
jvmArgs : [ "-Xmx1g"]
@ -95,7 +95,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
address("localhost:10026")
adminAddress("localhost:10027")
}
cordapps = ["$project.group:finance:$corda_release_version"]
cordapp project(':finance')
rpcUsers = ext.rpcUsers
extraConfig = [
jvmArgs : [ "-Xmx1g"]
@ -109,7 +109,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
address("localhost:10036")
adminAddress("localhost:10037")
}
cordapps = ["$project.group:finance:$corda_release_version"]
cordapp project(':finance')
rpcUsers = ext.rpcUsers
extraConfig = [
jvmArgs : [ "-Xmx1g"]

View File

@ -46,3 +46,4 @@ include 'samples:network-visualiser'
include 'samples:simm-valuation-demo'
include 'samples:notary-demo'
include 'samples:bank-of-corda-demo'
include 'samples:cordapp-configuration'

View File

@ -0,0 +1,77 @@
package net.corda.testing.node.internal.demorun
import net.corda.cordform.CordformDefinition
import net.corda.cordform.CordformNode
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.getOrThrow
import net.corda.testing.driver.JmxPolicy
import net.corda.testing.driver.PortAllocation
import net.corda.testing.node.internal.internalDriver
/**
* Creates a demo runner for this cordform definition
*/
fun CordformDefinition.nodeRunner() = CordformNodeRunner(this)
/**
* A node runner creates and runs nodes for a given [[CordformDefinition]].
*/
class CordformNodeRunner(val cordformDefinition: CordformDefinition) {
private var extraPackagesToScan = emptyList<String>()
/**
* Builder method to sets the extra cordapp scan packages
*/
fun scanPackages(packages: List<String>): CordformNodeRunner {
extraPackagesToScan = packages
return this
}
fun clean() {
System.err.println("Deleting: ${cordformDefinition.nodesDirectory}")
cordformDefinition.nodesDirectory.toFile().deleteRecursively()
}
/**
* Deploy the nodes specified in the given [CordformDefinition]. This will block until all the nodes and webservers
* have terminated.
*/
fun deployAndRunNodes() {
runNodes(waitForAllNodesToFinish = true) { }
}
/**
* Deploy the nodes specified in the given [CordformDefinition] and then execute the given [block] once all the nodes
* and webservers are up. After execution all these processes will be terminated.
*/
fun deployAndRunNodesThen(block: () -> Unit) {
runNodes(waitForAllNodesToFinish = false, block = block)
}
private fun runNodes(waitForAllNodesToFinish: Boolean, block: () -> Unit) {
clean()
val nodes = cordformDefinition.nodeConfigurers.map { configurer -> CordformNode().also { configurer.accept(it) } }
val maxPort = nodes
.flatMap { listOf(it.p2pAddress, it.rpcAddress, it.webAddress) }
.mapNotNull { address -> address?.let { NetworkHostAndPort.parse(it).port } }
.max()!!
internalDriver(
isDebug = true,
jmxPolicy = JmxPolicy(true),
driverDirectory = cordformDefinition.nodesDirectory,
extraCordappPackagesToScan = extraPackagesToScan,
// Notaries are manually specified in Cordform so we don't want the driver automatically starting any
notarySpecs = emptyList(),
// Start from after the largest port used to prevent port clash
portAllocation = PortAllocation.Incremental(maxPort + 1),
waitForAllNodesToFinish = waitForAllNodesToFinish
) {
cordformDefinition.setup(this)
startCordformNodes(nodes).getOrThrow() // Only proceed once everything is up and running
println("All nodes and webservers are ready...")
block()
}
}
}

View File

@ -1,57 +0,0 @@
@file:JvmName("DemoRunner")
package net.corda.testing.node.internal.demorun
import net.corda.cordform.CordformDefinition
import net.corda.cordform.CordformNode
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.getOrThrow
import net.corda.testing.driver.JmxPolicy
import net.corda.testing.driver.PortAllocation
import net.corda.testing.node.internal.internalDriver
fun CordformDefinition.clean() {
System.err.println("Deleting: $nodesDirectory")
nodesDirectory.toFile().deleteRecursively()
}
/**
* Deploy the nodes specified in the given [CordformDefinition]. This will block until all the nodes and webservers
* have terminated.
*/
fun CordformDefinition.deployNodes() {
runNodes(waitForAllNodesToFinish = true) { }
}
/**
* Deploy the nodes specified in the given [CordformDefinition] and then execute the given [block] once all the nodes
* and webservers are up. After execution all these processes will be terminated.
*/
fun CordformDefinition.deployNodesThen(block: () -> Unit) {
runNodes(waitForAllNodesToFinish = false, block = block)
}
private fun CordformDefinition.runNodes(waitForAllNodesToFinish: Boolean, block: () -> Unit) {
clean()
val nodes = nodeConfigurers.map { configurer -> CordformNode().also { configurer.accept(it) } }
val maxPort = nodes
.flatMap { listOf(it.p2pAddress, it.rpcAddress, it.webAddress) }
.mapNotNull { address -> address?.let { NetworkHostAndPort.parse(it).port } }
.max()!!
internalDriver(
isDebug = true,
jmxPolicy = JmxPolicy(true),
driverDirectory = nodesDirectory,
extraCordappPackagesToScan = cordappPackages,
// Notaries are manually specified in Cordform so we don't want the driver automatically starting any
notarySpecs = emptyList(),
// Start from after the largest port used to prevent port clash
portAllocation = PortAllocation.Incremental(maxPort + 1),
waitForAllNodesToFinish = waitForAllNodesToFinish
) {
setup(this)
startCordformNodes(nodes).getOrThrow() // Only proceed once everything is up and running
println("All nodes and webservers are ready...")
block()
}
}

View File

@ -0,0 +1,17 @@
package net.corda.testing.node
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import net.corda.core.internal.cordapp.CordappConfigProvider
class MockCordappConfigProvider : CordappConfigProvider {
val cordappConfigs = mutableMapOf<String, Config> ()
override fun getConfigByName(name: String): Config {
return if(cordappConfigs.containsKey(name)) {
cordappConfigs[name]!!
} else {
ConfigFactory.empty()
}
}
}

View File

@ -7,10 +7,17 @@ import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage
import net.corda.node.internal.cordapp.CordappLoader
import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.testing.node.MockCordappConfigProvider
import java.nio.file.Paths
import java.util.*
class MockCordappProvider(cordappLoader: CordappLoader, attachmentStorage: AttachmentStorage) : CordappProviderImpl(cordappLoader, attachmentStorage) {
class MockCordappProvider(
cordappLoader: CordappLoader,
attachmentStorage: AttachmentStorage,
val cordappConfigProvider: MockCordappConfigProvider = MockCordappConfigProvider()
) : CordappProviderImpl(cordappLoader, cordappConfigProvider, attachmentStorage) {
constructor(cordappLoader: CordappLoader, attachmentStorage: AttachmentStorage) : this(cordappLoader, attachmentStorage, MockCordappConfigProvider())
val cordappRegistry = mutableListOf<Pair<Cordapp, AttachmentId>>()
fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage) {