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:
Chris Rankin 2018-08-20 15:45:21 +01:00
commit a287673432
51 changed files with 1515 additions and 338 deletions

View File

@ -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
View File

@ -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" />

View File

@ -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>()

View File

@ -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)
}

View File

@ -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 {

View File

@ -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

View File

@ -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)
}

View File

@ -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))
}

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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 }
}

View File

@ -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()) {

View File

@ -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]

View File

@ -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

View File

@ -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 {

View File

@ -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
^^^^^^^^^^^^^

View File

@ -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:

View File

@ -1,4 +1,4 @@
Node configuration
Configuring a node
==================
.. contents::

View File

@ -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>`_.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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::

View File

@ -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

View File

@ -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

View File

@ -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> {

View File

@ -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)
""" +

View File

@ -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)
""" +

View File

@ -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

View File

@ -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.

View File

@ -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)
}
}
}

View File

@ -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)

View File

@ -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()

View File

@ -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)
}

View File

@ -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()) {

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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 {

View File

@ -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.
}
}

View File

@ -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'

View File

@ -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.

View File

@ -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) {

View 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"
}
}

View File

@ -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")
}
}
}

View File

@ -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())

View File

@ -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
}

View File

@ -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: []

View File

@ -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}")
}
}

View File

@ -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)

View File

@ -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
}
}

View File

@ -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"