diff --git a/core/src/main/kotlin/net/corda/core/utilities/LegalNameValidator.kt b/core/src/main/kotlin/net/corda/core/utilities/LegalNameValidator.kt index d874265096..8dc0c45270 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/LegalNameValidator.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/LegalNameValidator.kt @@ -9,12 +9,17 @@ import javax.security.auth.x500.X500Principal /** * The validation function will validate the input string using the following rules: + * * - No blacklisted words like "node", "server". - * - Restrict names to Latin scripts for now to avoid right-to-left issues, debugging issues when we can't pronounce names over the phone, and character confusability attacks. - * - Should start with a capital letter. + * - Restrict names to Latin scripts for now to avoid right-to-left issues, debugging issues when we can't pronounce + * names over the phone, and character confusability attacks. + * - Must consist of at least three letters and should start with a capital letter. * - No commas or equals signs. * - No dollars or quote marks, we might need to relax the quote mark constraint in future to handle Irish company names. + * + * @throws IllegalArgumentException if the name does not meet the required rules. The message indicates why not. */ +@Throws(IllegalArgumentException::class) fun validateLegalName(normalizedLegalName: String) { rules.forEach { it.validate(normalizedLegalName) } } @@ -36,7 +41,8 @@ private val rules: List> = listOf( // TODO: Implement confusable character detection if we add more scripts. UnicodeRangeRule(LATIN, COMMON, INHERITED), CapitalLetterRule(), - X500NameRule() + X500NameRule(), + MustHaveAtLeastTwoLettersRule() ) private class UnicodeNormalizationRule : Rule { @@ -52,9 +58,9 @@ private class UnicodeRangeRule(vararg supportScripts: Character.UnicodeScript) : require(pattern.matcher(legalName).matches()) { val illegalChars = legalName.replace(pattern.toRegex(), "").toSet() if (illegalChars.size > 1) { - "Illegal characters $illegalChars in \"$legalName\"." + "Forbidden characters $illegalChars in \"$legalName\"." } else { - "Illegal character $illegalChars in \"$legalName\"." + "Forbidden character $illegalChars in \"$legalName\"." } } } @@ -63,7 +69,7 @@ private class UnicodeRangeRule(vararg supportScripts: Character.UnicodeScript) : private class CharacterRule(vararg val bannedChars: Char) : Rule { override fun validate(legalName: String) { bannedChars.forEach { - require(!legalName.contains(it, true)) { "Illegal character: $it" } + require(!legalName.contains(it, true)) { "Character not allowed in legal names: $it" } } } } @@ -71,7 +77,7 @@ private class CharacterRule(vararg val bannedChars: Char) : Rule { private class WordRule(vararg val bannedWords: String) : Rule { override fun validate(legalName: String) { bannedWords.forEach { - require(!legalName.contains(it, ignoreCase = true)) { "Illegal word: $it" } + require(!legalName.contains(it, ignoreCase = true)) { "Word not allowed in legal names: $it" } } } } @@ -84,7 +90,7 @@ private class LengthRule(val maxLength: Int) : Rule { private class CapitalLetterRule : Rule { override fun validate(legalName: String) { - val capitalizedLegalName = legalName.split(" ").map(String::capitalize).joinToString(" ") + val capitalizedLegalName = legalName.capitalize() require(legalName == capitalizedLegalName) { "Legal name should be capitalized. i.e. '$capitalizedLegalName'" } } } @@ -96,6 +102,13 @@ private class X500NameRule : Rule { } } +private class MustHaveAtLeastTwoLettersRule : Rule { + override fun validate(legalName: String) { + // Try to exclude names like "/", "£", "X" etc. + require(legalName.count { it.isLetter() } >= 3) { "Must have at least two letters" } + } +} + private interface Rule { fun validate(legalName: T) } 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 974696ed41..810039af4b 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 @@ -81,19 +81,6 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { val nextPort: Int get() = port.andIncrement - fun isPortAvailable(port: Int): Boolean { - if (isPortValid(port)) { - try { - ServerSocket(port).close() - return true - } catch (e: IOException) { - return false - } - } else { - return false - } - } - fun isPortValid(port: Int) = (port >= minPort) && (port <= maxPort) fun keyExists(key: String) = nodes.keys.contains(key) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt index 617ed25211..0138ae92d2 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt @@ -7,8 +7,27 @@ import javafx.beans.property.SimpleStringProperty import net.corda.core.node.CityDatabase import tornadofx.* -class NodeData { +object SuggestedDetails { + val banks = listOf( + // Mike: Rome? Why Rome? + // Roger: Notaries public (also called "notaries", "notarial officers", or "public notaries") hold an office + // which can trace its origins back to the ancient Roman Republic, when they were called scribae ("scribes"), + // tabelliones forenses, or personae publicae.[4] + // Mike: Can't argue with that. It's even got a citation. + "Notary" to "Rome", + "Bank of Breakfast Tea" to "Liverpool", + "Bank of Big Apples" to "New York", + "Bank of Baguettes" to "Paris", + "Bank of Fondue" to "Geneve", + "Bank of Maple Syrup" to "Toronto" + ) + private var cursor = 0 + + val nextBank: Pair get() = banks[cursor++ % banks.size] +} + +class NodeData { val legalName = SimpleStringProperty("") val nearestCity = SimpleObjectProperty(CityDatabase["London"]!!) val p2pPort = SimpleIntegerProperty() @@ -16,5 +35,13 @@ class NodeData { val webPort = SimpleIntegerProperty() val h2Port = SimpleIntegerProperty() val extraServices = SimpleListProperty(mutableListOf().observable()) - +} + +class NodeDataModel : ItemViewModel(NodeData()) { + val legalName = bind { item?.legalName } + val nearestCity = bind { item?.nearestCity } + val p2pPort = bind { item?.p2pPort } + val rpcPort = bind { item?.rpcPort } + val webPort = bind { item?.webPort } + val h2Port = bind { item?.h2Port } } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeDataModel.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeDataModel.kt deleted file mode 100644 index 04b76021b8..0000000000 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeDataModel.kt +++ /dev/null @@ -1,14 +0,0 @@ -package net.corda.demobench.model - -import tornadofx.* - -class NodeDataModel : ItemViewModel(NodeData()) { - - val legalName = bind { item?.legalName } - val nearestCity = bind { item?.nearestCity } - val p2pPort = bind { item?.p2pPort } - val rpcPort = bind { item?.rpcPort } - val webPort = bind { item?.webPort } - val h2Port = bind { item?.h2Port } - -} 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 97269eb354..932711ccab 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 @@ -4,9 +4,9 @@ import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory import javafx.application.Platform +import javafx.beans.InvalidationListener import javafx.geometry.Pos import javafx.scene.control.ComboBox -import javafx.scene.control.SelectionMode.MULTIPLE import javafx.scene.image.Image import javafx.scene.image.ImageView import javafx.scene.input.KeyCode @@ -14,13 +14,20 @@ import javafx.scene.layout.Pane import javafx.scene.layout.Priority import javafx.stage.FileChooser import javafx.util.StringConverter +import net.corda.core.div +import net.corda.core.exists import net.corda.core.node.CityDatabase import net.corda.core.node.PhysicalLocation -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.core.readAllLines +import net.corda.core.utilities.normaliseLegalName +import net.corda.core.utilities.validateLegalName +import net.corda.core.writeLines import net.corda.demobench.model.* import net.corda.demobench.ui.CloseableTab +import org.controlsfx.control.CheckListView import tornadofx.* import java.nio.file.Path +import java.nio.file.Paths import java.util.* class NodeTabView : Fragment() { @@ -31,9 +38,27 @@ class NodeTabView : Fragment() { private companion object : Component() { const val textWidth = 465.0 - const val maxNameLength = 15 val jvm by inject() + val cordappPathsFile = jvm.dataHome / "cordapp-paths.txt" + + fun loadDefaultCordappPaths(): MutableList { + if (cordappPathsFile.exists()) + return cordappPathsFile.readAllLines().map { Paths.get(it) }.filter { it.exists() }.toMutableList() + else + return ArrayList() + } + + // This is shared between tabs. + private val cordapps = loadDefaultCordappPaths().observable() + + init { + // Save when the list is changed. + cordapps.addListener(InvalidationListener { + log.info("Writing cordapp paths to $cordappPathsFile") + cordappPathsFile.writeLines(cordapps.map { it.toAbsolutePath().toString() }) + }) + } } private val nodeController by inject() @@ -41,7 +66,6 @@ class NodeTabView : Fragment() { private val chooser = FileChooser() private val model = NodeDataModel() - private val cordapps = LinkedList().observable() private val availableServices: List = if (nodeController.hasNetworkMap()) serviceController.services else serviceController.notaries private val nodeTerminalView = find() @@ -73,17 +97,34 @@ class NodeTabView : Fragment() { } key.consume() } + cellCache { item -> + hbox { + label(item.fileName.toString()) + pane { + hboxConstraints { hgrow = Priority.ALWAYS } + } + val delete = FontAwesomeIconView(FontAwesomeIcon.MINUS_CIRCLE) + delete.setOnMouseClicked { + cordapps.remove(selectionModel.selectedItem) + } + delete.style += "; -fx-cursor: hand" + addChildIfPossible(delete) + } + } } } fieldset("Services") { styleClass.addAll("services-panel") - listview(availableServices.observable()) { + val servicesList = CheckListView(availableServices.observable()).apply { vboxConstraints { vGrow = Priority.ALWAYS } - selectionModel.selectionMode = MULTIPLE - model.item.extraServices.set(selectionModel.selectedItems) + model.item.extraServices.set(checkModel.checkedItems) + if (!nodeController.hasNetworkMap()) { + checkModel.check(0) + } } + add(servicesList) } } @@ -131,13 +172,15 @@ class NodeTabView : Fragment() { root.add(nodeConfigView) root.add(nodeTerminalView) - model.legalName.value = if (nodeController.hasNetworkMap()) "" else DUMMY_NOTARY.name - model.nearestCity.value = if (nodeController.hasNetworkMap()) null else CityDatabase["London"]!! model.p2pPort.value = nodeController.nextPort model.rpcPort.value = nodeController.nextPort model.webPort.value = nodeController.nextPort model.h2Port.value = nodeController.nextPort + val defaults = SuggestedDetails.nextBank + model.legalName.value = defaults.first + model.nearestCity.value = CityDatabase[defaults.second] + chooser.title = "CorDapps" chooser.initialDirectory = jvm.dataHome.toFile() chooser.extensionFilters.add(FileChooser.ExtensionFilter("CorDapps (*.jar)", "*.jar", "*.JAR")) @@ -151,15 +194,11 @@ class NodeTabView : Fragment() { if (it == null) { error("Node name is required") } else { - val name = it.trim() - if (name.isEmpty()) { - error("Node name is required") - } else if (nodeController.nameExists(name)) { - error("Node with this name already exists") - } else if (name.length > maxNameLength) { - error("Name is too long") - } else { + try { + validateLegalName(normaliseLegalName(it)) null + } catch (e: IllegalArgumentException) { + error(e.message) } } } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt index 2f25b561dc..3dd5e52bd6 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt @@ -11,6 +11,7 @@ import javafx.application.Platform import javafx.embed.swing.SwingNode import javafx.scene.control.Button import javafx.scene.control.Label +import javafx.scene.control.ProgressIndicator import javafx.scene.image.ImageView import javafx.scene.layout.StackPane import javafx.scene.layout.HBox @@ -142,10 +143,17 @@ class NodeTerminalView : Fragment() { return@setOnAction } launchWebButton.isDisable = true + val oldLabel = launchWebButton.text + launchWebButton.text = "" + launchWebButton.graphic = ProgressIndicator() log.info("Starting web server for ${config.legalName}") webServer.open(config) then { - Platform.runLater { launchWebButton.isDisable = false } + Platform.runLater { + launchWebButton.isDisable = false + launchWebButton.text = oldLabel + launchWebButton.graphic = null + } } success { log.info("Web server for ${config.legalName} started on $it") Platform.runLater { @@ -204,6 +212,8 @@ class NodeTerminalView : Fragment() { Platform.runLater { balance.value = if (cashBalances.isNullOrEmpty()) "0" else cashBalances } + } catch (e: ClassNotFoundException) { + // TODO: Remove this special case once Rick's serialisation work means we can deserialise states that weren't on our own classpath. } catch (e: Exception) { log.log(Level.WARNING, "Cash balance RPC failed: ${e.message}", e) } 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 195b1f91cc..932fd7ee70 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 @@ -26,7 +26,7 @@ -