diff --git a/java-samples/ping-pong/.ci/Jenkinsfile b/java-samples/ping-pong/.ci/Jenkinsfile new file mode 100644 index 0000000..41fc224 --- /dev/null +++ b/java-samples/ping-pong/.ci/Jenkinsfile @@ -0,0 +1,10 @@ +@Library('corda-shared-build-pipeline-steps@5.0') _ + +cordaPipeline( + nexusAppId: 'com.corda.CSDE-Java.5.0', + publishRepoPrefix: '', + slimBuild: true, + runUnitTests: false, + dedicatedJobForSnykDelta: false, + slackChannel: '#corda-corda5-dev-ex-build-notifications' + ) diff --git a/java-samples/ping-pong/.ci/nightly/JenkinsfileNexusScan b/java-samples/ping-pong/.ci/nightly/JenkinsfileNexusScan new file mode 100644 index 0000000..e2d589a --- /dev/null +++ b/java-samples/ping-pong/.ci/nightly/JenkinsfileNexusScan @@ -0,0 +1,5 @@ +@Library('corda-shared-build-pipeline-steps@5.0') _ + +cordaNexusScanPipeline( + nexusAppId: 'com.corda.CSDE-Java.5.0' +) diff --git a/java-samples/ping-pong/.ci/nightly/JenkinsfileSnykScan b/java-samples/ping-pong/.ci/nightly/JenkinsfileSnykScan new file mode 100644 index 0000000..c07efb7 --- /dev/null +++ b/java-samples/ping-pong/.ci/nightly/JenkinsfileSnykScan @@ -0,0 +1,6 @@ +@Library('corda-shared-build-pipeline-steps@5.0') _ + +cordaSnykScanPipeline ( + snykTokenId: 'r3-snyk-corda5', + snykAdditionalCommands: "--all-sub-projects -d" +) diff --git a/java-samples/ping-pong/.gitignore b/java-samples/ping-pong/.gitignore new file mode 100644 index 0000000..c81757e --- /dev/null +++ b/java-samples/ping-pong/.gitignore @@ -0,0 +1,88 @@ + +# Eclipse, ctags, Mac metadata, log files +.classpath +.project +.settings +tags +.DS_Store +*.log +*.orig + +# Created by .ignore support plugin (hsz.mobi) + +.gradle +local.properties +.gradletasknamecache + +# General build files +**/build/* + +lib/quasar.jar + +**/logs/* + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio + +*.iml + +## Directory-based project format: +.idea/*.xml +.idea/.name +.idea/copyright +.idea/inspectionProfiles +.idea/libraries +.idea/shelf +.idea/dataSources +.idea/markdown-navigator +.idea/runConfigurations +.idea/dictionaries + + +# Include the -parameters compiler option by default in IntelliJ required for serialization. +!.idea/codeStyleSettings.xml + + +## File-based project format: +*.ipr +*.iws + +## Plugin-specific files: + +# IntelliJ +**/out/ +/classes/ + + + +# vim +*.swp +*.swn +*.swo + + + +# Directory generated during Resolve and TestOSGi gradle tasks +bnd/ + +# Ignore Gradle build output directory +build +/.idea/codeStyles/codeStyleConfig.xml +/.idea/codeStyles/Project.xml + + + +# Ignore Visual studio directory +bin/ + + + +*.cpi +*.cpb +*.cpk +workspace/** +#CordaPID.dat +#*.pem +#*.pfx +#CPIFileStatus*.json +#GroupPolicy.json diff --git a/java-samples/ping-pong/.run/runConfigurations/DebugCorDapp.run.xml b/java-samples/ping-pong/.run/runConfigurations/DebugCorDapp.run.xml new file mode 100644 index 0000000..1d8da82 --- /dev/null +++ b/java-samples/ping-pong/.run/runConfigurations/DebugCorDapp.run.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/java-samples/ping-pong/.snyk b/java-samples/ping-pong/.snyk new file mode 100644 index 0000000..b4f98ac --- /dev/null +++ b/java-samples/ping-pong/.snyk @@ -0,0 +1,22 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.25.0 +# ignores vulnerabilities until expiry date; change duration by modifying expiry date +ignore: + SNYK-JAVA-ORGJETBRAINSKOTLIN-2393744: + - '*': + reason: >- + This vulnerability relates to information exposure via creation of + temporary files (via Kotlin functions) with insecure permissions. + Corda does not use any of the vulnerable functions so it is not + susceptible to this vulnerability + expires: 2023-06-19T17:15:26.836Z + created: 2023-02-02T17:15:26.839Z + SNYK-JAVA-ORGJETBRAINSKOTLIN-2628385: + - '*': + reason: >- + corda-simulator-runtime is a testRuntimeOnly dependency, as such this + dependency will not be included in any cordaApp produced by the CSDE + project Template + expires: 2023-06-19T17:16:00.009Z + created: 2023-02-02T17:16:00.016Z +patch: {} diff --git a/java-samples/ping-pong/README.md b/java-samples/ping-pong/README.md new file mode 100644 index 0000000..1ef8108 --- /dev/null +++ b/java-samples/ping-pong/README.md @@ -0,0 +1,62 @@ +# Ping-Pong CorDapp +This CorDapp allows a node to ping any other node on the network that also has this CorDapp installed. +It demonstrates how to use Corda for messaging and passing data using a [flow](https://docs.r3.com/en/platform/corda/5.0-beta/developing/ledger/flows.html) without saving any states or using any contracts. + + +### Concepts +The `ping` utility is normally used to send a Send ICMP ECHO_REQUEST packet to network hosts. The idea being that the receiving host will echo the message back. +In this example the Ping flow will send the String "Ping" to a other member in the network. +The otherMember will correspondingly reply with "Pong". + +## Flows +You'll notice in our code we call these two classes ping and pong, the flow that sends the `"ping"`, and the flow that returns with a `"pong"`. + +Take a look at [Ping.java](./workflows/src/main/java/com/r3/developers/pingpong/workflows/Ping.java). +You'll notice that this flow does what we expect, which is to send an outbound ping, and expect to receive a pong. +If we receive a pong, then our flow is successful. +And of course we see a similar behavior in [Pong.java](./workflows/src/main/java/com/r3/developers/pingpong/workflows/Pong.java). +We expect to receive data from a counterparty that contains a ping, when we receive it, we respond with a pong. + +## Pre-Requisites +For development environment setup, please refer to: [Setup Guide](https://docs.r3.com/). + + +## Running the nodes +1. We will begin our test deployment with clicking the `startCorda`. + `./gradlew startCorda` run this from the Intellij terminal + This task will load up the combined Corda workers in docker. + A successful deployment will allow you to open the REST APIs at: https://localhost:8888/api/v1/swagger#. + You can test out some functions to check connectivity.(GET /cpi function call should return an empty list as for now.) +2. We will now deploy the cordapp with a click of `5-vNodeSetup` task. Upon successful deployment of the CPI, + the GET /cpi function call should now return the meta data of the cpi you just upload + + +### Running the app +In Corda 5, flows will be triggered via `POST /flow/{holdingidentityshorthash}` and flow result will need to be view at +`GET /flow/{holdingidentityshorthash}/{clientrequestid}` +* holdingidentityshorthash: the id of the network participants, ie Bob, Alice, Charlie. You can view all the short + hashes of the network member with another gradle task called `ListVNodes` +* clientrequestid: the id you specify in the flow requestBody when you trigger a flow. + +#### Pinging a node: +Pick a VNode identity to initiate the ping, and get its short hash. Let's pick Alice. + +Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Alice's hash) and request body: +``` +{ + "clientRequestId": "ping-1", + "flowClassName": "Ping", + "requestBody": { + "otherMember": "CN=Bob, OU=Test Dept, O=R3, L=London, C=GB" + } +} +``` + +Now hop to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and enter the short +hash(Alice's hash) and clientrequestid to view the flow result + +##Stop corda +To stop the combined worker - run the task `stopCorda` from the terminal. +``` +./gradlew stopCorda +``` diff --git a/java-samples/ping-pong/build.gradle b/java-samples/ping-pong/build.gradle new file mode 100644 index 0000000..bc61353 --- /dev/null +++ b/java-samples/ping-pong/build.gradle @@ -0,0 +1,47 @@ +import static org.gradle.api.JavaVersion.VERSION_11 + +plugins { + id 'org.jetbrains.kotlin.jvm' + id 'net.corda.cordapp.cordapp-configuration' + id 'org.jetbrains.kotlin.plugin.jpa' + id 'java' + id 'maven-publish' + id 'csde' +} + +allprojects { + group 'net.corda.samples' + version '1.0-SNAPSHOT' + + def javaVersion = VERSION_11 + + // 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" + ] + } + + repositories { + // All dependencies are held in Maven Central + mavenCentral() + } + + tasks.withType(Test).configureEach { + useJUnitPlatform() + } + +} + +publishing { + publications { + maven(MavenPublication) { + artifactId "corda-CSDE-java-sample" + groupId project.group + artifact jar + } + + } +} + diff --git a/java-samples/ping-pong/buildSrc/build.gradle b/java-samples/ping-pong/buildSrc/build.gradle new file mode 100644 index 0000000..d21dbee --- /dev/null +++ b/java-samples/ping-pong/buildSrc/build.gradle @@ -0,0 +1,22 @@ + +plugins { + id 'groovy-gradle-plugin' + id 'java' +} + +repositories { + mavenCentral() + mavenLocal() +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' + + implementation "com.konghq:unirest-java:$unirestVersion" + implementation "com.konghq:unirest-objectmapper-jackson:$unirestVersion" + implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" + implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jacksonVersion" + + implementation "net.corda:corda-base:$cordaApiVersion" +} diff --git a/java-samples/ping-pong/buildSrc/gradle.properties b/java-samples/ping-pong/buildSrc/gradle.properties new file mode 100644 index 0000000..7ab643a --- /dev/null +++ b/java-samples/ping-pong/buildSrc/gradle.properties @@ -0,0 +1,4 @@ +jacksonVersion = 2.13.4 +unirestVersion=3.13.10 + +cordaApiVersion=5.0.0.665-Gecko1.0 diff --git a/java-samples/ping-pong/buildSrc/settings.gradle b/java-samples/ping-pong/buildSrc/settings.gradle new file mode 100644 index 0000000..86ac012 --- /dev/null +++ b/java-samples/ping-pong/buildSrc/settings.gradle @@ -0,0 +1 @@ +// File intentionally left blank diff --git a/java-samples/ping-pong/buildSrc/src/main/groovy/csde.gradle b/java-samples/ping-pong/buildSrc/src/main/groovy/csde.gradle new file mode 100644 index 0000000..a94ffa9 --- /dev/null +++ b/java-samples/ping-pong/buildSrc/src/main/groovy/csde.gradle @@ -0,0 +1,248 @@ +// 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. +// todo: fix logging level and make it configurable. + + +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.VNodesHelper +import com.r3.csde.NetworkConfig + +plugins { + id 'java-library' + id 'groovy' + id 'java' +} + + +configurations { + combinedWorker{ + canBeConsumed = false + canBeResolved= true + } + + myPostgresJDBC { + canBeConsumed = false + canBeResolved = true + } + + notaryServerCPB { + canBeConsumed = false + canBeResolved = true + } +} + +// Dependencies for supporting tools +dependencies { + combinedWorker "net.corda:corda-combined-worker:$combinedWorkerVersion" + myPostgresJDBC "org.postgresql:postgresql:$postgresqlVersion" + 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 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" +def signingCertFName = rootDir.toString() + "/config/gradle-plugin-default-key.pem" +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" + + +// 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' + + +// 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, + signingCertAlias, + signingCertFName, + keystoreAlias, + keystoreFName, + keystoreCertFName, + appCpiName, + notaryCpiName, + devEnvWorkspace, + cordaCliBinDir, + cordaNotaryServerDir, + workflowBuildDir, + cordaNotaryPluginsVersion +) + +def networkConfig = new NetworkConfig("config/static-network-config.json") + +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 = supportingGroup + doLast { + copy { + from configurations.myPostgresJDBC + into "$cordaJDBCDir" + } + } +} + +tasks.register("getDevCordaLite", Copy) { + group = supportingGroup + from configurations.combinedWorker + into cordaBinDir +} + + +// Corda status queries + +def cordaStatusQueries = new CordaStatusQueries(projectContext) + + +tasks.register('listVNodes') { + group = queriesGroup + doLast { + cordaStatusQueries.listVNodes() + } +} + +tasks.register('listCPIs') { + group = queriesGroup + doLast { + cordaStatusQueries.listCPIs() + } +} + +// Build CPI tasks + +def buildCPIsHelper = new BuildCPIsHelper(projectContext, networkConfig) + +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() + } +} + +// Setup VNodes tasks + +def vNodesHelper = new VNodesHelper(projectContext, networkConfig ) + +tasks.register('5-vNodeSetup') { + group = cordappGroup + dependsOn('4-deployCPIs') + doLast { + vNodesHelper.vNodesSetup() + } +} diff --git a/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/BuildCPIsHelper.java b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/BuildCPIsHelper.java new file mode 100644 index 0000000..96140ef --- /dev/null +++ b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/BuildCPIsHelper.java @@ -0,0 +1,278 @@ +package com.r3.csde; + +import java.io.*; +import java.util.LinkedList; +import java.util.List; + +// todo: This class needs refactoring, see https://r3-cev.atlassian.net/browse/CORE-11624 +public class BuildCPIsHelper { + + public ProjectContext pc; + public ProjectUtils utils; + + public NetworkConfig config; + public BuildCPIsHelper(ProjectContext _pc, NetworkConfig _config) { + pc = _pc; + utils = new ProjectUtils(pc); + config = _config; + } + + public void createGroupPolicy() throws IOException { + + File groupPolicyFile = new File(String.format("%s/GroupPolicy.json", pc.devEnvWorkspace)); + File devnetFile = new File(pc.project.getRootDir() + "/" + config.getConfigFilePath()); + + + if (!groupPolicyFile.exists() || groupPolicyFile.lastModified() < devnetFile.lastModified()) { + + pc.out.println("createGroupPolicy: Creating a GroupPolicy"); + + List configX500Ids = config.getX500Names(); + 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/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/CordaLifeCycleHelper.java b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/CordaLifeCycleHelper.java new file mode 100644 index 0000000..daa78a6 --- /dev/null +++ b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/CordaLifeCycleHelper.java @@ -0,0 +1,93 @@ +package com.r3.csde; + +import kong.unirest.Unirest; + +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 + */ +// todo: This class needs refactoring, see https://r3-cev.atlassian.net/browse/CORE-11624 +public class CordaLifeCycleHelper { + + ProjectContext pc; + ProjectUtils utils; + + public CordaLifeCycleHelper(ProjectContext _pc) { + pc = _pc; + utils = new ProjectUtils(pc); + Unirest.config().verifySsl(false); + } + + public void startCorda() throws IOException { + PrintStream pidStore = new PrintStream(new FileOutputStream(pc.cordaPidCache)); + File combinedWorkerJar = pc.project.getConfigurations().getByName("combinedWorker").getSingleFile(); + + // Manual version of the command to start postgres (for reference): + // docker run -d --rm -p5432:5432 --name CSDEpostgresql -e POSTGRES_DB=cordacluster -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=password postgres:latest + + 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: we should poll for readiness not wait 10 seconds, see https://r3-cev.atlassian.net/browse/CORE-11626 + utils.rpcWait(10000); + + ProcessBuilder procBuild = new ProcessBuilder(pc.javaBinDir + "/java", + "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", + "-DsecurityMangerEnabled=false", + "-Dlog4j.configurationFile=" + pc.project.getRootDir() + "/config/log4j2.xml", + "-Dco.paralleluniverse.fibers.verifyInstrumentation=true", + "-jar", + combinedWorkerJar.toString(), + "--instance-id=0", + "-mbus.busType=DATABASE", + "-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()); + proc.getInputStream().transferTo(pc.out); + + // todo: we should poll for readiness before completing the startCorda task, see https://r3-cev.atlassian.net/browse/CORE-11625 + } + + + 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/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/CordaStatusQueries.java b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/CordaStatusQueries.java new file mode 100644 index 0000000..95072ba --- /dev/null +++ b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/CordaStatusQueries.java @@ -0,0 +1,64 @@ +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; + +// todo: This class needs refactoring, see https://r3-cev.atlassian.net/browse/CORE-11624 +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/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/CsdeException.java b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/CsdeException.java new file mode 100644 index 0000000..72f8fea --- /dev/null +++ b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/CsdeException.java @@ -0,0 +1,10 @@ +package com.r3.csde; + +public class CsdeException extends Exception { + public CsdeException(String message, Throwable cause) { + super(message, cause); + } + public CsdeException(String message){ + super(message); + } +} \ No newline at end of file diff --git a/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/DeployCPIsHelper.java b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/DeployCPIsHelper.java new file mode 100644 index 0000000..fe1362b --- /dev/null +++ b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/DeployCPIsHelper.java @@ -0,0 +1,187 @@ +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; + +// todo: This class needs refactoring, see https://r3-cev.atlassian.net/browse/CORE-11624 +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, uploadStatusQualifier); + } + } + +} diff --git a/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/NetworkConfig.java b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/NetworkConfig.java new file mode 100644 index 0000000..9b3f0ca --- /dev/null +++ b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/NetworkConfig.java @@ -0,0 +1,40 @@ +package com.r3.csde; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.FileInputStream; +import java.util.List; +import java.util.stream.Collectors; + + +/** + * This class reads the network config from the json file and makes it available as a list of VNodes. + */ +public class NetworkConfig { + + private List vNodes; + private String configFilePath; + + public NetworkConfig(String _configFilePath) throws CsdeException { + configFilePath = _configFilePath; + + ObjectMapper mapper = new ObjectMapper(); + try { + FileInputStream in = new FileInputStream(configFilePath); + vNodes = mapper.readValue(in, new TypeReference>() { + }); + } catch (Exception e) { + throw new CsdeException("Failed to read static network configuration file, with exception: " + e); + } + } + + String getConfigFilePath() { return configFilePath; } + + List getVNodes() { return vNodes; } + + List getX500Names() { + return vNodes.stream().map(vn -> vn.getX500Name()).collect(Collectors.toList()); + } + +} diff --git a/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/ProjectContext.java b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/ProjectContext.java new file mode 100644 index 0000000..0e84567 --- /dev/null +++ b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/ProjectContext.java @@ -0,0 +1,84 @@ +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 NotaryCPIUploadBaseName = "CPIFileStatus-NotaryServer.json"; + String CPIUploadStatusFName; + String NotaryCPIUploadStatusFName; + 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; + NotaryCPIUploadStatusFName = workspaceDir + "/" + NotaryCPIUploadBaseName; + 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/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/ProjectUtils.java b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/ProjectUtils.java new file mode 100644 index 0000000..dbc3dd8 --- /dev/null +++ b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/ProjectUtils.java @@ -0,0 +1,36 @@ +package com.r3.csde; + +import kong.unirest.HttpResponse; +import kong.unirest.JsonNode; + +import static java.lang.Thread.sleep; + +// todo: This class needs refactoring, see https://r3-cev.atlassian.net/browse/CORE-11624 +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); + } + } + + public void reportError(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/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/VNode.java b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/VNode.java new file mode 100644 index 0000000..d77d85c --- /dev/null +++ b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/VNode.java @@ -0,0 +1,25 @@ +package com.r3.csde; + + +/** + * This class is a representation of a Vnode used to express the vNodes required on the network. + */ + +public class VNode { + private String x500Name; + private String cpi; + + private String serviceX500Name; + + public VNode() { } + + public String getX500Name(){ return x500Name; } + public void setX500Name(String _x500Name) { x500Name = _x500Name; } + + public String getCpi() { return cpi; } + public void setCpi(String _cpi) { cpi = _cpi; } + + public String getServiceX500Name() { return serviceX500Name; } + public void setServiceX500Name(String _name) { serviceX500Name = _name; } + +} diff --git a/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/VNodesHelper.java b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/VNodesHelper.java new file mode 100644 index 0000000..bafdc75 --- /dev/null +++ b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/VNodesHelper.java @@ -0,0 +1,288 @@ +package com.r3.csde; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.r3.csde.dtos.*; +import kong.unirest.HttpResponse; +import kong.unirest.JsonNode; +import kong.unirest.Unirest; +import java.io.FileInputStream; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import static java.net.HttpURLConnection.HTTP_OK; + +/** + * The VNodesHelper class is used to create and register the Vnodes specified in the static-network-config.json file. + */ + +public class VNodesHelper { + private ProjectContext pc; + private ProjectUtils utils; + private NetworkConfig config; + private ObjectMapper mapper; + + public VNodesHelper(ProjectContext _pc, NetworkConfig _config) { + pc = _pc; + utils = new ProjectUtils(pc); + config = _config; + Unirest.config().verifySsl(false); + mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + /** + * Entry point for setting up vnodes, called from the 5-vNodeSetup task in csde.gradle + */ + public void vNodesSetup() throws CsdeException { + List requiredNodes = config.getVNodes(); + createVNodes(requiredNodes); + registerVNodes(requiredNodes); + } + + /** + * Creates vnodes specified in the config if they don't already exist. + * @param requiredNodes represents the list of VNodes as specified in the network Config json file (static-network-config.json) + */ + private void createVNodes(List requiredNodes) throws CsdeException { + + // Get existing Nodes. + List existingVNodes = getExistingNodes(); + + // Check if each required vnode already exist, if not create it. + for (VNode vn : requiredNodes) { + // Match on x500 and cpi name + List matches = existingVNodes + .stream() + .filter(existing -> + existing.getHoldingIdentity().getX500Name().equals( vn.getX500Name()) && + existing.getCpiIdentifier().getCpiName().equals(vn.getCpi())) + .collect(Collectors.toList()); + + if (matches.size() == 0) { + createVNode(vn); + } + } + } + + /** + * Gets a list of the virtual nodes which have already been created. + * @return a list of the virtual nodes which have already been created. + */ + private List getExistingNodes () throws CsdeException { + + HttpResponse response = Unirest.get(pc.baseURL + "/api/v1/virtualnode") + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + if (response.getStatus() != HTTP_OK) { + throw new CsdeException("Failed to get Existing vNodes, response status: "+ response.getStatus()); + } + + try { + return mapper.readValue(response.getBody().toString(), VirtualNodesDTO.class).getVirtualNodes(); + } catch (Exception e){ + throw new CsdeException("Failed to get Existing vNodes with exception: " + e); + } + } + + /** + * Creates a vnode on the Corda cluster from the VNode info. + * @param vNode represents a virtual node using VNode Class. + */ + private void createVNode(VNode vNode) throws CsdeException { + + pc.out.println("Creating virtual node for "+ vNode.getX500Name()); + + // Reads the current CPIFileChecksum value and checks it has been uploaded. + String cpiCheckSum = getCpiCheckSum(vNode); + if (!checkCpiUploaded(cpiCheckSum)) throw new CsdeException("Cpi " + cpiCheckSum + " not uploaded."); + + // Creates the vnode on Cluster + HttpResponse response = Unirest.post(pc.baseURL + "/api/v1/virtualnode") + .body("{ \"request\" : { \"cpiFileChecksum\": \"" + cpiCheckSum + "\", \"x500Name\": \"" + vNode.getX500Name() + "\" } }") + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + if (response.getStatus() != HTTP_OK) { + throw new CsdeException("Creation of virtual node failed with response status: " + response.getStatus()); + } + } + + /** + * Reads the latest CPI checksums from file. + * @param vNode represents a virtual node using VNode Class. + */ + private String getCpiCheckSum(VNode vNode) throws CsdeException { + + try { + String file = (vNode.getServiceX500Name() == null) ? pc.CPIUploadStatusFName : pc.NotaryCPIUploadStatusFName; + FileInputStream in = new FileInputStream(file); + CPIFileStatusDTO statusDTO = mapper.readValue(in, CPIFileStatusDTO.class); + return statusDTO.getCpiFileChecksum().toString(); + } catch (Exception e) { + throw new CsdeException("Failed to read CPI checksum from file, with error: " + e); + } + } + + private boolean checkCpiUploaded(String cpiCheckSum) throws CsdeException { + + HttpResponse response = Unirest.get(pc.baseURL + "/api/v1/cpi") + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + if (response.getStatus() != HTTP_OK) { + throw new CsdeException("Failed to check cpis, response status: " + response.getStatus()); + } + + try { + GetCPIsResponseDTO cpisResponse = mapper.readValue( + response.getBody().toString(), GetCPIsResponseDTO.class); + + for (CpiMetadataDTO cpi: cpisResponse.getCpis()) { + if (Objects.equals(cpi.getCpiFileChecksum(), cpiCheckSum)) { + return true; + } + } + // Returns false if no cpis were returned or the cpiCheckSum didnt' match ay results. + return false; + } catch (Exception e) { + throw new CsdeException("Failed to check cpis with exception: " + e); + } + } + + /** + * Checks if the required virtual nodes have been registered and if not registers them. + * @param requiredNodes represents the list of VNodes as specified in the network Config json file (static-network-config.json) + */ + private void registerVNodes(List requiredNodes) throws CsdeException { + + // There appears to be a delay between the successful post /virtualnodes synchronous call and the + // vnodes being returned in the GET /virtualnodes call. Putting a thread wait here as a quick fix + // as this will move to async mechanism post beta2. + utils.rpcWait(2000); + List existingVNodes = getExistingNodes(); + + for (VNode vn: requiredNodes) { + // Match on x500 and cpi name + List matches = existingVNodes + .stream() + .filter(existing -> + existing.getHoldingIdentity().getX500Name().equals( vn.getX500Name()) && + existing.getCpiIdentifier().getCpiName().equals(vn.getCpi())) + .collect(Collectors.toList()); + + if (matches.size() == 0) { + throw new CsdeException("Registration failed because virtual node for '" + vn.getX500Name() + "' not found."); + } else if (matches.size() >1 ) { + throw new CsdeException(("Registration failed because more than virtual node for '" + vn.getX500Name() + "'")); + } + + String shortHash = matches.get(0).getHoldingIdentity().getShortHash(); + + if (!checkVNodeIsRegistered(shortHash)) { + registerVnode(vn, shortHash); + } + } + } + + /** + * Registers a virtual node. + * @param vnode represents the vnode to be registered. + * @param shortHash the shortHash of the virtual node to register. + */ + private void registerVnode(VNode vnode, String shortHash) throws CsdeException { + + pc.out.println("Registering vNode: " + vnode.getX500Name() + " with shortHash: " + shortHash); + + // Configure the registration body (notary vs non notary) + String registrationBody; + if (vnode.getServiceX500Name() == null) { + registrationBody = + "{ \"action\" : \"requestJoin\"," + + " \"context\" : {" + + " \"corda.key.scheme\" : \"CORDA.ECDSA.SECP256R1\" } }"; + } else { + registrationBody = + "{ \"action\" : \"requestJoin\"," + + " \"context\" : {" + + " \"corda.key.scheme\" : \"CORDA.ECDSA.SECP256R1\", " + + " \"corda.roles.0\" : \"notary\", " + + " \"corda.notary.service.name\" : \"" + vnode.getServiceX500Name() + "\", " + + " \"corda.notary.service.plugin\" : \"net.corda.notary.NonValidatingNotary\" } }"; + } + + HttpResponse response = Unirest.post(pc.baseURL + "/api/v1/membership/" + shortHash) + .body(registrationBody) + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + if (response.getStatus() == HTTP_OK) { + pc.out.println("Membership requested for node " + shortHash); + } else { + throw new CsdeException("Failed to register virtual node " + shortHash + ", response status: " + response.getStatus() ); + } + + // wait until Vnode registered + pollForRegistration(shortHash, 30000, 1000); + + } + + /** + * Checks if a virtual node with given shortHash has been registered + * @param shortHash shortHash of the node which is being checked. + * @return returns true if the vnode is registered + */ + private boolean checkVNodeIsRegistered(String shortHash) throws CsdeException { + + // Queries registration status for vnode. + HttpResponse response = Unirest.get(pc.baseURL + "/api/v1/membership/" + shortHash) + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + if (response.getStatus() != HTTP_OK) + throw new CsdeException("Failed to check registration status for virtual node '" + shortHash + + "' response status: " + response.getStatus()); + + try { + // If the response body is not empty check all previous requests for an "APPROVED" + if (!response.getBody().getArray().isEmpty()) { + List requests = mapper.readValue( + response.getBody().toString(), new TypeReference<>() { + }); + for (RegistrationRequestProgressDTO request : requests) { + if (Objects.equals(request.getRegistrationStatus(), "APPROVED")) { + return true; + } + } + } + // Returns false if array was empty or "APPROVED" wasn't found + return false; + + } catch (Exception e) { + throw new CsdeException("Failed to check registration status for " + shortHash + + " with exception " + e); + } + } + + /** + * Polls for registration of a vnode + * @param shortHash short hash of the node which is being poled for + * @param duration the number of milliseconds before the pollign times out + * @param cooldown the number of milliseconds in between poll attempts. + */ + private void pollForRegistration(String shortHash, int duration, int cooldown) throws CsdeException { + + int timer = 0; + while (timer < duration ) { + if (checkVNodeIsRegistered(shortHash)){ + pc.out.println("Vnode " + shortHash +" registered."); + return; + } + utils.rpcWait(cooldown); + timer += cooldown; + } + throw new CsdeException("Vnode " + shortHash + " failed to register in " + duration + " milliseconds"); + } +} diff --git a/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/CPIFileStatusDTO.java b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/CPIFileStatusDTO.java new file mode 100644 index 0000000..1c6e118 --- /dev/null +++ b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/CPIFileStatusDTO.java @@ -0,0 +1,16 @@ +package com.r3.csde.dtos; + +public class CPIFileStatusDTO { + private String status; + private String cpiFileChecksum; + + public CPIFileStatusDTO() {} + + public String getStatus() { return status; } + + public void setStatus(String status) { this.status = status; } + + public String getCpiFileChecksum() { return cpiFileChecksum; } + + public void setCpiFileChecksum(String cpiFileChecksum) { this.cpiFileChecksum = cpiFileChecksum; } +} diff --git a/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/CpiIdentifierDTO.java b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/CpiIdentifierDTO.java new file mode 100644 index 0000000..e5d5dde --- /dev/null +++ b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/CpiIdentifierDTO.java @@ -0,0 +1,23 @@ +package com.r3.csde.dtos; + +public class CpiIdentifierDTO { + + // Note, these DTOs don't cover all returned values, just the ones required for CSDE + private String cpiName; + private String cpiVersion; + private String signerSummaryHash; + + public CpiIdentifierDTO() { } + + public String getCpiName() { return cpiName; } + + public void setCpiName(String cpiName) { this.cpiName = cpiName; } + + public String getCpiVersion() { return cpiVersion; } + + public void setCpiVersion(String cpiVersion) { this.cpiVersion = cpiVersion; } + + public String getSignerSummaryHash() { return signerSummaryHash; } + + public void setSignerSummaryHash(String signerSummaryHash) { this.signerSummaryHash = signerSummaryHash; } +} diff --git a/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/CpiMetadataDTO.java b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/CpiMetadataDTO.java new file mode 100644 index 0000000..ef89c7d --- /dev/null +++ b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/CpiMetadataDTO.java @@ -0,0 +1,17 @@ +package com.r3.csde.dtos; + +public class CpiMetadataDTO { + // Note, these DTOs don't cover all returned values, just the ones required for CSDE. + private String cpiFileChecksum; + private CpiIdentifierDTO id; + + public CpiMetadataDTO() {} + + public String getCpiFileChecksum() { return cpiFileChecksum; } + + public void setCpiFileChecksum(String cpiFileChecksum) { this.cpiFileChecksum = cpiFileChecksum; } + + public CpiIdentifierDTO getId() { return id; } + + public void setId(CpiIdentifierDTO id) { this.id = id; } +} diff --git a/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/GetCPIsResponseDTO.java b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/GetCPIsResponseDTO.java new file mode 100644 index 0000000..a16e9a1 --- /dev/null +++ b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/GetCPIsResponseDTO.java @@ -0,0 +1,14 @@ +package com.r3.csde.dtos; + +import java.util.List; + +public class GetCPIsResponseDTO { + + private List cpis; + + public GetCPIsResponseDTO() {} + + public List getCpis() { return cpis; } + + public void setCpis(List cpis) { this.cpis = cpis; } +} diff --git a/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/HoldingIdentityDTO.java b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/HoldingIdentityDTO.java new file mode 100644 index 0000000..48c12c0 --- /dev/null +++ b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/HoldingIdentityDTO.java @@ -0,0 +1,27 @@ +package com.r3.csde.dtos; + +public class HoldingIdentityDTO { + // Note, these DTOs don't cover all returned values, just the ones required for CSDE. + private String fullHash; + private String groupId; + private String shortHash; + private String x500Name; + + public HoldingIdentityDTO() {} + + public String getFullHash() { return fullHash; } + + public void setFullHash(String fullHash) { this.fullHash = fullHash; } + + public String getGroupId() { return groupId; } + + public void setGroupID(String groupID) { this.groupId = groupId; } + + public String getShortHash() { return shortHash; } + + public void setShortHash(String shortHash) { this.shortHash = shortHash; } + + public String getX500Name() { return x500Name; } + + public void setX500Name(String x500Name) { this.x500Name = x500Name; } +} diff --git a/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/RegistrationRequestProgressDTO.java b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/RegistrationRequestProgressDTO.java new file mode 100644 index 0000000..b3e63b0 --- /dev/null +++ b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/RegistrationRequestProgressDTO.java @@ -0,0 +1,17 @@ +package com.r3.csde.dtos; + +public class RegistrationRequestProgressDTO { + // Note, these DTOs don't cover all returned values, just the ones required for CSDE. + private String registrationStatus; + private String reason; + + public RegistrationRequestProgressDTO() {} + + public String getRegistrationStatus() { return registrationStatus; } + + public void setRegistrationStatus(String registrationStatus) { this.registrationStatus = registrationStatus; } + + public String getReason() { return reason; } + + public void setReason(String reason) { this.reason = reason; } +} diff --git a/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/VirtualNodeInfoDTO.java b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/VirtualNodeInfoDTO.java new file mode 100644 index 0000000..152cf3e --- /dev/null +++ b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/VirtualNodeInfoDTO.java @@ -0,0 +1,17 @@ +package com.r3.csde.dtos; + +public class VirtualNodeInfoDTO { + // Note, these DTOs don't cover all returned values, just the ones required for CSDE. + private HoldingIdentityDTO holdingIdentity; + private CpiIdentifierDTO cpiIdentifier; + + public VirtualNodeInfoDTO() {} + + public HoldingIdentityDTO getHoldingIdentity() { return holdingIdentity; } + + public void setHoldingIdentity(HoldingIdentityDTO holdingIdentity) { this.holdingIdentity = holdingIdentity; } + + public CpiIdentifierDTO getCpiIdentifier() { return cpiIdentifier; } + + public void setCpiIdentifier(CpiIdentifierDTO cpiIdentifier) { this.cpiIdentifier = cpiIdentifier; } +} diff --git a/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/VirtualNodesDTO.java b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/VirtualNodesDTO.java new file mode 100644 index 0000000..86966b7 --- /dev/null +++ b/java-samples/ping-pong/buildSrc/src/main/java/com/r3/csde/dtos/VirtualNodesDTO.java @@ -0,0 +1,14 @@ +package com.r3.csde.dtos; + +import java.util.List; + +public class VirtualNodesDTO { + + private List virtualNodes; + + public VirtualNodesDTO() {} + + public List getVirtualNodes() { return virtualNodes; } + + public void setVirtualNodes(List virtualNodes) { this.virtualNodes = virtualNodes; } +} diff --git a/java-samples/ping-pong/config/gradle-plugin-default-key.pem b/java-samples/ping-pong/config/gradle-plugin-default-key.pem new file mode 100644 index 0000000..5294bbd --- /dev/null +++ b/java-samples/ping-pong/config/gradle-plugin-default-key.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB7zCCAZOgAwIBAgIEFyV7dzAMBggqhkjOPQQDAgUAMFsxCzAJBgNVBAYTAkdC +MQ8wDQYDVQQHDAZMb25kb24xDjAMBgNVBAoMBUNvcmRhMQswCQYDVQQLDAJSMzEe +MBwGA1UEAwwVQ29yZGEgRGV2IENvZGUgU2lnbmVyMB4XDTIwMDYyNTE4NTI1NFoX +DTMwMDYyMzE4NTI1NFowWzELMAkGA1UEBhMCR0IxDzANBgNVBAcTBkxvbmRvbjEO +MAwGA1UEChMFQ29yZGExCzAJBgNVBAsTAlIzMR4wHAYDVQQDExVDb3JkYSBEZXYg +Q29kZSBTaWduZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQDjSJtzQ+ldDFt +pHiqdSJebOGPZcvZbmC/PIJRsZZUF1bl3PfMqyG3EmAe0CeFAfLzPQtf2qTAnmJj +lGTkkQhxo0MwQTATBgNVHSUEDDAKBggrBgEFBQcDAzALBgNVHQ8EBAMCB4AwHQYD +VR0OBBYEFLMkL2nlYRLvgZZq7GIIqbe4df4pMAwGCCqGSM49BAMCBQADSAAwRQIh +ALB0ipx6EplT1fbUKqgc7rjH+pV1RQ4oKF+TkfjPdxnAAiArBdAI15uI70wf+xlL +zU+Rc5yMtcOY4/moZUq36r0Ilg== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/java-samples/ping-pong/config/log4j2.xml b/java-samples/ping-pong/config/log4j2.xml new file mode 100644 index 0000000..909222c --- /dev/null +++ b/java-samples/ping-pong/config/log4j2.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java-samples/ping-pong/config/static-network-config.json b/java-samples/ping-pong/config/static-network-config.json new file mode 100644 index 0000000..9adde9b --- /dev/null +++ b/java-samples/ping-pong/config/static-network-config.json @@ -0,0 +1,23 @@ +[ + { + "x500Name" : "CN=Alice, OU=Test Dept, O=R3, L=London, C=GB", + "cpi" : "cpi name" + }, + { + "x500Name" : "CN=Bob, OU=Test Dept, O=R3, L=London, C=GB", + "cpi" : "cpi name" + }, + { + "x500Name" : "CN=Charlie, OU=Test Dept, O=R3, L=London, C=GB", + "cpi" : "cpi name" + }, + { + "x500Name" : "CN=Dave, OU=Test Dept, O=R3, L=London, C=GB", + "cpi" : "cpi name" + }, + { + "x500Name" : "CN=NotaryRep1, OU=Test Dept, O=R3, L=London, C=GB", + "cpi" : "CSDE Notary Server CPI", + "serviceX500Name": "CN=NotaryService, OU=Test Dept, O=R3, L=London, C=GB" + } +] diff --git a/java-samples/ping-pong/gradle.properties b/java-samples/ping-pong/gradle.properties new file mode 100644 index 0000000..e2ceeaa --- /dev/null +++ b/java-samples/ping-pong/gradle.properties @@ -0,0 +1,40 @@ +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.665-Gecko1.0 + +# Settings For Development Utilities +combinedWorkerVersion=5.0.0.0-Gecko1.0 +simulatorVersion=5.0.0.0-Gecko1.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-Gecko1.0 + +# Specify the version of the cordapp-cpb and cordapp-cpk plugins +cordaPluginsVersion=7.0.1 + +# For the time being this just needs to be set to a dummy value. +platformVersion = 999 + +# Version of Kotlin to use. +# We recommend using a version close to that used by Corda-API. +kotlinVersion = 1.7.21 + +# Do not use default dependencies. +kotlin.stdlib.default.dependency=false + +# Test Tooling Dependency Versions +junitVersion = 5.8.2 +mockitoKotlinVersion=4.0.0 +mockitoVersion=4.6.1 +hamcrestVersion=2.2 + +postgresqlVersion=42.4.3 + +cordaClusterURL=https://localhost:8888 +cordaRpcUser=admin +cordaRpcPasswd=admin +devEnvWorkspace=workspace +dbContainerName=CSDEpostgresql \ No newline at end of file diff --git a/java-samples/ping-pong/gradle/wrapper/gradle-wrapper.jar b/java-samples/ping-pong/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..41d9927 Binary files /dev/null and b/java-samples/ping-pong/gradle/wrapper/gradle-wrapper.jar differ diff --git a/java-samples/ping-pong/gradle/wrapper/gradle-wrapper.properties b/java-samples/ping-pong/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..5ec4b8e --- /dev/null +++ b/java-samples/ping-pong/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/java-samples/ping-pong/gradlew b/java-samples/ping-pong/gradlew new file mode 100755 index 0000000..744e882 --- /dev/null +++ b/java-samples/ping-pong/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/java-samples/ping-pong/gradlew.bat b/java-samples/ping-pong/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/java-samples/ping-pong/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/java-samples/ping-pong/settings.gradle b/java-samples/ping-pong/settings.gradle new file mode 100644 index 0000000..7db63e8 --- /dev/null +++ b/java-samples/ping-pong/settings.gradle @@ -0,0 +1,23 @@ +pluginManagement { + // Declare the repositories where plugins are stored. + repositories { + gradlePluginPortal() + mavenCentral() + } + + // The plugin dependencies with versions of the plugins congruent with the specified CorDapp plugin version, + // Corda API version, and Kotlin version. + plugins { + id 'net.corda.plugins.cordapp-cpk2' version cordaPluginsVersion + id 'net.corda.plugins.cordapp-cpb2' version cordaPluginsVersion + id 'net.corda.cordapp.cordapp-configuration' version cordaApiVersion + id 'org.jetbrains.kotlin.jvm' version kotlinVersion + id 'org.jetbrains.kotlin.plugin.jpa' version kotlinVersion + id 'org.jetbrains.kotlin.plugin.allopen' version kotlinVersion + } +} + +// Root project name, used in naming the project as a whole and used in naming objects built by the project. +rootProject.name = 'ping-pong' +include ':workflows' + diff --git a/java-samples/ping-pong/workflows/build.gradle b/java-samples/ping-pong/workflows/build.gradle new file mode 100644 index 0000000..0b02763 --- /dev/null +++ b/java-samples/ping-pong/workflows/build.gradle @@ -0,0 +1,89 @@ +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: + + cordaProvided 'org.jetbrains.kotlin:kotlin-osgi-bundle' + + // 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 + // Todo: these are commented out as the simulator UTXO work has not been merged into Gecko yet. +// 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/java-samples/ping-pong/workflows/src/main/java/com/r3/developers/pingpong/workflows/Ping.java b/java-samples/ping-pong/workflows/src/main/java/com/r3/developers/pingpong/workflows/Ping.java new file mode 100644 index 0000000..bd3e563 --- /dev/null +++ b/java-samples/ping-pong/workflows/src/main/java/com/r3/developers/pingpong/workflows/Ping.java @@ -0,0 +1,58 @@ +package com.r3.developers.pingpong.workflows; + +import net.corda.v5.application.flows.*; +import net.corda.v5.application.marshalling.JsonMarshallingService; +import net.corda.v5.application.messaging.FlowMessaging; +import net.corda.v5.application.messaging.FlowSession; +import net.corda.v5.application.membership.MemberLookup; +import net.corda.v5.membership.MemberInfo; +import net.corda.v5.base.annotations.Suspendable; +import net.corda.v5.base.types.MemberX500Name; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@InitiatingFlow(protocol = "ping") +public class Ping implements ClientStartableFlow { + private final static Logger log = LoggerFactory.getLogger(Ping.class); + + // JsonMarshallingService provides a service for manipulating JSON. + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + // 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 operates in. + @CordaInject + public MemberLookup memberLookup; + + public Ping() {} + + @Suspendable + @Override + public String call(ClientRequestBody requestBody){ + log.info("Ping.call() called"); + + PingFlowArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, PingFlowArgs.class); + + // Obtain the MemberX500Name of the other member. + MemberX500Name otherMember = flowArgs.getOtherMember(); + + MemberInfo myInfo = memberLookup.myInfo(); + + FlowSession session = flowMessaging.initiateFlow(otherMember); + final String message = session.sendAndReceive(String.class, "ping"); + if(message.equals("pong")){ + log.info("Received Pong"); + return message; + } + + return null; + + +} + +} diff --git a/java-samples/ping-pong/workflows/src/main/java/com/r3/developers/pingpong/workflows/PingFlowArgs.java b/java-samples/ping-pong/workflows/src/main/java/com/r3/developers/pingpong/workflows/PingFlowArgs.java new file mode 100644 index 0000000..2421383 --- /dev/null +++ b/java-samples/ping-pong/workflows/src/main/java/com/r3/developers/pingpong/workflows/PingFlowArgs.java @@ -0,0 +1,19 @@ +package com.r3.developers.pingpong.workflows; + +import net.corda.v5.base.types.MemberX500Name; + +// A class to hold the arguments required to start the flow +public class PingFlowArgs { + private MemberX500Name otherMember; + + // The JSON Marshalling Service, which handles serialisation, needs this constructor. + public PingFlowArgs() {} + + public PingFlowArgs(MemberX500Name otherMember) { + this.otherMember = otherMember; + } + + public MemberX500Name getOtherMember() { + return otherMember; + } +} \ No newline at end of file diff --git a/java-samples/ping-pong/workflows/src/main/java/com/r3/developers/pingpong/workflows/Pong.java b/java-samples/ping-pong/workflows/src/main/java/com/r3/developers/pingpong/workflows/Pong.java new file mode 100644 index 0000000..387e9b3 --- /dev/null +++ b/java-samples/ping-pong/workflows/src/main/java/com/r3/developers/pingpong/workflows/Pong.java @@ -0,0 +1,42 @@ +package com.r3.developers.pingpong.workflows; + +import net.corda.v5.application.flows.CordaInject; +import net.corda.v5.application.flows.InitiatedBy; +import net.corda.v5.application.flows.ResponderFlow; +import net.corda.v5.application.membership.MemberLookup; +import net.corda.v5.application.messaging.FlowSession; +import net.corda.v5.base.annotations.Suspendable; +import net.corda.v5.application.messaging.FlowMessaging; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@InitiatedBy(protocol = "ping") +public class Pong implements ResponderFlow { + private final static Logger log = LoggerFactory.getLogger(Ping.class); + + // 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 operates in. + @CordaInject + public MemberLookup memberLookup; + + public Pong() {} + + @Override + @Suspendable + public void call(FlowSession session){ + log.info("Pong.call() called"); + + String message = session.receive(String.class); + if(message.equals("ping")){ + log.info("Received Ping"); + session.send("pong"); + } + + } + +} diff --git a/java-samples/ping-pong/workflows/src/test/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowTest.java b/java-samples/ping-pong/workflows/src/test/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowTest.java new file mode 100644 index 0000000..9dea76c --- /dev/null +++ b/java-samples/ping-pong/workflows/src/test/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowTest.java @@ -0,0 +1,41 @@ +//package com.r3.developers.csdetemplate.flowexample.workflows; +// +//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. +// Simulator simulator = new Simulator(); +// +// // 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 an instance of the MyFirstFlowStartArgs which contains the request arguments for starting the flow. +// MyFirstFlowStartArgs myFirstFlowStartArgs = new MyFirstFlowStartArgs(bobX500); +// +// // 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. +// ); +// +// // 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. +// assert(flowResponse.equals("Hello Alice, best wishes from Bob")); +// } +//}