mirror of
https://github.com/corda/corda.git
synced 2025-03-15 00:36:49 +00:00
Merge commit 'ff62df8d5a0ab9eabfe919b65a8c73baa3dca7f3' into chrisr3-os-merge
Conflicts: finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt node/src/main/kotlin/net/corda/node/NodeArgsParser.kt node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt
This commit is contained in:
commit
a287673432
@ -5767,7 +5767,7 @@ public class net.corda.client.jackson.StringToMethodCallParser extends java.lang
|
||||
@NotNull
|
||||
public final net.corda.client.jackson.StringToMethodCallParser<T>$ParsedMethodCall parse(T, String)
|
||||
@NotNull
|
||||
public final Object[] parseArguments(String, java.util.List<? extends kotlin.Pair<String, ? extends Class<?>>>, String)
|
||||
public final Object[] parseArguments(String, java.util.List<? extends kotlin.Pair<String, ? extends reflect.Type>>, String)
|
||||
public static final net.corda.client.jackson.StringToMethodCallParser$Companion Companion
|
||||
##
|
||||
public static final class net.corda.client.jackson.StringToMethodCallParser$Companion extends java.lang.Object
|
||||
|
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@ -275,6 +275,8 @@
|
||||
<module name="source-example-code_integrationTest" target="1.8" />
|
||||
<module name="source-example-code_main" target="1.8" />
|
||||
<module name="source-example-code_test" target="1.8" />
|
||||
<module name="test-cli_main" target="1.8" />
|
||||
<module name="test-cli_test" target="1.8" />
|
||||
<module name="test-common_main" target="1.8" />
|
||||
<module name="test-common_test" target="1.8" />
|
||||
<module name="test-utils_integrationTest" target="1.8" />
|
||||
|
@ -423,10 +423,10 @@ object JacksonSupport {
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("This is an internal class, do not use")
|
||||
@Deprecated("Do not use - Replaced by Corda's internal AmountDeserializer and TokenDeserializer classes")
|
||||
object AmountDeserializer : JsonDeserializer<Amount<*>>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): Amount<*> {
|
||||
return if (parser.currentToken == JsonToken.VALUE_STRING) {
|
||||
return if (parser.currentToken() == JsonToken.VALUE_STRING) {
|
||||
Amount.parseCurrency(parser.text)
|
||||
} else {
|
||||
val wrapper = parser.readValueAs<CurrencyAmountWrapper>()
|
||||
|
@ -20,6 +20,7 @@ import net.corda.core.CordaException
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import java.lang.reflect.Constructor
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Type
|
||||
import java.util.concurrent.Callable
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import kotlin.reflect.KClass
|
||||
@ -183,7 +184,7 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
|
||||
// and fail for that too.
|
||||
for ((index, method) in methods.withIndex()) {
|
||||
try {
|
||||
val args = parseArguments(name, paramNamesFromMethod(method).zip(method.parameterTypes), argStr)
|
||||
val args = parseArguments(name, paramNamesFromMethod(method).zip(method.genericParameterTypes), argStr)
|
||||
return ParsedMethodCall(target, method, args)
|
||||
} catch (e: UnparseableCallException) {
|
||||
if (index == methods.size - 1)
|
||||
@ -199,15 +200,16 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
|
||||
* @param methodNameHint A name that will be used in exceptions if thrown; not used for any other purpose.
|
||||
*/
|
||||
@Throws(UnparseableCallException::class)
|
||||
fun parseArguments(methodNameHint: String, parameters: List<Pair<String, Class<*>>>, args: String): Array<Any?> {
|
||||
fun parseArguments(methodNameHint: String, parameters: List<Pair<String, Type>>, args: String): Array<Any?> {
|
||||
// If we have parameters, wrap them in {} to allow the Yaml parser to eat them on a single line.
|
||||
val parameterString = "{ $args }"
|
||||
val tree: JsonNode = om.readTree(parameterString) ?: throw UnparseableCallException(args)
|
||||
if (tree.size() > parameters.size) throw UnparseableCallException.TooManyParameters(methodNameHint, args)
|
||||
val inOrderParams: List<Any?> = parameters.mapIndexed { _, (argName, argType) ->
|
||||
val entry = tree[argName] ?: throw UnparseableCallException.MissingParameter(methodNameHint, argName, args)
|
||||
val entryType = om.typeFactory.constructType(argType)
|
||||
try {
|
||||
om.readValue(entry.traverse(om), argType)
|
||||
om.readValue<Any>(entry.traverse(om), entryType)
|
||||
} catch (e: Exception) {
|
||||
throw UnparseableCallException.FailedParse(e)
|
||||
}
|
||||
|
@ -2,10 +2,9 @@
|
||||
|
||||
package net.corda.client.jackson.internal
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.annotation.*
|
||||
import com.fasterxml.jackson.annotation.JsonCreator.Mode.*
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||
import com.fasterxml.jackson.annotation.JsonValue
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
@ -13,6 +12,9 @@ import com.fasterxml.jackson.core.JsonToken
|
||||
import com.fasterxml.jackson.databind.*
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier
|
||||
import com.fasterxml.jackson.databind.deser.ContextualDeserializer
|
||||
import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.databind.node.IntNode
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
@ -40,15 +42,18 @@ import net.corda.serialization.internal.amqp.SerializerFactory
|
||||
import net.corda.serialization.internal.amqp.constructorForDeserialization
|
||||
import net.corda.serialization.internal.amqp.hasCordaSerializable
|
||||
import net.corda.serialization.internal.amqp.propertiesForSerialization
|
||||
import java.math.BigDecimal
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class CordaModule : SimpleModule("corda-core") {
|
||||
override fun setupModule(context: SetupContext) {
|
||||
super.setupModule(context)
|
||||
|
||||
context.addBeanSerializerModifier(CordaSerializableBeanSerializerModifier())
|
||||
context.addBeanDeserializerModifier(AmountBeanDeserializerModifier())
|
||||
|
||||
context.setMixInAnnotations(PartyAndCertificate::class.java, PartyAndCertificateMixin::class.java)
|
||||
context.setMixInAnnotations(NetworkHostAndPort::class.java, NetworkHostAndPortMixin::class.java)
|
||||
@ -407,9 +412,78 @@ private interface SecureHashSHA256Mixin
|
||||
@JsonDeserialize(using = JacksonSupport.PublicKeyDeserializer::class)
|
||||
private interface PublicKeyMixin
|
||||
|
||||
@Suppress("unused_parameter")
|
||||
@ToStringSerialize
|
||||
@JsonDeserialize(using = JacksonSupport.AmountDeserializer::class)
|
||||
private interface AmountMixin
|
||||
private abstract class AmountMixin @JsonCreator(mode = DISABLED) constructor(
|
||||
quantity: Long,
|
||||
displayTokenSize: BigDecimal,
|
||||
token: Any
|
||||
) {
|
||||
/**
|
||||
* This mirrors the [Amount] constructor that we want Jackson to use, and
|
||||
* requires that we also tell Jackson NOT to use [Amount]'s primary constructor.
|
||||
*/
|
||||
@JsonCreator constructor(
|
||||
@JsonProperty("quantity")
|
||||
quantity: Long,
|
||||
|
||||
@JsonDeserialize(using = TokenDeserializer::class)
|
||||
@JsonProperty("token")
|
||||
token: Any
|
||||
) : this(quantity, Amount.getDisplayTokenSize(token), token)
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements polymorphic deserialization for [Amount.token]. Kotlin must
|
||||
* be able to determine the concrete [Amount] type at runtime, or it will
|
||||
* fall back to using [Currency].
|
||||
*/
|
||||
private class TokenDeserializer(private val tokenType: Class<*>) : JsonDeserializer<Any>(), ContextualDeserializer {
|
||||
@Suppress("unused")
|
||||
constructor() : this(Currency::class.java)
|
||||
|
||||
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): Any = parser.readValueAs(tokenType)
|
||||
|
||||
override fun createContextual(ctxt: DeserializationContext, property: BeanProperty?): TokenDeserializer {
|
||||
if (property == null) return this
|
||||
return TokenDeserializer(property.type.rawClass.let { type ->
|
||||
if (type == Any::class.java) Currency::class.java else type
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepts bean-based deserialization for the generic [Amount] type.
|
||||
*/
|
||||
private class AmountBeanDeserializerModifier : BeanDeserializerModifier() {
|
||||
override fun modifyDeserializer(config: DeserializationConfig, description: BeanDescription, deserializer: JsonDeserializer<*>): JsonDeserializer<*> {
|
||||
val modified = super.modifyDeserializer(config, description, deserializer)
|
||||
return if (Amount::class.java.isAssignableFrom(description.beanClass)) {
|
||||
AmountDeserializer(modified)
|
||||
} else {
|
||||
modified
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AmountDeserializer(delegate: JsonDeserializer<*>) : DelegatingDeserializer(delegate) {
|
||||
override fun newDelegatingInstance(newDelegatee: JsonDeserializer<*>) = AmountDeserializer(newDelegatee)
|
||||
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext?): Any {
|
||||
return if (parser.currentToken() == JsonToken.VALUE_STRING) {
|
||||
/*
|
||||
* This is obviously specific to Amount<Currency>, and is here to
|
||||
* preserve the original deserializing behaviour for this case.
|
||||
*/
|
||||
Amount.parseCurrency(parser.text)
|
||||
} else {
|
||||
/*
|
||||
* Otherwise continue deserializing our Bean as usual.
|
||||
*/
|
||||
_delegatee.deserialize(parser, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonDeserialize(using = JacksonSupport.OpaqueBytesDeserializer::class)
|
||||
private interface ByteSequenceMixin {
|
||||
|
@ -3,10 +3,7 @@ package net.corda.client.jackson.internal
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.*
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer
|
||||
import com.fasterxml.jackson.module.kotlin.convertValue
|
||||
|
@ -0,0 +1,88 @@
|
||||
package net.corda.client.jackson
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.core.type.TypeReference
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.node.TextNode
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
|
||||
import net.corda.client.jackson.internal.CordaModule
|
||||
import net.corda.core.contracts.Amount
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
|
||||
class AmountTest {
|
||||
private companion object {
|
||||
private val CO2 = CarbonCredit("CO2")
|
||||
private val jsonMapper: ObjectMapper = ObjectMapper().registerModule(CordaModule())
|
||||
private val yamlMapper: ObjectMapper = ObjectMapper(YAMLFactory()).registerModule(CordaModule())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Amount(Currency) JSON deserialization`() {
|
||||
val str = """{ "quantity": 100, "token": "USD" }"""
|
||||
val amount = jsonMapper.readValue<Amount<Currency>>(str, object : TypeReference<Amount<Currency>>() {})
|
||||
assertThat(amount.quantity).isEqualTo(100)
|
||||
assertThat(amount.token).isEqualTo(Currency.getInstance("USD"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Amount(Currency) YAML deserialization`() {
|
||||
val str = """{ quantity: 100, token: USD }"""
|
||||
val amount = yamlMapper.readValue<Amount<Currency>>(str, object : TypeReference<Amount<Currency>>() {})
|
||||
assertThat(amount.quantity).isEqualTo(100)
|
||||
assertThat(amount.token).isEqualTo(Currency.getInstance("USD"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Amount(CarbonCredit) JSON deserialization`() {
|
||||
val str = """{ "quantity": 200, "token": { "type": "CO2" } }"""
|
||||
val amount = jsonMapper.readValue<Amount<CarbonCredit>>(str, object : TypeReference<Amount<CarbonCredit>>() {})
|
||||
assertThat(amount.quantity).isEqualTo(200)
|
||||
assertThat(amount.token).isEqualTo(CO2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Amount(CarbonCredit) YAML deserialization`() {
|
||||
val str = """{ quantity: 250, token: { type: CO2 } }"""
|
||||
val amount = yamlMapper.readValue<Amount<CarbonCredit>>(str, object : TypeReference<Amount<CarbonCredit>>() {})
|
||||
assertThat(amount.quantity).isEqualTo(250)
|
||||
assertThat(amount.token).isEqualTo(CO2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Amount(Unknown) JSON deserialization`() {
|
||||
val str = """{ "quantity": 100, "token": "USD" }"""
|
||||
val amount = jsonMapper.readValue<Amount<*>>(str, object : TypeReference<Amount<*>>() {})
|
||||
assertThat(amount.quantity).isEqualTo(100)
|
||||
assertThat(amount.token).isEqualTo(Currency.getInstance("USD"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Amount(Unknown) YAML deserialization`() {
|
||||
val str = """{ quantity: 100, token: USD }"""
|
||||
val amount = yamlMapper.readValue<Amount<*>>(str, object : TypeReference<Amount<*>>() {})
|
||||
assertThat(amount.quantity).isEqualTo(100)
|
||||
assertThat(amount.token).isEqualTo(Currency.getInstance("USD"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Amount(Currency) YAML serialization`() {
|
||||
assertThat(yamlMapper.valueToTree<TextNode>(Amount.parseCurrency("£25000000"))).isEqualTo(TextNode("25000000.00 GBP"))
|
||||
assertThat(yamlMapper.valueToTree<TextNode>(Amount.parseCurrency("$250000"))).isEqualTo(TextNode("250000.00 USD"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Amount(CarbonCredit) JSON serialization`() {
|
||||
assertThat(jsonMapper.writeValueAsString(Amount(123456, CO2)).trim())
|
||||
.isEqualTo(""""123456 CarbonCredit(type=CO2)"""")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Amount(CarbonCredit) YAML serialization`() {
|
||||
assertThat(yamlMapper.writeValueAsString(Amount(123456, CO2)).trim())
|
||||
.isEqualTo("""--- "123456 CarbonCredit(type=CO2)"""")
|
||||
}
|
||||
|
||||
data class CarbonCredit(@JsonProperty("type") val type: String)
|
||||
}
|
@ -105,10 +105,14 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
||||
@Test
|
||||
fun `Amount(Currency) deserialization`() {
|
||||
val old = mapOf(
|
||||
"quantity" to 2500000000,
|
||||
"token" to "USD"
|
||||
"quantity" to 2500000000,
|
||||
"token" to "USD"
|
||||
)
|
||||
assertThat(mapper.convertValue<Amount<Currency>>(old)).isEqualTo(Amount(2_500_000_000, USD))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Amount(Currency) Text deserialization`() {
|
||||
assertThat(mapper.convertValue<Amount<Currency>>(TextNode("$25000000"))).isEqualTo(Amount(2_500_000_000, USD))
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,9 @@ package net.corda.client.jackson
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class StringToMethodCallParserTest {
|
||||
@Suppress("UNUSED")
|
||||
@ -23,6 +25,7 @@ class StringToMethodCallParserTest {
|
||||
fun twoStrings(a: String, b: String) = a + b
|
||||
fun simpleObject(hash: SecureHash.SHA256) = hash.toString()
|
||||
fun complexObject(pair: Pair<Int, String>) = pair
|
||||
fun complexNestedObject(pairs: Pair<Int, Deque<Char>>) = pairs
|
||||
|
||||
fun overload(a: String) = a
|
||||
fun overload(a: String, b: String) = a + b
|
||||
@ -48,30 +51,68 @@ class StringToMethodCallParserTest {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* It would be unreasonable to expect "[ A, B, C ]" to deserialise as "Deque<Char>" by default.
|
||||
* Deque is chosen as we still expect it to preserve the order of its elements.
|
||||
*/
|
||||
@Test
|
||||
fun complexNestedGenericMethod() {
|
||||
val parser = StringToMethodCallParser(Target::class)
|
||||
val result = parser.parse(Target(), "complexNestedObject pairs: { first: 101, second: [ A, B, C ] }").invoke()
|
||||
|
||||
assertTrue(result is Pair<*,*>)
|
||||
result as Pair<*,*>
|
||||
|
||||
assertEquals(101, result.first)
|
||||
|
||||
assertTrue(result.second is Deque<*>)
|
||||
val deque = result.second as Deque<*>
|
||||
assertArrayEquals(arrayOf('A', 'B', 'C'), deque.toTypedArray())
|
||||
}
|
||||
|
||||
@Suppress("UNUSED")
|
||||
class ConstructorTarget(val someWord: String, val aDifferentThing: Int) {
|
||||
constructor(alternativeWord: String) : this(alternativeWord, 0)
|
||||
constructor(numbers: List<Long>) : this(numbers.map(Long::toString).joinToString("+"), numbers.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ctor1() {
|
||||
val clazz = ConstructorTarget::class.java
|
||||
val parser = StringToMethodCallParser(clazz)
|
||||
val ctor = clazz.constructors.single { it.parameterCount == 2 }
|
||||
val ctor = clazz.getDeclaredConstructor(String::class.java, Int::class.java)
|
||||
val names: List<String> = parser.paramNamesFromConstructor(ctor)
|
||||
assertEquals(listOf("someWord", "aDifferentThing"), names)
|
||||
val args: Array<Any?> = parser.parseArguments(clazz.name, names.zip(ctor.parameterTypes), "someWord: Blah blah blah, aDifferentThing: 12")
|
||||
assertArrayEquals(args, arrayOf<Any?>("Blah blah blah", 12))
|
||||
assertArrayEquals(arrayOf("Blah blah blah", 12), args)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ctor2() {
|
||||
val clazz = ConstructorTarget::class.java
|
||||
val parser = StringToMethodCallParser(clazz)
|
||||
val ctor = clazz.constructors.single { it.parameterCount == 1 }
|
||||
val ctor = clazz.getDeclaredConstructor(String::class.java)
|
||||
val names: List<String> = parser.paramNamesFromConstructor(ctor)
|
||||
assertEquals(listOf("alternativeWord"), names)
|
||||
val args: Array<Any?> = parser.parseArguments(clazz.name, names.zip(ctor.parameterTypes), "alternativeWord: Foo bar!")
|
||||
assertArrayEquals(args, arrayOf<Any?>("Foo bar!"))
|
||||
assertArrayEquals(arrayOf("Foo bar!"), args)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun constructorWithGenericArgs() {
|
||||
val clazz = ConstructorTarget::class.java
|
||||
val ctor = clazz.getDeclaredConstructor(List::class.java)
|
||||
StringToMethodCallParser(clazz).apply {
|
||||
val names = paramNamesFromConstructor(ctor)
|
||||
assertEquals(listOf("numbers"), names)
|
||||
|
||||
val commandLine = "numbers: [ 1, 2, 3 ]"
|
||||
|
||||
val args = parseArguments(clazz.name, names.zip(ctor.parameterTypes), commandLine)
|
||||
assertArrayEquals(arrayOf(listOf(1, 2, 3)), args)
|
||||
|
||||
val trueArgs = parseArguments(clazz.name, names.zip(ctor.genericParameterTypes), commandLine)
|
||||
assertArrayEquals(arrayOf(listOf(1L, 2L, 3L)), trueArgs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ test {
|
||||
includeTestsMatching "net.corda.deterministic.data.GenerateData"
|
||||
}
|
||||
}
|
||||
assemble.finalizedBy test
|
||||
|
||||
artifacts {
|
||||
testData file: file("$buildDir/test-data.jar"), type: 'jar', builtBy: test
|
||||
|
@ -113,7 +113,8 @@ sealed class InvocationOrigin {
|
||||
/**
|
||||
* Origin was an RPC call.
|
||||
*/
|
||||
data class RPC(private val actor: Actor) : InvocationOrigin() {
|
||||
// Field `actor` needs to stay public for AMQP / JSON serialization to work.
|
||||
data class RPC(val actor: Actor) : InvocationOrigin() {
|
||||
override fun principal() = Principal { actor.id.value }
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.internal.concurrent.doneFuture
|
||||
import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.node.services.Vault.StateModificationStatus.*
|
||||
import net.corda.core.node.services.Vault.StateStatus
|
||||
import net.corda.core.node.services.vault.*
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
@ -115,6 +116,26 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
|
||||
UNCONSUMED, CONSUMED, ALL
|
||||
}
|
||||
|
||||
/**
|
||||
* If the querying node is a participant in a state then it is classed as [MODIFIABLE], although technically the
|
||||
* state is only _potentially_ modifiable as the contract code may forbid them from performing any actions.
|
||||
*
|
||||
* If the querying node is not a participant in a state then it is classed as [NOT_MODIFIABLE]. These types of
|
||||
* states can still be recorded in the vault if the transaction containing them was recorded with the
|
||||
* [StatesToRecord.ALL_VISIBLE] flag. This will typically happen for things like reference data which can be
|
||||
* referenced in transactions as a [ReferencedStateAndRef] but cannot be modified by any party but the maintainer.
|
||||
*
|
||||
* If both [MODIFIABLE] and [NOT_MODIFIABLE] states are required to be returned from a query, then the [ALL] flag
|
||||
* can be used.
|
||||
*
|
||||
* NOTE: Default behaviour is for ALL STATES to be returned as this is how Corda behaved before the introduction of
|
||||
* this query criterion.
|
||||
*/
|
||||
@CordaSerializable
|
||||
enum class StateModificationStatus {
|
||||
MODIFIABLE, NOT_MODIFIABLE, ALL
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class UpdateType {
|
||||
GENERAL, NOTARY_CHANGE, CONTRACT_UPGRADE
|
||||
@ -141,14 +162,40 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
|
||||
val otherResults: List<Any>)
|
||||
|
||||
@CordaSerializable
|
||||
data class StateMetadata(val ref: StateRef,
|
||||
val contractStateClassName: String,
|
||||
val recordedTime: Instant,
|
||||
val consumedTime: Instant?,
|
||||
val status: Vault.StateStatus,
|
||||
val notary: AbstractParty?,
|
||||
val lockId: String?,
|
||||
val lockUpdateTime: Instant?)
|
||||
data class StateMetadata constructor(
|
||||
val ref: StateRef,
|
||||
val contractStateClassName: String,
|
||||
val recordedTime: Instant,
|
||||
val consumedTime: Instant?,
|
||||
val status: Vault.StateStatus,
|
||||
val notary: AbstractParty?,
|
||||
val lockId: String?,
|
||||
val lockUpdateTime: Instant?,
|
||||
val isModifiable: Vault.StateModificationStatus?
|
||||
) {
|
||||
constructor(ref: StateRef,
|
||||
contractStateClassName: String,
|
||||
recordedTime: Instant,
|
||||
consumedTime: Instant?,
|
||||
status: Vault.StateStatus,
|
||||
notary: AbstractParty?,
|
||||
lockId: String?,
|
||||
lockUpdateTime: Instant?
|
||||
) : this(ref, contractStateClassName, recordedTime, consumedTime, status, notary, lockId, lockUpdateTime, null)
|
||||
|
||||
fun copy(
|
||||
ref: StateRef = this.ref,
|
||||
contractStateClassName: String = this.contractStateClassName,
|
||||
recordedTime: Instant = this.recordedTime,
|
||||
consumedTime: Instant? = this.consumedTime,
|
||||
status: Vault.StateStatus = this.status,
|
||||
notary: AbstractParty? = this.notary,
|
||||
lockId: String? = this.lockId,
|
||||
lockUpdateTime: Instant? = this.lockUpdateTime
|
||||
): StateMetadata {
|
||||
return StateMetadata(ref, contractStateClassName, recordedTime, consumedTime, status, notary, lockId, lockUpdateTime, null)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Deprecated("No longer used. The vault does not emit empty updates")
|
||||
@ -191,7 +238,10 @@ interface VaultService {
|
||||
*/
|
||||
@DeleteForDJVM
|
||||
fun whenConsumed(ref: StateRef): CordaFuture<Vault.Update<ContractState>> {
|
||||
val query = QueryCriteria.VaultQueryCriteria(stateRefs = listOf(ref), status = Vault.StateStatus.CONSUMED)
|
||||
val query = QueryCriteria.VaultQueryCriteria(
|
||||
stateRefs = listOf(ref),
|
||||
status = Vault.StateStatus.CONSUMED
|
||||
)
|
||||
val result = trackBy<ContractState>(query)
|
||||
val snapshot = result.snapshot.states
|
||||
return if (snapshot.isNotEmpty()) {
|
||||
|
@ -83,6 +83,7 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
|
||||
|
||||
abstract class CommonQueryCriteria : QueryCriteria() {
|
||||
abstract val status: Vault.StateStatus
|
||||
open val isModifiable: Vault.StateModificationStatus = Vault.StateModificationStatus.ALL
|
||||
abstract val contractStateTypes: Set<Class<out ContractState>>?
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
return parser.parseCriteria(this)
|
||||
@ -92,51 +93,124 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
|
||||
/**
|
||||
* VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates]
|
||||
*/
|
||||
data class VaultQueryCriteria @JvmOverloads constructor(override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null,
|
||||
val stateRefs: List<StateRef>? = null,
|
||||
val notary: List<AbstractParty>? = null,
|
||||
val softLockingCondition: SoftLockingCondition? = null,
|
||||
val timeCondition: TimeCondition? = null) : CommonQueryCriteria() {
|
||||
data class VaultQueryCriteria @JvmOverloads constructor(
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null,
|
||||
val stateRefs: List<StateRef>? = null,
|
||||
val notary: List<AbstractParty>? = null,
|
||||
val softLockingCondition: SoftLockingCondition? = null,
|
||||
val timeCondition: TimeCondition? = null,
|
||||
override val isModifiable: Vault.StateModificationStatus = Vault.StateModificationStatus.ALL
|
||||
) : CommonQueryCriteria() {
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
super.visit(parser)
|
||||
return parser.parseCriteria(this)
|
||||
}
|
||||
|
||||
fun copy(
|
||||
status: Vault.StateStatus = this.status,
|
||||
contractStateTypes: Set<Class<out ContractState>>? = this.contractStateTypes,
|
||||
stateRefs: List<StateRef>? = this.stateRefs,
|
||||
notary: List<AbstractParty>? = this.notary,
|
||||
softLockingCondition: SoftLockingCondition? = this.softLockingCondition,
|
||||
timeCondition: TimeCondition? = this.timeCondition
|
||||
): VaultQueryCriteria {
|
||||
return VaultQueryCriteria(
|
||||
status,
|
||||
contractStateTypes,
|
||||
stateRefs,
|
||||
notary,
|
||||
softLockingCondition,
|
||||
timeCondition
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LinearStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultLinearState]
|
||||
*/
|
||||
data class LinearStateQueryCriteria @JvmOverloads constructor(val participants: List<AbstractParty>? = null,
|
||||
val uuid: List<UUID>? = null,
|
||||
val externalId: List<String>? = null,
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null) : CommonQueryCriteria() {
|
||||
constructor(participants: List<AbstractParty>? = null,
|
||||
linearId: List<UniqueIdentifier>? = null,
|
||||
status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
contractStateTypes: Set<Class<out ContractState>>? = null) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes)
|
||||
data class LinearStateQueryCriteria @JvmOverloads constructor(
|
||||
val participants: List<AbstractParty>? = null,
|
||||
val uuid: List<UUID>? = null,
|
||||
val externalId: List<String>? = null,
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null,
|
||||
override val isModifiable: Vault.StateModificationStatus = Vault.StateModificationStatus.ALL
|
||||
) : CommonQueryCriteria() {
|
||||
constructor(
|
||||
participants: List<AbstractParty>? = null,
|
||||
linearId: List<UniqueIdentifier>? = null,
|
||||
status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
contractStateTypes: Set<Class<out ContractState>>? = null,
|
||||
isRelevant: Vault.StateModificationStatus
|
||||
) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes, isRelevant)
|
||||
|
||||
constructor(
|
||||
participants: List<AbstractParty>? = null,
|
||||
linearId: List<UniqueIdentifier>? = null,
|
||||
status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
contractStateTypes: Set<Class<out ContractState>>? = null
|
||||
) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes)
|
||||
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
super.visit(parser)
|
||||
return parser.parseCriteria(this)
|
||||
}
|
||||
|
||||
fun copy(
|
||||
participants: List<AbstractParty>? = this.participants,
|
||||
uuid: List<UUID>? = this.uuid,
|
||||
externalId: List<String>? = this.externalId,
|
||||
status: Vault.StateStatus = this.status,
|
||||
contractStateTypes: Set<Class<out ContractState>>? = this.contractStateTypes
|
||||
): LinearStateQueryCriteria {
|
||||
return LinearStateQueryCriteria(
|
||||
participants,
|
||||
uuid,
|
||||
externalId,
|
||||
status,
|
||||
contractStateTypes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FungibleStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultFungibleStates]
|
||||
*/
|
||||
data class FungibleAssetQueryCriteria @JvmOverloads constructor(val participants: List<AbstractParty>? = null,
|
||||
val owner: List<AbstractParty>? = null,
|
||||
val quantity: ColumnPredicate<Long>? = null,
|
||||
val issuer: List<AbstractParty>? = null,
|
||||
val issuerRef: List<OpaqueBytes>? = null,
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null) : CommonQueryCriteria() {
|
||||
data class FungibleAssetQueryCriteria @JvmOverloads constructor(
|
||||
val participants: List<AbstractParty>? = null,
|
||||
val owner: List<AbstractParty>? = null,
|
||||
val quantity: ColumnPredicate<Long>? = null,
|
||||
val issuer: List<AbstractParty>? = null,
|
||||
val issuerRef: List<OpaqueBytes>? = null,
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null,
|
||||
override val isModifiable: Vault.StateModificationStatus = Vault.StateModificationStatus.ALL
|
||||
) : CommonQueryCriteria() {
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
super.visit(parser)
|
||||
return parser.parseCriteria(this)
|
||||
}
|
||||
|
||||
fun copy(
|
||||
participants: List<AbstractParty>? = this.participants,
|
||||
owner: List<AbstractParty>? = this.owner,
|
||||
quantity: ColumnPredicate<Long>? = this.quantity,
|
||||
issuer: List<AbstractParty>? = this.issuer,
|
||||
issuerRef: List<OpaqueBytes>? = this.issuerRef,
|
||||
status: Vault.StateStatus = this.status,
|
||||
contractStateTypes: Set<Class<out ContractState>>? = this.contractStateTypes
|
||||
): FungibleAssetQueryCriteria {
|
||||
return FungibleAssetQueryCriteria(
|
||||
participants,
|
||||
owner,
|
||||
quantity,
|
||||
issuer,
|
||||
issuerRef,
|
||||
status,
|
||||
contractStateTypes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -147,14 +221,28 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
|
||||
* Params
|
||||
* [expression] refers to a (composable) type safe [CriteriaExpression]
|
||||
*/
|
||||
data class VaultCustomQueryCriteria<L : PersistentState> @JvmOverloads constructor
|
||||
(val expression: CriteriaExpression<L, Boolean>,
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null) : CommonQueryCriteria() {
|
||||
data class VaultCustomQueryCriteria<L : PersistentState> @JvmOverloads constructor(
|
||||
val expression: CriteriaExpression<L, Boolean>,
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null,
|
||||
override val isModifiable: Vault.StateModificationStatus = Vault.StateModificationStatus.ALL
|
||||
) : CommonQueryCriteria() {
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
super.visit(parser)
|
||||
return parser.parseCriteria(this)
|
||||
}
|
||||
|
||||
fun copy(
|
||||
expression: CriteriaExpression<L, Boolean> = this.expression,
|
||||
status: Vault.StateStatus = this.status,
|
||||
contractStateTypes: Set<Class<out ContractState>>? = this.contractStateTypes
|
||||
): VaultCustomQueryCriteria<L> {
|
||||
return VaultCustomQueryCriteria(
|
||||
expression,
|
||||
status,
|
||||
contractStateTypes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// timestamps stored in the vault states table [VaultSchema.VaultStates]
|
||||
|
@ -13,8 +13,6 @@ package net.corda.core.utilities
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.Try.Failure
|
||||
import net.corda.core.utilities.Try.Success
|
||||
|
||||
/**
|
||||
* Representation of an operation that has either succeeded with a result (represented by [Success]) or failed with an
|
||||
@ -70,6 +68,35 @@ sealed class Try<out A> {
|
||||
is Failure -> uncheckedCast(this)
|
||||
}
|
||||
|
||||
/** Applies the given action to the value if [Success], or does nothing if [Failure]. Returns `this` for chaining. */
|
||||
fun doOnSuccess(action: (A) -> Unit): Try<A> {
|
||||
when (this) {
|
||||
is Success -> action.invoke(value)
|
||||
is Failure -> {}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/** Applies the given action to the error if [Failure], or does nothing if [Success]. Returns `this` for chaining. */
|
||||
fun doOnFailure(action: (Throwable) -> Unit): Try<A> {
|
||||
when (this) {
|
||||
is Success -> {}
|
||||
is Failure -> action.invoke(exception)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/** Applies the given action to the exception if [Failure], rethrowing [Error]s. Does nothing if [Success]. Returns `this` for chaining. */
|
||||
fun doOnException(action: (Exception) -> Unit): Try<A> {
|
||||
return doOnFailure { error ->
|
||||
if (error is Exception) {
|
||||
action.invoke(error)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@KeepForDJVM
|
||||
data class Success<out A>(val value: A) : Try<A>() {
|
||||
override val isSuccess: Boolean get() = true
|
||||
|
@ -5,6 +5,7 @@ import net.corda.core.contracts.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.StatesToRecord
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
@ -102,7 +103,10 @@ internal class UseRefState(val linearId: UniqueIdentifier) : FlowLogic<SignedTra
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val notary = serviceHub.networkMapCache.notaryIdentities.first()
|
||||
val query = QueryCriteria.LinearStateQueryCriteria(linearId = listOf(linearId))
|
||||
val query = QueryCriteria.LinearStateQueryCriteria(
|
||||
linearId = listOf(linearId),
|
||||
isRelevant = Vault.StateModificationStatus.ALL
|
||||
)
|
||||
val referenceState = serviceHub.vaultService.queryBy<ContractState>(query).states.single()
|
||||
return subFlow(FinalityFlow(
|
||||
transaction = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply {
|
||||
|
@ -201,19 +201,22 @@ CorDapps specific to their role in the network.
|
||||
|
||||
Running the network
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
When using a ``MockNetwork``, you must be careful to ensure that all the nodes have processed all the relevant messages
|
||||
before making assertions about the result of performing some action. For example, if you start a flow to update the ledger
|
||||
but don't wait until all the nodes involved have processed all the resulting messages, your nodes' vaults may not be in
|
||||
the state you expect.
|
||||
|
||||
Regular Corda nodes automatically process received messages. When using a ``MockNetwork`` with
|
||||
``networkSendManuallyPumped`` set to ``false``, you must manually initiate the processing of received messages.
|
||||
|
||||
When ``networkSendManuallyPumped`` is set to ``false``, you must manually initiate the processing of received messages.
|
||||
You manually process received messages as follows:
|
||||
|
||||
* ``StartedMockNode.pumpReceive`` to process a single message from the node's queue
|
||||
|
||||
* ``MockNetwork.runNetwork`` to process all the messages in every node's queue. This may generate additional messages
|
||||
that must in turn be processed
|
||||
|
||||
* ``network.runNetwork(-1)`` (the default in Kotlin) will exchange messages until there are no further messages to
|
||||
* ``StartedMockNode.pumpReceive()`` processes a single message from the node's queue
|
||||
* ``MockNetwork.runNetwork()`` processes all the messages in every node's queue until there are no further messages to
|
||||
process
|
||||
|
||||
When ``networkSendManuallyPumped`` is set to ``true``, nodes will automatically process the messages they receive. You
|
||||
can block until all messages have been processed using ``MockNetwork.waitQuiescent()``.
|
||||
|
||||
.. warning:: If ``threadPerNode`` is set to ``true``, ``networkSendManuallyPumped`` must also be set to ``true``.
|
||||
|
||||
Running flows
|
||||
^^^^^^^^^^^^^
|
||||
|
@ -1,27 +1,52 @@
|
||||
Client RPC
|
||||
==========
|
||||
.. highlight:: kotlin
|
||||
.. raw:: html
|
||||
|
||||
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||
|
||||
Interacting with a node
|
||||
=======================
|
||||
|
||||
.. contents::
|
||||
|
||||
Overview
|
||||
--------
|
||||
Corda provides a client library that allows you to easily write clients in a JVM-compatible language to interact
|
||||
with a running node. The library connects to the node using a message queue protocol and then provides a simple RPC
|
||||
interface to interact with the node. You make calls on a Java object as normal, and the marshalling back and forth is
|
||||
handled for you.
|
||||
You should interact with your node using the `CordaRPCClient`_ library. This library that allows you to easily
|
||||
write clients in a JVM-compatible language to interact with a running node. The library connects to the node using a
|
||||
message queue protocol and then provides a simple RPC interface to interact with the node. You make calls on a JVM
|
||||
object as normal, and the marshalling back and forth is handled for you.
|
||||
|
||||
The starting point for the client library is the `CordaRPCClient`_ class. `CordaRPCClient`_ provides a ``start`` method
|
||||
that returns a `CordaRPCConnection`_. A `CordaRPCConnection`_ allows you to access an implementation of the
|
||||
`CordaRPCOps`_ interface with ``proxy`` in Kotlin or ``getProxy()`` in Java. The observables that are returned by RPC
|
||||
operations can be subscribed to in order to receive an ongoing stream of updates from the node. More detail on this
|
||||
functionality is provided in the docs for the ``proxy`` method.
|
||||
.. warning:: The built-in Corda webserver is deprecated and unsuitable for production use. If you want to interact with
|
||||
your node via HTTP, you will need to stand up your own webserver, then create an RPC connection between your node
|
||||
and this webserver using the `CordaRPCClient`_ library. You can find an example of how to do this
|
||||
`here <https://github.com/corda/spring-webserver>`_.
|
||||
|
||||
Connecting to a node via RPC
|
||||
----------------------------
|
||||
`CordaRPCClient`_ provides a ``start`` method that takes the node's RPC address and returns a `CordaRPCConnection`_.
|
||||
`CordaRPCConnection`_ provides a ``proxy`` method that takes an RPC username and password and returns a `CordaRPCOps`_
|
||||
object that you can use to interact with the node.
|
||||
|
||||
Here is an example of using `CordaRPCClient`_ to connect to a node and log the current time on its internal clock:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcExample.kt
|
||||
:language: kotlin
|
||||
:start-after: START 1
|
||||
:end-before: END 1
|
||||
|
||||
.. literalinclude:: example-code/src/main/java/net/corda/docs/ClientRpcExampleJava.java
|
||||
:language: java
|
||||
:start-after: START 1
|
||||
:end-before: END 1
|
||||
|
||||
.. warning:: The returned `CordaRPCConnection`_ is somewhat expensive to create and consumes a small amount of
|
||||
server side resources. When you're done with it, call ``close`` on it. Alternatively you may use the ``use``
|
||||
method on `CordaRPCClient`_ which cleans up automatically after the passed in lambda finishes. Don't create
|
||||
a new proxy for every call you make - reuse an existing one.
|
||||
|
||||
For a brief tutorial on using the RPC API, see :doc:`tutorial-clientrpc-api`.
|
||||
For further information on using the RPC API, see :doc:`tutorial-clientrpc-api`.
|
||||
|
||||
RPC permissions
|
||||
---------------
|
||||
@ -276,7 +301,7 @@ will be freed automatically.
|
||||
is non-deterministic.
|
||||
|
||||
.. note:: Observables can only be used as return arguments of an RPC call. It is not currently possible to pass
|
||||
Observables as parameters to the RPC methods.
|
||||
Observables as parameters to the RPC methods.
|
||||
|
||||
Futures
|
||||
-------
|
||||
@ -311,7 +336,7 @@ This does not expose internal information to clients, strengthening privacy and
|
||||
|
||||
Connection management
|
||||
---------------------
|
||||
It is possible to not be able to connect to the server on the first attempt. In that case, the ``CordaRPCCLient.start()``
|
||||
It is possible to not be able to connect to the server on the first attempt. In that case, the ``CordaRPCClient.start()``
|
||||
method will throw an exception. The following code snippet is an example of how to write a simple retry mechanism for
|
||||
such situations:
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
Node configuration
|
||||
Configuring a node
|
||||
==================
|
||||
|
||||
.. contents::
|
||||
|
@ -1,5 +1,5 @@
|
||||
Building a CorDapp
|
||||
==================
|
||||
Building and installing a CorDapp
|
||||
=================================
|
||||
|
||||
.. contents::
|
||||
|
||||
@ -187,5 +187,5 @@ These files are loaded when a CorDapp context is created and so can change durin
|
||||
|
||||
CorDapp configuration can be accessed from ``CordappContext::config`` whenever a ``CordappContext`` is available.
|
||||
|
||||
There is an example project that demonstrates in ``samples` called ``cordapp-configuration`` and API documentation in
|
||||
<api/kotlin/corda/net.corda.core.cordapp/index.html>`_.
|
||||
There is an example project that demonstrates in ``samples`` called ``cordapp-configuration`` and API documentation in
|
||||
`<api/kotlin/corda/net.corda.core.cordapp/index.html>`_.
|
||||
|
@ -3,20 +3,38 @@ What is a CorDapp?
|
||||
|
||||
CorDapps (Corda Distributed Applications) are distributed applications that run on the Corda platform. The goal of a
|
||||
CorDapp is to allow nodes to reach agreement on updates to the ledger. They achieve this goal by defining flows that
|
||||
Corda node owners can invoke through RPC calls:
|
||||
Corda node owners can invoke over RPC:
|
||||
|
||||
.. image:: resources/node-diagram.png
|
||||
:scale: 25%
|
||||
:align: center
|
||||
|
||||
CorDapps are made up of the following key components:
|
||||
CorDapp components
|
||||
------------------
|
||||
CorDapps take the form of a set of JAR files containing class definitions written in Java and/or Kotlin.
|
||||
|
||||
* States, defining the facts over which agreement is reached (see :doc:`Key Concepts - States <key-concepts-states>`)
|
||||
These class definitions will commonly include the following elements:
|
||||
|
||||
* Flows: Define a routine for the node to run, usually to update the ledger
|
||||
(see :doc:`Key Concepts - Flows <key-concepts-flows>`). They subclass ``FlowLogic``
|
||||
* States: Define the facts over which agreement is reached (see :doc:`Key Concepts - States <key-concepts-states>`).
|
||||
They implement the ``ContractState`` interface
|
||||
* Contracts, defining what constitutes a valid ledger update (see
|
||||
:doc:`Key Concepts - Contracts <key-concepts-contracts>`)
|
||||
* Services, providing long-lived utilities within the node
|
||||
* Serialisation whitelists, restricting what types your node will receive off the wire
|
||||
:doc:`Key Concepts - Contracts <key-concepts-contracts>`). They implement the ``Contract`` interface
|
||||
* Services, providing long-lived utilities within the node. They subclass ``SingletonSerializationToken``
|
||||
* Serialisation whitelists, restricting what types your node will receive off the wire. They implement the
|
||||
``SerializationWhitelist`` interface
|
||||
|
||||
Each CorDapp is installed at the level of the individual node, rather than on the network itself. For example, a node
|
||||
owner may choose to install the Bond Trading CorDapp, with the following components:
|
||||
But the CorDapp JAR can also include other class definitions. These may include:
|
||||
|
||||
* APIs and static web content: These are served by Corda's built-in webserver. This webserver is not
|
||||
production-ready, and should be used for testing purposes only
|
||||
* Utility classes
|
||||
|
||||
An example
|
||||
----------
|
||||
Suppose a node owner wants their node to be able to trade bonds. They may choose to install a Bond Trading CorDapp with
|
||||
the following components:
|
||||
|
||||
* A ``BondState``, used to represent bonds as shared facts on the ledger
|
||||
* A ``BondContract``, used to govern which ledger updates involving ``BondState`` states are valid
|
||||
|
@ -0,0 +1,34 @@
|
||||
package net.corda.docs;
|
||||
|
||||
// START 1
|
||||
import net.corda.client.rpc.CordaRPCClient;
|
||||
import net.corda.client.rpc.CordaRPCConnection;
|
||||
import net.corda.core.messaging.CordaRPCOps;
|
||||
import net.corda.core.utilities.NetworkHostAndPort;
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
class ExampleRpcClientJava {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ExampleRpcClient.class);
|
||||
|
||||
public static void main(String[] args) throws ActiveMQException, InterruptedException, ExecutionException {
|
||||
if (args.length != 3) {
|
||||
throw new IllegalArgumentException("Usage: TemplateClient <node address> <username> <password>");
|
||||
}
|
||||
final NetworkHostAndPort nodeAddress = NetworkHostAndPort.parse(args[0]);
|
||||
String username = args[1];
|
||||
String password = args[2];
|
||||
|
||||
final CordaRPCClient client = new CordaRPCClient(nodeAddress);
|
||||
final CordaRPCConnection connection = client.start(username, password);
|
||||
final CordaRPCOps cordaRPCOperations = connection.getProxy();
|
||||
|
||||
logger.info(cordaRPCOperations.currentNodeTime().toString());
|
||||
|
||||
connection.notifyServerAndClose();
|
||||
}
|
||||
}
|
||||
// END 1
|
@ -0,0 +1,31 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package net.corda.docs
|
||||
|
||||
// START 1
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.utilities.NetworkHostAndPort.Companion.parse
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import org.slf4j.Logger
|
||||
|
||||
class ExampleRpcClient {
|
||||
companion object {
|
||||
val logger: Logger = loggerFor<ExampleRpcClient>()
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
require(args.size == 3) { "Usage: TemplateClient <node address> <username> <password>" }
|
||||
val nodeAddress = parse(args[0])
|
||||
val username = args[1]
|
||||
val password = args[2]
|
||||
|
||||
val client = CordaRPCClient(nodeAddress)
|
||||
val connection = client.start(username, password)
|
||||
val cordaRPCOperations = connection.proxy
|
||||
|
||||
logger.info(cordaRPCOperations.currentNodeTime().toString())
|
||||
|
||||
connection.notifyServerAndClose()
|
||||
}
|
||||
}
|
||||
// END 1
|
@ -4,8 +4,8 @@
|
||||
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||
|
||||
Shell
|
||||
=====
|
||||
Node shell
|
||||
==========
|
||||
|
||||
.. contents::
|
||||
|
||||
|
@ -1,3 +1,9 @@
|
||||
.. highlight:: kotlin
|
||||
.. raw:: html
|
||||
|
||||
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||
|
||||
.. _graphstream: http://graphstream-project.org/
|
||||
|
||||
Using the client RPC API
|
||||
|
@ -1,58 +1,70 @@
|
||||
Writing a CorDapp
|
||||
CorDapp structure
|
||||
=================
|
||||
|
||||
.. contents::
|
||||
|
||||
Overview
|
||||
--------
|
||||
CorDapps can be written in either Java, Kotlin, or a combination of the two. Each CorDapp component takes the form
|
||||
of a JVM class that subclasses or implements a Corda library type:
|
||||
|
||||
* Flows subclass ``FlowLogic``
|
||||
* States implement ``ContractState``
|
||||
* Contracts implement ``Contract``
|
||||
* Services subclass ``SingletonSerializationToken``
|
||||
* Serialisation whitelists implement ``SerializationWhitelist``
|
||||
|
||||
Web content and RPC clients
|
||||
---------------------------
|
||||
For testing purposes, CorDapps may also include:
|
||||
|
||||
* **APIs and static web content**: These are served by Corda's built-in webserver. This webserver is not
|
||||
production-ready, and should be used for testing purposes only
|
||||
|
||||
* **RPC clients**: These are programs that automate the process of interacting with a node via RPC
|
||||
|
||||
In production, a production-ready webserver should be used, and these files should be moved into a different module or
|
||||
project so that they do not bloat the CorDapp at build time.
|
||||
|
||||
.. _cordapp-structure:
|
||||
|
||||
Structure and dependencies
|
||||
--------------------------
|
||||
You should base your project on the Java template (for CorDapps written in Java) or the Kotlin template (for CorDapps
|
||||
written in Kotlin):
|
||||
Modules
|
||||
-------
|
||||
The source code for a CorDapp is divided into one or more modules, each of which will be compiled into a separate JAR.
|
||||
Together, these JARs represent a single CorDapp. Typically, a Cordapp contains all the classes required for it to be
|
||||
used standalone. However, some Cordapps are only libraries for other Cordapps and cannot be run standalone.
|
||||
|
||||
* `Java Template CorDapp <https://github.com/corda/cordapp-template-java>`_
|
||||
* `Kotlin Template CorDapp <https://github.com/corda/cordapp-template-kotlin>`_
|
||||
A common pattern is to have:
|
||||
|
||||
Please checkout the branch of the template that corresponds to the version of Corda you are using. For example, someone
|
||||
building a CorDapp on Corda 3 should use the ``release-V3`` branch of the template.
|
||||
* One module containing only the CorDapp's contracts and/or states, as well as any required dependencies
|
||||
* A second module containing the remaining classes that depend on these contracts and/or states
|
||||
|
||||
The required dependencies are defined by the ``build.gradle`` file in the root directory of the template.
|
||||
This is because each time a contract is used in a transaction, the entire JAR containing the contract's definition is
|
||||
attached to the transaction. This is to ensure that the exact same contract and state definitions are used when
|
||||
verifying this transaction at a later date. Because of this, you will want to keep this module, and therefore the
|
||||
resulting JAR file, as small as possible to reduce the size of your transactions and keep your node performant.
|
||||
|
||||
The project should be split into two modules:
|
||||
However, this two-module structure is not prescriptive:
|
||||
|
||||
* A ``cordapp-contracts-states`` module containing classes such as contracts and states that will be sent across the
|
||||
wire as part of a flow
|
||||
* A ``cordapp`` module containing the remaining classes
|
||||
* A library CorDapp containing only contracts and states would only need a single module
|
||||
|
||||
Each module will be compiled into its own CorDapp. This minimises the size of the JAR that has to be sent across the
|
||||
wire when nodes are agreeing ledger updates.
|
||||
* In a CorDapp with multiple sets of contracts and states that **do not** depend on each other, each independent set of
|
||||
contracts and states would go in a separate module to reduce transaction size
|
||||
|
||||
* In a CorDapp with multiple sets of contracts and states that **do** depend on each other, either keep them in the
|
||||
same module or create separate modules that depend on each other
|
||||
|
||||
* The module containing the flows and other classes can be structured in any way because it is not attached to
|
||||
transactions
|
||||
|
||||
Template CorDapps
|
||||
-----------------
|
||||
You should base your project on one of the following templates:
|
||||
|
||||
* `Java Template CorDapp <https://github.com/corda/cordapp-template-java>`_ (for CorDapps written in Java)
|
||||
* `Kotlin Template CorDapp <https://github.com/corda/cordapp-template-kotlin>`_ (for CorDapps written in Kotlin)
|
||||
|
||||
Please use the branch of the template that corresponds to the major version of Corda you are using. For example,
|
||||
someone building a CorDapp on Corda 3.2 should use the ``release-V3`` branch of the template.
|
||||
|
||||
Build system
|
||||
^^^^^^^^^^^^
|
||||
|
||||
The templates are built using Gradle. A Gradle wrapper is provided in the ``wrapper`` folder, and the dependencies are
|
||||
defined in the ``build.gradle`` files. See :doc:`cordapp-build-systems` for more information.
|
||||
|
||||
No templates are currently provided for Maven or other build systems.
|
||||
|
||||
Modules
|
||||
^^^^^^^
|
||||
The templates are split into two modules:
|
||||
|
||||
* A ``cordapp-contracts-states`` module containing the contracts and states
|
||||
* A ``cordapp`` module containing the remaining classes that depends on the ``cordapp-contracts-states`` module
|
||||
|
||||
These modules will be compiled into two JARs - a ``cordapp-contracts-states`` JAR and a ``cordapp`` JAR - which
|
||||
together represent the Template CorDapp.
|
||||
|
||||
Module one - cordapp-contracts-states
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Here is the structure of the ``src`` directory for the ``cordapp-contracts-states`` module:
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Here is the structure of the ``src`` directory for the ``cordapp-contracts-states`` module of the Java template:
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
@ -73,8 +85,8 @@ These are definitions for classes that we expect to have to send over the wire.
|
||||
CorDapp.
|
||||
|
||||
Module two - cordapp
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
Here is the structure of the ``src`` directory for the ``cordapp`` module:
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
Here is the structure of the ``src`` directory for the ``cordapp`` module of the Java template:
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
@ -116,37 +128,27 @@ The ``src`` directory is structured as follows:
|
||||
|
||||
Within ``main``, we have the following directories:
|
||||
|
||||
* ``resources/META-INF/services`` contains registries of the CorDapp's serialisation whitelists and web plugins
|
||||
* ``resources/certificates`` contains dummy certificates for test purposes
|
||||
* ``resources/templateWeb`` contains a dummy front-end
|
||||
* ``java`` (or ``kotlin`` in the Kotlin template), which includes the source-code for our CorDapp
|
||||
* ``java``, which contains the source-code for our CorDapp:
|
||||
|
||||
The source-code for our CorDapp breaks down as follows:
|
||||
* ``TemplateFlow.java``, which contains a template ``FlowLogic`` subclass
|
||||
* ``TemplateState.java``, which contains a template ``ContractState`` implementation
|
||||
* ``TemplateContract.java``, which contains a template ``Contract`` implementation
|
||||
* ``TemplateSerializationWhitelist.java``, which contains a template ``SerializationWhitelist`` implementation
|
||||
* ``TemplateApi.java``, which contains a template API for the deprecated Corda webserver
|
||||
* ``TemplateWebPlugin.java``, which registers the API and front-end for the deprecated Corda webserver
|
||||
* ``TemplateClient.java``, which contains a template RPC client for interacting with our CorDapp
|
||||
|
||||
* ``TemplateFlow.java``, which contains a dummy ``FlowLogic`` subclass
|
||||
* ``TemplateState.java``, which contains a dummy ``ContractState`` implementation
|
||||
* ``TemplateContract.java``, which contains a dummy ``Contract`` implementation
|
||||
* ``TemplateSerializationWhitelist.java``, which contains a dummy ``SerializationWhitelist`` implementation
|
||||
* ``resources/META-INF/services``, which contains various registries:
|
||||
|
||||
In developing your CorDapp, you should start by modifying these classes to define the components of your CorDapp. A
|
||||
single CorDapp can define multiple flows, states, and contracts.
|
||||
* ``net.corda.core.serialization.SerializationWhitelist``, which registers the CorDapp's serialisation whitelists
|
||||
* ``net.corda.webserver.services.WebServerPluginRegistry``, which registers the CorDapp's web plugins
|
||||
|
||||
The template also includes a web API and RPC client:
|
||||
* ``resources/templateWeb``, which contains a template front-end
|
||||
|
||||
* ``TemplateApi.java``
|
||||
* ``TemplateClient.java``
|
||||
* ``TemplateWebPlugin.java``
|
||||
In a production CorDapp:
|
||||
|
||||
These are for testing purposes and would be removed in a production CorDapp.
|
||||
* We would remove the files related to the deprecated Corda webserver (``TemplateApi.java``,
|
||||
``TemplateWebPlugin.java``, ``resources/templateWeb``, and ``net.corda.webserver.services.WebServerPluginRegistry``)
|
||||
and replace them with a production-ready webserver
|
||||
|
||||
Resources
|
||||
---------
|
||||
In writing a CorDapp, these pages may be particularly helpful:
|
||||
|
||||
* :doc:`getting-set-up`, to set up your development environment.
|
||||
* The :doc:`hello-world-introduction` tutorial to write your first CorDapp.
|
||||
* :doc:`cordapp-build-systems` to build and run your CorDapp.
|
||||
* The `API docs </api/javadoc/index.html>`_ to read about the API available in developing CorDapps.
|
||||
* There is also a :doc:`cheat-sheet` recapping the key types.
|
||||
* The :doc:`flow-cookbook` to see code examples of how to perform common flow tasks.
|
||||
* `Sample CorDapps <https://www.corda.net/samples/>`_ showing various parts of Corda's functionality.
|
||||
* We would also move ``TemplateClient.java`` into a separate module so that it is not included in the CorDapp
|
@ -31,7 +31,8 @@ private fun generateCashSumCriteria(currency: Currency): QueryCriteria {
|
||||
val sumCriteria = QueryCriteria.VaultCustomQueryCriteria(sum)
|
||||
|
||||
val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(currency.currencyCode) }
|
||||
val ccyCriteria = QueryCriteria.VaultCustomQueryCriteria(ccyIndex)
|
||||
// This query should only return cash states the calling node is a participant of (meaning they can be modified/spent).
|
||||
val ccyCriteria = QueryCriteria.VaultCustomQueryCriteria(ccyIndex, isModifiable = Vault.StateModificationStatus.MODIFIABLE)
|
||||
return sumCriteria.and(ccyCriteria)
|
||||
}
|
||||
|
||||
@ -40,7 +41,8 @@ private fun generateCashSumsCriteria(): QueryCriteria {
|
||||
CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency),
|
||||
orderBy = Sort.Direction.DESC)
|
||||
}
|
||||
return QueryCriteria.VaultCustomQueryCriteria(sum)
|
||||
// This query should only return cash states the calling node is a participant of (meaning they can be modified/spent).
|
||||
return QueryCriteria.VaultCustomQueryCriteria(sum, isModifiable = Vault.StateModificationStatus.MODIFIABLE)
|
||||
}
|
||||
|
||||
private fun rowsToAmount(currency: Currency, rows: Vault.Page<FungibleAsset<*>>): Amount<Currency> {
|
||||
|
@ -43,11 +43,14 @@ class CashSelectionH2Impl : AbstractCashSelection() {
|
||||
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
|
||||
connection.createStatement().use { it.execute("CALL SET(@t, CAST(0 AS BIGINT));") }
|
||||
|
||||
// state_status = 0 -> UNCONSUMED.
|
||||
// is_modifiable = 0 -> MODIFIABLE.
|
||||
val selectJoin = """
|
||||
SELECT vs.transaction_id, vs.output_index, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id
|
||||
FROM vault_states AS vs, contract_cash_states AS ccs
|
||||
WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
|
||||
AND vs.state_status = 0
|
||||
AND vs.is_modifiable = 0
|
||||
AND ccs.ccy_code = ? and @t < ?
|
||||
AND (vs.lock_id = ? OR vs.lock_id is null)
|
||||
""" +
|
||||
|
@ -14,7 +14,9 @@ import net.corda.core.contracts.Amount
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import java.sql.Connection
|
||||
import java.sql.DatabaseMetaData
|
||||
import java.sql.ResultSet
|
||||
@ -39,6 +41,8 @@ class CashSelectionPostgreSQLImpl : AbstractCashSelection() {
|
||||
// appear in the WHERE clause, hence restricting row selection and adjusting the returned total in the outer query.
|
||||
// 3) Currently (version 9.6), FOR UPDATE cannot be specified with window functions
|
||||
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
|
||||
// state_status = 0 -> UNCONSUMED.
|
||||
// is_modifiable = 0 -> MODIFIABLE.
|
||||
val selectJoin = """SELECT nested.transaction_id, nested.output_index, nested.pennies,
|
||||
nested.total+nested.pennies as total_pennies, nested.lock_id
|
||||
FROM
|
||||
@ -48,6 +52,7 @@ class CashSelectionPostgreSQLImpl : AbstractCashSelection() {
|
||||
FROM vault_states AS vs, contract_cash_states AS ccs
|
||||
WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
|
||||
AND vs.state_status = 0
|
||||
AND vs.is_modifiable = 0
|
||||
AND ccs.ccy_code = ?
|
||||
AND (vs.lock_id = ? OR vs.lock_id is null)
|
||||
""" +
|
||||
|
@ -16,6 +16,7 @@ import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import java.sql.Connection
|
||||
import java.sql.DatabaseMetaData
|
||||
import java.sql.ResultSet
|
||||
@ -37,40 +38,77 @@ class CashSelectionSQLServerImpl : AbstractCashSelection(maxRetries = 16, retryS
|
||||
|
||||
override fun toString() = "${this::class.qualifiedName} for '$JDBC_DRIVER_NAME'"
|
||||
|
||||
// This is one MSSQL implementation of the query to select just enough cash states to meet the desired amount.
|
||||
// We select the cash states with smaller amounts first so that as the result, we minimize the numbers of
|
||||
// unspent cash states remaining in the vault.
|
||||
//
|
||||
// If there is not enough cash, the query will return an empty resultset, which should signal to the caller
|
||||
// of an exception, since the desired amount is assumed to always > 0.
|
||||
// NOTE: The other two implementations, H2 and PostgresSQL, behave differently in this case - they return
|
||||
// all in the vault instead of nothing. That seems to give the caller an extra burden to verify total returned
|
||||
// >= amount.
|
||||
// In addition, extra data fetched results in unnecessary I/O.
|
||||
// Nevertheless, if so desired, we can achieve the same by changing the last FROM clause to
|
||||
// FROM CTE LEFT JOIN Boundary AS B ON 1 = 1
|
||||
// WHERE B.seqNo IS NULL OR CTE.seqNo <= B.seqNo
|
||||
//
|
||||
// Common Table Expression and Windowed functions help make the query more readable.
|
||||
// Query plan does index scan on pennies_idx, which may be unavoidable due to the nature of the query.
|
||||
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?,
|
||||
onlyFromIssuerParties: Set<AbstractParty>,
|
||||
withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
|
||||
|
||||
val selectJoin = """
|
||||
WITH row(transaction_id, output_index, pennies, total, lock_id) AS
|
||||
val sb = StringBuilder()
|
||||
// state_status = 0 -> UNCONSUMED.
|
||||
// is_modifiable = 0 -> MODIFIABLE.
|
||||
sb.append( """
|
||||
;WITH CTE AS
|
||||
(
|
||||
SELECT vs.transaction_id, vs.output_index, ccs.pennies,
|
||||
SUM(ccs.pennies) OVER (ORDER BY ccs.transaction_id, ccs.output_index RANGE UNBOUNDED PRECEDING), vs.lock_id
|
||||
FROM contract_cash_states AS ccs, vault_states AS vs
|
||||
WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
|
||||
AND vs.state_status = 0
|
||||
AND ccs.ccy_code = ?
|
||||
AND (vs.lock_id = ? OR vs.lock_id is null)"""+
|
||||
(if (notary != null)
|
||||
" AND vs.notary_name = ?" else "") +
|
||||
// mssql-server driver does not implement setArray(), so in the following way
|
||||
// we explicitly unpack the parameters list
|
||||
(if (onlyFromIssuerParties.isNotEmpty()) {
|
||||
val repeats = generateSequence { "?" }
|
||||
.take(onlyFromIssuerParties.size)
|
||||
.joinToString (",")
|
||||
" AND ccs.issuer_key_hash IN ($repeats)"
|
||||
} else { "" }) +
|
||||
(if (withIssuerRefs.isNotEmpty()) {
|
||||
val repeats = generateSequence { "?" }
|
||||
.take(withIssuerRefs.size)
|
||||
.joinToString (",")
|
||||
" AND ccs.issuer_ref IN ($repeats)"
|
||||
} else { "" }) +
|
||||
""")
|
||||
SELECT row.transaction_id, row.output_index, row.pennies, row.total, row.lock_id
|
||||
FROM row where row.total < ? + row.pennies"""
|
||||
|
||||
SELECT
|
||||
vs.transaction_id,
|
||||
vs.output_index,
|
||||
ccs.pennies,
|
||||
vs.lock_id,
|
||||
total_pennies = SUM(ccs.pennies) OVER (ORDER BY ccs.pennies),
|
||||
seqNo = ROW_NUMBER() OVER (ORDER BY ccs.pennies)
|
||||
FROM vault_states AS vs INNER JOIN contract_cash_states AS ccs
|
||||
ON vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
|
||||
WHERE
|
||||
vs.state_status = 0
|
||||
vs.is_modifiable = 0
|
||||
AND ccs.ccy_code = ?
|
||||
AND (vs.lock_id = ? OR vs.lock_id IS NULL)
|
||||
"""
|
||||
)
|
||||
if (notary != null)
|
||||
sb.append("""
|
||||
AND vs.notary_name = ?
|
||||
""")
|
||||
if (onlyFromIssuerParties.isNotEmpty()) {
|
||||
val repeats = generateSequence { "?" }.take(onlyFromIssuerParties.size).joinToString(",")
|
||||
sb.append("""
|
||||
AND ccs.issuer_key_hash IN ($repeats)
|
||||
""")
|
||||
}
|
||||
if (withIssuerRefs.isNotEmpty()) {
|
||||
val repeats = generateSequence { "?" }.take(withIssuerRefs.size).joinToString(",")
|
||||
sb.append("""
|
||||
AND ccs.issuer_ref IN ($repeats)
|
||||
""")
|
||||
}
|
||||
sb.append(
|
||||
"""
|
||||
),
|
||||
Boundary AS
|
||||
(
|
||||
SELECT TOP (1) * FROM CTE WHERE total_pennies >= ? ORDER BY seqNo
|
||||
)
|
||||
SELECT CTE.transaction_id, CTE.output_index, CTE.pennies, CTE.total_pennies, CTE.lock_id
|
||||
FROM CTE INNER JOIN Boundary AS B ON CTE.seqNo <= B.seqNo
|
||||
;
|
||||
"""
|
||||
)
|
||||
val selectJoin = sb.toString()
|
||||
log.debug { selectJoin }
|
||||
// Use prepared statement for protection against SQL Injection
|
||||
connection.prepareStatement(selectJoin).use { statement ->
|
||||
var pIndex = 0
|
||||
|
@ -58,7 +58,7 @@ sourceSets {
|
||||
jib.container {
|
||||
mainClass = "net.corda.node.Corda"
|
||||
args = ['--log-to-console', '--no-local-shell', '--config-file=/config/node.conf']
|
||||
jvmFlags = ['-Xmx1g', "-javaagent:/app/libs/quasar-core-${quasar_version}.jar"]
|
||||
jvmFlags = ['-Xmx1g', '-javaagent:/app/libs/quasar-core-' + "${quasar_version}" + '-jdk8.jar']
|
||||
}
|
||||
|
||||
// Use manual resource copying of log4j2.xml rather than source sets.
|
||||
|
@ -0,0 +1,48 @@
|
||||
package net.corda.services.vault
|
||||
|
||||
import net.corda.core.CordaRuntimeException
|
||||
import net.corda.core.contracts.FungibleAsset
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.messaging.vaultQueryBy
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.testing.core.DUMMY_BANK_A_NAME
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.OutOfProcess
|
||||
import net.corda.testing.driver.driver
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.Test
|
||||
|
||||
class VaultRestartTest {
|
||||
|
||||
@Test
|
||||
fun `restart and query vault after adding some cash states`() {
|
||||
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = false,
|
||||
extraCordappPackagesToScan = listOf("net.corda.finance.contracts", "net.corda.finance.schemas"))) {
|
||||
val node = startNode(providedName = DUMMY_BANK_A_NAME, customOverrides = mapOf("p2pAddress" to "localhost:30000")).getOrThrow()
|
||||
|
||||
val expected = 500.DOLLARS
|
||||
val ref = OpaqueBytes.of(0x01)
|
||||
val notary = node.rpc.notaryIdentities().firstOrNull() ?: throw CordaRuntimeException("Missing notary")
|
||||
val issueTx = node.rpc.startFlow(::CashIssueFlow, expected, ref, notary).returnValue.getOrThrow()
|
||||
println("Issued transaction: $issueTx")
|
||||
|
||||
// Query vault
|
||||
Assertions.assertThat(node.rpc.vaultQueryBy<Cash.State>().states).hasSize(1)
|
||||
Assertions.assertThat(node.rpc.vaultQueryBy<FungibleAsset<*>>().states).hasSize(1)
|
||||
|
||||
// Restart the node and re-query the vault
|
||||
println("Shutting down the node ...")
|
||||
(node as OutOfProcess).process.destroyForcibly()
|
||||
node.stop()
|
||||
|
||||
println("Restarting the node ...")
|
||||
val restartedNode = startNode(providedName = DUMMY_BANK_A_NAME, customOverrides = mapOf("p2pAddress" to "localhost:30000")).getOrThrow()
|
||||
Assertions.assertThat(restartedNode.rpc.vaultQueryBy<Cash.State>().states).hasSize(1)
|
||||
Assertions.assertThat(restartedNode.rpc.vaultQueryBy<FungibleAsset<*>>().states).hasSize(1)
|
||||
}
|
||||
}
|
||||
}
|
@ -80,7 +80,8 @@ class NodeArgsParser : AbstractArgsParser<CmdLineOptions>() {
|
||||
.normalize()
|
||||
.toAbsolutePath()
|
||||
|
||||
val configFile = baseDirectory / optionSet.valueOf(configFileArg)
|
||||
val configFilePath = Paths.get(optionSet.valueOf(configFileArg))
|
||||
val configFile = if (configFilePath.isAbsolute) configFilePath else baseDirectory / configFilePath.toString()
|
||||
val loggingLevel = optionSet.valueOf(loggerLevel)
|
||||
val logToConsole = optionSet.has(logToConsoleArg)
|
||||
val isRegistration = optionSet.has(isRegistrationArg)
|
||||
|
@ -111,106 +111,108 @@ open class NodeStartup(val args: Array<String>) {
|
||||
|
||||
drawBanner(versionInfo)
|
||||
Node.printBasicNodeInfo(LOGS_CAN_BE_FOUND_IN_STRING, System.getProperty("log-path"))
|
||||
val conf = try {
|
||||
val (rawConfig, conf0Result) = loadConfigFile(cmdlineOptions)
|
||||
if (cmdlineOptions.devMode) {
|
||||
println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}")
|
||||
}
|
||||
val conf0 = conf0Result.getOrThrow()
|
||||
if (cmdlineOptions.bootstrapRaftCluster) {
|
||||
if (conf0 is NodeConfigurationImpl) {
|
||||
println("Bootstrapping raft cluster (starting up as seed node).")
|
||||
// Ignore the configured clusterAddresses to make the node bootstrap a cluster instead of joining.
|
||||
conf0.copy(notary = conf0.notary?.copy(raft = conf0.notary?.raft?.copy(clusterAddresses = emptyList())))
|
||||
} else {
|
||||
println("bootstrap-raft-notaries flag not recognized, exiting...")
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
conf0
|
||||
}
|
||||
} catch (e: UnknownConfigurationKeysException) {
|
||||
logger.error(e.message)
|
||||
return false
|
||||
} catch (e: ConfigException.IO) {
|
||||
println("""
|
||||
Unable to load the node config file from '${cmdlineOptions.configFile}'.
|
||||
|
||||
Try experimenting with the --base-directory flag to change which directory the node
|
||||
is looking in, or use the --config-file flag to specify it explicitly.
|
||||
""".trimIndent())
|
||||
return false
|
||||
} catch (e: Exception) {
|
||||
logger.error("Unexpected error whilst reading node configuration", e)
|
||||
return false
|
||||
}
|
||||
val errors = conf.validate()
|
||||
val configuration = (attempt { loadConfiguration(cmdlineOptions) }.doOnException(handleConfigurationLoadingError(cmdlineOptions.configFile)) as? Try.Success)?.let(Try.Success<NodeConfiguration>::value) ?: return false
|
||||
|
||||
val errors = configuration.validate()
|
||||
if (errors.isNotEmpty()) {
|
||||
logger.error("Invalid node configuration. Errors where:${System.lineSeparator()}${errors.joinToString(System.lineSeparator())}")
|
||||
logger.error("Invalid node configuration. Errors were:${System.lineSeparator()}${errors.joinToString(System.lineSeparator())}")
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
banJavaSerialisation(conf)
|
||||
preNetworkRegistration(conf)
|
||||
if (cmdlineOptions.nodeRegistrationOption != null) {
|
||||
// Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig]
|
||||
registerWithNetwork(conf, versionInfo, cmdlineOptions.nodeRegistrationOption)
|
||||
// At this point the node registration was successful. We can delete the marker file.
|
||||
deleteNodeRegistrationMarker(cmdlineOptions.baseDirectory)
|
||||
return true
|
||||
}
|
||||
logStartupInfo(versionInfo, cmdlineOptions, conf)
|
||||
} catch (e: NodeRegistrationException) {
|
||||
logger.warn("Node registration service is unavailable. Perhaps try to perform the initial registration again after a while.", e)
|
||||
return false
|
||||
} catch (e: Exception) {
|
||||
logger.error("Exception during node registration", e)
|
||||
return false
|
||||
attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success ?: return false
|
||||
|
||||
attempt { preNetworkRegistration(configuration) }.doOnException(handleRegistrationError) as? Try.Success ?: return false
|
||||
|
||||
cmdlineOptions.nodeRegistrationOption?.let {
|
||||
// Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig]
|
||||
attempt { registerWithNetwork(configuration, versionInfo, cmdlineOptions.nodeRegistrationOption) }.doOnException(handleRegistrationError) as? Try.Success ?: return false
|
||||
|
||||
// At this point the node registration was successful. We can delete the marker file.
|
||||
deleteNodeRegistrationMarker(cmdlineOptions.baseDirectory)
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
cmdlineOptions.baseDirectory.createDirectories()
|
||||
startNode(conf, versionInfo, startTime, cmdlineOptions)
|
||||
logStartupInfo(versionInfo, cmdlineOptions, configuration)
|
||||
|
||||
} catch (e: DatabaseMigrationException) {
|
||||
logger.error(e.message)
|
||||
return false
|
||||
} catch (e: MultipleCordappsForFlowException) {
|
||||
logger.error(e.message)
|
||||
return false
|
||||
} catch (e: CouldNotCreateDataSourceException) {
|
||||
logger.error(e.message, e.cause)
|
||||
return false
|
||||
} catch (e: CheckpointIncompatibleException) {
|
||||
logger.error(e.message)
|
||||
return false
|
||||
} catch (e: AddressBindingException) {
|
||||
logger.error(e.message)
|
||||
return false
|
||||
} catch (e: NetworkParametersReader.Error) {
|
||||
logger.error(e.message)
|
||||
return false
|
||||
} catch (e: DatabaseIncompatibleException) {
|
||||
e.message?.let { Node.printWarning(it) }
|
||||
logger.error(e.message)
|
||||
return false
|
||||
} catch (e: Exception) {
|
||||
if (e is Errors.NativeIoException && e.message?.contains("Address already in use") == true) {
|
||||
logger.error("One of the ports required by the Corda node is already in use.")
|
||||
return false
|
||||
}
|
||||
if (e.message?.startsWith("Unknown named curve:") == true) {
|
||||
logger.error("Exception during node startup - ${e.message}. " +
|
||||
"This is a known OpenJDK issue on some Linux distributions, please use OpenJDK from zulu.org or Oracle JDK.")
|
||||
} else {
|
||||
logger.error("Exception during node startup", e)
|
||||
}
|
||||
return false
|
||||
return attempt { startNode(configuration, versionInfo, startTime, cmdlineOptions) }.doOnSuccess { logger.info("Node exiting successfully") }.doOnException(handleStartError).isSuccess
|
||||
}
|
||||
|
||||
private fun <RESULT> attempt(action: () -> RESULT): Try<RESULT> = Try.on(action)
|
||||
|
||||
private fun Exception.isExpectedWhenStartingNode() = startNodeExpectedErrors.any { error -> error.isInstance(this) }
|
||||
|
||||
private val startNodeExpectedErrors = setOf(DatabaseMigrationException::class, MultipleCordappsForFlowException::class, CheckpointIncompatibleException::class, AddressBindingException::class, NetworkParametersReader::class, DatabaseIncompatibleException::class)
|
||||
|
||||
private fun Exception.logAsExpected(message: String? = this.message, print: (String?) -> Unit = logger::error) = print("$message [errorCode=${errorCode()}]")
|
||||
|
||||
private fun Exception.logAsUnexpected(message: String? = this.message, error: Exception = this, print: (String?, Throwable) -> Unit = logger::error) = print("$message [errorCode=${errorCode()}]", error)
|
||||
|
||||
private fun Exception.isOpenJdkKnownIssue() = message?.startsWith("Unknown named curve:") == true
|
||||
|
||||
private fun Exception.errorCode(): String {
|
||||
|
||||
val hash = staticLocationBasedHash()
|
||||
return Integer.toOctalString(hash)
|
||||
}
|
||||
|
||||
private fun Throwable.staticLocationBasedHash(visited: Set<Throwable> = setOf(this)): Int {
|
||||
|
||||
val cause = this.cause
|
||||
return when {
|
||||
cause != null && !visited.contains(cause) -> Objects.hash(this::class.java.name, stackTrace, cause.staticLocationBasedHash(visited + cause))
|
||||
else -> Objects.hash(this::class.java.name, stackTrace)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Node exiting successfully")
|
||||
return true
|
||||
private val handleRegistrationError = { error: Exception ->
|
||||
when (error) {
|
||||
is NodeRegistrationException -> error.logAsExpected("Node registration service is unavailable. Perhaps try to perform the initial registration again after a while.")
|
||||
else -> error.logAsUnexpected("Exception during node registration")
|
||||
}
|
||||
}
|
||||
|
||||
private val handleStartError = { error: Exception ->
|
||||
when {
|
||||
error.isExpectedWhenStartingNode() -> error.logAsExpected()
|
||||
error is CouldNotCreateDataSourceException -> error.logAsUnexpected()
|
||||
error is Errors.NativeIoException && error.message?.contains("Address already in use") == true -> error.logAsExpected("One of the ports required by the Corda node is already in use.")
|
||||
error.isOpenJdkKnownIssue() -> error.logAsExpected("Exception during node startup - ${error.message}. This is a known OpenJDK issue on some Linux distributions, please use OpenJDK from zulu.org or Oracle JDK.")
|
||||
else -> error.logAsUnexpected("Exception during node startup")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleConfigurationLoadingError(configFile: Path) = { error: Exception ->
|
||||
when (error) {
|
||||
is UnknownConfigurationKeysException -> error.logAsExpected()
|
||||
is ConfigException.IO -> error.logAsExpected(configFileNotFoundMessage(configFile), ::println)
|
||||
else -> error.logAsUnexpected("Unexpected error whilst reading node configuration")
|
||||
}
|
||||
}
|
||||
|
||||
private fun configFileNotFoundMessage(configFile: Path): String {
|
||||
return """
|
||||
Unable to load the node config file from '$configFile'.
|
||||
|
||||
Try setting the --base-directory flag to change which directory the node
|
||||
is looking in, or use the --config-file flag to specify it explicitly.
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun loadConfiguration(cmdlineOptions: CmdLineOptions): NodeConfiguration {
|
||||
|
||||
val (rawConfig, configurationResult) = loadConfigFile(cmdlineOptions)
|
||||
if (cmdlineOptions.devMode) {
|
||||
println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}")
|
||||
}
|
||||
val configuration = configurationResult.getOrThrow()
|
||||
return if (cmdlineOptions.bootstrapRaftCluster) {
|
||||
println("Bootstrapping raft cluster (starting up as seed node).")
|
||||
// Ignore the configured clusterAddresses to make the node bootstrap a cluster instead of joining.
|
||||
(configuration as NodeConfigurationImpl).copy(notary = configuration.notary?.copy(raft = configuration.notary?.raft?.copy(clusterAddresses = emptyList())))
|
||||
} else {
|
||||
configuration
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkRegistrationMode(): Boolean {
|
||||
@ -252,7 +254,7 @@ open class NodeStartup(val args: Array<String>) {
|
||||
marker.delete()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Could not delete the marker file that was created for `--initial-registration`.", e)
|
||||
e.logAsUnexpected("Could not delete the marker file that was created for `--initial-registration`.", print = logger::warn)
|
||||
}
|
||||
}
|
||||
|
||||
@ -261,6 +263,8 @@ open class NodeStartup(val args: Array<String>) {
|
||||
protected open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo)
|
||||
|
||||
protected open fun startNode(conf: NodeConfiguration, versionInfo: VersionInfo, startTime: Long, cmdlineOptions: CmdLineOptions) {
|
||||
|
||||
cmdlineOptions.baseDirectory.createDirectories()
|
||||
val node = createNode(conf, versionInfo)
|
||||
if (cmdlineOptions.clearNetworkMapCache) {
|
||||
node.clearNetworkMapCache()
|
||||
|
@ -199,7 +199,9 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
|
||||
private fun checkDbTransaction(isPresent: Boolean) {
|
||||
if (isPresent) {
|
||||
requireNotNull(contextTransactionOrNull)
|
||||
requireNotNull(contextTransactionOrNull) {
|
||||
"Transaction context is missing. This might happen if a suspendable method is not annotated with @Suspendable annotation."
|
||||
}
|
||||
} else {
|
||||
require(contextTransactionOrNull == null)
|
||||
}
|
||||
|
@ -496,6 +496,20 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
|
||||
}
|
||||
}
|
||||
|
||||
// state relevance.
|
||||
if (criteria.isModifiable != Vault.StateModificationStatus.ALL) {
|
||||
val predicateID = Pair(VaultSchemaV1.VaultStates::isModifiable.name, EqualityComparisonOperator.EQUAL)
|
||||
if (commonPredicates.containsKey(predicateID)) {
|
||||
val existingStatus = ((commonPredicates[predicateID] as ComparisonPredicate).rightHandOperand as LiteralExpression).literal
|
||||
if (existingStatus != criteria.isModifiable) {
|
||||
log.warn("Overriding previous attribute [${VaultSchemaV1.VaultStates::isModifiable.name}] value $existingStatus with ${criteria.status}")
|
||||
commonPredicates.replace(predicateID, criteriaBuilder.equal(vaultStates.get<Vault.StateModificationStatus>(VaultSchemaV1.VaultStates::isModifiable.name), criteria.isModifiable))
|
||||
}
|
||||
} else {
|
||||
commonPredicates[predicateID] = criteriaBuilder.equal(vaultStates.get<Vault.StateModificationStatus>(VaultSchemaV1.VaultStates::isModifiable.name), criteria.isModifiable)
|
||||
}
|
||||
}
|
||||
|
||||
// contract state types
|
||||
val contractStateTypes = deriveContractStateTypes(criteria.contractStateTypes)
|
||||
if (contractStateTypes.isNotEmpty()) {
|
||||
|
@ -147,16 +147,39 @@ class NodeVaultService(
|
||||
FlowStateMachineImpl.currentStateMachine()?.hasSoftLockedStates = true
|
||||
log.trace { "Reserving soft lock for flow id $uuid and state ${stateAndRef.key}" }
|
||||
}
|
||||
val state = VaultSchemaV1.VaultStates(
|
||||
val stateOnly = stateAndRef.value.state.data
|
||||
// TODO: Optimise this.
|
||||
//
|
||||
// For EVERY state to be committed to the vault, this checks whether it is spendable by the recording
|
||||
// node. The behaviour is as follows:
|
||||
//
|
||||
// 1) All vault updates marked as MODIFIABLE will, of, course all have isModifiable = true.
|
||||
// 2) For ALL_VISIBLE updates, those which are not modifiable will have isModifiable = false.
|
||||
//
|
||||
// This is useful when it comes to querying for fungible states, when we do not want non-modifiable states
|
||||
// included in the result.
|
||||
//
|
||||
// The same functionality could be obtained by passing in a list of participants to the vault query,
|
||||
// however this:
|
||||
//
|
||||
// * requires a join on the participants table which results in slow queries
|
||||
// * states may flip from being non-modifiable to modifiable
|
||||
// * it's more complicated for CorDapp developers
|
||||
//
|
||||
// Adding a new column in the "VaultStates" table was considered the best approach.
|
||||
val keys = stateOnly.participants.map { it.owningKey }
|
||||
val isModifiable = isModifiable(stateOnly, keyManagementService.filterMyKeys(keys).toSet())
|
||||
val stateToAdd = VaultSchemaV1.VaultStates(
|
||||
notary = stateAndRef.value.state.notary,
|
||||
contractStateClassName = stateAndRef.value.state.data.javaClass.name,
|
||||
stateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
recordedTime = now,
|
||||
isModifiable = if (isModifiable) Vault.StateModificationStatus.MODIFIABLE else Vault.StateModificationStatus.NOT_MODIFIABLE,
|
||||
lockId = uuid,
|
||||
lockUpdateTime = if (uuid == null) null else now
|
||||
)
|
||||
state.stateRef = PersistentStateRef(stateAndRef.key)
|
||||
session.save(state)
|
||||
stateToAdd.stateRef = PersistentStateRef(stateAndRef.key)
|
||||
session.save(stateToAdd)
|
||||
}
|
||||
if (consumedStateRefs.isNotEmpty()) {
|
||||
// We have to do this so that the session does not hold onto the prior version of the states status. i.e.
|
||||
@ -211,7 +234,7 @@ class NodeVaultService(
|
||||
val ourNewStates = when (statesToRecord) {
|
||||
StatesToRecord.NONE -> throw AssertionError("Should not reach here")
|
||||
StatesToRecord.ONLY_RELEVANT -> tx.outputs.withIndex().filter {
|
||||
isRelevant(it.value.data, keyManagementService.filterMyKeys(tx.outputs.flatMap { it.data.participants.map { it.owningKey } }).toSet())
|
||||
isModifiable(it.value.data, keyManagementService.filterMyKeys(tx.outputs.flatMap { it.data.participants.map { it.owningKey } }).toSet())
|
||||
}
|
||||
StatesToRecord.ALL_VISIBLE -> tx.outputs.withIndex()
|
||||
}.map {
|
||||
@ -244,7 +267,7 @@ class NodeVaultService(
|
||||
val myKeys by lazy { keyManagementService.filterMyKeys(ltx.outputs.flatMap { it.data.participants.map { it.owningKey } }) }
|
||||
val (consumedStateAndRefs, producedStates) = ltx.inputs.zip(ltx.outputs).filter { (_, output) ->
|
||||
if (statesToRecord == StatesToRecord.ONLY_RELEVANT) {
|
||||
isRelevant(output.data, myKeys.toSet())
|
||||
isModifiable(output.data, myKeys.toSet())
|
||||
} else {
|
||||
true
|
||||
}
|
||||
@ -444,12 +467,15 @@ class NodeVaultService(
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
// Enrich QueryCriteria with additional default attributes (such as soft locks)
|
||||
// Enrich QueryCriteria with additional default attributes (such as soft locks).
|
||||
// We only want to return MODIFIABLE states here.
|
||||
val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF)
|
||||
val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))
|
||||
val enrichedCriteria = QueryCriteria.VaultQueryCriteria(
|
||||
contractStateTypes = setOf(contractStateType),
|
||||
softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)))
|
||||
softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)),
|
||||
isModifiable = Vault.StateModificationStatus.MODIFIABLE
|
||||
)
|
||||
val results = queryBy(contractStateType, enrichedCriteria.and(eligibleStatesQuery), sorter)
|
||||
|
||||
var claimedAmount = 0L
|
||||
@ -472,9 +498,11 @@ class NodeVaultService(
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun isRelevant(state: ContractState, myKeys: Set<PublicKey>): Boolean {
|
||||
internal fun isModifiable(state: ContractState, myKeys: Set<PublicKey>): Boolean {
|
||||
val keysToCheck = when (state) {
|
||||
is OwnableState -> listOf(state.owner.owningKey)
|
||||
// Sometimes developers forget to add the owning key to participants for OwnableStates.
|
||||
// TODO: This logic should probably be moved to OwnableState so we can just do a simple intersection here.
|
||||
is OwnableState -> (state.participants.map { it.owningKey } + state.owner.owningKey).toSet()
|
||||
else -> state.participants.map { it.owningKey }
|
||||
}
|
||||
return keysToCheck.any { it in myKeys }
|
||||
@ -553,7 +581,8 @@ class NodeVaultService(
|
||||
vaultState.stateStatus,
|
||||
vaultState.notary,
|
||||
vaultState.lockId,
|
||||
vaultState.lockUpdateTime))
|
||||
vaultState.lockUpdateTime,
|
||||
vaultState.isModifiable))
|
||||
} else {
|
||||
// TODO: improve typing of returned other results
|
||||
log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" }
|
||||
@ -610,7 +639,7 @@ class NodeVaultService(
|
||||
val contractTypes = deriveContractTypes(it)
|
||||
contractTypes.map {
|
||||
val contractStateType = contractStateTypeMappings.getOrPut(it.name) { mutableSetOf() }
|
||||
contractStateType.add(it.name)
|
||||
contractStateType.add(concreteType.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,6 +70,10 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio
|
||||
@Column(name = "lock_id", nullable = true)
|
||||
var lockId: String? = null,
|
||||
|
||||
/** Used to determine whether a state is modifiable by the recording node */
|
||||
@Column(name = "is_modifiable", nullable = false)
|
||||
var isModifiable: Vault.StateModificationStatus,
|
||||
|
||||
/** refers to the last time a lock was taken (reserved) or updated (released, re-reserved) */
|
||||
@Column(name = "lock_timestamp", nullable = true)
|
||||
var lockUpdateTime: Instant? = null
|
||||
|
@ -90,13 +90,6 @@ class NodeArgsParserTest {
|
||||
assertThat(cmdLineOptions.configFile).isEqualTo(configFile)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `both base-directory and config-file`() {
|
||||
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
|
||||
parser.parse("--base-directory", "base", "--config-file", "conf")
|
||||
}.withMessageContaining("base-directory").withMessageContaining("config-file")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `base-directory without argument`() {
|
||||
assertThatExceptionOfType(OptionException::class.java).isThrownBy {
|
||||
|
@ -38,6 +38,8 @@ import net.corda.finance.utils.sumCash
|
||||
import net.corda.node.services.api.IdentityServiceInternal
|
||||
import net.corda.node.services.api.WritableTransactionStorage
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.contracts.DummyState
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.internal.LogHelper
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
@ -61,7 +63,7 @@ import kotlin.test.assertTrue
|
||||
|
||||
class NodeVaultServiceTest {
|
||||
private companion object {
|
||||
val cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName)
|
||||
val cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName, "net.corda.testing.contracts")
|
||||
val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10)
|
||||
val DUMMY_CASH_ISSUER = dummyCashIssuer.ref(1)
|
||||
val bankOfCorda = TestIdentity(BOC_NAME)
|
||||
@ -536,17 +538,17 @@ class NodeVaultServiceTest {
|
||||
val amount = Amount(1000, Issued(BOC.ref(1), GBP))
|
||||
val wellKnownCash = Cash.State(amount, identity.party)
|
||||
val myKeys = services.keyManagementService.filterMyKeys(listOf(wellKnownCash.owner.owningKey))
|
||||
assertTrue { service.isRelevant(wellKnownCash, myKeys.toSet()) }
|
||||
assertTrue { service.isModifiable(wellKnownCash, myKeys.toSet()) }
|
||||
|
||||
val anonymousIdentity = services.keyManagementService.freshKeyAndCert(identity, false)
|
||||
val anonymousCash = Cash.State(amount, anonymousIdentity.party)
|
||||
val anonymousKeys = services.keyManagementService.filterMyKeys(listOf(anonymousCash.owner.owningKey))
|
||||
assertTrue { service.isRelevant(anonymousCash, anonymousKeys.toSet()) }
|
||||
assertTrue { service.isModifiable(anonymousCash, anonymousKeys.toSet()) }
|
||||
|
||||
val thirdPartyIdentity = AnonymousParty(generateKeyPair().public)
|
||||
val thirdPartyCash = Cash.State(amount, thirdPartyIdentity)
|
||||
val thirdPartyKeys = services.keyManagementService.filterMyKeys(listOf(thirdPartyCash.owner.owningKey))
|
||||
assertFalse { service.isRelevant(thirdPartyCash, thirdPartyKeys.toSet()) }
|
||||
assertFalse { service.isModifiable(thirdPartyCash, thirdPartyKeys.toSet()) }
|
||||
}
|
||||
|
||||
// TODO: Unit test linear state relevancy checks
|
||||
@ -737,4 +739,43 @@ class NodeVaultServiceTest {
|
||||
}
|
||||
assertThat(recordedStates).isEqualTo(coins.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test state relevance criteria`() {
|
||||
fun createTx(number: Int, vararg participants: Party): SignedTransaction {
|
||||
return services.signInitialTransaction(TransactionBuilder(DUMMY_NOTARY).apply {
|
||||
addOutputState(DummyState(number, participants.toList()), DummyContract.PROGRAM_ID)
|
||||
addCommand(DummyCommandData, listOf(megaCorp.publicKey))
|
||||
})
|
||||
}
|
||||
|
||||
fun List<StateAndRef<DummyState>>.getNumbers() = map { it.state.data.magicNumber }.toSet()
|
||||
|
||||
services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx(1, megaCorp.party)))
|
||||
services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx(2, miniCorp.party)))
|
||||
services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx(3, miniCorp.party, megaCorp.party)))
|
||||
services.recordTransactions(StatesToRecord.ALL_VISIBLE, listOf(createTx(4, miniCorp.party)))
|
||||
services.recordTransactions(StatesToRecord.ALL_VISIBLE, listOf(createTx(5, bankOfCorda.party)))
|
||||
services.recordTransactions(StatesToRecord.ALL_VISIBLE, listOf(createTx(6, megaCorp.party, bankOfCorda.party)))
|
||||
services.recordTransactions(StatesToRecord.NONE, listOf(createTx(7, bankOfCorda.party)))
|
||||
|
||||
// Test one.
|
||||
// StateModificationStatus is MODIFIABLE by default. This should return two states.
|
||||
val resultOne = vaultService.queryBy<DummyState>().states.getNumbers()
|
||||
assertEquals(setOf(1, 3, 4, 5, 6), resultOne)
|
||||
|
||||
// Test two.
|
||||
// StateModificationStatus set to NOT_MODIFIABLE.
|
||||
val criteriaTwo = VaultQueryCriteria(isModifiable = Vault.StateModificationStatus.NOT_MODIFIABLE)
|
||||
val resultTwo = vaultService.queryBy<DummyState>(criteriaTwo).states.getNumbers()
|
||||
assertEquals(setOf(4, 5), resultTwo)
|
||||
|
||||
// Test three.
|
||||
// StateModificationStatus set to ALL.
|
||||
val criteriaThree = VaultQueryCriteria(isModifiable = Vault.StateModificationStatus.MODIFIABLE)
|
||||
val resultThree = vaultService.queryBy<DummyState>(criteriaThree).states.getNumbers()
|
||||
assertEquals(setOf(1, 3, 6), resultThree)
|
||||
|
||||
// We should never see 2 or 7.
|
||||
}
|
||||
}
|
||||
|
@ -41,12 +41,13 @@ include 'experimental:corda-utils'
|
||||
include 'experimental:rpc-worker'
|
||||
include 'jdk8u-deterministic'
|
||||
include 'test-common'
|
||||
include 'test-cli'
|
||||
include 'test-utils'
|
||||
include 'smoke-test-utils'
|
||||
include 'node-driver'
|
||||
include 'perftestcordapp'
|
||||
// Avoid making 'testing' a project, and allow build.gradle files to refer to these by their simple names:
|
||||
['test-common', 'test-utils', 'smoke-test-utils', 'node-driver'].each {
|
||||
['test-common', 'test-utils', 'test-cli', 'smoke-test-utils', 'node-driver'].each {
|
||||
project(":$it").projectDir = new File("$settingsDir/testing/$it")
|
||||
}
|
||||
include 'testing:qa:behave:tools:rpc-proxy'
|
||||
|
@ -81,7 +81,7 @@ data class MockNodeParameters constructor(
|
||||
* Immutable builder for configuring a [MockNetwork]. Kotlin users can also use named parameters to the constructor
|
||||
* of [MockNetwork], which is more convenient.
|
||||
*
|
||||
* @property networkSendManuallyPumped If true then messages will not be routed from sender to receiver until you use
|
||||
* @property networkSendManuallyPumped If false then messages will not be routed from sender to receiver until you use
|
||||
* the [MockNetwork.runNetwork] method. This is useful for writing single-threaded unit test code that can examine the
|
||||
* state of the mock network before and after a message is sent, without races and without the receiving node immediately
|
||||
* sending a response. The default is false, so you must call runNetwork.
|
||||
@ -290,7 +290,7 @@ inline fun <reified F : FlowLogic<*>> StartedMockNode.registerResponderFlow(
|
||||
* @property cordappPackages A [List] of cordapp packages to scan for any cordapp code, e.g. contract verification code, flows and services.
|
||||
* @property defaultParameters A [MockNetworkParameters] object which contains the same parameters as the constructor, provided
|
||||
* as a convenience for Java users.
|
||||
* @property networkSendManuallyPumped If true then messages will not be routed from sender to receiver until you use
|
||||
* @property networkSendManuallyPumped If false then messages will not be routed from sender to receiver until you use
|
||||
* the [MockNetwork.runNetwork] method. This is useful for writing single-threaded unit test code that can examine the
|
||||
* state of the mock network before and after a message is sent, without races and without the receiving node immediately
|
||||
* sending a response. The default is false, so you must call runNetwork.
|
||||
|
@ -517,7 +517,8 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun runNetwork(rounds: Int = -1) {
|
||||
check(!networkSendManuallyPumped)
|
||||
check(!networkSendManuallyPumped) { "MockNetwork.runNetwork() should only be used when networkSendManuallyPumped == false. " +
|
||||
"You can use MockNetwork.waitQuiescent() to wait for all the nodes to process all the messages on their queues instead." }
|
||||
fun pumpAll() = messagingNetwork.endpoints.map { it.pumpReceive(false) }
|
||||
|
||||
if (rounds == -1) {
|
||||
|
17
testing/test-cli/build.gradle
Normal file
17
testing/test-cli/build.gradle
Normal file
@ -0,0 +1,17 @@
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'kotlin'
|
||||
|
||||
dependencies {
|
||||
compile group: 'info.picocli', name: 'picocli', version: '3.0.1'
|
||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
compile group: "com.fasterxml.jackson.dataformat", name: "jackson-dataformat-yaml", version: "2.9.0"
|
||||
compile group: "com.fasterxml.jackson.core", name: "jackson-databind", version: "2.9.0"
|
||||
compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.+"
|
||||
compile "junit:junit:$junit_version"
|
||||
|
||||
}
|
||||
compileKotlin {
|
||||
kotlinOptions {
|
||||
languageVersion = "1.2"
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package net.corda.testing
|
||||
|
||||
import junit.framework.AssertionFailedError
|
||||
|
||||
open class CliBackwardsCompatibleTest {
|
||||
|
||||
|
||||
fun checkBackwardsCompatibility(clazz: Class<*>) {
|
||||
val checker = CommandLineCompatibilityChecker()
|
||||
val checkResults = checker.checkCommandLineIsBackwardsCompatible(clazz)
|
||||
|
||||
if (checkResults.isNotEmpty()) {
|
||||
val exceptionMessage= checkResults.map { it.message }.joinToString(separator = "\n")
|
||||
throw AssertionFailedError("Command line is not backwards compatible:\n$exceptionMessage")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,188 @@
|
||||
package net.corda.testing
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
|
||||
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
|
||||
import picocli.CommandLine
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
|
||||
class CommandLineCompatibilityChecker {
|
||||
|
||||
fun topoSort(commandLine: CommandLine): List<CommandDescription> {
|
||||
val toVisit = Stack<CommandLine>()
|
||||
toVisit.push(commandLine)
|
||||
val sorted: MutableList<CommandLine> = ArrayList();
|
||||
while (toVisit.isNotEmpty()) {
|
||||
val visiting = toVisit.pop()
|
||||
sorted.add(visiting)
|
||||
visiting.subcommands.values.sortedBy { it.commandName }.forEach {
|
||||
toVisit.push(it)
|
||||
}
|
||||
}
|
||||
return buildDescriptors(sorted)
|
||||
}
|
||||
|
||||
private fun buildDescriptors(result: MutableList<CommandLine>): List<CommandDescription> {
|
||||
return result.map { ::parseToDescription.invoke(it) }
|
||||
}
|
||||
|
||||
internal fun parseToDescription(it: CommandLine): CommandDescription {
|
||||
val commandSpec = it.commandSpec
|
||||
val options = commandSpec.options().filterNot { it.usageHelp() || it.versionHelp() }
|
||||
.map { hit -> hit.names().map { it to hit } }
|
||||
.flatMap { it }
|
||||
.sortedBy { it.first }
|
||||
.map {
|
||||
val type = it.second.type()
|
||||
ParameterDescription(it.first, type.componentType?.canonicalName
|
||||
?: type.canonicalName, it.second.required(), isMultiple(type), determineAcceptableOptions(type))
|
||||
}
|
||||
|
||||
val positionals = commandSpec.positionalParameters().sortedBy { it.index() }.map {
|
||||
val type = it.type()
|
||||
ParameterDescription(it.index().toString(), type.componentType?.canonicalName
|
||||
?: type.canonicalName, it.required(), isMultiple(type))
|
||||
}
|
||||
return CommandDescription(it.commandName, positionals, options)
|
||||
}
|
||||
|
||||
private fun determineAcceptableOptions(type: Class<*>?): List<String> {
|
||||
return if (type?.isEnum == true) {
|
||||
type.enumConstants.map { it.toString() }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun isMultiple(clazz: Class<*>): Boolean {
|
||||
return Iterable::class.java.isAssignableFrom(clazz) || Array<Any>::class.java.isAssignableFrom(clazz)
|
||||
}
|
||||
|
||||
fun printCommandDescription(commandLine: CommandLine) {
|
||||
val objectMapper = ObjectMapper(YAMLFactory()).registerKotlinModule()
|
||||
val results = topoSort(commandLine)
|
||||
println(objectMapper.writeValueAsString(results))
|
||||
}
|
||||
|
||||
fun readCommandDescription(inputStream: InputStream): List<CommandDescription> {
|
||||
val objectMapper = ObjectMapper(YAMLFactory()).registerKotlinModule()
|
||||
return objectMapper.readValue<List<CommandDescription>>(inputStream, object : TypeReference<List<CommandDescription>>() {});
|
||||
}
|
||||
|
||||
fun checkAllCommandsArePresent(old: List<CommandDescription>, new: List<CommandDescription>): List<CliBackwardsCompatibilityValidationCheck> {
|
||||
val oldSet = old.map { it.commandName }.toSet()
|
||||
val newSet = new.map { it.commandName }.toSet()
|
||||
val newIsSuperSetOfOld = newSet.containsAll(oldSet)
|
||||
return if (!newIsSuperSetOfOld) {
|
||||
oldSet.filterNot { newSet.contains(it) }.map {
|
||||
CommandsChangedError("SubCommand: $it has been removed from the CLI")
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun checkAllOptionsArePresent(old: CommandDescription, new: CommandDescription): List<CliBackwardsCompatibilityValidationCheck> {
|
||||
if (old.commandName != new.commandName) {
|
||||
throw IllegalArgumentException("Commands must match (${old.commandName} != ${new.commandName})")
|
||||
}
|
||||
val oldSet = old.params.map { it.parameterName }.toSet()
|
||||
val newSet = new.params.map { it.parameterName }.toSet()
|
||||
|
||||
val newIsSuperSetOfOld = newSet.containsAll(oldSet)
|
||||
|
||||
return if (!newIsSuperSetOfOld) {
|
||||
oldSet.filterNot { newSet.contains(it) }.map {
|
||||
OptionsChangedError("Parameter: $it has been removed from subcommand: ${old.commandName}")
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun checkAllPositionalCharactersArePresent(old: CommandDescription, new: CommandDescription): List<CliBackwardsCompatibilityValidationCheck> {
|
||||
if (old.commandName != new.commandName) {
|
||||
throw IllegalArgumentException("Commands must match (${old.commandName} != ${new.commandName})")
|
||||
}
|
||||
val oldSet = old.positionalParams.sortedBy { it.parameterName }.toSet()
|
||||
val newSet = new.positionalParams.sortedBy { it.parameterName}.toSet()
|
||||
val newIsSuperSetOfOld = newSet.containsAll(oldSet)
|
||||
return if (!newIsSuperSetOfOld) {
|
||||
oldSet.filterNot { newSet.contains(it) }.map {
|
||||
PositionalArgumentsChangedError("Positional Parameter [ ${it.parameterName} ] has been removed from subcommand: ${old.commandName}")
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun checkAllParamsAreOfTheSameType(old: CommandDescription, new: CommandDescription): List<CliBackwardsCompatibilityValidationCheck> {
|
||||
|
||||
val oldMap = old.params.map { it.parameterName to it.parameterType }.toMap()
|
||||
val newMap = new.params.map { it.parameterName to it.parameterType }.toMap()
|
||||
|
||||
val changedTypes = oldMap.filter { newMap[it.key] != null && newMap[it.key] != it.value }.map {
|
||||
TypesChangedError("Parameter [ ${it.key} has changed from type: ${it.value} to ${newMap[it.key]}")
|
||||
}
|
||||
val oldAcceptableTypes = old.params.map { it.parameterName to it.acceptableValues }.toMap()
|
||||
val newAcceptableTypes = new.params.map { it.parameterName to it.acceptableValues }.toMap()
|
||||
val potentiallyChanged = oldAcceptableTypes.filter { newAcceptableTypes[it.key] != null && newAcceptableTypes[it.key]!!.toSet() != it.value.toSet() }
|
||||
val missingEnumErrors = potentiallyChanged.map {
|
||||
val oldEnums = it.value
|
||||
val newEnums = newAcceptableTypes[it.key]!!
|
||||
if (!newEnums.containsAll(oldEnums)) {
|
||||
val toPrint = oldEnums.toMutableSet()
|
||||
toPrint.removeAll(newAcceptableTypes[it.key]!!)
|
||||
EnumOptionsChangedError(it.key + " on command ${old.commandName} previously accepted: $oldEnums, and now is missing $toPrint}")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.filterNotNull()
|
||||
return changedTypes + missingEnumErrors
|
||||
|
||||
}
|
||||
|
||||
fun checkCommandLineIsBackwardsCompatible(commandLineToCheck: Class<*>): List<CliBackwardsCompatibilityValidationCheck> {
|
||||
val commandLineToCheckName = commandLineToCheck.canonicalName
|
||||
val instance = commandLineToCheck.newInstance()
|
||||
val resourceAsStream = this.javaClass.classLoader.getResourceAsStream("$commandLineToCheckName.yml")
|
||||
?: throw IllegalStateException("no Descriptor for $commandLineToCheckName found on classpath")
|
||||
val old = readCommandDescription(resourceAsStream)
|
||||
val new = topoSort(CommandLine(instance))
|
||||
return checkCommandLineIsBackwardsCompatible(old, new)
|
||||
}
|
||||
|
||||
|
||||
fun checkBackwardsCompatibility(old: CommandLine, new: CommandLine): List<CliBackwardsCompatibilityValidationCheck> {
|
||||
val topoSortOld= topoSort(old)
|
||||
val topoSortNew= topoSort(new)
|
||||
return checkCommandLineIsBackwardsCompatible(topoSortOld, topoSortNew)
|
||||
}
|
||||
|
||||
private fun checkCommandLineIsBackwardsCompatible(old: List<CommandDescription>, new: List<CommandDescription>): List<CliBackwardsCompatibilityValidationCheck> {
|
||||
val results = ArrayList<CliBackwardsCompatibilityValidationCheck>()
|
||||
results += checkAllCommandsArePresent(old, new)
|
||||
for (oldCommand in old) {
|
||||
new.find { it.commandName == oldCommand.commandName }?.let { newCommand ->
|
||||
results += checkAllOptionsArePresent(oldCommand, newCommand)
|
||||
results += checkAllParamsAreOfTheSameType(oldCommand, newCommand)
|
||||
results += checkAllPositionalCharactersArePresent(oldCommand, newCommand)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
open class CliBackwardsCompatibilityValidationCheck(val message: String)
|
||||
class OptionsChangedError(error: String) : CliBackwardsCompatibilityValidationCheck(error)
|
||||
class TypesChangedError(error: String) : CliBackwardsCompatibilityValidationCheck(error)
|
||||
class EnumOptionsChangedError(error: String) : CliBackwardsCompatibilityValidationCheck(error)
|
||||
class CommandsChangedError(error: String) : CliBackwardsCompatibilityValidationCheck(error)
|
||||
class PositionalArgumentsChangedError(error: String) : CliBackwardsCompatibilityValidationCheck(error)
|
||||
data class CommandDescription(val commandName: String, val positionalParams: List<ParameterDescription>, val params: List<ParameterDescription>)
|
||||
data class ParameterDescription(val parameterName: String, val parameterType: String, val required: Boolean, val multiParam: Boolean, val acceptableValues: List<String> = emptyList())
|
@ -0,0 +1,106 @@
|
||||
package net.corda.testing
|
||||
|
||||
import org.hamcrest.CoreMatchers.*
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import picocli.CommandLine
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class CommandLineCompatibilityCheckerTest {
|
||||
|
||||
enum class AllOptions {
|
||||
YES, NO, MAYBZ
|
||||
}
|
||||
|
||||
enum class BinaryOptions {
|
||||
YES, NO
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `should detect missing parameter`() {
|
||||
val value1 = object {
|
||||
@CommandLine.Option(names = arrayOf("-d", "--directory"), description = arrayOf("the directory to run in"))
|
||||
var baseDirectory: String? = null
|
||||
}
|
||||
val value2 = object {
|
||||
@CommandLine.Option(names = arrayOf("--directory"), description = arrayOf("the directory to run in"))
|
||||
var baseDirectory: String? = null
|
||||
}
|
||||
val breaks = CommandLineCompatibilityChecker().checkBackwardsCompatibility(CommandLine(value1), CommandLine(value2))
|
||||
Assert.assertThat(breaks.size, `is`(1))
|
||||
Assert.assertThat(breaks.first(), `is`(instanceOf(OptionsChangedError::class.java)))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `should detect changes in positional parameters`() {
|
||||
val value1 = object {
|
||||
@CommandLine.Parameters(index = "0")
|
||||
var baseDirectory: String? = null
|
||||
@CommandLine.Parameters(index = "1")
|
||||
var depth: Pattern? = null
|
||||
}
|
||||
val value2 = object {
|
||||
@CommandLine.Parameters(index = "1")
|
||||
var baseDirectory: String? = null
|
||||
@CommandLine.Parameters(index = "0")
|
||||
var depth: Int? = null
|
||||
}
|
||||
val breaks = CommandLineCompatibilityChecker().checkBackwardsCompatibility(CommandLine(value1), CommandLine(value2))
|
||||
Assert.assertThat(breaks.size, `is`(2))
|
||||
Assert.assertThat(breaks.first(), `is`(instanceOf(PositionalArgumentsChangedError::class.java)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should detect removal of a subcommand`() {
|
||||
@CommandLine.Command(subcommands = [ListCommand::class, StatusCommand::class])
|
||||
class Dummy
|
||||
|
||||
@CommandLine.Command(subcommands = [ListCommand::class])
|
||||
class Dummy2
|
||||
|
||||
val breaks = CommandLineCompatibilityChecker().checkBackwardsCompatibility(CommandLine(Dummy()), CommandLine(Dummy2()))
|
||||
Assert.assertThat(breaks.size, `is`(1))
|
||||
Assert.assertThat(breaks.first(), `is`(instanceOf(CommandsChangedError::class.java)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should detect change of parameter type`() {
|
||||
val value1 = object {
|
||||
@CommandLine.Option(names = ["--directory"], description = ["the directory to run in"])
|
||||
var baseDirectory: String? = null
|
||||
}
|
||||
val value2 = object {
|
||||
@CommandLine.Option(names = ["--directory"], description = ["the directory to run in"])
|
||||
var baseDirectory: Pattern? = null
|
||||
}
|
||||
|
||||
val breaks = CommandLineCompatibilityChecker().checkBackwardsCompatibility(CommandLine(value1), CommandLine(value2))
|
||||
Assert.assertThat(breaks.size, `is`(1))
|
||||
Assert.assertThat(breaks.first(), `is`(instanceOf(TypesChangedError::class.java)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should detect change of enum options`() {
|
||||
val value1 = object {
|
||||
@CommandLine.Option(names = ["--directory"], description = ["the directory to run in"])
|
||||
var baseDirectory: AllOptions? = null
|
||||
}
|
||||
val value2 = object {
|
||||
@CommandLine.Option(names = ["--directory"], description = ["the directory to run in"])
|
||||
var baseDirectory: BinaryOptions? = null
|
||||
}
|
||||
|
||||
val breaks = CommandLineCompatibilityChecker().checkBackwardsCompatibility(CommandLine(value1), CommandLine(value2))
|
||||
Assert.assertThat(breaks.filter { it is EnumOptionsChangedError }.size, `is`(1))
|
||||
Assert.assertThat(breaks.first { it is EnumOptionsChangedError }.message, containsString(AllOptions.MAYBZ.name))
|
||||
}
|
||||
|
||||
@CommandLine.Command(name = "status")
|
||||
class StatusCommand
|
||||
|
||||
@CommandLine.Command(name = "ls")
|
||||
class ListCommand
|
||||
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
- commandName: "<main class>"
|
||||
positionalParams:
|
||||
- parameterName: "0"
|
||||
parameterType: "java.net.InetAddress"
|
||||
required: true
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "1"
|
||||
parameterType: "int"
|
||||
required: true
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
params:
|
||||
- parameterName: "--directory"
|
||||
parameterType: "java.lang.String"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "-d"
|
||||
parameterType: "java.lang.String"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- commandName: "status"
|
||||
positionalParams: []
|
||||
params:
|
||||
- parameterName: "--pattern"
|
||||
parameterType: "java.lang.String"
|
||||
required: false
|
||||
multiParam: true
|
||||
acceptableValues: []
|
||||
- parameterName: "--style"
|
||||
parameterType: "net.corda.testing.DummyEnum"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues:
|
||||
- "FULL"
|
||||
- "DIR"
|
||||
- "FILE"
|
||||
- "DISK"
|
||||
- parameterName: "-p"
|
||||
parameterType: "java.lang.String"
|
||||
required: false
|
||||
multiParam: true
|
||||
acceptableValues: []
|
||||
- parameterName: "-s"
|
||||
parameterType: "net.corda.testing.DummyEnum"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues:
|
||||
- "FULL"
|
||||
- "DIR"
|
||||
- "FILE"
|
||||
- "DISK"
|
||||
- commandName: "ls"
|
||||
positionalParams:
|
||||
- parameterName: "0"
|
||||
parameterType: "java.lang.String"
|
||||
required: true
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "1"
|
||||
parameterType: "int"
|
||||
required: true
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
params:
|
||||
- parameterName: "--depth"
|
||||
parameterType: "java.lang.Integer"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "-d"
|
||||
parameterType: "java.lang.Integer"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
@ -332,16 +332,16 @@ object InteractiveShell {
|
||||
for (ctor in clazz.constructors) {
|
||||
var paramNamesFromConstructor: List<String>? = null
|
||||
fun getPrototype(): List<String> {
|
||||
val argTypes = ctor.parameterTypes.map { it.simpleName }
|
||||
val argTypes = ctor.genericParameterTypes.map { it.typeName }
|
||||
return paramNamesFromConstructor!!.zip(argTypes).map { (name, type) -> "$name: $type" }
|
||||
}
|
||||
|
||||
try {
|
||||
// Attempt construction with the given arguments.
|
||||
paramNamesFromConstructor = parser.paramNamesFromConstructor(ctor)
|
||||
val args = parser.parseArguments(clazz.name, paramNamesFromConstructor.zip(ctor.parameterTypes), inputData)
|
||||
if (args.size != ctor.parameterTypes.size) {
|
||||
errors.add("${getPrototype()}: Wrong number of arguments (${args.size} provided, ${ctor.parameterTypes.size} needed)")
|
||||
val args = parser.parseArguments(clazz.name, paramNamesFromConstructor.zip(ctor.genericParameterTypes), inputData)
|
||||
if (args.size != ctor.genericParameterTypes.size) {
|
||||
errors.add("${getPrototype()}: Wrong number of arguments (${args.size} provided, ${ctor.genericParameterTypes.size} needed)")
|
||||
continue
|
||||
}
|
||||
val flow = ctor.newInstance(*args) as FlowLogic<*>
|
||||
@ -355,10 +355,10 @@ object InteractiveShell {
|
||||
} catch (e: StringToMethodCallParser.UnparseableCallException.TooManyParameters) {
|
||||
errors.add("${getPrototype()}: too many parameters")
|
||||
} catch (e: StringToMethodCallParser.UnparseableCallException.ReflectionDataMissing) {
|
||||
val argTypes = ctor.parameterTypes.map { it.simpleName }
|
||||
val argTypes = ctor.genericParameterTypes.map { it.typeName }
|
||||
errors.add("$argTypes: <constructor missing parameter reflection data>")
|
||||
} catch (e: StringToMethodCallParser.UnparseableCallException) {
|
||||
val argTypes = ctor.parameterTypes.map { it.simpleName }
|
||||
val argTypes = ctor.genericParameterTypes.map { it.typeName }
|
||||
errors.add("$argTypes: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
package net.corda.tools.shell;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import com.google.common.collect.Lists;
|
||||
import kotlin.Pair;
|
||||
import net.corda.client.jackson.JacksonSupport;
|
||||
import net.corda.client.jackson.internal.ToStringSerialize;
|
||||
import net.corda.core.contracts.Amount;
|
||||
import net.corda.core.crypto.SecureHash;
|
||||
import net.corda.core.flows.FlowException;
|
||||
@ -26,18 +28,20 @@ import rx.Observable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class InteractiveShellJavaTest {
|
||||
private static TestIdentity megaCorp = new TestIdentity(new CordaX500Name("MegaCorp", "London", "GB"));
|
||||
|
||||
// should guarantee that FlowA will have synthetic method to access this field
|
||||
private static String synthetic = "synth";
|
||||
private static final String synthetic = "synth";
|
||||
|
||||
abstract static class StringFlow extends FlowLogic<String> {
|
||||
abstract String getA();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static class FlowA extends StringFlow {
|
||||
|
||||
private String a;
|
||||
@ -68,6 +72,18 @@ public class InteractiveShellJavaTest {
|
||||
this(party.getName().toString());
|
||||
}
|
||||
|
||||
public FlowA(Integer b, Amount<UserValue> amount) {
|
||||
this(String.format("%d %s", amount.getQuantity() + (b == null ? 0 : b), amount.getToken()));
|
||||
}
|
||||
|
||||
public FlowA(String[] b) {
|
||||
this(String.join("+", b));
|
||||
}
|
||||
|
||||
public FlowA(Amount<UserValue>[] amounts) {
|
||||
this(String.join("++", Arrays.stream(amounts).map(Amount::toString).collect(toList())));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ProgressTracker getProgressTracker() {
|
||||
@ -75,7 +91,7 @@ public class InteractiveShellJavaTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String call() throws FlowException {
|
||||
public String call() {
|
||||
return a;
|
||||
}
|
||||
|
||||
@ -106,9 +122,7 @@ public class InteractiveShellJavaTest {
|
||||
FlowSession session = initiateFlow(party);
|
||||
|
||||
|
||||
Integer integer = session.receive(Integer.class).unwrap((i) -> {
|
||||
return i;
|
||||
});
|
||||
Integer integer = session.receive(Integer.class).unwrap((i) -> i);
|
||||
|
||||
return integer.toString();
|
||||
|
||||
@ -120,6 +134,24 @@ public class InteractiveShellJavaTest {
|
||||
}
|
||||
}
|
||||
|
||||
@ToStringSerialize
|
||||
public static class UserValue {
|
||||
private final String label;
|
||||
|
||||
public UserValue(@JsonProperty("label") String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
|
||||
private InMemoryIdentityService ids = new InMemoryIdentityService(Lists.newArrayList(megaCorp.getIdentity()), InternalTestConstantsKt.getDEV_ROOT_CA().getCertificate());
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@ -159,9 +191,35 @@ public class InteractiveShellJavaTest {
|
||||
@Test
|
||||
public void flowStartWithNestedTypes() throws InteractiveShell.NoApplicableConstructor {
|
||||
check(
|
||||
"pair: { first: $100.12, second: df489807f81c8c8829e509e1bcb92e6692b9dd9d624b7456435cb2f51dc82587 }",
|
||||
"($100.12, df489807f81c8c8829e509e1bcb92e6692b9dd9d624b7456435cb2f51dc82587)",
|
||||
FlowA.class);
|
||||
"pair: { first: $100.12, second: df489807f81c8c8829e509e1bcb92e6692b9dd9d624b7456435cb2f51dc82587 }",
|
||||
"(100.12 USD, DF489807F81C8C8829E509E1BCB92E6692B9DD9D624B7456435CB2F51DC82587)",
|
||||
FlowA.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flowStartWithUserAmount() throws InteractiveShell.NoApplicableConstructor {
|
||||
check(
|
||||
"b: 500, amount: { \"quantity\": 10001, \"token\":{ \"label\": \"of value\" } }",
|
||||
"10501 of value",
|
||||
FlowA.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flowStartWithArrayType() throws InteractiveShell.NoApplicableConstructor {
|
||||
check(
|
||||
"b: [ One, Two, Three, Four ]",
|
||||
"One+Two+Three+Four",
|
||||
FlowA.class
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flowStartWithArrayOfNestedType() throws InteractiveShell.NoApplicableConstructor {
|
||||
check(
|
||||
"amounts: [ { \"quantity\": 10, \"token\": { \"label\": \"(1)\" } }, { \"quantity\": 200, \"token\": { \"label\": \"(2)\" } } ]",
|
||||
"10 (1)++200 (2)",
|
||||
FlowA.class
|
||||
);
|
||||
}
|
||||
|
||||
@Test(expected = InteractiveShell.NoApplicableConstructor.class)
|
||||
|
@ -10,8 +10,10 @@
|
||||
|
||||
package net.corda.tools.shell
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
|
||||
import net.corda.client.jackson.JacksonSupport
|
||||
import net.corda.client.jackson.internal.ToStringSerialize
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
@ -41,6 +43,9 @@ class InteractiveShellTest {
|
||||
constructor(amount: Amount<Currency>) : this(amount.toString())
|
||||
constructor(pair: Pair<Amount<Currency>, SecureHash.SHA256>) : this(pair.toString())
|
||||
constructor(party: Party) : this(party.name.toString())
|
||||
constructor(b: Int?, amount: Amount<UserValue>) : this("${(b ?: 0) + amount.quantity} ${amount.token}")
|
||||
constructor(b: Array<String>) : this(b.joinToString("+"))
|
||||
constructor(amounts: Array<Amount<UserValue>>) : this(amounts.map(Amount<UserValue>::toString).joinToString("++"))
|
||||
|
||||
override val progressTracker = ProgressTracker()
|
||||
override fun call() = a
|
||||
@ -75,8 +80,26 @@ class InteractiveShellTest {
|
||||
|
||||
@Test
|
||||
fun flowStartWithNestedTypes() = check(
|
||||
"pair: { first: $100.12, second: df489807f81c8c8829e509e1bcb92e6692b9dd9d624b7456435cb2f51dc82587 }",
|
||||
"($100.12, df489807f81c8c8829e509e1bcb92e6692b9dd9d624b7456435cb2f51dc82587)"
|
||||
input = "pair: { first: $100.12, second: df489807f81c8c8829e509e1bcb92e6692b9dd9d624b7456435cb2f51dc82587 }",
|
||||
expected = "(100.12 USD, DF489807F81C8C8829E509E1BCB92E6692B9DD9D624B7456435CB2F51DC82587)"
|
||||
)
|
||||
|
||||
@Test
|
||||
fun flowStartWithArrayType() = check(
|
||||
input = "b: [ One, Two, Three, Four ]",
|
||||
expected = "One+Two+Three+Four"
|
||||
)
|
||||
|
||||
@Test
|
||||
fun flowStartWithUserAmount() = check(
|
||||
input = """b: 500, amount: { "quantity": 10001, "token":{ "label": "of value" } }""",
|
||||
expected = "10501 of value"
|
||||
)
|
||||
|
||||
@Test
|
||||
fun flowStartWithArrayOfNestedTypes() = check(
|
||||
input = """amounts: [ { "quantity": 10, "token": { "label": "(1)" } }, { "quantity": 200, "token": { "label": "(2)" } } ]""",
|
||||
expected = "10 (1)++200 (2)"
|
||||
)
|
||||
|
||||
@Test(expected = InteractiveShell.NoApplicableConstructor::class)
|
||||
@ -90,4 +113,9 @@ class InteractiveShellTest {
|
||||
|
||||
@Test
|
||||
fun party() = check("party: \"${megaCorp.name}\"", megaCorp.name.toString())
|
||||
|
||||
@ToStringSerialize
|
||||
data class UserValue(@JsonProperty("label") val label: String) {
|
||||
override fun toString() = label
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ dependencies {
|
||||
compile "org.eclipse.jetty:jetty-continuation:${jetty_version}"
|
||||
|
||||
compile "org.glassfish.jersey.core:jersey-server:$jersey_version"
|
||||
compile "org.glassfish.jersey.containers:jersey-container-servlet-core:$jersey_version"
|
||||
compile "org.glassfish.jersey.containers:jersey-container-servlet:$jersey_version"
|
||||
compile "org.glassfish.jersey.containers:jersey-container-jetty-http:$jersey_version"
|
||||
compile "org.glassfish.jersey.media:jersey-media-json-jackson:$jersey_version"
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user