diff --git a/.ci/Jenkinsfile b/.ci/Jenkinsfile index 02c6491..41fc224 100644 --- a/.ci/Jenkinsfile +++ b/.ci/Jenkinsfile @@ -1,5 +1,5 @@ -@Library('corda-shared-build-pipeline-steps@chrisbarratt/CORE-8075-create-pipeline-for-running-the-autotester') _ - +@Library('corda-shared-build-pipeline-steps@5.0') _ + cordaPipeline( nexusAppId: 'com.corda.CSDE-Java.5.0', publishRepoPrefix: '', diff --git a/README.md b/README.md index 88c9652..d7ef618 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # CSDE-cordapp-template-java +## Note: This cut of CSDE is work in progress and has not been released yet, hence may not function as expected. + + To help make the process of prototyping Cordapps on Developer Preview 2 more straight forward we have developed the Cordapp Standard Development Environment (CSDE). The CSDE is obtained by cloning this CSDE-Cordapp-Template-java to your local machine. The CSDE provides: diff --git a/build.gradle b/build.gradle index f4962e6..9cb78fe 100644 --- a/build.gradle +++ b/build.gradle @@ -3,11 +3,6 @@ import static org.gradle.api.JavaVersion.VERSION_11 plugins { id 'org.jetbrains.kotlin.jvm' - // Include the cordapp-cpb plugin. This automatically includes the cordapp-cpk plugin as well. - // These extend existing build environment so that CPB and CPK files can be built. - // This includes a CorDapp DSL that allows the developer to supply metadata for the CorDapp - // required by Corda. - id 'net.corda.plugins.cordapp-cpb2' id 'net.corda.cordapp.cordapp-configuration' id 'org.jetbrains.kotlin.plugin.jpa' @@ -18,110 +13,39 @@ plugins { id 'csde' } -group 'com.r3.hellocorda' -version '1.0-SNAPSHOT' +allprojects { + group 'com.r3.hellocorda' + version '1.0-SNAPSHOT' -def javaVersion = VERSION_11 + def javaVersion = VERSION_11 -// The CordApp section. -// This is part of the DSL provided by the corda plugins to define metadata for our CorDapp. -// Each component of the CorDapp would get its own CorDapp section in the build.gradle file for the component’s -// subproject. -// This is required by the corda plugins to build the CorDapp. -cordapp { - // "targetPlatformVersion" and "minimumPlatformVersion" are intended to specify the preferred - // and earliest versions of the Corda platform that the CorDapp will run on respectively. - // Enforced versioning has not implemented yet so we need to pass in a dummy value for now. - // The platform version will correspond to and be roughly equivalent to the Corda API version. - targetPlatformVersion platformVersion.toInteger() - minimumPlatformVersion platformVersion.toInteger() - - // The cordapp section contains either a workflow or contract subsection depending on the type of component. - // Declares the type and metadata of the CPK (this CPB has one CPK). - workflow { - name "ModuleNameHere" - versionId 1 - vendor "VendorNameHere" + // Declare the set of Java compiler options we need to build a CorDapp. + tasks.withType(JavaCompile) { + // -parameters - Needed for reflection and serialization to work correctly. + options.compilerArgs += [ + "-parameters" + ] } -} -// Declare the set of Kotlin compiler options we need to build a CorDapp. -tasks.withType(JavaCompile) { - - // -parameters - Needed for reflection and serialization to work correctly. - options.compilerArgs += [ - "-parameters" - ] -} - -repositories { - // All dependencies are held in Maven Central - mavenCentral() - maven { - url = "$artifactoryContextUrl/" + repositories { + // All dependencies are held in Maven Central + mavenCentral() } -} -// Declare dependencies for the modules we will use. -// A cordaProvided declaration is required for anything that we use that the Corda API provides. -// This is required to allow us to build CorDapp modules as OSGi bundles that CPI and CPB files are built on. -dependencies { - // We need a version of kotlin-stdlib-jdk8 built as an OSGi bundle, this is "kotlin-stdlib-jdk8-osgi". - // R3 builds kotlin-stdlib-jdk8-osgi from Kotlin's kotlin-stdlib-jdk8. - // NB: - // Kotlin's kotlin-osgi-bundle does not provide all of the Kotlin API that is required, - // There is no kotlin-stdlib-jdk11, but one is not needed even though we are targetting Java 11. - cordaProvided 'net.corda.kotlin:kotlin-stdlib-jdk8-osgi' + tasks.withType(Test).configureEach { + useJUnitPlatform() + } - // Declare a "platform" so that we use the correct set of dependency versions for the version of the - // Corda API specified. - cordaProvided platform("net.corda:corda-api:$cordaApiVersion") - - // If using transistive dependencies this will provide most of Corda-API: - // cordaProvided 'net.corda:corda-application' - - // Alternatively we can explicitly specify all our Corda-API dependencies: - cordaProvided 'net.corda:corda-base' - cordaProvided 'net.corda:corda-application' - cordaProvided 'net.corda:corda-crypto' - cordaProvided 'net.corda:corda-membership' - // cordaProvided 'net.corda:corda-persistence' - cordaProvided 'net.corda:corda-serialization' - - // Not yet fully implemented: - // cordaProvided 'net.corda:corda-ledger' - - // The CorDapp uses the slf4j logging framework. Corda-API provides this so we need a 'cordaProvided' declaration. - cordaProvided 'org.slf4j:slf4j-api' - - // Dependencies Required By Test Tooling - testImplementation "net.corda:corda-simulator-api:$simulatorVersion" - testRuntimeOnly "net.corda:corda-simulator-runtime:$simulatorVersion" - - // 3rd party libraries - // Required - testImplementation "org.slf4j:slf4j-simple:2.0.0" - testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" - - // Optional but used by exmaple tests. - testImplementation "org.mockito:mockito-core:$mockitoVersion" - testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" - testImplementation "org.hamcrest:hamcrest-library:$hamcrestVersion" - -} - - -test { - useJUnitPlatform() } publishing { publications { - maven(MavenPublication) { - artifactId "corda-CSDE-java-sample" - groupId project.group - artifact jar - } + maven(MavenPublication) { + artifactId "corda-CSDE-java-sample" + groupId project.group + artifact jar } + + } } + diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 49f7a1f..750b4c5 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -6,9 +6,6 @@ plugins { repositories { mavenCentral() - maven { - url = "$artifactoryContextUrl/" - } } dependencies { diff --git a/buildSrc/gradle.properties b/buildSrc/gradle.properties index 3636833..a0d6994 100644 --- a/buildSrc/gradle.properties +++ b/buildSrc/gradle.properties @@ -1,6 +1,4 @@ -jacksonVersion = 2.13.3 +jacksonVersion = 2.13.4 unirestVersion=3.13.10 -cordaApiVersion=5.0.0.505-Beta1.0-HC00 -# R3 internal repository -artifactoryContextUrl=https://staging.download.corda.net/maven/20ede3c6-29c0-11ed-966d-b7c36748b9f6-Beta1.0-HC00 +cordaApiVersion=5.0.0.523-Fox1.0 diff --git a/buildSrc/src/main/groovy/csde.gradle b/buildSrc/src/main/groovy/csde.gradle index f2fe6a4..9e6c525 100644 --- a/buildSrc/src/main/groovy/csde.gradle +++ b/buildSrc/src/main/groovy/csde.gradle @@ -1,4 +1,24 @@ -import com.r3.csde.CsdeRpcInterface +// Note, IntelliJ does not recognise the imported Java Classes, hence they are +// highlighted in Red. However, they are recognised in the gradle compilation. + + +// todo: look at the declaration of the script variables, can they be combined with the declaration of the Project Context +// todo: investigate adding corda-cli to the class path then executing it directly - might not work as gradle has to set up the jar file, so its not their when you start. +// Todo: write a test flow runner helper function?? +// todo: rename deployCPIsHelper +// todo: add proper logging, rather than reading Stdout +// todo: add test corda running/live task +// todo: add a test to check docker is running and display error if not + halt start corda +// todo: add a clean corda task. + + +import com.r3.csde.CordaLifeCycleHelper +import com.r3.csde.ProjectContext +import com.r3.csde.DeployCPIsHelper +import com.r3.csde.BuildCPIsHelper +import com.r3.csde.ProjectUtils +import com.r3.csde.CordaStatusQueries +import com.r3.csde.CreateAndRegisterVNodesHelper plugins { id 'java-library' @@ -18,21 +38,37 @@ configurations { canBeResolved = true } + notaryServerCPB { + canBeConsumed = false + canBeResolved = true + } } // Dependencies for supporting tools dependencies { combinedWorker "net.corda:corda-combined-worker:$combinedWorkerVersion" myPostgresJDBC 'org.postgresql:postgresql:42.4.1' + notaryServerCPB("com.r3.corda.notary.plugin.nonvalidating:notary-plugin-non-validating-server:$cordaNotaryPluginsVersion") { + artifact { + classifier = 'package' + extension = 'cpb' + } + } + implementation "org.codehaus.groovy:groovy-json:3.0.9" } +// task groupings +def cordaGroup = 'csde-corda' // corda lifecycle tasks +def cordappGroup = 'csde-cordapp' // tasks to build and deploy corDapps +def queriesGroup = 'csde-queries' // tasks which query corda status +def supportingGroup = 'supporting' // tasks which should be hidden from the csde user -def pluginGroupName = "CSDE" -def pluginImplGroupName = "other" -def cordaBinDir= System.getProperty('user.home') + "/.corda/corda5" -def cordaCliBinDir = System.getProperty('user.home') + "/.corda/cli" + +def cordaBinDir = System.getenv("CSDE_CORDA_BIN") ?: System.getProperty('user.home') + "/.corda/corda5" +def cordaCliBinDir = System.getenv("CSDE_CORDA_CLI") ?:System.getProperty('user.home') + "/.corda/cli" def cordaJDBCDir = cordaBinDir + "/jdbcDrivers" +def cordaNotaryServerDir = cordaBinDir + "/notaryserver" def signingCertAlias="gradle-plugin-default-key" // Get error if this is not a autotyped object // def signingCertFName = "$rootDir/config/gradle-plugin-default-key.pem" @@ -41,25 +77,75 @@ def keystoreAlias = "my-signing-key" def keystoreFName = devEnvWorkspace + "/signingkeys.pfx" def keystoreCertFName = devEnvWorkspace + "/signingkey1.pem" def combiWorkerPidCacheFile = devEnvWorkspace + "/CordaPID.dat" +// todo: can we rely on the build directory always being /workflow/build? aslo, is the +// workflow directory the correct place to build the cpb to. shoudl it be the main build directory. +def workflowBuildDir = rootDir.toString() + "/workflows/build" -// Need to read things from cordapp plugin -def cpiName = 'cpi name' +// todo: Need to read things from cordapp plugin - the cordapp names will be changed by the user +def appCpiName = 'cpi name' +def notaryCpiName = 'CSDE Notary Server CPI' -def csdeHelper = new CsdeRpcInterface(project, + +// todo: there should be a better way to set up these project context variables. +def projectContext = new ProjectContext(project, cordaClusterURL.toString(), cordaRpcUser, cordaRpcPasswd, devEnvWorkspace, + // todo: why is this not obtained in the groovy def's abouve - its inconsistent. new String("${System.getProperty("java.home")}/bin"), dbContainerName, cordaJDBCDir, - combiWorkerPidCacheFile + combiWorkerPidCacheFile, + signingCertAlias, + signingCertFName, + keystoreAlias, + keystoreFName, + keystoreCertFName, + appCpiName, + notaryCpiName, + devEnvWorkspace, + cordaCliBinDir, + cordaNotaryServerDir, + workflowBuildDir, + cordaNotaryPluginsVersion ) +def utils = new ProjectUtils() + +// Initiate workspace folder + +tasks.register('projInit') { + group = supportingGroup + doLast { + mkdir devEnvWorkspace + } +} + + +// CordaLifeCycle tasks + +def cordaLifeCycle = new CordaLifeCycleHelper(projectContext) + +tasks.register("startCorda") { + group = cordaGroup + dependsOn('getDevCordaLite', 'getPostgresJDBC') + doLast { + mkdir devEnvWorkspace + cordaLifeCycle.startCorda() + } +} + +tasks.register("stopCorda") { + group = cordaGroup + doLast { + cordaLifeCycle.stopCorda() + } +} tasks.register("getPostgresJDBC") { - group = pluginImplGroupName + group = supportingGroup doLast { copy { from configurations.myPostgresJDBC @@ -68,183 +154,99 @@ tasks.register("getPostgresJDBC") { } } -tasks.register('projInit') { - group = pluginImplGroupName - doLast { - mkdir devEnvWorkspace - } -} - -tasks.register("createGroupPolicy") { - group = pluginImplGroupName - dependsOn('projInit') - doLast { - - def groupPolicyFName = new String("${devEnvWorkspace}/GroupPolicy.json") - def devnetFName = new String("$rootDir/config/dev-net.json") - File groupPolicyFile = new File(groupPolicyFName) - File devnetFile = new File(devnetFName) - if (!groupPolicyFile.exists() || groupPolicyFile.lastModified() < devnetFile.lastModified()) { - def configX500Ids = csdeHelper.getConfigX500Ids() - - println("createGroupPolicy: Creating a GroupPolicy") - - javaexec { - classpath = files("$cordaCliBinDir/corda-cli.jar") - jvmArgs = ["-Dpf4j.pluginsDir=$cordaCliBinDir/plugins/"] - standardOutput = new FileOutputStream(groupPolicyFName) - LinkedList myArgs = new LinkedList() - myArgs.add("mgm") - myArgs.add("groupPolicy") - configX500Ids.forEach { - myArgs.add("--name") - myArgs.add("$it") - } - - myArgs.add("--endpoint-protocol=1") - myArgs.add("--endpoint=http://localhost:1080") - args = myArgs - } - - } else { - println("createPolicyTask: everything up to date; nothing to do.") - } - - } -} - tasks.register("getDevCordaLite", Copy) { - group = pluginImplGroupName + group = supportingGroup from configurations.combinedWorker into cordaBinDir } -tasks.register('createKeystore') { - group = pluginImplGroupName - dependsOn('projInit') - doLast { - File keystoreFile = new File(keystoreFName) - if(!keystoreFile.exists()) { - println('createKeystore: Create a keystore') - exec { - commandLine "${System.getProperty("java.home")}/bin/keytool", "-genkeypair", - "-alias", keystoreAlias, - "-keystore", keystoreFName, - "-storepass", "keystore password", - "-dname", "CN=CPI Example - My Signing Key, O=CorpOrgCorp, L=London, C=GB", - "-keyalg", "RSA", - "-storetype", "pkcs12", - "-validity", "4000" - } - // Need to add the default signing key to the keystore - exec { - commandLine "${System.getProperty("java.home")}/bin/keytool", "-importcert", - "-keystore", keystoreFName, - "-storepass", "keystore password", - "-noprompt", - "-alias", signingCertAlias, - "-file", signingCertFName - } - // keytool -exportcert -rfc -alias "signing key 1" -keystore signingkeys.pfx -storepass "keystore password" -file signingkey1.pem - exec { - commandLine "${System.getProperty("java.home")}/bin/keytool", - "-exportcert", "-rfc", "-alias", keystoreAlias, - "-keystore", keystoreFName, - "-storepass", "keystore password", - "-file", keystoreCertFName - } - } - else { - println('createKeystore: keystore already created; nothing to do.') - } - } -} +// Corda status queries +def cordaStatusQueries = new CordaStatusQueries(projectContext) -tasks.register('buildCPI') { - group = pluginGroupName - dependsOn('build', 'createGroupPolicy', 'createKeystore') - - doLast{ - def cpiFile= buildDir.toString() + "/" + project.archivesBaseName + "-" + project.version + ".cpi" - delete { delete cpiFile } - File srcDir - srcDir = file('build/libs') - - // Create a file collection using a closure - def collection = layout.files { srcDir.listFiles() } - def cpbs = collection.filter { it.getName().endsWith(".cpb") } - - javaexec { - classpath = files("$cordaCliBinDir/corda-cli.jar") - jvmArgs = ["-Dpf4j.pluginsDir=$cordaCliBinDir/plugins/"] - args = ['package', 'create-cpi', - '--cpb', cpbs.singleFile.absolutePath, - '--group-policy', "${devEnvWorkspace}/GroupPolicy.json", - '--cpi-name', cpiName, - '--cpi-version', project.version, - '--file', cpiFile, - '--keystore', "${devEnvWorkspace}/signingkeys.pfx", - '--storepass', 'keystore password', - '--key', 'my-signing-key' ] - } - } - -} - -tasks.register("deployCPI") { - group = pluginImplGroupName - dependsOn('buildCPI') - doLast { - csdeHelper.uploadCertificate(signingCertAlias, signingCertFName) - csdeHelper.uploadCertificate(keystoreAlias, keystoreCertFName) - csdeHelper.deployCPI("${buildDir}/${project.archivesBaseName}-${project.version}.cpi", cpiName, project.version) - } -} - -tasks.register("createAndRegVNodes") { - group = pluginImplGroupName - dependsOn('deployCPI') - doLast { - csdeHelper.createAndRegVNodes() - } -} tasks.register('listVNodes') { - group = pluginGroupName + group = queriesGroup doLast { - csdeHelper.listVNodes() + cordaStatusQueries.listVNodes() } } tasks.register('listCPIs') { - group = pluginImplGroupName + group = queriesGroup doLast { - csdeHelper.listCPIs() + cordaStatusQueries.listCPIs() + } +} + +// Build CPI tasks + +def buildCPIsHelper = new BuildCPIsHelper(projectContext) + +tasks.register("1-createGroupPolicy") { + group = cordappGroup + dependsOn('projInit') + doLast { + buildCPIsHelper.createGroupPolicy() + } +} + +tasks.register("getNotaryServerCPB", Copy) { + group = supportingGroup + from configurations.notaryServerCPB + into cordaNotaryServerDir +} + +tasks.register('2-createKeystore') { + group = cordappGroup + dependsOn('projInit') + doLast { + buildCPIsHelper.createKeyStore() + } +} + +tasks.register('3-buildCPIs') { + group = cordappGroup + def dependsOnTasks = subprojects.collect {it.tasks.findByName("build") } + dependsOnTasks.add('1-createGroupPolicy') + dependsOnTasks.add('2-createKeystore') + dependsOnTasks.add('getNotaryServerCPB') + dependsOn dependsOnTasks + doLast{ + buildCPIsHelper.buildCPIs() + } +} + + +// deploy CPI tasks + +def deployCPIsHelper = new DeployCPIsHelper(projectContext) + +tasks.register("4-deployCPIs") { + group = cordappGroup + dependsOn('3-buildCPIs') + doLast { + deployCPIsHelper.deployCPIs() + } +} + +// create and register Vnodes Tasks + +def createAndRegisterVNodesHelper = new CreateAndRegisterVNodesHelper(projectContext) + +tasks.register("5-createAndRegVNodes") { + group = cordappGroup + dependsOn('4-deployCPIs') + doLast { + createAndRegisterVNodesHelper.createAndRegVNodes() } } // Empty task, just acts as the Task user entry point task. -tasks.register('deployCordapp') { - group = pluginGroupName - dependsOn("createAndRegVNodes") -} - -tasks.register("startCorda") { - group = pluginGroupName - dependsOn('getDevCordaLite', 'getPostgresJDBC') - doLast { - mkdir devEnvWorkspace - csdeHelper.startCorda() - } -} - -tasks.register("stopCorda") { - group = pluginGroupName - doLast { - csdeHelper.stopCorda() - } +tasks.register('quickDeployCordapp') { + group = cordappGroup + dependsOn("5-createAndRegVNodes") } diff --git a/buildSrc/src/main/java/com/r3/csde/BuildCPIsHelper.java b/buildSrc/src/main/java/com/r3/csde/BuildCPIsHelper.java new file mode 100644 index 0000000..7b20764 --- /dev/null +++ b/buildSrc/src/main/java/com/r3/csde/BuildCPIsHelper.java @@ -0,0 +1,270 @@ +package com.r3.csde; + +import java.io.*; +import java.util.LinkedList; + +public class BuildCPIsHelper { + + public ProjectContext pc; + public ProjectUtils utils ; + public BuildCPIsHelper(ProjectContext _pc) { + pc = _pc; + utils = new ProjectUtils(pc); + } + + public void createGroupPolicy() throws IOException { + + File groupPolicyFile = new File(String.format("%s/GroupPolicy.json", pc.devEnvWorkspace)); + File devnetFile = new File(String.format("%s/config/dev-net.json", pc.project.getRootDir())); + + if (!groupPolicyFile.exists() || groupPolicyFile.lastModified() < devnetFile.lastModified()) { + + pc.out.println("createGroupPolicy: Creating a GroupPolicy"); + + LinkedList configX500Ids = utils.getConfigX500Ids(pc.X500ConfigFile); + LinkedList commandList = new LinkedList<>(); + + commandList.add(String.format("%s/java", pc.javaBinDir)); + commandList.add(String.format("-Dpf4j.pluginsDir=%s/plugins/", pc.cordaCliBinDir)); + commandList.add("-jar"); + commandList.add(String.format("%s/corda-cli.jar", pc.cordaCliBinDir)); + commandList.add("mgm"); + commandList.add("groupPolicy"); + for (String id : configX500Ids) { + commandList.add("--name"); + commandList.add(id); + } + commandList.add("--endpoint-protocol=1"); + commandList.add("--endpoint=http://localhost:1080"); + + ProcessBuilder pb = new ProcessBuilder(commandList); + pb.redirectErrorStream(true); + Process proc = pb.start(); + BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream())); + + // todo add exception catching + FileWriter fileWriter = new FileWriter(groupPolicyFile); + String line; + while (( line = reader.readLine()) != null){ + fileWriter.write(line + "\n"); + } + fileWriter.close(); + + } else { + pc.out.println("createPolicyTask: everything up to date; nothing to do."); + } + + } + + public void createKeyStore() throws IOException, InterruptedException { + + File keystoreFile = new File(pc.keystoreFName); + if(!keystoreFile.exists()) { + pc.out.println("createKeystore: Create a keystore"); + + generateKeyPair(); + addDefaultSigningKey(); + exportCert(); + + } else { + pc.out.println("createKeystore: keystore already created; nothing to do."); + } + + } + + private void generateKeyPair() throws IOException, InterruptedException { + + LinkedList cmdArray = new LinkedList<>(); + + cmdArray.add(pc.javaBinDir + "/keytool"); + cmdArray.add("-genkeypair"); + cmdArray.add("-alias"); + cmdArray.add(pc.keystoreAlias); + cmdArray.add("-keystore"); + cmdArray.add(pc.keystoreFName); + cmdArray.add("-storepass"); + cmdArray.add("keystore password"); + cmdArray.add("-dname"); + cmdArray.add("CN=CPI Example - My Signing Key, O=CorpOrgCorp, L=London, C=GB"); + cmdArray.add("-keyalg"); + cmdArray.add("RSA"); + cmdArray.add("-storetype"); + cmdArray.add("pkcs12"); + cmdArray.add("-validity"); + cmdArray.add("4000"); + + ProcessBuilder pb = new ProcessBuilder(cmdArray); + pb.redirectErrorStream(true); + Process proc = pb.start(); + proc.waitFor(); + + } + + private void addDefaultSigningKey() throws IOException, InterruptedException { + + LinkedList cmdArray = new LinkedList<>(); + + cmdArray.add(pc.javaBinDir + "/keytool"); + cmdArray.add("-importcert"); + cmdArray.add("-keystore"); + cmdArray.add(pc.keystoreFName); + cmdArray.add("-storepass"); + cmdArray.add("keystore password"); + cmdArray.add("-noprompt"); + cmdArray.add("-alias"); + cmdArray.add(pc.signingCertAlias); + cmdArray.add("-file"); + cmdArray.add(pc.signingCertFName); + + ProcessBuilder pb = new ProcessBuilder(cmdArray); + pb.redirectErrorStream(true); + Process proc = pb.start(); + proc.waitFor(); + } + + private void exportCert() throws IOException, InterruptedException { + + LinkedList cmdArray = new LinkedList<>(); + + cmdArray.add(pc.javaBinDir + "/keytool"); + cmdArray.add("-exportcert"); + cmdArray.add("-rfc"); + cmdArray.add("-alias"); + cmdArray.add(pc.keystoreAlias); + cmdArray.add("-keystore"); + cmdArray.add(pc.keystoreFName); + cmdArray.add("-storepass"); + cmdArray.add("keystore password"); + cmdArray.add("-file"); + cmdArray.add(pc.keystoreCertFName); + + ProcessBuilder pb = new ProcessBuilder(cmdArray); + pb.redirectErrorStream(true); + Process proc = pb.start(); + proc.waitFor(); + + } + + public void buildCPIs() throws IOException, InterruptedException, CsdeException { + createCorDappCPI(); + createNotaryCPI(); + } + + private void createCorDappCPI() throws IOException, InterruptedException, CsdeException { + + String appCPIFilePath = pc.workflowBuildDir + "/" + + pc.project.getRootProject().getName() + "-" + + pc.project.getVersion() + ".cpi"; + + File appCPIFile = new File(appCPIFilePath); + appCPIFile.delete(); + + File srcDir = new File(pc.workflowBuildDir + "/libs"); + File[] appCPBs = srcDir.listFiles(( x , name ) -> name.endsWith(".cpb")); + if (appCPBs == null) throw new CsdeException("Expecting exactly one CPB but no CPB found."); + if (appCPBs.length != 1) throw new CsdeException("Expecting exactly one CPB but more than one found."); + + pc.out.println("appCpbs:"); + pc.out.println(appCPBs[0].getAbsolutePath()); + + LinkedList commandList = new LinkedList<>(); + + commandList.add(String.format("%s/java", pc.javaBinDir)); + commandList.add(String.format("-Dpf4j.pluginsDir=%s/plugins/", pc.cordaCliBinDir)); + commandList.add("-jar"); + commandList.add(String.format("%s/corda-cli.jar", pc.cordaCliBinDir)); + commandList.add("package"); + commandList.add("create-cpi"); + commandList.add("--cpb"); + commandList.add(appCPBs[0].getAbsolutePath()); + commandList.add("--group-policy"); + commandList.add(pc.devEnvWorkspace + "/GroupPolicy.json"); + commandList.add("--cpi-name"); + commandList.add(pc.appCPIName); + commandList.add("--cpi-version"); + commandList.add(pc.project.getVersion().toString()); + commandList.add("--file"); + commandList.add(appCPIFilePath); + commandList.add("--keystore"); + commandList.add(pc.devEnvWorkspace + "/signingkeys.pfx"); + commandList.add("--storepass"); + commandList.add("keystore password"); + commandList.add("--key"); + commandList.add("my-signing-key"); // todo: should be passed as context property + + ProcessBuilder pb = new ProcessBuilder(commandList); + pb.redirectErrorStream(true); + Process proc = pb.start(); + proc.waitFor(); + +// todo: work out how to capture error code better than the following code + +// BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream())); +// File tempOutputFile = new File(String.format("%s/tempOutput.txt", pc.devEnvWorkspace)); +// tempOutputFile.delete(); +// FileWriter fileWriter = new FileWriter(tempOutputFile); +// String line; +// while (( line = reader.readLine()) != null){ +// fileWriter.write(line + "\n"); +// } +// fileWriter.close(); + + } + + private void createNotaryCPI() throws CsdeException, IOException, InterruptedException { + + String notaryCPIFilePath = pc.workflowBuildDir + "/" + + pc.notaryCPIName.replace(' ', '-').toLowerCase() + "-" + + pc.project.getVersion() + ".cpi"; + + File notaryCPIFile = new File(notaryCPIFilePath); + notaryCPIFile.delete(); + + File srcDir = new File(pc.cordaNotaryServiceDir); + File[] notaryCPBs = srcDir.listFiles(( x , name ) -> name.endsWith(".cpb") && name.contains(pc.cordaNotaryPluginsVersion)); + if (notaryCPBs == null) throw new CsdeException("Expecting exactly one notary CPB but no CPB found."); + if (notaryCPBs.length != 1) throw new CsdeException("Expecting exactly one notary CPB but more than one found."); + + pc.out.println("notaryCpbs:"); + pc.out.println(notaryCPBs[0]); + + LinkedList commandList = new LinkedList<>(); + + commandList.add(String.format("%s/java", pc.javaBinDir)); + commandList.add(String.format("-Dpf4j.pluginsDir=%s/plugins/", pc.cordaCliBinDir)); + commandList.add("-jar"); + commandList.add(String.format("%s/corda-cli.jar", pc.cordaCliBinDir)); + commandList.add("package"); + commandList.add("create-cpi"); + commandList.add("--cpb"); + commandList.add(notaryCPBs[0].getAbsolutePath()); + commandList.add("--group-policy"); + commandList.add(pc.devEnvWorkspace + "/GroupPolicy.json"); + commandList.add("--cpi-name"); + commandList.add(pc.notaryCPIName); + commandList.add("--cpi-version"); + commandList.add(pc.project.getVersion().toString()); + commandList.add("--file"); + commandList.add(notaryCPIFilePath); + commandList.add("--keystore"); + commandList.add(pc.devEnvWorkspace + "/signingkeys.pfx"); + commandList.add("--storepass"); + commandList.add("keystore password"); + commandList.add("--key"); + commandList.add("my-signing-key"); + + ProcessBuilder pb = new ProcessBuilder(commandList); + pb.redirectErrorStream(true); + Process proc = pb.start(); + proc.waitFor(); + + } + + // todo: this might be needed for improved logging + private void printCmdArray(LinkedList cmdArray) { + for (int i = 0; i < cmdArray.size(); i++) { + pc.out.print(cmdArray.get(i) + " "); + } + } + +} diff --git a/buildSrc/src/main/java/com/r3/csde/CordaLifeCycleHelper.java b/buildSrc/src/main/java/com/r3/csde/CordaLifeCycleHelper.java new file mode 100644 index 0000000..12a63dd --- /dev/null +++ b/buildSrc/src/main/java/com/r3/csde/CordaLifeCycleHelper.java @@ -0,0 +1,94 @@ +package com.r3.csde; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.Scanner; + +/** + * Manages Bringing corda up, testing for liveness and taking corda down + */ + +public class CordaLifeCycleHelper { + + ProjectContext pc; + ProjectUtils utils; + + public CordaLifeCycleHelper(ProjectContext _pc) { + pc = _pc; + utils = new ProjectUtils(pc); + } + + + public void startCorda() throws IOException { + PrintStream pidStore = new PrintStream(new FileOutputStream(pc.cordaPidCache)); + File combinedWorkerJar = pc.project.getConfigurations().getByName("combinedWorker").getSingleFile(); + + + // todo: make consistent with other ProcessBuilder set ups (use cmdArray) + new ProcessBuilder( + "docker", + "run", "-d", "--rm", + "-p", "5432:5432", + "--name", pc.dbContainerName, + "-e", "POSTGRES_DB=cordacluster", + "-e", "POSTGRES_USER=postgres", + "-e", "POSTGRES_PASSWORD=password", + "postgres:latest").start(); + // todo: is there a better way of doing this - ie poll for readiness + utils.rpcWait(10000); + + ProcessBuilder procBuild = new ProcessBuilder(pc.javaBinDir + "/java", + "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", + "-Dco.paralleluniverse.fibers.verifyInstrumentation=true", + "-jar", + combinedWorkerJar.toString(), + "--instanceId=0", + "-mbus.busType=DATABASE", + "-spassphrase=password", + "-ssalt=salt", + "-spassphrase=password", + "-ssalt=salt", + "-ddatabase.user=user", + "-ddatabase.pass=password", + "-ddatabase.jdbc.url=jdbc:postgresql://localhost:5432/cordacluster", + "-ddatabase.jdbc.directory="+pc.JDBCDir); + + + procBuild.redirectErrorStream(true); + Process proc = procBuild.start(); + pidStore.print(proc.pid()); + pc.out.println("Corda Process-id="+proc.pid()); + + // todo: should poll for readiness before returning + // Chris comment - We probably do not want to poll for readiness here. + // The combined-worker takes serveral minutes to come up. + // It might be better to warn the user of that and have the readiness detection and polling logic used in other tasks involved in creating v-nodes and deploying the CPI. + // Matt comment - I'm not sure I agree, we need to investigate + + } + + + public void stopCorda() throws IOException, CsdeException { + File cordaPIDFile = new File(pc.cordaPidCache); + if(cordaPIDFile.exists()) { + Scanner sc = new Scanner(cordaPIDFile); + long pid = sc.nextLong(); + pc.out.println("pid to kill=" + pid); + + if (System.getProperty("os.name").toLowerCase().contains("windows")) { + new ProcessBuilder("Powershell", "-Command", "Stop-Process", "-Id", Long.toString(pid), "-PassThru").start(); + } else { + new ProcessBuilder("kill", "-9", Long.toString(pid)).start(); + } + + Process proc = new ProcessBuilder("docker", "stop", pc.dbContainerName).start(); + + cordaPIDFile.delete(); + } + else { + throw new CsdeException("Cannot stop the Combined worker\nCached process ID file " + pc.cordaPidCache + " missing.\nWas the combined worker not started?"); + } + } +} diff --git a/buildSrc/src/main/java/com/r3/csde/CordaStatusQueries.java b/buildSrc/src/main/java/com/r3/csde/CordaStatusQueries.java new file mode 100644 index 0000000..a3ef066 --- /dev/null +++ b/buildSrc/src/main/java/com/r3/csde/CordaStatusQueries.java @@ -0,0 +1,63 @@ +package com.r3.csde; + +import kong.unirest.JsonNode; +import kong.unirest.Unirest; +import kong.unirest.json.JSONArray; +import kong.unirest.json.JSONObject; +import kong.unirest.HttpResponse; + +public class CordaStatusQueries { + + ProjectContext pc; + public CordaStatusQueries(ProjectContext _pc){ pc = _pc; } + + + public HttpResponse getVNodeInfo() { + Unirest.config().verifySsl(false); + return Unirest.get(pc.baseURL + "/api/v1/virtualnode/") + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + } + public void listVNodesVerbose() { + HttpResponse vnodeResponse = getVNodeInfo(); + pc.out.println("VNodes:\n" + vnodeResponse.getBody().toPrettyString()); + } + + // X500Name, shorthash, cpiname + public void listVNodes() { + HttpResponse vnodeResponse = getVNodeInfo(); + + JSONArray virtualNodesJson = (JSONArray) vnodeResponse.getBody().getObject().get("virtualNodes"); + pc.out.println("X500 Name\tHolding identity short hash\tCPI Name"); + for(Object o: virtualNodesJson){ + if(o instanceof JSONObject) { + JSONObject idObj = ((JSONObject) o).getJSONObject("holdingIdentity"); + JSONObject cpiObj = ((JSONObject) o).getJSONObject("cpiIdentifier"); + pc.out.print("\"" + idObj.get("x500Name") + "\""); + pc.out.print("\t\"" + idObj.get("shortHash") + "\""); + pc.out.println("\t\"" + cpiObj.get("cpiName") + "\""); + } + } + } + + public HttpResponse getCpiInfo() { + Unirest.config().verifySsl(false); + return Unirest.get(pc.baseURL + "/api/v1/cpi/") + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + } + + public void listCPIs() { + HttpResponse cpiResponse = getCpiInfo(); + JSONArray jArray = (JSONArray) cpiResponse.getBody().getObject().get("cpis"); + + for(Object o: jArray){ + if(o instanceof JSONObject) { + JSONObject idObj = ((JSONObject) o).getJSONObject("id"); + pc.out.print("cpiName=" + idObj.get("cpiName")); + pc.out.println(", cpiVersion=" + idObj.get("cpiVersion")); + } + } + } + +} diff --git a/buildSrc/src/main/java/com/r3/csde/CreateAndRegisterVNodesHelper.java b/buildSrc/src/main/java/com/r3/csde/CreateAndRegisterVNodesHelper.java new file mode 100644 index 0000000..31b2292 --- /dev/null +++ b/buildSrc/src/main/java/com/r3/csde/CreateAndRegisterVNodesHelper.java @@ -0,0 +1,289 @@ +package com.r3.csde; + +import com.fasterxml.jackson.databind.ObjectMapper; +import kong.unirest.HttpResponse; +import kong.unirest.JsonNode; +import kong.unirest.Unirest; +import kong.unirest.json.JSONArray; +import kong.unirest.json.JSONObject; +import net.corda.v5.base.types.MemberX500Name; +import org.jetbrains.annotations.NotNull; + +import javax.naming.ConfigurationException; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static java.net.HttpURLConnection.*; + +public class CreateAndRegisterVNodesHelper { + + ProjectContext pc; + ProjectUtils utils; + CordaStatusQueries queries; + + public CreateAndRegisterVNodesHelper(ProjectContext _pc) { + pc = _pc; + queries = new CordaStatusQueries(pc); + utils = new ProjectUtils(pc); + } + + public void createAndRegVNodes() throws IOException, CsdeException, ConfigurationException { + Unirest.config().verifySsl(false); + String appCpiCheckSum = getLastCPIUploadChkSum( pc.CPIUploadStatusFName ); + String notaryCpiCheckSum = getLastCPIUploadChkSum( pc.CPIUploadStatusFName, "-NotaryServer" ); + + LinkedList x500Ids = utils.getConfigX500Ids(pc.X500ConfigFile); + + // For each identity check that it already exists. + Set existingX500 = new HashSet<>(); + HttpResponse vnodeListResponse = queries.getVNodeInfo(); + + JSONArray virtualNodesJson = (JSONArray) vnodeListResponse.getBody().getObject().get("virtualNodes"); + for(Object o: virtualNodesJson){ + if(o instanceof JSONObject) { + JSONObject idObj = ((JSONObject) o).getJSONObject("holdingIdentity"); + String x500id = (String) idObj.get("x500Name"); + existingX500.add(MemberX500Name.parse( x500id) ); + } + } + + Map>> responses = new LinkedHashMap<>(); + + // Create the VNodes + for(String x500id: x500Ids) { + if(!existingX500.contains(MemberX500Name.parse(x500id) )) { + String cpiCheckSum = getNotaryRepresentatives().containsKey(x500id) ? notaryCpiCheckSum : appCpiCheckSum; + + pc.out.println("Creating VNode for x500id=\"" + x500id + "\" cpi checksum=" + cpiCheckSum); + responses.put(x500id, Unirest + .post(pc.baseURL + "/api/v1/virtualnode") + .body("{ \"request\" : { \"cpiFileChecksum\": " + cpiCheckSum + ", \"x500Name\": \"" + x500id + "\" } }") + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJsonAsync() + ); + } + else { + pc.out.println("Not creating a vnode for \"" + x500id + "\", vnode already exists."); + } + } + + pc.out.println("Waiting for VNode creation results..."); + + for (Map.Entry>> response: responses.entrySet()) { + try { + HttpResponse jsonNode = response.getValue().get(); + // need to check this and report errors. + // 200/HTTP_OK - OK + // 409/HTTP_CONFLICT - Vnode already exists + // 500/HTTP_INTERNAL_ERROR + // - Can mean that the request timed out. + // - However, the cluster may still have created the V-node successfully, so we want to poll later. + pc.out.println("Vnode creation end point status:" + jsonNode.getStatus()); + switch(jsonNode.getStatus()) { + case HTTP_OK: break; + case HTTP_CONFLICT: break; + case HTTP_INTERNAL_ERROR: break; + default: + utils.reportError(jsonNode); + } + + } catch (ExecutionException | InterruptedException e) { + throw new CsdeException("Unexpected exception while waiting for response to " + + "membership submission for holding identity" + response.getKey()); + } + } + + Map OKHoldingX500AndShortIds = pollForVNodeShortHoldingHashIds(x500Ids, 60, 5000); + + // Register the VNodes + responses.clear(); + + for(String okId: OKHoldingX500AndShortIds.keySet()) { + responses.put(okId, Unirest + .post(pc.baseURL + "/api/v1/membership/" + OKHoldingX500AndShortIds.get(okId)) + .body(getMemberRegistrationBody(okId)) + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJsonAsync( response -> + pc.out.println("Vnode membership submission for \"" + okId + "\"" + + System.lineSeparator() + response.getBody().toPrettyString())) + ); + } + + pc.out.println("Vnode membership requests submitted, waiting for acknowledgement from MGM..."); + + for (Map.Entry>> response: responses.entrySet()) { + try { + response.getValue().get(); + } catch (ExecutionException | InterruptedException e) { + throw new CsdeException("Unexpected exception while waiting for response to " + + "membership submission for holding identity" + response.getKey()); + } + } + + pollForCompleteMembershipRegistration(OKHoldingX500AndShortIds); + } + + public String getLastCPIUploadChkSum(@NotNull String CPIUploadStatusFName) throws IOException, NullPointerException { + return getLastCPIUploadChkSum(CPIUploadStatusFName, ""); + } + + public String getLastCPIUploadChkSum(@NotNull String CPIUploadStatusFName, + String uploadStatusQualifier) throws IOException, NullPointerException { + + String qualifiedCPIUploadStatusFName = + CPIUploadStatusFName.replace(".json", uploadStatusQualifier + ".json"); + + ObjectMapper mapper = new ObjectMapper(); + FileInputStream in = new FileInputStream(qualifiedCPIUploadStatusFName); + com.fasterxml.jackson.databind.JsonNode jsonNode = mapper.readTree(in); + + String checksum = jsonNode.get("cpiFileChecksum").toString(); + if(checksum == null || checksum.equals("null")) { + throw new NullPointerException("Missing cpiFileChecksum in file " + + qualifiedCPIUploadStatusFName + " with contents:" + jsonNode); + } + return checksum; + } + + // KV pairs of representative x500 name and corresponding notary service x500 name + public Map getNotaryRepresentatives() throws IOException, ConfigurationException { + if (pc.notaryRepresentatives == null) { + pc.notaryRepresentatives = new HashMap<>(); + + ObjectMapper mapper = new ObjectMapper(); + + FileInputStream in = new FileInputStream(pc.X500ConfigFile); + com.fasterxml.jackson.databind.JsonNode jsonNode = mapper.readTree(in); + + List identities = utils.getConfigX500Ids(pc.X500ConfigFile); + + for (com.fasterxml.jackson.databind.JsonNode notary : jsonNode.get("notaries")) { + + String svcX500Id = utils.jsonNodeToString(notary.get("serviceX500Name")); + + com.fasterxml.jackson.databind.JsonNode repsForThisService = notary.get("representatives"); + + if (repsForThisService.isEmpty()) { + throw new ConfigurationException( + "Notary service \"" + svcX500Id + "\" must have at least one representative."); + } else if (repsForThisService.size() > 1) { + // Temporary restriction while the MGM only supports a 1-1 association + throw new ConfigurationException( + "Notary service \"" + svcX500Id + "\" can only have a single representative at this time."); + } + + for (com.fasterxml.jackson.databind.JsonNode representative : repsForThisService) { + + String repAsString = utils.jsonNodeToString(representative); + + if (identities.contains(repAsString)) { + pc.notaryRepresentatives.put(repAsString, svcX500Id); + } else { + throw new ConfigurationException( + "Notary representative \"" + repAsString + "\" is not a valid identity"); + } + } + } + } + + return pc.notaryRepresentatives; + } + + private String getMemberRegistrationBody(String memberX500Name) throws ConfigurationException, IOException { + Map notaryReps = getNotaryRepresentatives(); + + String context = "\"corda.key.scheme\" : \"CORDA.ECDSA.SECP256R1\"" + ( + notaryReps.containsKey(memberX500Name) + ? ", \"corda.roles.0\" : \"notary\", " + + "\"corda.notary.service.name\" : \"" + notaryReps.get(memberX500Name) + "\", " + + // This will need revisiting in the long term when additional protocols are added, and will + // need to be specified in config. We will also need to review the hard-coded name once + // notary plugin selection logic is re-instated in CORE-7248. + "\"corda.notary.service.plugin\" : \"net.corda.notary.NonValidatingNotary\"" + : "" + ); + + return "{ \"memberRegistrationRequest\": { \"action\": \"requestJoin\", \"context\": { " + context + " } } }"; + } + + + Map pollForVNodeShortHoldingHashIds(List x500Ids, int retryCount, int coolDownMs ) throws CsdeException { + HashMap x500NameToShortHashes = new HashMap<>(); + Set vnodesToCheck = new HashSet(x500Ids); + while(!vnodesToCheck.isEmpty() && retryCount-- > 0) { + utils.rpcWait(coolDownMs); + kong.unirest.json.JSONArray virtualNodes = (JSONArray) queries.getVNodeInfo().getBody().getObject().get("virtualNodes"); + Map vnodesMap = new HashMap(); + for (Object virtualNode : virtualNodes) { + if (virtualNode instanceof JSONObject) { + JSONObject idObj = ((JSONObject) virtualNode).getJSONObject("holdingIdentity"); + vnodesMap.put(idObj.get("x500Name").toString(), idObj.get("shortHash").toString()); + } + } + for(String x500Name: vnodesToCheck) { + if(vnodesMap.containsKey(x500Name)) { + x500NameToShortHashes.put(x500Name, vnodesMap.get(x500Name)); + } + } + vnodesMap.keySet().forEach(vnodesToCheck::remove); + } + if(!vnodesToCheck.isEmpty()) { + throw new CsdeException("VNode creation timed out. Not all expected vnodes were reported as created:" + vnodesToCheck.toString()); + } + return x500NameToShortHashes; + } + + private void pollForCompleteMembershipRegistration(Map X500ToShortIdHash) throws CsdeException { + HashSet vnodesToCheck = new HashSet(X500ToShortIdHash.keySet()); + LinkedList approved = new LinkedList(); + while (!vnodesToCheck.isEmpty()) { + utils.rpcWait(2000); + approved.clear(); + for (String vnodeX500 : vnodesToCheck) { + try { + pc.out.println("Checking membership registration progress for v-node '" + vnodeX500 + "':"); + HttpResponse statusResponse = Unirest + .get(pc.baseURL + "/api/v1/membership/" + X500ToShortIdHash.get(vnodeX500) + "/") + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + if (isMembershipRegComplete(statusResponse)) { + approved.add(vnodeX500); + } + } catch (Exception e) { + throw new CsdeException("Error when registering V-Node '" + vnodeX500 + "'", e); + } + } + approved.forEach(vnodesToCheck::remove); + } + } + + + private boolean isMembershipRegComplete(HttpResponse response) throws CsdeException { + if(response.getStatus() == HTTP_OK) { + JsonNode responseBody = response.getBody(); + pc.out.println(responseBody.toPrettyString()); + if(responseBody.getArray().length() > 0) { + JSONObject memRegStatusInfo = (JSONObject) responseBody + .getArray() + .getJSONObject(0); + String memRegStatus = memRegStatusInfo.get("registrationStatus").toString(); + if (memRegStatus.equals("DECLINED")) { + throw new CsdeException("V-Node membership registration declined by Corda"); + } + return memRegStatus.equals("APPROVED"); + } + else { + return false; + } + } + else { + utils.reportError(response); + } + return false; + } + + +} diff --git a/buildSrc/src/main/java/com/r3/csde/CsdeException.java b/buildSrc/src/main/java/com/r3/csde/CsdeException.java index 163d49b..72f8fea 100644 --- a/buildSrc/src/main/java/com/r3/csde/CsdeException.java +++ b/buildSrc/src/main/java/com/r3/csde/CsdeException.java @@ -1,6 +1,9 @@ package com.r3.csde; public class CsdeException extends Exception { + public CsdeException(String message, Throwable cause) { + super(message, cause); + } public CsdeException(String message){ super(message); } diff --git a/buildSrc/src/main/java/com/r3/csde/CsdeRpcInterface.java b/buildSrc/src/main/java/com/r3/csde/CsdeRpcInterface.java deleted file mode 100644 index 93f89e7..0000000 --- a/buildSrc/src/main/java/com/r3/csde/CsdeRpcInterface.java +++ /dev/null @@ -1,415 +0,0 @@ -package com.r3.csde; - -import kong.unirest.json.JSONArray; -import org.gradle.api.Project; -import net.corda.v5.base.types.MemberX500Name; -import kong.unirest.Unirest; -import kong.unirest.json.JSONObject; -import org.jetbrains.annotations.NotNull; -import java.io.*; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.Scanner; -import java.util.Set; -import static java.lang.Thread.sleep; - -public class CsdeRpcInterface { - private Project project; - private String baseURL = "https://localhost:8888"; - private String rpcUser = "admin"; - private String rpcPasswd = "admin"; - private String workspaceDir = "workspace"; - static private int retryWaitMs = 1000; - static PrintStream out = System.out; - static private String CPIUploadStatusBaseName = "CPIFileStatus.json"; - static private String CPIUploadStatusFName; - static private String X500ConfigFile = "config/dev-net.json"; - static private String javaBinDir; - static private String cordaPidCache = "CordaPIDCache.dat"; - static private String dbContainerName; - private String JDBCDir; - private String combinedWorkerBinRe; - - public CsdeRpcInterface() { - } - - public CsdeRpcInterface (Project inProject, - String inBaseUrl, - String inRpcUser, - String inRpcPasswd, - String inWorkspaceDir, - String inJavaBinDir, - String inDbContainerName, - String inJDBCDir, - String inCordaPidCache - ) { - project = inProject; - baseURL = inBaseUrl; - rpcUser = inRpcUser; - rpcPasswd = inRpcPasswd; - workspaceDir = inWorkspaceDir; - javaBinDir = inJavaBinDir; - cordaPidCache = inCordaPidCache; - dbContainerName = inDbContainerName; - JDBCDir = inJDBCDir; - CPIUploadStatusFName = workspaceDir +"/"+ CPIUploadStatusBaseName; - - } - - - static private void rpcWait(int millis) { - try { - sleep(millis); - } - catch(InterruptedException e) { - throw new UnsupportedOperationException("Interrupts not supported.", e); - } - } - - static private void rpcWait() { - rpcWait(retryWaitMs); - } - - public LinkedList getConfigX500Ids() throws IOException { - LinkedList x500Ids = new LinkedList<>(); - com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); - - FileInputStream in = new FileInputStream(X500ConfigFile); - com.fasterxml.jackson.databind.JsonNode jsonNode = mapper.readTree(in); - for( com.fasterxml.jackson.databind.JsonNode identity: jsonNode.get("identities")) { - String idAsString = identity.toString(); - x500Ids.add(idAsString.substring(1,idAsString.length()-1)); - } - return x500Ids; - } - - static public String getLastCPIUploadChkSum(@NotNull String CPIUploadStatusFName) throws IOException, NullPointerException { - - com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); - FileInputStream in = new FileInputStream(CPIUploadStatusFName); - com.fasterxml.jackson.databind.JsonNode jsonNode = mapper.readTree(in); - - - String checksum = jsonNode.get("cpiFileChecksum").toString(); - if(checksum == null || checksum.equals("null")) { - throw new NullPointerException("Missing cpiFileChecksum in file " + CPIUploadStatusFName+ " with contents:" + jsonNode); - } - return checksum; - } - - - public void reportError(@NotNull kong.unirest.HttpResponse response) throws CsdeException { - - out.println("*** *** ***"); - out.println("Unexpected response from Corda"); - out.println("Status="+ response.getStatus()); - out.println("*** Headers ***\n"+ response.getHeaders()); - out.println("*** Body ***\n"+ response.getBody()); - out.println("*** *** ***"); - throw new CsdeException("Error: unexpected response from Corda."); - } - - public void downloadFile(String url, String targetPath) { - Unirest.get(url) - .asFile(targetPath) - .getBody(); - } - - public kong.unirest.HttpResponse getVNodeInfo() { - Unirest.config().verifySsl(false); - return Unirest.get(baseURL + "/api/v1/virtualnode/") - .basicAuth(rpcUser, rpcPasswd) - .asJson(); - } - - public void listVNodesVerbose() { - kong.unirest.HttpResponse vnodeResponse = getVNodeInfo(); - out.println("VNodes:\n" + vnodeResponse.getBody().toPrettyString()); - } - - // X500Name, cpiname, shorthash, - public void listVNodes() { - kong.unirest.HttpResponse vnodeResponse = getVNodeInfo(); - - kong.unirest.json.JSONArray virtualNodesJson = (JSONArray) vnodeResponse.getBody().getObject().get("virtualNodes"); - out.println("X500 Name\tHolding identity short hash"); - for(Object o: virtualNodesJson){ - if(o instanceof kong.unirest.json.JSONObject) { - kong.unirest.json.JSONObject idObj = ((kong.unirest.json.JSONObject) o).getJSONObject("holdingIdentity"); - out.print("\"" + idObj.get("x500Name") + "\""); - out.println("\t\"" + idObj.get("shortHash") + "\""); - } - } - } - - public kong.unirest.HttpResponse getCpiInfo() { - Unirest.config().verifySsl(false); - return Unirest.get(baseURL + "/api/v1/cpi/") - .basicAuth(rpcUser, rpcPasswd) - .asJson(); - - } - - public void listCPIs() { - kong.unirest.HttpResponse cpiResponse = getCpiInfo(); - kong.unirest.json.JSONArray jArray = (JSONArray) cpiResponse.getBody().getObject().get("cpis"); - - for(Object o: jArray){ - if(o instanceof kong.unirest.json.JSONObject) { - kong.unirest.json.JSONObject idObj = ((kong.unirest.json.JSONObject) o).getJSONObject("id"); - out.print("cpiName=" + idObj.get("cpiName")); - out.println(", cpiVersion=" + idObj.get("cpiVersion")); - } - } - } - - public void uploadCertificate(String certAlias, String certFName) { - Unirest.config().verifySsl(false); - kong.unirest.HttpResponse uploadResponse = Unirest.put(baseURL + "/api/v1/certificates/cluster/code-signer") - .field("alias", certAlias) - .field("certificate", new File(certFName)) - .basicAuth(rpcUser, rpcPasswd) - .asJson(); - out.println("Certificate/key upload, alias "+certAlias+" certificate/key file "+certFName); - out.println(uploadResponse.getBody().toPrettyString()); - } - - public void forceuploadCPI(String cpiFName) throws FileNotFoundException, CsdeException { - Unirest.config().verifySsl(false); - kong.unirest.HttpResponse jsonResponse = Unirest.post(baseURL + "/api/v1/maintenance/virtualnode/forcecpiupload/") - .field("upload", new File(cpiFName)) - .basicAuth(rpcUser, rpcPasswd) - .asJson(); - - if(jsonResponse.getStatus() == 200) { - String id = (String) jsonResponse.getBody().getObject().get("id"); - out.println("get id:\n" +id); - kong.unirest.HttpResponse statusResponse = uploadStatus(id); - - if (statusResponse.getStatus() == 200) { - PrintStream cpiUploadStatus = new PrintStream(new FileOutputStream(CPIUploadStatusFName)); - cpiUploadStatus.print(statusResponse.getBody()); - out.println("Caching CPI file upload status:\n" + statusResponse.getBody()); - } else { - reportError(statusResponse); - } - } - else { - reportError(jsonResponse); - } - } - - private boolean uploadStatusRetry(kong.unirest.HttpResponse response) { - int status = response.getStatus(); - kong.unirest.JsonNode body = response.getBody(); - // Do not retry on success - if(status == 200) { - // Keep retrying until we get "OK" may move through "Validateing upload", "Persisting CPI" - return !(body.getObject().get("status").equals("OK")); - } - else if (status == 400){ - JSONObject details = response.getBody().getObject().getJSONObject("details"); - if( details != null ){ - String code = (String) details.getString("code"); - return !code.equals("BAD_REQUEST"); - } - else { - // 400 otherwise means some transient problem - return true; - } - } - return false; - } - - public kong.unirest.HttpResponse uploadStatus(String requestId) { - kong.unirest.HttpResponse statusResponse = null; - do { - rpcWait(1000); - statusResponse = Unirest - .get(baseURL + "/api/v1/cpi/status/" + requestId + "/") - .basicAuth(rpcUser, rpcPasswd) - .asJson(); - out.println("Upload status="+statusResponse.getStatus()+", status query response:\n"+statusResponse.getBody().toPrettyString()); - } - while(uploadStatusRetry(statusResponse)); - - return statusResponse; - } - - public void deployCPI(String cpiFName, String cpiName, String cpiVersion) throws FileNotFoundException, CsdeException { - Unirest.config().verifySsl(false); - - kong.unirest.HttpResponse cpiResponse = getCpiInfo(); - kong.unirest.json.JSONArray jArray = (JSONArray) cpiResponse.getBody().getObject().get("cpis"); - - - int matches = 0; - for(Object o: jArray.toList() ) { - if(o instanceof JSONObject) { - JSONObject idObj = ((JSONObject) o).getJSONObject("id"); - if((idObj.get("cpiName").toString().equals(cpiName) - && idObj.get("cpiVersion").toString().equals(cpiVersion))) { - matches++; - } - } - } - out.println("Matching CPIS="+matches); - - - if(matches == 0) { - kong.unirest.HttpResponse uploadResponse = Unirest.post(baseURL + "/api/v1/cpi/") - .field("upload", new File(cpiFName)) - .basicAuth(rpcUser, rpcPasswd) - .asJson(); - - kong.unirest.JsonNode body = uploadResponse.getBody(); - - int status = uploadResponse.getStatus(); - - out.println("Upload Status:" + status); - out.println("Pretty print the body\n" + body.toPrettyString()); - - // We expect the id field to be a string. - if (status == 200) { - String id = (String) body.getObject().get("id"); - out.println("get id:\n" + id); - - kong.unirest.HttpResponse statusResponse = uploadStatus(id); - if (statusResponse.getStatus() == 200) { - PrintStream cpiUploadStatus = new PrintStream(new FileOutputStream(CPIUploadStatusFName)); - cpiUploadStatus.print(statusResponse.getBody()); - out.println("Caching CPI file upload status:\n" + statusResponse.getBody()); - } else { - reportError(statusResponse); - } - } else { - reportError(uploadResponse); - } - } - else { - out.println("CPI already uploaded doing a 'force' upload."); - forceuploadCPI(cpiFName); - } - } - - public void createAndRegVNodes() throws IOException, CsdeException{ - Unirest.config().verifySsl(false); - String cpiCheckSum = getLastCPIUploadChkSum( CPIUploadStatusFName ); - - LinkedList x500Ids = getConfigX500Ids(); - LinkedList OKHoldingShortIds = new LinkedList<>(); - - // For each identity check that it already exists. - Set existingX500 = new HashSet<>(); - kong.unirest.HttpResponse vnodeListResponse = getVNodeInfo(); - - kong.unirest.json.JSONArray virtualNodesJson = (JSONArray) vnodeListResponse.getBody().getObject().get("virtualNodes"); - for(Object o: virtualNodesJson){ - if(o instanceof kong.unirest.json.JSONObject) { - kong.unirest.json.JSONObject idObj = ((kong.unirest.json.JSONObject) o).getJSONObject("holdingIdentity"); - String x500id = (String) idObj.get("x500Name"); - existingX500.add(MemberX500Name.parse( x500id) ); - } - } - - // Create the VNodes - for(String x500id: x500Ids) { - if(!existingX500.contains(MemberX500Name.parse(x500id) )) { - out.println("Creating VNode for x500id=\"" + x500id + "\" cpi checksum=" + cpiCheckSum); - kong.unirest.HttpResponse jsonNode = Unirest.post(baseURL + "/api/v1/virtualnode") - .body("{ \"request\" : { \"cpiFileChecksum\": " + cpiCheckSum + ", \"x500Name\": \"" + x500id + "\" } }") - .basicAuth(rpcUser, rpcPasswd) - .asJson(); - // Logging. - - // need to check this and report errors. - // 200 - OK - // 409 - Vnode already exists - if (jsonNode.getStatus() != 409) { - if (jsonNode.getStatus() != 200) { - reportError(jsonNode); - } else { - JSONObject thing = jsonNode.getBody().getObject().getJSONObject("holdingIdentity"); - String shortHash = (String) thing.get("shortHash"); - OKHoldingShortIds.add(shortHash); - } - } - } - else { - out.println("Not creating a vnode for \"" + x500id + "\", vnode already exists."); - } - } - - // Register the VNodes - for(String shortHoldingIdHash: OKHoldingShortIds) { - kong.unirest.HttpResponse vnodeResponse = Unirest.post(baseURL + "/api/v1/membership/" + shortHoldingIdHash) - .body("{ \"memberRegistrationRequest\": { \"action\": \"requestJoin\", \"context\": { \"corda.key.scheme\" : \"CORDA.ECDSA.SECP256R1\" } } }") - .basicAuth(rpcUser, rpcPasswd) - .asJson(); - - out.println("Vnode membership submission:\n" + vnodeResponse.getBody().toPrettyString()); - } - - } - - public void startCorda() throws IOException { - PrintStream pidStore = new PrintStream(new FileOutputStream(cordaPidCache)); - File combinedWorkerJar = project.getConfigurations().getByName("combinedWorker").getSingleFile(); - - new ProcessBuilder( - "docker", - "run", "-d", "--rm", - "-p", "5432:5432", - "--name", dbContainerName, - "-e", "POSTGRES_DB=cordacluster", - "-e", "POSTGRES_USER=postgres", - "-e", "POSTGRES_PASSWORD=password", - "postgres:latest").start(); - rpcWait(10000); - - ProcessBuilder procBuild = new ProcessBuilder(javaBinDir + "/java", - "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", - "-Dco.paralleluniverse.fibers.verifyInstrumentation=true", - "-jar", - combinedWorkerJar.toString(), - "--instanceId=0", - "-mbus.busType=DATABASE", - "-spassphrase=password", - "-ssalt=salt", - "-spassphrase=password", - "-ssalt=salt", - "-ddatabase.user=user", - "-ddatabase.pass=password", - "-ddatabase.jdbc.url=jdbc:postgresql://localhost:5432/cordacluster", - "-ddatabase.jdbc.directory="+JDBCDir); - - - procBuild.redirectErrorStream(true); - Process proc = procBuild.start(); - pidStore.print(proc.pid()); - out.println("Corda Process-id="+proc.pid()); - } - - public void stopCorda() throws IOException, NoPidFile { - File cordaPIDFile = new File(cordaPidCache); - if(cordaPIDFile.exists()) { - Scanner sc = new Scanner(cordaPIDFile); - long pid = sc.nextLong(); - out.println("pid to kill=" + pid); - - if (System.getProperty("os.name").toLowerCase().contains("windows")) { - new ProcessBuilder("Powershell", "-Command", "Stop-Process", "-Id", Long.toString(pid), "-PassThru").start(); - } else { - new ProcessBuilder("kill", "-9", Long.toString(pid)).start(); - } - - Process proc = new ProcessBuilder("docker", "stop", dbContainerName).start(); - - cordaPIDFile.delete(); - } - else { - throw new NoPidFile("Cannot stop the Combined worker\nCached process ID file " + cordaPidCache + " missing.\nWas the combined worker not started?"); - } - } - -} diff --git a/buildSrc/src/main/java/com/r3/csde/DeployCPIsHelper.java b/buildSrc/src/main/java/com/r3/csde/DeployCPIsHelper.java new file mode 100644 index 0000000..d8b6d3f --- /dev/null +++ b/buildSrc/src/main/java/com/r3/csde/DeployCPIsHelper.java @@ -0,0 +1,188 @@ +package com.r3.csde; + +import kong.unirest.JsonNode; +import kong.unirest.Unirest; +import kong.unirest.json.JSONArray; +import kong.unirest.json.JSONObject; +import kong.unirest.HttpResponse; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.PrintStream; + +import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; +import static java.net.HttpURLConnection.HTTP_OK; + +public class DeployCPIsHelper { + + public DeployCPIsHelper() { + } + ProjectContext pc; + CordaStatusQueries queries; + ProjectUtils utils; + + public DeployCPIsHelper(ProjectContext _pc) { + pc = _pc; + queries = new CordaStatusQueries(pc); + utils = new ProjectUtils(pc); + } + + public void deployCPIs() throws FileNotFoundException, CsdeException{ + + uploadCertificate(pc.signingCertAlias, pc.signingCertFName); + uploadCertificate(pc.keystoreAlias, pc.keystoreCertFName); + + // todo: make consistent with other string building code - remove String.format + String appCPILocation = String.format("%s/%s-%s.cpi", + pc.workflowBuildDir, + pc.project.getName(), + pc.project.getVersion()); + deployCPI(appCPILocation, pc.appCPIName,pc.project.getVersion().toString()); + + String notaryCPILocation = String.format("%s/%s-%s.cpi", + pc.workflowBuildDir, + pc.notaryCPIName.replace(' ','-').toLowerCase(), + pc.project.getVersion()); + deployCPI(notaryCPILocation, + pc.notaryCPIName, + pc.project.getVersion().toString(), + "-NotaryServer" ); + + } + + public void uploadCertificate(String certAlias, String certFName) { + Unirest.config().verifySsl(false); + HttpResponse uploadResponse = Unirest.put(pc.baseURL + "/api/v1/certificates/cluster/code-signer") + .field("alias", certAlias) + .field("certificate", new File(certFName)) + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + pc.out.println("Certificate/key upload, alias "+certAlias+" certificate/key file "+certFName); + pc.out.println(uploadResponse.getBody().toPrettyString()); + } + + public void forceuploadCPI(String cpiFName) throws FileNotFoundException, CsdeException { + forceuploadCPI(cpiFName, ""); + } + + public void forceuploadCPI(String cpiFName, String uploadStatusQualifier) throws FileNotFoundException, CsdeException { + Unirest.config().verifySsl(false); + HttpResponse jsonResponse = Unirest.post(pc.baseURL + "/api/v1/maintenance/virtualnode/forcecpiupload/") + .field("upload", new File(cpiFName)) + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + if(jsonResponse.getStatus() == HTTP_OK) { + String id = (String) jsonResponse.getBody().getObject().get("id"); + pc.out.println("get id:\n" +id); + HttpResponse statusResponse = uploadStatus(id); + + if (statusResponse.getStatus() == HTTP_OK) { + PrintStream cpiUploadStatus = new PrintStream(new FileOutputStream( + pc.CPIUploadStatusFName.replace(".json", uploadStatusQualifier + ".json" ))); + cpiUploadStatus.print(statusResponse.getBody()); + pc.out.println("Caching CPI file upload status:\n" + statusResponse.getBody()); + } else { + utils.reportError(statusResponse); + } + } + else { + utils.reportError(jsonResponse); + } + } + + private boolean uploadStatusRetry(HttpResponse response) { + int status = response.getStatus(); + JsonNode body = response.getBody(); + // Do not retry on success // todo: need to think through the possible outcomes here - what if the bodyTitle is null, it won't retry + if(status == HTTP_OK) { + // Keep retrying until we get "OK" may move through "Validating upload", "Persisting CPI" + return !(body.getObject().get("status").equals("OK")); + } + else if (status == HTTP_BAD_REQUEST){ + String bodyTitle = response.getBody().getObject().getString("title"); + return bodyTitle != null && bodyTitle.matches("No such requestId=[-0-9a-f]+"); + } + return false; + } + + public HttpResponse uploadStatus(String requestId) { + HttpResponse statusResponse = null; + do { + utils.rpcWait(1000); + statusResponse = Unirest + .get(pc.baseURL + "/api/v1/cpi/status/" + requestId + "/") + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + pc.out.println("Upload status="+statusResponse.getStatus()+", status query response:\n"+statusResponse.getBody().toPrettyString()); + } + while(uploadStatusRetry(statusResponse)); + + return statusResponse; + } + + public void deployCPI(String cpiFName, String cpiName, String cpiVersion) throws FileNotFoundException, CsdeException { + deployCPI(cpiFName, cpiName, cpiVersion, ""); + } + + public void deployCPI(String cpiFName, + String cpiName, + String cpiVersion, + String uploadStatusQualifier) throws FileNotFoundException, CsdeException { + // todo: where is the primary instance declared? + Unirest.config().verifySsl(false); + + HttpResponse cpiResponse = queries.getCpiInfo(); + JSONArray jArray = (JSONArray) cpiResponse.getBody().getObject().get("cpis"); + + int matches = 0; + for(Object o: jArray.toList() ) { + if(o instanceof JSONObject) { + JSONObject idObj = ((JSONObject) o).getJSONObject("id"); + if((idObj.get("cpiName").toString().equals(cpiName) + && idObj.get("cpiVersion").toString().equals(cpiVersion))) { + matches++; + } + } + } + pc.out.println("Matching CPIS="+matches); + + if(matches == 0) { + HttpResponse uploadResponse = Unirest.post(pc.baseURL + "/api/v1/cpi/") + .field("upload", new File(cpiFName)) + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + JsonNode body = uploadResponse.getBody(); + + int status = uploadResponse.getStatus(); + + pc.out.println("Upload Status:" + status); + pc.out.println("Pretty print the body\n" + body.toPrettyString()); + + // We expect the id field to be a string. + if (status == HTTP_OK) { + String id = (String) body.getObject().get("id"); + pc.out.println("get id:\n" + id); + + HttpResponse statusResponse = uploadStatus(id); + if (statusResponse.getStatus() == HTTP_OK) { + PrintStream cpiUploadStatus = new PrintStream(new FileOutputStream( + pc.CPIUploadStatusFName.replace(".json", uploadStatusQualifier + ".json" ))); + cpiUploadStatus.print(statusResponse.getBody()); + pc.out.println("Caching CPI file upload status:\n" + statusResponse.getBody()); + } else { + utils.reportError(statusResponse); + } + } else { + utils.reportError(uploadResponse); + } + } + else { + pc.out.println("CPI already uploaded doing a 'force' upload."); + forceuploadCPI(cpiFName); + } + } + +} diff --git a/buildSrc/src/main/java/com/r3/csde/NoPidFile.java b/buildSrc/src/main/java/com/r3/csde/NoPidFile.java deleted file mode 100644 index d7bb3dc..0000000 --- a/buildSrc/src/main/java/com/r3/csde/NoPidFile.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.r3.csde; - -public class NoPidFile extends Exception { - public NoPidFile(String message){ - super(message); - } -} \ No newline at end of file diff --git a/buildSrc/src/main/java/com/r3/csde/ProjectContext.java b/buildSrc/src/main/java/com/r3/csde/ProjectContext.java new file mode 100644 index 0000000..63e1fac --- /dev/null +++ b/buildSrc/src/main/java/com/r3/csde/ProjectContext.java @@ -0,0 +1,83 @@ +package com.r3.csde; + +import org.gradle.api.Project; + +import java.io.PrintStream; +import java.util.Map; + +public class ProjectContext { + Project project; + String baseURL = "https://localhost:8888"; + String rpcUser = "admin"; + String rpcPasswd = "admin"; + String workspaceDir = "workspace"; + int retryWaitMs = 1000; + PrintStream out = System.out; + String CPIUploadStatusBaseName = "CPIFileStatus.json"; + String CPIUploadStatusFName; + String X500ConfigFile = "config/dev-net.json"; + String javaBinDir; + String cordaPidCache = "CordaPIDCache.dat"; + String dbContainerName; + String JDBCDir; + String combinedWorkerBinRe; + Map notaryRepresentatives = null; + String signingCertAlias; + String signingCertFName; + String keystoreAlias; + String keystoreFName; + String keystoreCertFName; + String appCPIName; + String notaryCPIName; + String devEnvWorkspace; + String cordaCliBinDir; + String cordaNotaryServiceDir; + String workflowBuildDir; + String cordaNotaryPluginsVersion; + + public ProjectContext (Project inProject, + String inBaseUrl, + String inRpcUser, + String inRpcPasswd, + String inWorkspaceDir, + String inJavaBinDir, + String inDbContainerName, + String inJDBCDir, + String inCordaPidCache, + String inSigningCertAlias, + String inSigningCertFName, + String inKeystoreAlias, + String inKeystoreFName, + String inKeystoreCertFName, + String inAppCPIName, + String inNotaryCPIName, + String inDevEnvWorkspace, + String inCordaCLiBinDir, + String inCordaNotaryServiceDir, + String inWorkflowBuildDir, + String inCordaNotaryPluginsVersion + ) { + project = inProject; + baseURL = inBaseUrl; + rpcUser = inRpcUser; + rpcPasswd = inRpcPasswd; + workspaceDir = inWorkspaceDir; + javaBinDir = inJavaBinDir; + cordaPidCache = inCordaPidCache; + dbContainerName = inDbContainerName; + JDBCDir = inJDBCDir; + CPIUploadStatusFName = workspaceDir + "/" + CPIUploadStatusBaseName; + signingCertAlias = inSigningCertAlias; + signingCertFName = inSigningCertFName; + keystoreAlias = inKeystoreAlias; + keystoreFName = inKeystoreFName; + keystoreCertFName = inKeystoreCertFName; + appCPIName = inAppCPIName; + notaryCPIName = inNotaryCPIName; + devEnvWorkspace = inDevEnvWorkspace; + cordaCliBinDir = inCordaCLiBinDir; + cordaNotaryServiceDir = inCordaNotaryServiceDir; + workflowBuildDir = inWorkflowBuildDir; + cordaNotaryPluginsVersion = inCordaNotaryPluginsVersion; + } +} diff --git a/buildSrc/src/main/java/com/r3/csde/ProjectUtils.java b/buildSrc/src/main/java/com/r3/csde/ProjectUtils.java new file mode 100644 index 0000000..0b8b85d --- /dev/null +++ b/buildSrc/src/main/java/com/r3/csde/ProjectUtils.java @@ -0,0 +1,71 @@ +package com.r3.csde; + +import com.fasterxml.jackson.databind.ObjectMapper; +import kong.unirest.HttpResponse; +import kong.unirest.JsonNode; +import kong.unirest.Unirest; +import org.jetbrains.annotations.NotNull; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.LinkedList; + +import static java.lang.Thread.sleep; + +public class ProjectUtils { + + ProjectContext pc; + ProjectUtils(ProjectContext _pc) { + pc = _pc; + } + + + void rpcWait(int millis) { + try { + sleep(millis); + } + catch(InterruptedException e) { + throw new UnsupportedOperationException("Interrupts not supported.", e); + } + } + + void rpcWait() { + rpcWait( pc.retryWaitMs); + } + + public LinkedList getConfigX500Ids(String configFile) throws IOException { + LinkedList x500Ids = new LinkedList<>(); +// com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); + ObjectMapper mapper = new ObjectMapper(); + + + FileInputStream in = new FileInputStream(configFile); + com.fasterxml.jackson.databind.JsonNode jsonNode = mapper.readTree(in); + for( com.fasterxml.jackson.databind.JsonNode identity: jsonNode.get("identities")) { + x500Ids.add(jsonNodeToString(identity)); + } + return x500Ids; + } + + public String jsonNodeToString(com.fasterxml.jackson.databind.JsonNode jsonNode) { + String jsonString = jsonNode.toString(); + return jsonString.substring(1, jsonString.length()-1); + } + + public void downloadFile(String url, String targetPath) { + Unirest.get(url) + .asFile(targetPath) + .getBody(); + } + + public void reportError(@NotNull HttpResponse response) throws CsdeException { + + pc.out.println("*** *** ***"); + pc.out.println("Unexpected response from Corda"); + pc.out.println("Status="+ response.getStatus()); + pc.out.println("*** Headers ***\n"+ response.getHeaders()); + pc.out.println("*** Body ***\n"+ response.getBody()); + pc.out.println("*** *** ***"); + throw new CsdeException("Error: unexpected response from Corda."); + } +} diff --git a/buildSrc/src/test/java/CsdeRpcInterfaceTests.java b/buildSrc/src/test/java/CsdeRpcInterfaceTests.java deleted file mode 100644 index dcf57d3..0000000 --- a/buildSrc/src/test/java/CsdeRpcInterfaceTests.java +++ /dev/null @@ -1,4 +0,0 @@ -import com.r3.csde.CsdeRpcInterface; - -public class CsdeRpcInterfaceTests { -} diff --git a/config/dev-net.json b/config/dev-net.json index 0d5aa28..db8a299 100644 --- a/config/dev-net.json +++ b/config/dev-net.json @@ -1,7 +1,12 @@ { -"identities" : [ - "CN=Alice, OU=Test Dept, O=R3, L=London, C=GB", - "CN=Bob, OU=Test Dept, O=R3, L=London, C=GB", - "CN=Charlie, OU=Test Dept, O=R3, L=London, C=GB" - ] + "identities" : [ + "CN=Alice, OU=Test Dept, O=R3, L=London, C=GB", + "CN=Bob, OU=Test Dept, O=R3, L=London, C=GB", + "CN=Charlie, OU=Test Dept, O=R3, L=London, C=GB", + "CN=NotaryRep1, OU=Test Dept, O=R3, L=London, C=GB"], + "notaries" : [ + { + "serviceX500Name": "CN=NotaryService, OU=Test Dept, O=R3, L=London, C=GB", + "representatives": ["CN=NotaryRep1, OU=Test Dept, O=R3, L=London, C=GB"] + }] } diff --git a/contracts/build.gradle b/contracts/build.gradle new file mode 100644 index 0000000..2921da5 --- /dev/null +++ b/contracts/build.gradle @@ -0,0 +1,91 @@ + +plugins { + // Include the cordapp-cpb plugin. This automatically includes the cordapp-cpk plugin as well. + // These extend existing build environment so that CPB and CPK files can be built. + // This includes a CorDapp DSL that allows the developer to supply metadata for the CorDapp + // required by Corda. + id 'net.corda.plugins.cordapp-cpb2' + id 'org.jetbrains.kotlin.jvm' + id 'maven-publish' +} + +// Declare dependencies for the modules we will use. +// A cordaProvided declaration is required for anything that we use that the Corda API provides. +// This is required to allow us to build CorDapp modules as OSGi bundles that CPI and CPB files are built on. +dependencies { + // We need a version of kotlin-stdlib-jdk8 built as an OSGi bundle, this is "kotlin-stdlib-jdk8-osgi". + // R3 builds kotlin-stdlib-jdk8-osgi from Kotlin's kotlin-stdlib-jdk8. + // NB: + // Kotlin's kotlin-osgi-bundle does not provide all of the Kotlin API that is required, + // There is no kotlin-stdlib-jdk11, but one is not needed even though we are targetting Java 11. + cordaProvided 'net.corda.kotlin:kotlin-stdlib-jdk8-osgi' + + // Declare a "platform" so that we use the correct set of dependency versions for the version of the + // Corda API specified. + cordaProvided platform("net.corda:corda-api:$cordaApiVersion") + + // If using transistive dependencies this will provide most of Corda-API: + // cordaProvided 'net.corda:corda-application' + + // Alternatively we can explicitly specify all our Corda-API dependencies: + cordaProvided 'net.corda:corda-base' + cordaProvided 'net.corda:corda-application' + cordaProvided 'net.corda:corda-crypto' + cordaProvided 'net.corda:corda-membership' + // cordaProvided 'net.corda:corda-persistence' + cordaProvided 'net.corda:corda-serialization' + cordaProvided 'net.corda:corda-ledger-utxo' + cordaProvided 'net.corda:corda-ledger-consensual' + + // CorDapps that use the UTXO ledger must include at least one notary client plugin + cordapp "com.r3.corda.notary.plugin.nonvalidating:notary-plugin-non-validating-client:$cordaNotaryPluginsVersion" + + // The CorDapp uses the slf4j logging framework. Corda-API provides this so we need a 'cordaProvided' declaration. + cordaProvided 'org.slf4j:slf4j-api' + + // This are shared so should be here. + // Dependencies Required By Test Tooling + testImplementation "net.corda:corda-simulator-api:$simulatorVersion" + testRuntimeOnly "net.corda:corda-simulator-runtime:$simulatorVersion" + + // 3rd party libraries + // Required + testImplementation "org.slf4j:slf4j-simple:2.0.0" + testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + // Optional but used by exmaple tests. + testImplementation "org.mockito:mockito-core:$mockitoVersion" + testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" + testImplementation "org.hamcrest:hamcrest-library:$hamcrestVersion" +} + +// The CordApp section. +// This is part of the DSL provided by the corda plugins to define metadata for our CorDapp. +// Each component of the CorDapp would get its own CorDapp section in the build.gradle file for the component’s +// subproject. +// This is required by the corda plugins to build the CorDapp. +cordapp { + // "targetPlatformVersion" and "minimumPlatformVersion" are intended to specify the preferred + // and earliest versions of the Corda platform that the CorDapp will run on respectively. + // Enforced versioning has not implemented yet so we need to pass in a dummy value for now. + // The platform version will correspond to and be roughly equivalent to the Corda API version. + targetPlatformVersion = platformVersion.toInteger() + minimumPlatformVersion = platformVersion.toInteger() + + // The cordapp section contains either a workflow or contract subsection depending on the type of component. + // Declares the type and metadata of the CPK (this CPB has one CPK). + contract { + name "ContractsModuleNameHere" + versionId 1 + vendor "VendorNameHere" + } +} + +publishing { + publications { + maven(MavenPublication) { + from components.cordapp + } + } +} diff --git a/gradle.properties b/gradle.properties index ec4e7fc..156d05f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,11 +2,18 @@ kotlin.code.style=official # Specify the version of the Corda-API to use. # This needs to match the version supported by the Corda Cluster the CorDapp will run on. -# cordaApiVersion=5.0.0.190-DevPreview-2 -cordaApiVersion=5.0.0.505-Beta1.0-HC00 +cordaApiVersion=5.0.0.523-Fox1.0 + +# Settings For Development Utilities +combinedWorkerVersion=5.0.0.0-Fox1.0 +simulatorVersion=5.0.0.0-Fox1.0 + +# Specify the version of the notary plugins to use. +# Currently packaged as part of corda-runtime-os, so should be set to a corda-runtime-os version. +cordaNotaryPluginsVersion=5.0.0.0-Fox1.0 # Specify the version of the cordapp-cpb and cordapp-cpk plugins -cordaPluginsVersion=7.0.0 +cordaPluginsVersion=7.0.1 # For the time being this just needs to be set to a dummy value. platformVersion = 999 @@ -24,15 +31,9 @@ mockitoKotlinVersion=4.0.0 mockitoVersion=4.6.1 hamcrestVersion=2.2 -# Settings For Development Utilities -combinedWorkerVersion=5.0.0.0-Beta1.0-HC00 -simulatorVersion=5.0.0.0-Beta1.0-HC00 - cordaClusterURL=https://localhost:8888 cordaRpcUser=admin cordaRpcPasswd=admin devEnvWorkspace=workspace dbContainerName=CSDEpostgresql -# R3 internal repository -artifactoryContextUrl=https://staging.download.corda.net/maven/20ede3c6-29c0-11ed-966d-b7c36748b9f6-Beta1.0-HC00 \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index d18ef5a..fb1520b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,9 +2,7 @@ pluginManagement { // Declare the repositories where plugins are stored. repositories { gradlePluginPortal() - maven { - url = "$artifactoryContextUrl/" - } + mavenCentral() } // The plugin dependencies with versions of the plugins congruent with the specified CorDapp plugin version, @@ -21,5 +19,9 @@ pluginManagement { // Root project name, used in naming the project as a whole and used in naming objects built by the project. rootProject.name = 'csde-cordapp-template-java' +include ':workflows' +include ':contracts' + + diff --git a/src/main/java/com/r3/developers/csdetemplate/MyFirstFlowStartArgs.java b/src/main/java/com/r3/developers/csdetemplate/MyFirstFlowStartArgs.java deleted file mode 100644 index 99f7f22..0000000 --- a/src/main/java/com/r3/developers/csdetemplate/MyFirstFlowStartArgs.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.r3.developers.csdetemplate; - -import net.corda.v5.base.types.MemberX500Name; - -// A class to hold the arguments required to start the flow -public class MyFirstFlowStartArgs { - public MemberX500Name otherMember; - - public MemberX500Name getOtherMember() { - return otherMember; - } - - public MyFirstFlowStartArgs(MemberX500Name otherMember) { - this.otherMember = otherMember; - } - - // Without the following we get - // "Cannot construct instance of `com.r3.developers.csdetemplate.MyFirstFlowStartArgs` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)\n at [Source: (String)\"{\"otherMember\":\"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB\"}\"; line: 1, column: 2]" - public MyFirstFlowStartArgs() {} -} diff --git a/workflows/build.gradle b/workflows/build.gradle new file mode 100644 index 0000000..5ad0c76 --- /dev/null +++ b/workflows/build.gradle @@ -0,0 +1,94 @@ +plugins { + // Include the cordapp-cpb plugin. This automatically includes the cordapp-cpk plugin as well. + // These extend existing build environment so that CPB and CPK files can be built. + // This includes a CorDapp DSL that allows the developer to supply metadata for the CorDapp + // required by Corda. + id 'net.corda.plugins.cordapp-cpb2' + id 'org.jetbrains.kotlin.jvm' + id 'maven-publish' +} + +// Declare dependencies for the modules we will use. +// A cordaProvided declaration is required for anything that we use that the Corda API provides. +// This is required to allow us to build CorDapp modules as OSGi bundles that CPI and CPB files are built on. +dependencies { + // From other subprojects: + cordapp project(':contracts') + + // We need a version of kotlin-stdlib-jdk8 built as an OSGi bundle, this is "kotlin-stdlib-jdk8-osgi". + // R3 builds kotlin-stdlib-jdk8-osgi from Kotlin's kotlin-stdlib-jdk8. + // NB: + // Kotlin's kotlin-osgi-bundle does not provide all of the Kotlin API that is required, + // There is no kotlin-stdlib-jdk11, but one is not needed even though we are targetting Java 11. + cordaProvided 'net.corda.kotlin:kotlin-stdlib-jdk8-osgi' + + // Declare a "platform" so that we use the correct set of dependency versions for the version of the + // Corda API specified. + cordaProvided platform("net.corda:corda-api:$cordaApiVersion") + + // If using transistive dependencies this will provide most of Corda-API: + // cordaProvided 'net.corda:corda-application' + + // Alternatively we can explicitly specify all our Corda-API dependencies: + cordaProvided 'net.corda:corda-base' + cordaProvided 'net.corda:corda-application' + cordaProvided 'net.corda:corda-crypto' + cordaProvided 'net.corda:corda-membership' + // cordaProvided 'net.corda:corda-persistence' + cordaProvided 'net.corda:corda-serialization' + cordaProvided 'net.corda:corda-ledger-utxo' + cordaProvided 'net.corda:corda-ledger-consensual' + + // CorDapps that use the UTXO ledger must include at least one notary client plugin + cordapp "com.r3.corda.notary.plugin.nonvalidating:notary-plugin-non-validating-client:$cordaNotaryPluginsVersion" + + // The CorDapp uses the slf4j logging framework. Corda-API provides this so we need a 'cordaProvided' declaration. + cordaProvided 'org.slf4j:slf4j-api' + + // This are shared so should be here. + // Dependencies Required By Test Tooling + testImplementation "net.corda:corda-simulator-api:$simulatorVersion" + testRuntimeOnly "net.corda:corda-simulator-runtime:$simulatorVersion" + + // 3rd party libraries + // Required + testImplementation "org.slf4j:slf4j-simple:2.0.0" + testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + // Optional but used by exmaple tests. + testImplementation "org.mockito:mockito-core:$mockitoVersion" + testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" + testImplementation "org.hamcrest:hamcrest-library:$hamcrestVersion" + +} + +// The CordApp section. +// This is part of the DSL provided by the corda plugins to define metadata for our CorDapp. +// Each component of the CorDapp would get its own CorDapp section in the build.gradle file for the component’s +// subproject. +// This is required by the corda plugins to build the CorDapp. +cordapp { + // "targetPlatformVersion" and "minimumPlatformVersion" are intended to specify the preferred + // and earliest versions of the Corda platform that the CorDapp will run on respectively. + // Enforced versioning has not implemented yet so we need to pass in a dummy value for now. + // The platform version will correspond to and be roughly equivalent to the Corda API version. + targetPlatformVersion = platformVersion.toInteger() + minimumPlatformVersion = platformVersion.toInteger() + + // The cordapp section contains either a workflow or contract subsection depending on the type of component. + // Declares the type and metadata of the CPK (this CPB has one CPK). + workflow { + name "WorkflowsModuleNameHere" + versionId 1 + vendor "VendorNameHere" + } +} + +publishing { + publications { + maven(MavenPublication) { + from components.cordapp + } + } +} diff --git a/src/main/java/com/r3/developers/csdetemplate/Message.java b/workflows/src/main/java/com/r3/developers/csdetemplate/workflows/Message.java similarity index 68% rename from src/main/java/com/r3/developers/csdetemplate/Message.java rename to workflows/src/main/java/com/r3/developers/csdetemplate/workflows/Message.java index 36e718c..c19960d 100644 --- a/src/main/java/com/r3/developers/csdetemplate/Message.java +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/workflows/Message.java @@ -1,17 +1,17 @@ -package com.r3.developers.csdetemplate; +package com.r3.developers.csdetemplate.workflows; import net.corda.v5.base.annotations.CordaSerializable; import net.corda.v5.base.types.MemberX500Name; -// A class which will contain a message, It must be marked with @CordaSerializable for Corda -// to be able to send from one virtual node to another. +// Where a class contains a message, mark it with @CordaSerializable to enable Corda to +// send it from one virtual node to another. @CordaSerializable public class Message { - // public Message() {} public Message(MemberX500Name sender, String message) { this.sender = sender; this.message = message; } + public MemberX500Name getSender() { return sender; } diff --git a/src/main/java/com/r3/developers/csdetemplate/MyFirstFlow.java b/workflows/src/main/java/com/r3/developers/csdetemplate/workflows/MyFirstFlow.java similarity index 67% rename from src/main/java/com/r3/developers/csdetemplate/MyFirstFlow.java rename to workflows/src/main/java/com/r3/developers/csdetemplate/workflows/MyFirstFlow.java index 0fcc95e..0263bc1 100644 --- a/src/main/java/com/r3/developers/csdetemplate/MyFirstFlow.java +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/workflows/MyFirstFlow.java @@ -1,4 +1,4 @@ -package com.r3.developers.csdetemplate; +package com.r3.developers.csdetemplate.workflows; import net.corda.v5.application.flows.*; import net.corda.v5.application.marshalling.JsonMarshallingService; @@ -14,50 +14,50 @@ import org.slf4j.LoggerFactory; // MyFirstFlow is an initiating flow, it's corresponding responder flow is called MyFirstFlowResponder (defined below) // to link the two sides of the flow together they need to have the same protocol. -@InitiatingFlow(protocol = "another-flow") +@InitiatingFlow(protocol = "my-first-flow") // MyFirstFlow should inherit from RPCStartableFlow, which tells Corda it can be started via an RPC call public class MyFirstFlow implements RPCStartableFlow { - // It is useful to be able to log messages from the flows for debugging. + // Log messages from the flows for debugging. private final Logger log = LoggerFactory.getLogger(MyFirstFlow.class); // Corda has a set of injectable services which are injected into the flow at runtime. // Flows declare them with @CordaInjectable, then the flows have access to their services. - // JsonMarshallingService provides a Service for manipulating json + // JsonMarshallingService provides a service for manipulating JSON. @CordaInject public JsonMarshallingService jsonMarshallingService; - // FlowMessaging provides a service for establishing flow sessions between Virtual Nodes and - // sending and receiving payloads between them + // FlowMessaging provides a service that establishes flow sessions between virtual nodes + // that send and receive payloads between them. @CordaInject public FlowMessaging flowMessaging; - // MemberLookup provides a service for looking up information about members of the Virtual Network which - // this CorDapp is operating in. + // MemberLookup provides a service for looking up information about members of the virtual network which + // this CorDapp operates in. @CordaInject public MemberLookup memberLookup; public MyFirstFlow() {} - // When a flow is invoked it's call() method is called. - // call() methods must be marked as @Suspendable, this allows Corda to pause mid-execution to wait - // for a response from the other flows and services + // When a flow is invoked its call() method is called. + // Call() methods must be marked as @Suspendable, this allows Corda to pause mid-execution to wait + // for a response from the other flows and services. @NotNull @Suspendable @Override - public String call(@NotNull RPCRequestData requestBody) { + public String call(RPCRequestData requestBody) { - // Useful logging to follow what's happening in the console or logs + // Follow what happens in the console or logs. log.info("MFF: MyFirstFlow.call() called"); - // Show the requestBody in the logs - this can be used to help establish the format for starting a flow on corda + // Show the requestBody in the logs - this can be used to help establish the format for starting a flow on Corda. log.info("MFF: requestBody: " + requestBody.getRequestBody()); - // Deserialize the Json requestBody into the MyfirstFlowStartArgs class using the JsonSerialisation Service + // Deserialize the Json requestBody into the MyfirstFlowStartArgs class using the JsonSerialisation service. MyFirstFlowStartArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, MyFirstFlowStartArgs.class); - // Obtain the MemberX500Name of counterparty + // Obtain the MemberX500Name of the counterparty. MemberX500Name otherMember = flowArgs.otherMember; // Get our identity from the MemberLookup service. @@ -69,19 +69,19 @@ public class MyFirstFlow implements RPCStartableFlow { // Log the message to be sent. log.info("MFF: message.message: " + message.message); - // Start a flow session with the otherMember using the FlowMessaging service - // The otherMember's Virtual Node will run the corresponding MyFirstFlowResponder responder flow + // Start a flow session with the otherMember using the FlowMessaging service. + // The otherMember's virtual node will run the corresponding MyFirstFlowResponder responder flow. FlowSession session = flowMessaging.initiateFlow(otherMember); - // Send the Payload using the send method on the session to the MyFirstFlowResponder Responder flow + // Send the Payload using the send method on the session to the MyFirstFlowResponder responder flow. session.send(message); - // Receive a response from the Responder flow + // Receive a response from the responder flow. Message response = session.receive(Message.class); - // The return value of a RPCStartableFlow must always be a String, this string will be passed + // The return value of a RPCStartableFlow must always be a String. This will be passed // back as the REST RPC response when the status of the flow is queried on Corda, or as the return - // value from the flow when testing using the Simulator + // value from the flow when testing using the simulator. return response.message; } } @@ -91,9 +91,9 @@ public class MyFirstFlow implements RPCStartableFlow { RequestBody for triggering the flow via http-rpc: { "clientRequestId": "r1", - "flowClassName": "com.r3.developers.csdetemplate.MyFirstFlow", + "flowClassName": "com.r3.developers.csdetemplate.workflows.MyFirstFlow", "requestData": { "otherMember":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB" } } - */ \ No newline at end of file + */ diff --git a/src/main/java/com/r3/developers/csdetemplate/MyFirstFlowResponder.java b/workflows/src/main/java/com/r3/developers/csdetemplate/workflows/MyFirstFlowResponder.java similarity index 71% rename from src/main/java/com/r3/developers/csdetemplate/MyFirstFlowResponder.java rename to workflows/src/main/java/com/r3/developers/csdetemplate/workflows/MyFirstFlowResponder.java index de27380..e7a1ff2 100644 --- a/src/main/java/com/r3/developers/csdetemplate/MyFirstFlowResponder.java +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/workflows/MyFirstFlowResponder.java @@ -1,4 +1,4 @@ -package com.r3.developers.csdetemplate; +package com.r3.developers.csdetemplate.workflows; import net.corda.v5.application.flows.CordaInject; import net.corda.v5.application.flows.InitiatedBy; @@ -11,36 +11,36 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -// MyFirstFlowResponder is a responder flow, it's corresponding initiating flow is called MyFirstFlow (defined above) +// MyFirstFlowResponder is a responder flow, its corresponding initiating flow is called MyFirstFlow (defined in MyFirstFlow.java) // to link the two sides of the flow together they need to have the same protocol. -@InitiatedBy(protocol = "another-flow") +@InitiatedBy(protocol = "my-first-flow") // Responder flows must inherit from ResponderFlow public class MyFirstFlowResponder implements ResponderFlow { - // It is useful to be able to log messages from the flows for debugging. + // Log messages from the flows for debugging. private final Logger log = LoggerFactory.getLogger(MyFirstFlowResponder.class); - // MemberLookup provides a service for looking up information about members of the Virtual Network which - // this CorDapp is operating in. + // MemberLookup looks for information about members of the virtual network which + // this CorDapp operates in. @CordaInject public MemberLookup memberLookup; public MyFirstFlowResponder() {} - // Responder flows are invoked when an initiating flow makes a call via a session set up with the Virtual - // node hosting the Responder flow. When a responder flow is invoked it's call() method is called. - // call() methods must be marked as @Suspendable, this allows Corda to pause mid-execution to wait - // for a response from the other flows and services/ + // Responder flows are invoked when an initiating flow makes a call via a session set up with the virtual + // node hosting the responder flow. When a responder flow is invoked its call() method is called. + // Call() methods must be marked as @Suspendable, this allows Corda to pause mid-execution to wait + // for a response from the other flows and services. // The Call method has the flow session passed in as a parameter by Corda so the session is available to // responder flow code, you don't need to inject the FlowMessaging service. @Suspendable @Override public void call(FlowSession session) { - // Useful logging to follow what's happening in the console or logs + // Follow what happens in the console or logs. log.info("MFF: MyFirstResponderFlow.call() called"); - // Receive the payload and deserialize it into a Message class + // Receive the payload and deserialize it into a message class. Message receivedMessage = session.receive(Message.class); // Log the message as a proxy for performing some useful operation on it. @@ -49,7 +49,7 @@ public class MyFirstFlowResponder implements ResponderFlow { // Get our identity from the MemberLookup service. MemberX500Name ourIdentity = memberLookup.myInfo().getName(); - // Create a response to greet the sender + // Create a message to greet the sender. Message response = new Message(ourIdentity, "Hello " + session.getCounterparty().getCommonName() + ", best wishes from " + ourIdentity.getCommonName()); diff --git a/workflows/src/main/java/com/r3/developers/csdetemplate/workflows/MyFirstFlowStartArgs.java b/workflows/src/main/java/com/r3/developers/csdetemplate/workflows/MyFirstFlowStartArgs.java new file mode 100644 index 0000000..51a83c6 --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/workflows/MyFirstFlowStartArgs.java @@ -0,0 +1,15 @@ +package com.r3.developers.csdetemplate.workflows; + +import net.corda.v5.base.types.MemberX500Name; + +// A class to hold the arguments required to start the flow +public class MyFirstFlowStartArgs { + public MemberX500Name otherMember; + + public MyFirstFlowStartArgs(MemberX500Name otherMember) { + this.otherMember = otherMember; + } + + // The JSON Marshalling Service, which handles serialisation, needs this constructor. + public MyFirstFlowStartArgs() {} +} diff --git a/src/test/java/com/r3/developers/csdetemplate/MyFirstFlowTest.java b/workflows/src/test/java/com/r3/developers/csdetemplate/workflows/MyFirstFlowTest.java similarity index 51% rename from src/test/java/com/r3/developers/csdetemplate/MyFirstFlowTest.java rename to workflows/src/test/java/com/r3/developers/csdetemplate/workflows/MyFirstFlowTest.java index 1aa828f..af2a32d 100644 --- a/src/test/java/com/r3/developers/csdetemplate/MyFirstFlowTest.java +++ b/workflows/src/test/java/com/r3/developers/csdetemplate/workflows/MyFirstFlowTest.java @@ -1,50 +1,41 @@ -package com.r3.developers.csdetemplate; +package com.r3.developers.csdetemplate.workflows; - -import net.corda.simulator.HoldingIdentity; import net.corda.simulator.RequestData; import net.corda.simulator.SimulatedVirtualNode; import net.corda.simulator.Simulator; import net.corda.v5.base.types.MemberX500Name; import org.junit.jupiter.api.Test; - class MyFirstFlowTest { - // Names picked to match the corda network in config/dev-net.json private MemberX500Name aliceX500 = MemberX500Name.parse("CN=Alice, OU=Test Dept, O=R3, L=London, C=GB"); private MemberX500Name bobX500 = MemberX500Name.parse("CN=Bob, OU=Test Dept, O=R3, L=London, C=GB"); @Test + @SuppressWarnings("unchecked") public void test_that_MyFirstFLow_returns_correct_message() { - - // Instantiate an instance of the Simulator + // Instantiate an instance of the simulator. Simulator simulator = new Simulator(); - // Create Alice's and Bob HoldingIDs - HoldingIdentity aliceHoldingID = HoldingIdentity.Companion.create(aliceX500); - HoldingIdentity bobHoldingID = HoldingIdentity.Companion.create(bobX500); + // Create Alice's and Bob's virtual nodes, including the classes of the flows which will be registered on each node. + // Don't assign Bob's virtual node to a value. You don't need it for this particular test. + SimulatedVirtualNode aliceVN = simulator.createVirtualNode(aliceX500, MyFirstFlow.class); + simulator.createVirtualNode(bobX500, MyFirstFlowResponder.class); - // Create Alice and Bob's virtual nodes, including the Class's of the flows which will be registered on each node. - // We don't assign Bob's virtual node to a val because we don't need it for this particular test. - SimulatedVirtualNode aliceVN = simulator.createVirtualNode(aliceHoldingID, MyFirstFlow.class); - simulator.createVirtualNode(bobHoldingID, MyFirstFlowResponder.class); - - // Create an instance of the MyFirstFlowStartArgs which contains the request arguments for starting the flow + // Create an instance of the MyFirstFlowStartArgs which contains the request arguments for starting the flow. MyFirstFlowStartArgs myFirstFlowStartArgs = new MyFirstFlowStartArgs(bobX500); - // Create a requestData object + // Create a requestData object. RequestData requestData = RequestData.Companion.create( - "request no 1", // A unique reference for the instance of the flow request - MyFirstFlow.class, // The name of the flow class which is to be started - myFirstFlowStartArgs // The object which contains the start arguments of the flow + "request no 1", // A unique reference for the instance of the flow request. + MyFirstFlow.class, // The name of the flow class which is to be started. + myFirstFlowStartArgs // The object which contains the start arguments of the flow. ); - // Call the Flow on Alice's virtual node and capture the response from the flow + // Call the flow on Alice's virtual node and capture the response. String flowResponse = aliceVN.callFlow(requestData); - // Check that the flow has returned the expected string + // Check that the flow has returned the expected string. assert(flowResponse.equals("Hello Alice, best wishes from Bob")); } } -