diff --git a/docs/source/network-bootstrapper.rst b/docs/source/network-bootstrapper.rst index d386afc0eb..e1bd8c9936 100644 --- a/docs/source/network-bootstrapper.rst +++ b/docs/source/network-bootstrapper.rst @@ -82,7 +82,7 @@ Whitelisting contracts Any CorDapps provided when bootstrapping a network will be scanned for contracts which will be used to create the *Zone whitelist* (see :doc:`api-contract-constraints`) for the network. -.. note:: If you only wish to whitelist the CorDapps but not copy them to each node then run with the ``--no-copy`` flag. +.. note:: If you only wish to whitelist the CorDapps but not copy them to each node then run with the ``--copy-cordapps=No`` option. The CorDapp JARs will be hashed and scanned for ``Contract`` classes. These contract class implementations will become part of the whitelisted contracts in the network parameters (see ``NetworkParameters.whitelistedContractImplementations`` :doc:`network-map`). @@ -401,16 +401,17 @@ The Network Bootstrapper can be started with the following command line options: .. code-block:: shell - bootstrapper [-hvV] [--no-copy] [--dir=] [--event-horizon=] - [--logging-level=] + bootstrapper [-hvV] [--copy-cordapps=] [--dir=] + [--event-horizon=] [--logging-level=] [--max-message-size=] [--max-transaction-size=] [--minimum-platform-version=] [-n=] [COMMAND] * ``--dir=``: Root directory containing the node configuration files and CorDapp JARs that will form the test network. - It may also contain existing node directories. Defaults to the current directory. -* ``--no-copy``: Don't copy the CorDapp JARs into the nodes' "cordapps" directories. + It may also contain existing node directories. Defaults to the current directory. +* ``--copy-cordapps=``: Whether or not to copy the CorDapp JARs into the nodes' 'cordapps' directory. Possible values: + FirstRunOnly, Yes, No. Default: FirstRunOnly. * ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file. * ``--logging-level=``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO. * ``--help``, ``-h``: Show this help message and exit. @@ -419,8 +420,8 @@ The Network Bootstrapper can be started with the following command line options: * ``--max-message-size``: The maximum message size to use in the network-parameters, in bytes. * ``--max-transaction-size``: The maximum transaction size to use in the network-parameters, in bytes. * ``--event-horizon``: The event horizon to use in the network-parameters. -* ``--network-parameter-overrides=``, ``-n=`: Overrides the default network parameters with those - in the given file. See `Overriding network parameters via a file`_ for more information. +* ``--network-parameter-overrides=``, ``-n=``: Overrides the default network parameters with those + in the given file. See `Overriding network parameters via a file`_ for more information. Sub-commands diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index 92d5e72751..5bccb4fd93 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -75,6 +75,8 @@ internal constructor(private val initSerEnv: Boolean, private const val LOGS_DIR_NAME = "logs" + private val jarsThatArentCordapps = setOf("corda.jar", "runnodes.jar") + private fun extractEmbeddedCordaJar(): InputStream { return Thread.currentThread().contextClassLoader.getResourceAsStream("corda.jar") } @@ -182,21 +184,21 @@ internal constructor(private val initSerEnv: Boolean, * TODO: Remove once the gradle plugins are updated to 4.0.30 */ fun bootstrap(directory: Path, cordappJars: List) { - bootstrap(directory, cordappJars, copyCordapps = true, fromCordform = true) + bootstrap(directory, cordappJars, CopyCordapps.Yes, fromCordform = true) } /** Entry point for Cordform */ fun bootstrapCordform(directory: Path, cordappJars: List) { - bootstrap(directory, cordappJars, copyCordapps = false, fromCordform = true) + bootstrap(directory, cordappJars, CopyCordapps.No, fromCordform = true) } /** Entry point for the tool */ - override fun bootstrap(directory: Path, copyCordapps: Boolean, networkParameterOverrides: NetworkParametersOverrides) { + override fun bootstrap(directory: Path, copyCordapps: CopyCordapps, networkParameterOverrides: NetworkParametersOverrides) { require(networkParameterOverrides.minimumPlatformVersion == null || networkParameterOverrides.minimumPlatformVersion <= PLATFORM_VERSION) { "Minimum platform version cannot be greater than $PLATFORM_VERSION" } // Don't accidentally include the bootstrapper jar as a CorDapp! val bootstrapperJar = javaClass.location.toPath() val cordappJars = directory.list { paths -> - paths.filter { it.toString().endsWith(".jar") && !it.isSameAs(bootstrapperJar) && it.fileName.toString() != "corda.jar" } + paths.filter { it.toString().endsWith(".jar") && !it.isSameAs(bootstrapperJar) && !jarsThatArentCordapps.contains(it.fileName.toString().toLowerCase()) } .toList() } bootstrap(directory, cordappJars, copyCordapps, fromCordform = false, networkParametersOverrides = networkParameterOverrides) @@ -205,16 +207,13 @@ internal constructor(private val initSerEnv: Boolean, private fun bootstrap( directory: Path, cordappJars: List, - copyCordapps: Boolean, + copyCordapps: CopyCordapps, fromCordform: Boolean, networkParametersOverrides: NetworkParametersOverrides = NetworkParametersOverrides() ) { directory.createDirectories() println("Bootstrapping local test network in $directory") - if (!fromCordform) { - println("Found the following CorDapps: ${cordappJars.map { it.fileName }}") - } - createNodeDirectoriesIfNeeded(directory, fromCordform) + val networkAlreadyExists = createNodeDirectoriesIfNeeded(directory, fromCordform) val nodeDirs = gatherNodeDirectories(directory) require(nodeDirs.isNotEmpty()) { "No nodes found" } @@ -224,19 +223,9 @@ internal constructor(private val initSerEnv: Boolean, val configs = nodeDirs.associateBy({ it }, { ConfigFactory.parseFile((it / "node.conf").toFile()) }) checkForDuplicateLegalNames(configs.values) - if (copyCordapps && cordappJars.isNotEmpty()) { - println("Copying CorDapp JARs into node directories") - for (nodeDir in nodeDirs) { - val cordappsDir = (nodeDir / "cordapps").createDirectories() - cordappJars.forEach { - try { - it.copyToDirectory(cordappsDir) - } catch (e: FileAlreadyExistsException) { - println("WARNING: ${it.fileName} already exists in $cordappsDir, ignoring and leaving existing CorDapp untouched") - } - } - } - } + + copyCordapps.copy(cordappJars, nodeDirs, networkAlreadyExists, fromCordform) + generateServiceIdentitiesForNotaryClusters(configs) if (initSerEnv) { initialiseSerialization() @@ -268,7 +257,8 @@ internal constructor(private val initSerEnv: Boolean, } } - private fun createNodeDirectoriesIfNeeded(directory: Path, fromCordform: Boolean) { + private fun createNodeDirectoriesIfNeeded(directory: Path, fromCordform: Boolean): Boolean { + var networkAlreadyExists = false val cordaJar = directory / "corda.jar" var usingEmbedded = false if (!cordaJar.exists()) { @@ -284,6 +274,10 @@ internal constructor(private val initSerEnv: Boolean, for (confFile in confFiles) { val nodeName = confFile.fileName.toString().removeSuffix("_node.conf") println("Generating node directory for $nodeName") + if ((directory / nodeName).exists()) { + //directory already exists, so assume this network has been bootstrapped before + networkAlreadyExists = true + } val nodeDir = (directory / nodeName).createDirectories() confFile.copyTo(nodeDir / "node.conf", REPLACE_EXISTING) webServerConfFiles.firstOrNull { directory.relativize(it).toString().removeSuffix("_web-server.conf") == nodeName } @@ -306,6 +300,7 @@ internal constructor(private val initSerEnv: Boolean, if (fromCordform || usingEmbedded) { cordaJar.delete() } + return networkAlreadyExists } private fun gatherNodeDirectories(directory: Path): List { @@ -467,7 +462,8 @@ fun NetworkParameters.overrideWith(override: NetworkParametersOverrides): Networ maxMessageSize = override.maxMessageSize ?: this.maxMessageSize, maxTransactionSize = override.maxTransactionSize ?: this.maxTransactionSize, eventHorizon = override.eventHorizon ?: this.eventHorizon, - packageOwnership = override.packageOwnership?.map { it.javaPackageName to it.publicKey }?.toMap() ?: this.packageOwnership + packageOwnership = override.packageOwnership?.map { it.javaPackageName to it.publicKey }?.toMap() + ?: this.packageOwnership ) } @@ -482,5 +478,50 @@ data class NetworkParametersOverrides( ) interface NetworkBootstrapperWithOverridableParameters { - fun bootstrap(directory: Path, copyCordapps: Boolean, networkParameterOverrides: NetworkParametersOverrides = NetworkParametersOverrides()) + fun bootstrap(directory: Path, copyCordapps: CopyCordapps, networkParameterOverrides: NetworkParametersOverrides = NetworkParametersOverrides()) +} + +enum class CopyCordapps { + FirstRunOnly { + override fun copy(cordappJars: List, nodeDirs: List, networkAlreadyExists: Boolean) { + if (networkAlreadyExists) { + println("Not copying CorDapp JARs as --copy-cordapps is set to FirstRunOnly, and it looks like this network has already been bootstrapped.") + return + } + cordappJars.copy(nodeDirs) + } + }, + + Yes { + override fun copy(cordappJars: List, nodeDirs: List, networkAlreadyExists: Boolean) = cordappJars.copy(nodeDirs) + }, + + No { + override fun copy(cordappJars: List, nodeDirs: List, networkAlreadyExists: Boolean) = println("Not copying CorDapp JARs as --copy-cordapps is set to No.") + }; + + protected abstract fun copy(cordappJars: List, nodeDirs: List, networkAlreadyExists: Boolean) + + protected fun List.copy(nodeDirs: List) { + if (this.isNotEmpty()) { + println("Copying CorDapp JARs into node directories") + for (nodeDir in nodeDirs) { + val cordappsDir = (nodeDir / "cordapps").createDirectories() + this.forEach { + try { + it.copyToDirectory(cordappsDir) + } catch (e: FileAlreadyExistsException) { + println("WARNING: ${it.fileName} already exists in $cordappsDir, ignoring and leaving existing CorDapp untouched") + } + } + } + } + } + + fun copy(cordappJars: List, nodeDirs: List, networkAlreadyExists: Boolean, fromCordform: Boolean) { + if (!fromCordform) { + println("Found the following CorDapps: ${cordappJars.map { it.fileName }}") + } + this.copy(cordappJars, nodeDirs, networkAlreadyExists) + } } \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt index 6590ead935..b02b0f3b7d 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt @@ -184,7 +184,7 @@ class NetworkBootstrapperTest { fun `no copy CorDapps`() { createNodeConfFile("alice", aliceConfig) val cordappBytes = createFakeCordappJar("sample-app", listOf("contract.class")) - bootstrap(copyCordapps = false) + bootstrap(copyCordapps = CopyCordapps.No) val networkParameters = assertBootstrappedNetwork(fakeEmbeddedCordaJar, "alice" to aliceConfig) assertThat(rootDir / "alice" / "cordapps" / "sample-app.jar").doesNotExist() assertThat(networkParameters.whitelistedContractImplementations).isEqualTo(mapOf( @@ -305,7 +305,7 @@ class NetworkBootstrapperTest { return bytes } - private fun bootstrap(copyCordapps: Boolean = true, + private fun bootstrap(copyCordapps: CopyCordapps = CopyCordapps.FirstRunOnly, packageOwnership: Map? = emptyMap(), minimumPlatformVerison: Int? = PLATFORM_VERSION, maxMessageSize: Int? = DEFAULT_MAX_MESSAGE_SIZE, diff --git a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt index 38dfb7221c..bb15468241 100644 --- a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt +++ b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt @@ -2,18 +2,13 @@ package net.corda.bootstrapper import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigParseOptions -import net.corda.cliutils.CordaCliWrapper -import net.corda.cliutils.ExitCodes -import net.corda.cliutils.printError -import net.corda.cliutils.start +import net.corda.cliutils.* import net.corda.common.configuration.parsing.internal.Configuration import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.internal.exists -import net.corda.nodeapi.internal.network.NetworkBootstrapper +import net.corda.nodeapi.internal.network.* import net.corda.nodeapi.internal.network.NetworkBootstrapper.Companion.DEFAULT_MAX_MESSAGE_SIZE import net.corda.nodeapi.internal.network.NetworkBootstrapper.Companion.DEFAULT_MAX_TRANSACTION_SIZE -import net.corda.nodeapi.internal.network.NetworkBootstrapperWithOverridableParameters -import net.corda.nodeapi.internal.network.NetworkParametersOverrides import picocli.CommandLine.Option import java.io.FileNotFoundException import java.nio.file.Path @@ -30,8 +25,11 @@ class NetworkBootstrapperRunner(private val bootstrapper: NetworkBootstrapperWit "It may also contain existing node directories."]) var dir: Path = Paths.get(".") - @Option(names = ["--no-copy"], description = ["""Don't copy the CorDapp JARs into the nodes' "cordapps" directories."""]) - var noCopy: Boolean = false + @Option(names = ["--no-copy"], hidden = true, description = ["""DEPRECATED. Don't copy the CorDapp JARs into the nodes' "cordapps" directories."""]) + var noCopy: Boolean? = null + + @Option(names = ["--copy-cordapps"], description = ["Whether or not to copy the CorDapp JARs into the nodes' 'cordapps' directory. \${COMPLETION-CANDIDATES}"]) + var copyCordapps: CopyCordapps = CopyCordapps.FirstRunOnly @Option(names = ["--minimum-platform-version"], description = ["The minimum platform version to use in the network-parameters. Current default is $PLATFORM_VERSION."]) var minimumPlatformVersion: Int? = null @@ -85,10 +83,14 @@ class NetworkBootstrapperRunner(private val bootstrapper: NetworkBootstrapperWit } override fun runProgram(): Int { + if (noCopy != null) { + printWarning("The --no-copy parameter has been deprecated and been replaced with the --copy-cordapps parameter.") + copyCordapps = if (noCopy == true) CopyCordapps.No else CopyCordapps.Yes + } verifyInputs() val networkParameterOverrides = getNetworkParametersOverrides().doOnErrors(::reportErrors).optional ?: return ExitCodes.FAILURE bootstrapper.bootstrap(dir.toAbsolutePath().normalize(), - copyCordapps = !noCopy, + copyCordapps = copyCordapps, networkParameterOverrides = networkParameterOverrides ) return ExitCodes.SUCCESS diff --git a/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/NetworkBootstrapperRunnerTests.kt b/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/NetworkBootstrapperRunnerTests.kt index 300c9182fd..b38b0796b1 100644 --- a/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/NetworkBootstrapperRunnerTests.kt +++ b/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/NetworkBootstrapperRunnerTests.kt @@ -6,12 +6,11 @@ import net.corda.core.internal.copyTo import net.corda.core.internal.deleteRecursively import net.corda.core.internal.div import net.corda.core.utilities.days +import net.corda.nodeapi.internal.network.CopyCordapps import net.corda.nodeapi.internal.network.NetworkBootstrapperWithOverridableParameters import net.corda.nodeapi.internal.network.NetworkParametersOverrides import net.corda.nodeapi.internal.network.PackageOwner import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.BOB_NAME -import net.corda.testing.core.CHARLIE_NAME import net.corda.testing.core.JarSignatureTestUtils.generateKey import net.corda.testing.core.JarSignatureTestUtils.getPublicKey import org.junit.* @@ -92,7 +91,7 @@ class NetworkBootstrapperRunnerTests { fun `test when defaults are run bootstrapper is called correctly`() { val (runner, mockBootstrapper) = getRunner() val exitCode = runner.runProgram() - verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides()) + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), CopyCordapps.FirstRunOnly, NetworkParametersOverrides()) assertEquals(0, exitCode) } @@ -102,16 +101,25 @@ class NetworkBootstrapperRunnerTests { val tempDir = createTempDir() runner.dir = tempDir.toPath() val exitCode = runner.runProgram() - verify(mockBootstrapper).bootstrap(tempDir.toPath().toAbsolutePath().normalize(), true, NetworkParametersOverrides()) + verify(mockBootstrapper).bootstrap(tempDir.toPath().toAbsolutePath().normalize(), CopyCordapps.FirstRunOnly, NetworkParametersOverrides()) + assertEquals(0, exitCode) + } + + @Test + fun `test when no copy flag is specified it is passed through to the bootstrapper`() { + val (runner, mockBootstrapper) = getRunner() + runner.noCopy = true + val exitCode = runner.runProgram() + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), CopyCordapps.No, NetworkParametersOverrides()) assertEquals(0, exitCode) } @Test fun `test when copy cordapps is specified it is passed through to the bootstrapper`() { val (runner, mockBootstrapper) = getRunner() - runner.noCopy = true + runner.copyCordapps = CopyCordapps.No val exitCode = runner.runProgram() - verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), false, NetworkParametersOverrides()) + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), CopyCordapps.No, NetworkParametersOverrides()) assertEquals(0, exitCode) } @@ -120,7 +128,7 @@ class NetworkBootstrapperRunnerTests { val (runner, mockBootstrapper) = getRunner() runner.minimumPlatformVersion = 1 val exitCode = runner.runProgram() - verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides(minimumPlatformVersion = 1)) + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), CopyCordapps.FirstRunOnly, NetworkParametersOverrides(minimumPlatformVersion = 1)) assertEquals(0, exitCode) } @@ -137,7 +145,7 @@ class NetworkBootstrapperRunnerTests { val (runner, mockBootstrapper) = getRunner() runner.maxMessageSize = 1 val exitCode = runner.runProgram() - verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides(maxMessageSize = 1)) + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), CopyCordapps.FirstRunOnly, NetworkParametersOverrides(maxMessageSize = 1)) assertEquals(0, exitCode) } @@ -154,7 +162,7 @@ class NetworkBootstrapperRunnerTests { val (runner, mockBootstrapper) = getRunner() runner.maxTransactionSize = 1 val exitCode = runner.runProgram() - verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides(maxTransactionSize = 1)) + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), CopyCordapps.FirstRunOnly, NetworkParametersOverrides(maxTransactionSize = 1)) assertEquals(0, exitCode) } @@ -171,7 +179,7 @@ class NetworkBootstrapperRunnerTests { val (runner, mockBootstrapper) = getRunner() runner.eventHorizon = 7.days val exitCode = runner.runProgram() - verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides(eventHorizon = 7.days)) + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), CopyCordapps.FirstRunOnly, NetworkParametersOverrides(eventHorizon = 7.days)) assertEquals(0, exitCode) } @@ -189,7 +197,7 @@ class NetworkBootstrapperRunnerTests { val conf = correctNetworkFile.copyToTestDir() runner.networkParametersFile = conf val exitCode = runner.runProgram() - verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides( + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), CopyCordapps.FirstRunOnly, NetworkParametersOverrides( maxMessageSize = 10000, maxTransactionSize = 2000, eventHorizon = 5.days, @@ -204,7 +212,7 @@ class NetworkBootstrapperRunnerTests { val conf = aliceConfigFile.copyToTestDir() runner.networkParametersFile = conf val exitCode = runner.runProgram() - verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides( + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), CopyCordapps.FirstRunOnly, NetworkParametersOverrides( packageOwnership = listOf(PackageOwner("com.example.stuff", publicKey = alicePublicKey)) )) assertEquals(0, exitCode) @@ -216,7 +224,7 @@ class NetworkBootstrapperRunnerTests { val conf = aliceConfigFile.copyToTestDir(dirAliceEC) runner.networkParametersFile = conf val exitCode = runner.runProgram() - verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides( + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), CopyCordapps.FirstRunOnly, NetworkParametersOverrides( packageOwnership = listOf(PackageOwner("com.example.stuff", publicKey = alicePublicKeyEC)) )) assertEquals(0, exitCode) @@ -228,7 +236,7 @@ class NetworkBootstrapperRunnerTests { val conf = aliceConfigFile.copyToTestDir(dirAliceDSA) runner.networkParametersFile = conf val exitCode = runner.runProgram() - verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides( + verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), CopyCordapps.FirstRunOnly, NetworkParametersOverrides( packageOwnership = listOf(PackageOwner("com.example.stuff", publicKey = alicePublicKeyDSA)) )) assertEquals(0, exitCode) diff --git a/tools/bootstrapper/src/test/resources/net.corda.bootstrapper.NetworkBootstrapperRunner.yml b/tools/bootstrapper/src/test/resources/net.corda.bootstrapper.NetworkBootstrapperRunner.yml index f8e50ac29b..0b7c25b280 100644 --- a/tools/bootstrapper/src/test/resources/net.corda.bootstrapper.NetworkBootstrapperRunner.yml +++ b/tools/bootstrapper/src/test/resources/net.corda.bootstrapper.NetworkBootstrapperRunner.yml @@ -22,7 +22,7 @@ - "DEBUG" - "TRACE" - parameterName: "--no-copy" - parameterType: "boolean" + parameterType: "java.lang.Boolean" required: false multiParam: false acceptableValues: []