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"));
+// }
+//}