From 0f73b68d3951b0c1dcb508bcff761f79a02ea23c Mon Sep 17 00:00:00 2001
From: Chris Rankin <chris.rankin@r3.com>
Date: Mon, 20 Feb 2017 15:23:27 +0000
Subject: [PATCH] CORPRIV-661: Implement saving profiles.

---
 .../net/corda/demobench/model/NodeConfig.kt   |  2 +-
 .../corda/demobench/model/NodeController.kt   |  5 +-
 .../demobench/profile/ProfileController.kt    | 57 +++++++++-----
 .../kotlin/net/corda/demobench/rpc/NodeRPC.kt |  2 +-
 .../corda/demobench/views/DemoBenchView.kt    | 76 ++++++++++++-------
 .../net/corda/demobench/views/NodeTabView.kt  |  1 +
 .../corda/demobench/views/DemoBenchView.fxml  |  3 +-
 7 files changed, 94 insertions(+), 52 deletions(-)

diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt
index 7c6aac3464..0034101c98 100644
--- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt
+++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt
@@ -66,7 +66,7 @@ class NodeConfig(
             .withValue("h2port", valueFor(h2Port))
             .withValue("useTestClock", valueFor(true))
 
-    fun toText() = toFileConfig().root().render(renderOptions)
+    fun toText(): String = toFileConfig().root().render(renderOptions)
 
     fun moveTo(baseDir: Path) = NodeConfig(
         baseDir, legalName, artemisPort, nearestCity, webPort, h2Port, extraServices, users
diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt
index a946f795b6..d8589a3a89 100644
--- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt
+++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt
@@ -5,7 +5,6 @@ import java.lang.management.ManagementFactory
 import java.net.ServerSocket
 import java.text.SimpleDateFormat
 import java.util.*
-import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.atomic.AtomicInteger
 import net.corda.demobench.pty.R3Pty
 import tornadofx.Controller
@@ -27,13 +26,13 @@ class NodeController : Controller() {
     private val cordaPath = jvm.applicationDir.resolve("corda").resolve("corda.jar")
     private val command = jvm.commandFor(cordaPath)
 
-    private val nodes = ConcurrentHashMap<String, NodeConfig>()
+    private val nodes = LinkedHashMap<String, NodeConfig>()
     private val port = AtomicInteger(FIRST_PORT)
 
     private var networkMapConfig: NetworkMapConfig? = null
 
     val activeNodes: List<NodeConfig> get() = nodes.values.filter {
-        it.state == NodeState.RUNNING
+        (it.state == NodeState.RUNNING) || (it.state == NodeState.STARTING)
     }
 
     init {
diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt
index 2bd2024d96..8e5438f19f 100644
--- a/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt
+++ b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt
@@ -3,10 +3,13 @@ package net.corda.demobench.profile
 import com.google.common.net.HostAndPort
 import com.typesafe.config.Config
 import com.typesafe.config.ConfigFactory
-import java.nio.file.FileSystems
-import java.nio.file.Files
-import java.nio.file.Path
+import java.io.File
+import java.net.URI
+import java.nio.charset.StandardCharsets.UTF_8
+import java.nio.file.*
+import java.nio.file.attribute.BasicFileAttributes
 import java.util.*
+import java.util.function.BiPredicate
 import javafx.stage.FileChooser
 import javafx.stage.FileChooser.ExtensionFilter
 import net.corda.demobench.model.*
@@ -14,6 +17,10 @@ import tornadofx.Controller
 
 class ProfileController : Controller() {
 
+    private companion object ConfigAcceptor : BiPredicate<Path, BasicFileAttributes> {
+        override fun test(p: Path?, attr: BasicFileAttributes?) = "node.conf" == p?.fileName.toString()
+    }
+
     private val jvm by inject<JVMConfig>()
     private val baseDir = jvm.userHome.resolve("demobench")
     private val nodeController by inject<NodeController>()
@@ -21,34 +28,50 @@ class ProfileController : Controller() {
     private val chooser = FileChooser()
 
     init {
+        chooser.title = "DemoBench Profiles"
         chooser.initialDirectory = baseDir.toFile()
         chooser.extensionFilters.add(ExtensionFilter("DemoBench profiles (*.zip)", "*.zip", "*.ZIP"))
     }
 
-    fun saveAs() {
-        log.info("Save as")
-    }
+    fun saveProfile(): Boolean {
+        var target = chooser.showSaveDialog(null) ?: return false
+        if (target.extension.isEmpty()) {
+            target = File(target.parent, target.name + ".zip")
+        }
 
-    fun save() {
-        log.info("Save")
+        log.info("Save profile as: $target")
+
+        val configs = nodeController.activeNodes
+
+        FileSystems.newFileSystem(URI.create("jar:" + target.toURI()), mapOf("create" to "true")).use {
+            fs -> configs.forEach { it ->
+                val nodeDir = Files.createDirectories(fs.getPath(it.key))
+                val conf = Files.write(nodeDir.resolve("node.conf"), it.toText().toByteArray(UTF_8))
+                log.info("Wrote: $conf")
+            }
+        }
+
+        return true
     }
 
     fun openProfile(): List<NodeConfig>? {
         val chosen = chooser.showOpenDialog(null) ?: return null
-        log.info("Selected profile: ${chosen}")
+        log.info("Selected profile: $chosen")
 
         val configs = LinkedList<NodeConfig>()
 
         FileSystems.newFileSystem(chosen.toPath(), null).use {
             fs -> fs.rootDirectories.forEach {
-                root -> Files.walk(root).forEach {
-                    if ((it.nameCount == 2) && ("node.conf" == it.fileName.toString())) {
-                        try {
-                            configs.add(toNodeConfig(parse(it)))
-                        } catch (e: Exception) {
-                            log.severe("Failed to parse '$it': ${e.message}")
-                            throw e
-                        }
+                root -> Files.find(root, 2, ConfigAcceptor).forEach {
+                    try {
+                        // Java seems to "walk" through the ZIP file backwards.
+                        // So add new config to the front of the list, so that
+                        // our final list is ordered to match the file.
+                        configs.addFirst(toNodeConfig(parse(it)))
+                        log.info("Loaded: $it")
+                    } catch (e: Exception) {
+                        log.severe("Failed to parse '$it': ${e.message}")
+                        throw e
                     }
                 }
             }
diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt
index abfec68131..d88989739d 100644
--- a/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt
+++ b/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt
@@ -10,7 +10,7 @@ import net.corda.node.services.messaging.CordaRPCClient
 
 class NodeRPC(config: NodeConfig, start: () -> Unit, invoke: (CordaRPCOps) -> Unit): AutoCloseable {
 
-    private companion object Data {
+    private companion object {
         val log = loggerFor<NodeRPC>()
         val ONE_SECOND = SECONDS.toMillis(1)
     }
diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/DemoBenchView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/DemoBenchView.kt
index dd262976d4..cef277c1b8 100644
--- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/DemoBenchView.kt
+++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/DemoBenchView.kt
@@ -23,39 +23,51 @@ class DemoBenchView : View("Corda Demo Bench") {
     private val addNodeButton by fxid<Button>()
     private val nodeTabPane by fxid<TabPane>()
     private val menuOpen by fxid<MenuItem>()
-    private val menuSave by fxid<MenuItem>()
     private val menuSaveAs by fxid<MenuItem>()
 
     init {
         importStylesheet("/net/corda/demobench/style.css")
 
-        primaryStage.setOnCloseRequest {
-            log.info("Exiting")
+        configureShutdown()
 
-            // Prevent any new NodeTabViews from being created.
-            addNodeButton.isDisable = true
+        configureProfileSaveAs()
+        configureProfileOpen()
 
-            closeAllTabs()
-            Platform.exit()
-        }
+        configureAddNode()
+    }
 
-        menuSaveAs.setOnAction {
-            profileController.saveAs()
-        }
-        menuSave.setOnAction {
-            profileController.save()
-        }
-        menuOpen.setOnAction {
-            try {
-                val profile = profileController.openProfile()
-                if (profile != null) {
-                    loadProfile(profile)
-                }
-            } catch (e: Exception) {
-                ExceptionDialog(e).apply { initOwner(root.scene.window) }.showAndWait()
+    private fun configureShutdown() = primaryStage.setOnCloseRequest {
+        log.info("Exiting")
+
+        // Prevent any new NodeTabViews from being created.
+        addNodeButton.isDisable = true
+
+        closeAllTabs()
+        Platform.exit()
+    }
+
+    private fun configureProfileSaveAs() = menuSaveAs.setOnAction {
+        try {
+            if (profileController.saveProfile()) {
+                menuSaveAs.isDisable = true
             }
+        } catch (e: Exception) {
+            ExceptionDialog(e).apply { initOwner(root.scene.window) }.showAndWait()
         }
+    }
 
+    private fun configureProfileOpen() = menuOpen.setOnAction {
+        try {
+            val profile = profileController.openProfile()
+            if (profile != null) {
+                loadProfile(profile)
+            }
+        } catch (e: Exception) {
+            ExceptionDialog(e).apply { initOwner(root.scene.window) }.showAndWait()
+        }
+    }
+
+    private fun configureAddNode() {
         addNodeButton.setOnAction {
             val nodeTabView = createNodeTabView(true)
             nodeTabPane.selectionModel.select(nodeTabView.nodeTab)
@@ -66,19 +78,17 @@ class DemoBenchView : View("Corda Demo Bench") {
         addNodeButton.fire()
     }
 
-    private fun closeAllTabs() {
-        ArrayList<Tab>(nodeTabPane.tabs).forEach {
-            (it as CloseableTab).requestClose()
-        }
+    private fun closeAllTabs() = ArrayList<Tab>(nodeTabPane.tabs).forEach {
+        (it as CloseableTab).requestClose()
     }
 
-    fun createNodeTabView(showConfig: Boolean): NodeTabView {
+    private fun createNodeTabView(showConfig: Boolean): NodeTabView {
         val nodeTabView = find<NodeTabView>(mapOf("showConfig" to showConfig))
         nodeTabPane.tabs.add(nodeTabView.nodeTab)
         return nodeTabView
     }
 
-    fun loadProfile(nodes: List<NodeConfig>) {
+    private fun loadProfile(nodes: List<NodeConfig>) {
         closeAllTabs()
         nodeController.reset()
 
@@ -90,6 +100,16 @@ class DemoBenchView : View("Corda Demo Bench") {
         enableAddNodes()
     }
 
+    /**
+     * Enable the "save profile" menu item.
+     */
+    fun enableSaveProfile() {
+        menuSaveAs.isDisable = false
+    }
+
+    /**
+     * Enables the button that allows us to create a new node.
+     */
     fun enableAddNodes() {
         addNodeButton.isDisable = false
     }
diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt
index c8671e75a4..73e090e441 100644
--- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt
+++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt
@@ -155,6 +155,7 @@ class NodeTabView : Fragment() {
                     if (model.validate()) {
                         launch()
                         main.enableAddNodes()
+                        main.enableSaveProfile()
                     }
                 }
             }
diff --git a/tools/demobench/src/main/resources/net/corda/demobench/views/DemoBenchView.fxml b/tools/demobench/src/main/resources/net/corda/demobench/views/DemoBenchView.fxml
index 7c52ca7c3c..31a44f4f3f 100644
--- a/tools/demobench/src/main/resources/net/corda/demobench/views/DemoBenchView.fxml
+++ b/tools/demobench/src/main/resources/net/corda/demobench/views/DemoBenchView.fxml
@@ -13,8 +13,7 @@
    <MenuBar>
       <Menu text="File">
          <MenuItem fx:id="menuOpen" text="Open"/>
-         <MenuItem fx:id="menuSave" text="Save"/>
-         <MenuItem fx:id="menuSaveAs" text="Save As"/>
+         <MenuItem fx:id="menuSaveAs" disable="true" text="Save As"/>
       </Menu>
    </MenuBar>
    <StackPane>