mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Merge branch 'master' into jlocke/merge_4.1_20190827
This commit is contained in:
commit
35d0170b0a
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@ -23,6 +23,8 @@
|
||||
<module name="behave_test" target="1.8" />
|
||||
<module name="blobinspector_main" target="1.8" />
|
||||
<module name="blobinspector_test" target="1.8" />
|
||||
<module name="blobwriter_main" target="1.8" />
|
||||
<module name="blobwriter_test" target="1.8" />
|
||||
<module name="bootstrapper_main" target="1.8" />
|
||||
<module name="bootstrapper_test" target="1.8" />
|
||||
<module name="buildSrc_main" target="1.8" />
|
||||
|
@ -3,6 +3,8 @@
|
||||
package net.corda.client.jackson.internal
|
||||
|
||||
import com.fasterxml.jackson.annotation.*
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect.Value
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility
|
||||
import com.fasterxml.jackson.annotation.JsonCreator.Mode.DISABLED
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
@ -12,10 +14,14 @@ 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.cfg.MapperConfig
|
||||
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.deser.std.FromStringDeserializer
|
||||
import com.fasterxml.jackson.databind.introspect.AnnotatedClass
|
||||
import com.fasterxml.jackson.databind.introspect.BasicClassIntrospector
|
||||
import com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.databind.node.IntNode
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
@ -32,7 +38,6 @@ import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.identity.*
|
||||
import net.corda.core.internal.DigitalSignatureWithCert
|
||||
import net.corda.core.internal.createComponentGroups
|
||||
import net.corda.core.internal.kotlinObjectInstance
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
@ -56,7 +61,13 @@ class CordaModule : SimpleModule("corda-core") {
|
||||
override fun setupModule(context: SetupContext) {
|
||||
super.setupModule(context)
|
||||
|
||||
// For classes which are annotated with CordaSerializable we want to use the same set of properties as the Corda serilasation scheme.
|
||||
// To do that we use CordaSerializableClassIntrospector to first turn on field visibility for these classes (the Jackson default is
|
||||
// private fields are not included) and then we use CordaSerializableBeanSerializerModifier to remove any extra properties that Jackson
|
||||
// might pick up.
|
||||
context.setClassIntrospector(CordaSerializableClassIntrospector(context))
|
||||
context.addBeanSerializerModifier(CordaSerializableBeanSerializerModifier())
|
||||
|
||||
context.addBeanDeserializerModifier(AmountBeanDeserializerModifier())
|
||||
|
||||
context.setMixInAnnotations(PartyAndCertificate::class.java, PartyAndCertificateMixin::class.java)
|
||||
@ -88,9 +99,22 @@ class CordaModule : SimpleModule("corda-core") {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the same properties that AMQP serialization uses if the POJO is @CordaSerializable
|
||||
*/
|
||||
private class CordaSerializableClassIntrospector(private val context: Module.SetupContext) : BasicClassIntrospector() {
|
||||
override fun constructPropertyCollector(
|
||||
config: MapperConfig<*>?,
|
||||
ac: AnnotatedClass?,
|
||||
type: JavaType,
|
||||
forSerialization: Boolean,
|
||||
mutatorPrefix: String?
|
||||
): POJOPropertiesCollector {
|
||||
if (hasCordaSerializable(type.rawClass)) {
|
||||
// Adjust the field visibility of CordaSerializable classes on the fly as they are encountered.
|
||||
context.configOverride(type.rawClass).visibility = Value.defaultVisibility().withFieldVisibility(Visibility.ANY)
|
||||
}
|
||||
return super.constructPropertyCollector(config, ac, type, forSerialization, mutatorPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
private class CordaSerializableBeanSerializerModifier : BeanSerializerModifier() {
|
||||
// We need to pass in a SerializerFactory when scanning for properties, but don't actually do any serialisation so any will do.
|
||||
private val serializerFactory = SerializerFactoryBuilder.build(AllWhitelist, javaClass.classLoader)
|
||||
@ -99,17 +123,10 @@ private class CordaSerializableBeanSerializerModifier : BeanSerializerModifier()
|
||||
beanDesc: BeanDescription,
|
||||
beanProperties: MutableList<BeanPropertyWriter>): MutableList<BeanPropertyWriter> {
|
||||
val beanClass = beanDesc.beanClass
|
||||
if (hasCordaSerializable(beanClass) && beanClass.kotlinObjectInstance == null && !SerializeAsToken::class.java.isAssignableFrom(beanClass)) {
|
||||
if (hasCordaSerializable(beanClass) && !SerializeAsToken::class.java.isAssignableFrom(beanClass)) {
|
||||
val typeInformation = serializerFactory.getTypeInformation(beanClass)
|
||||
val properties = typeInformation.propertiesOrEmptyMap
|
||||
val amqpProperties = properties.mapNotNull { (name, property) ->
|
||||
if (property.isCalculated) null else name
|
||||
}
|
||||
val propertyRenames = beanDesc.findProperties().associateBy({ it.name }, { it.internalName })
|
||||
(amqpProperties - propertyRenames.values).let {
|
||||
check(it.isEmpty()) { "Jackson didn't provide serialisers for $it" }
|
||||
}
|
||||
beanProperties.removeIf { propertyRenames[it.name] !in amqpProperties }
|
||||
val propertyNames = typeInformation.propertiesOrEmptyMap.mapNotNull { if (it.value.isCalculated) null else it.key }
|
||||
beanProperties.removeIf { it.name !in propertyNames }
|
||||
}
|
||||
return beanProperties
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import net.corda.core.node.services.NetworkParametersService
|
||||
import net.corda.core.node.services.TransactionStorage
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
@ -658,6 +659,15 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
||||
assertThat(mapper.convertValue<NonCtorPropertiesData>(json)).isEqualTo(data)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `LinearState where the linearId property does not match the backing field`() {
|
||||
val funkyLinearState = FunkyLinearState(UniqueIdentifier())
|
||||
// As a sanity check, show that this is a valid CordaSerializable class
|
||||
assertThat(funkyLinearState.serialize().deserialize()).isEqualTo(funkyLinearState)
|
||||
val json = mapper.valueToTree<ObjectNode>(funkyLinearState)
|
||||
assertThat(mapper.convertValue<FunkyLinearState>(json)).isEqualTo(funkyLinearState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `kotlin object`() {
|
||||
val json = mapper.valueToTree<ObjectNode>(KotlinObject)
|
||||
@ -713,6 +723,11 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
||||
val nonCtor: Int get() = value
|
||||
}
|
||||
|
||||
private data class FunkyLinearState(private val linearID: UniqueIdentifier) : LinearState {
|
||||
override val linearId: UniqueIdentifier get() = linearID
|
||||
override val participants: List<AbstractParty> get() = emptyList()
|
||||
}
|
||||
|
||||
private object KotlinObject
|
||||
|
||||
@CordaSerializable
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.corda.node.services.rpc
|
||||
|
||||
import co.paralleluniverse.fibers.Stack
|
||||
import co.paralleluniverse.strands.Strand
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility
|
||||
import com.fasterxml.jackson.annotation.JsonFormat
|
||||
@ -363,9 +362,12 @@ class CheckpointDumper(private val checkpointStorage: CheckpointStorage, private
|
||||
override fun changeProperties(config: SerializationConfig,
|
||||
beanDesc: BeanDescription,
|
||||
beanProperties: MutableList<BeanPropertyWriter>): MutableList<BeanPropertyWriter> {
|
||||
// Remove references to any node singletons
|
||||
beanProperties.removeIf { it.type.isTypeOrSubTypeOf(SerializeAsToken::class.java) }
|
||||
if (FlowLogic::class.java.isAssignableFrom(beanDesc.beanClass)) {
|
||||
if (SerializeAsToken::class.java.isAssignableFrom(beanDesc.beanClass)) {
|
||||
// Do not serialise node singletons
|
||||
// TODO This will cause the singleton to appear as an empty object. Ideally we don't want it to appear at all but this will
|
||||
// have to do for now.
|
||||
beanProperties.clear()
|
||||
} else if (FlowLogic::class.java.isAssignableFrom(beanDesc.beanClass)) {
|
||||
beanProperties.removeIf {
|
||||
it.type.isTypeOrSubTypeOf(ProgressTracker::class.java) || it.name == "_stateMachine" || it.name == "deprecatedPartySessionMap"
|
||||
}
|
||||
|
@ -22,12 +22,12 @@ dependencies {
|
||||
// Configure these by hand. It should be a minimal subset of dependencies,
|
||||
// and without any obviously non-deterministic ones such as Hibernate.
|
||||
|
||||
// This dependency will become "compile" scoped in our published POM.
|
||||
// These dependencies will become "compile" scoped in our published POM.
|
||||
// See publish.dependenciesFrom.defaultScope.
|
||||
deterministicLibraries project(path: ':core-deterministic', configuration: 'deterministicArtifacts')
|
||||
deterministicLibraries "org.apache.qpid:proton-j:$protonj_version"
|
||||
|
||||
// These "implementation" dependencies will become "runtime" scoped in our published POM.
|
||||
implementation "org.apache.qpid:proton-j:$protonj_version"
|
||||
implementation "org.iq80.snappy:snappy:$snappy_version"
|
||||
implementation "com.google.guava:guava:$guava_version"
|
||||
}
|
||||
|
@ -13,11 +13,16 @@ import net.corda.serialization.internal.carpenter.Schema
|
||||
@Suppress("UNUSED")
|
||||
fun createSerializerFactoryFactory(): SerializerFactoryFactory = DeterministicSerializerFactoryFactory()
|
||||
|
||||
/**
|
||||
* Creates a [ClassCarpenter] suitable for the DJVM, i.e. one that doesn't work.
|
||||
*/
|
||||
fun createClassCarpenter(context: SerializationContext): ClassCarpenter = DummyClassCarpenter(context.whitelist, context.deserializationClassLoader)
|
||||
|
||||
private class DeterministicSerializerFactoryFactory : SerializerFactoryFactory {
|
||||
override fun make(context: SerializationContext) =
|
||||
SerializerFactoryBuilder.build(
|
||||
whitelist = context.whitelist,
|
||||
classCarpenter = DummyClassCarpenter(context.whitelist, context.deserializationClassLoader))
|
||||
classCarpenter = createClassCarpenter(context))
|
||||
}
|
||||
|
||||
private class DummyClassCarpenter(
|
||||
|
@ -3,14 +3,21 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.serialization.internal.carpenter.ClassCarpenter
|
||||
import net.corda.serialization.internal.carpenter.ClassCarpenterImpl
|
||||
|
||||
fun createSerializerFactoryFactory(): SerializerFactoryFactory = SerializerFactoryFactoryImpl()
|
||||
|
||||
fun createClassCarpenter(context: SerializationContext): ClassCarpenter = ClassCarpenterImpl(
|
||||
whitelist = context.whitelist,
|
||||
cl = context.deserializationClassLoader,
|
||||
lenient = context.lenientCarpenterEnabled
|
||||
)
|
||||
|
||||
open class SerializerFactoryFactoryImpl : SerializerFactoryFactory {
|
||||
override fun make(context: SerializationContext): SerializerFactory {
|
||||
return SerializerFactoryBuilder.build(context.whitelist,
|
||||
ClassCarpenterImpl(context.whitelist, context.deserializationClassLoader, context.lenientCarpenterEnabled),
|
||||
createClassCarpenter(context),
|
||||
mustPreserveDataWhenEvolving = context.preventDataLoss
|
||||
)
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ object AMQPTypeIdentifiers {
|
||||
Float::class to "float",
|
||||
Double::class to "double",
|
||||
Decimal32::class to "decimal32",
|
||||
Decimal64::class to "decimal62",
|
||||
Decimal64::class to "decimal64",
|
||||
Decimal128::class to "decimal128",
|
||||
Date::class to "timestamp",
|
||||
UUID::class to "uuid",
|
||||
@ -62,4 +62,4 @@ object AMQPTypeIdentifiers {
|
||||
private val primitiveByteArrayType = TypeIdentifier.ArrayOf(TypeIdentifier.forClass(Byte::class.javaPrimitiveType!!))
|
||||
|
||||
fun nameForType(type: Type): String = nameForType(TypeIdentifier.forGenericType(type))
|
||||
}
|
||||
}
|
||||
|
@ -88,7 +88,9 @@ open class ArraySerializer(override val type: Type, factory: LocalSerializerFact
|
||||
context: SerializationContext
|
||||
): Any {
|
||||
if (obj is List<*>) {
|
||||
return obj.map { input.readObjectOrNull(it, schemas, elementType, context) }.toArrayOfType(elementType)
|
||||
return obj.map {
|
||||
input.readObjectOrNull(redescribe(it, elementType), schemas, elementType, context)
|
||||
}.toArrayOfType(elementType)
|
||||
} else throw AMQPNotSerializableException(type, "Expected a List but found $obj")
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.serialization.internal.amqp.AMQPTypeIdentifiers.isPrimitive
|
||||
import net.corda.serialization.internal.model.*
|
||||
import org.apache.qpid.proton.amqp.Binary
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
@ -18,7 +19,7 @@ interface PropertyReadStrategy {
|
||||
* Select the correct strategy for reading properties, based on the property type.
|
||||
*/
|
||||
fun make(name: String, typeIdentifier: TypeIdentifier, type: Type): PropertyReadStrategy =
|
||||
if (AMQPTypeIdentifiers.isPrimitive(typeIdentifier)) {
|
||||
if (isPrimitive(typeIdentifier)) {
|
||||
when (typeIdentifier) {
|
||||
in characterTypes -> AMQPCharPropertyReadStrategy
|
||||
else -> AMQPPropertyReadStrategy
|
||||
@ -47,7 +48,7 @@ interface PropertyWriteStrategy {
|
||||
fun make(name: String, propertyInformation: LocalPropertyInformation, factory: LocalSerializerFactory): PropertyWriteStrategy {
|
||||
val reader = PropertyReader.make(propertyInformation)
|
||||
val type = propertyInformation.type
|
||||
return if (AMQPTypeIdentifiers.isPrimitive(type.typeIdentifier)) {
|
||||
return if (isPrimitive(type.typeIdentifier)) {
|
||||
when (type.typeIdentifier) {
|
||||
in characterTypes -> AMQPCharPropertyWriteStategy(reader)
|
||||
else -> AMQPPropertyWriteStrategy(reader)
|
||||
@ -199,7 +200,7 @@ class DescribedTypeReadStrategy(name: String,
|
||||
|
||||
override fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any? =
|
||||
ifThrowsAppend({ nameForDebug }) {
|
||||
input.readObjectOrNull(obj, schemas, type, context)
|
||||
input.readObjectOrNull(redescribe(obj, type), schemas, type, context)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,11 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
||||
*/
|
||||
open val additionalSerializers: Iterable<CustomSerializer<out Any>> = emptyList()
|
||||
|
||||
/**
|
||||
* This custom serializer is also allowed to deserialize these classes. This allows us
|
||||
* to deserialize objects into completely different types, e.g. `A` -> `sandbox.A`.
|
||||
*/
|
||||
open val deserializationAliases: Set<Class<*>> = emptySet()
|
||||
|
||||
protected abstract val descriptor: Descriptor
|
||||
/**
|
||||
@ -53,6 +58,14 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
||||
abstract fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext)
|
||||
|
||||
/**
|
||||
* [CustomSerializerRegistry.findCustomSerializer] will invoke this method on the [CustomSerializer]
|
||||
* that it selects to give that serializer an opportunity to customise its behaviour. The serializer
|
||||
* can also return `null` here, in which case [CustomSerializerRegistry] will proceed as if no
|
||||
* serializer is available for [declaredType].
|
||||
*/
|
||||
open fun specialiseFor(declaredType: Type): AMQPSerializer<T>? = this
|
||||
|
||||
/**
|
||||
* This custom serializer represents a sort of symbolic link from a subclass to a super class, where the super
|
||||
* class custom serializer is responsible for the "on the wire" format but we want to create a reference to the
|
||||
@ -110,7 +123,7 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
||||
*/
|
||||
abstract class CustomSerializerImp<T : Any>(protected val clazz: Class<T>, protected val withInheritance: Boolean) : CustomSerializer<T>() {
|
||||
override val type: Type get() = clazz
|
||||
override val typeDescriptor: Symbol = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${AMQPTypeIdentifiers.nameForType(clazz)}")
|
||||
override val typeDescriptor: Symbol = typeDescriptorFor(clazz)
|
||||
override fun writeClassInfo(output: SerializationOutput) {}
|
||||
override val descriptor: Descriptor = Descriptor(typeDescriptor)
|
||||
override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz
|
||||
@ -119,11 +132,13 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
||||
/**
|
||||
* Additional base features for a custom serializer for a particular class, that excludes subclasses.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
abstract class Is<T : Any>(clazz: Class<T>) : CustomSerializerImp<T>(clazz, false)
|
||||
|
||||
/**
|
||||
* Additional base features for a custom serializer for all implementations of a particular interface or super class.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
abstract class Implements<T : Any>(clazz: Class<T>) : CustomSerializerImp<T>(clazz, true)
|
||||
|
||||
/**
|
||||
@ -133,6 +148,7 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
||||
* The proxy class must use only types which are either native AMQP or other types for which there are pre-registered
|
||||
* custom serializers.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
abstract class Proxy<T : Any, P : Any>(clazz: Class<T>,
|
||||
protected val proxyClass: Class<P>,
|
||||
protected val factory: LocalSerializerFactory,
|
||||
@ -191,6 +207,7 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
||||
* @param maker A lambda for constructing an instance, that defaults to calling a constructor that expects a string.
|
||||
* @param unmaker A lambda that extracts the string value for an instance, that defaults to the [toString] method.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
abstract class ToString<T : Any>(clazz: Class<T>, withInheritance: Boolean = false,
|
||||
private val maker: (String) -> T = clazz.getConstructor(String::class.java).let { `constructor` ->
|
||||
{ string -> `constructor`.newInstance(string) }
|
||||
|
@ -44,7 +44,7 @@ interface CustomSerializerRegistry {
|
||||
*
|
||||
* @param clazz The actual class to look for a custom serializer for.
|
||||
* @param declaredType The declared type to look for a custom serializer for.
|
||||
* @return The custom serializer handing the class, if found, or `null`.
|
||||
* @return The custom serializer handling the class, if found, or `null`.
|
||||
*
|
||||
* @throws IllegalCustomSerializerException If a custom serializer identifies itself as the serializer for
|
||||
* a class annotated with [CordaSerializable], since all such classes should be serializable via standard object
|
||||
@ -57,8 +57,10 @@ interface CustomSerializerRegistry {
|
||||
}
|
||||
|
||||
class CachingCustomSerializerRegistry(
|
||||
private val descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry)
|
||||
: CustomSerializerRegistry {
|
||||
private val descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry,
|
||||
private val allowedFor: Set<Class<*>>
|
||||
) : CustomSerializerRegistry {
|
||||
constructor(descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry) : this(descriptorBasedSerializerRegistry, emptySet())
|
||||
|
||||
companion object {
|
||||
val logger = contextLogger()
|
||||
@ -84,7 +86,7 @@ class CachingCustomSerializerRegistry(
|
||||
}
|
||||
|
||||
private val customSerializersCache: MutableMap<CustomSerializerIdentifier, CustomSerializerLookupResult> = DefaultCacheProvider.createCache()
|
||||
private var customSerializers: List<SerializerFor> = emptyList()
|
||||
private val customSerializers: MutableList<SerializerFor> = mutableListOf()
|
||||
|
||||
/**
|
||||
* Register a custom serializer for any type that cannot be serialized or deserialized by the default serializer
|
||||
@ -93,7 +95,7 @@ class CachingCustomSerializerRegistry(
|
||||
override fun register(customSerializer: CustomSerializer<out Any>) {
|
||||
logger.trace("action=\"Registering custom serializer\", class=\"${customSerializer.type}\"")
|
||||
|
||||
if (!customSerializersCache.isEmpty()) {
|
||||
if (customSerializersCache.isNotEmpty()) {
|
||||
logger.warn("Attempting to register custom serializer $customSerializer.type} in an active cache." +
|
||||
"All serializers should be registered before the cache comes into use.")
|
||||
}
|
||||
@ -103,14 +105,23 @@ class CachingCustomSerializerRegistry(
|
||||
for (additional in customSerializer.additionalSerializers) {
|
||||
register(additional)
|
||||
}
|
||||
|
||||
for (alias in customSerializer.deserializationAliases) {
|
||||
val aliasDescriptor = typeDescriptorFor(alias)
|
||||
if (aliasDescriptor != customSerializer.typeDescriptor) {
|
||||
descriptorBasedSerializerRegistry[aliasDescriptor.toString()] = customSerializer
|
||||
}
|
||||
}
|
||||
|
||||
customSerializer
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun registerExternal(customSerializer: CorDappCustomSerializer) {
|
||||
logger.trace("action=\"Registering external serializer\", class=\"${customSerializer.type}\"")
|
||||
|
||||
if (!customSerializersCache.isEmpty()) {
|
||||
if (customSerializersCache.isNotEmpty()) {
|
||||
logger.warn("Attempting to register custom serializer ${customSerializer.type} in an active cache." +
|
||||
"All serializers must be registered before the cache comes into use.")
|
||||
}
|
||||
@ -120,7 +131,7 @@ class CachingCustomSerializerRegistry(
|
||||
customSerializer
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer<Any>? {
|
||||
val typeIdentifier = CustomSerializerIdentifier(
|
||||
TypeIdentifier.forClass(clazz),
|
||||
@ -164,13 +175,21 @@ class CachingCustomSerializerRegistry(
|
||||
throw IllegalCustomSerializerException(declaredSerializers.first(), clazz)
|
||||
}
|
||||
|
||||
return declaredSerializers.first()
|
||||
return declaredSerializers.first().let {
|
||||
if (it is CustomSerializer<Any>) {
|
||||
it.specialiseFor(declaredType)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val Class<*>.isCustomSerializationForbidden: Boolean get() = when {
|
||||
AMQPTypeIdentifiers.isPrimitive(this) -> true
|
||||
isSubClassOf(CordaThrowable::class.java) -> false
|
||||
allowedFor.any { it.isAssignableFrom(this) } -> false
|
||||
isAnnotationPresent(CordaSerializable::class.java) -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -142,12 +142,12 @@ class DeserializationInput constructor(
|
||||
envelope)
|
||||
}
|
||||
|
||||
internal fun readObjectOrNull(obj: Any?, schema: SerializationSchemas, type: Type, context: SerializationContext
|
||||
fun readObjectOrNull(obj: Any?, schema: SerializationSchemas, type: Type, context: SerializationContext
|
||||
): Any? {
|
||||
return if (obj == null) null else readObject(obj, schema, type, context)
|
||||
}
|
||||
|
||||
internal fun readObject(obj: Any, schemas: SerializationSchemas, type: Type, context: SerializationContext): Any =
|
||||
fun readObject(obj: Any, schemas: SerializationSchemas, type: Type, context: SerializationContext): Any =
|
||||
if (obj is DescribedType && ReferencedObject.DESCRIPTOR == obj.descriptor) {
|
||||
// It must be a reference to an instance that has already been read, cheaply and quickly returning it by reference.
|
||||
val objectIndex = (obj.described as UnsignedInteger).toInt()
|
||||
|
@ -17,6 +17,13 @@ interface EvolutionSerializerFactory {
|
||||
fun getEvolutionSerializer(
|
||||
remote: RemoteTypeInformation,
|
||||
local: LocalTypeInformation): AMQPSerializer<Any>?
|
||||
|
||||
/**
|
||||
* A mapping between Java object types and their equivalent Java primitive types.
|
||||
* Predominantly for the sake of the DJVM sandbox where e.g. `char` will map to
|
||||
* sandbox.java.lang.Character instead of java.lang.Character.
|
||||
*/
|
||||
val primitiveTypes: Map<Class<*>, Class<*>>
|
||||
}
|
||||
|
||||
class EvolutionSerializationException(remoteTypeInformation: RemoteTypeInformation, reason: String)
|
||||
@ -32,7 +39,9 @@ class EvolutionSerializationException(remoteTypeInformation: RemoteTypeInformati
|
||||
class DefaultEvolutionSerializerFactory(
|
||||
private val localSerializerFactory: LocalSerializerFactory,
|
||||
private val classLoader: ClassLoader,
|
||||
private val mustPreserveDataWhenEvolving: Boolean): EvolutionSerializerFactory {
|
||||
private val mustPreserveDataWhenEvolving: Boolean,
|
||||
override val primitiveTypes: Map<Class<*>, Class<*>>
|
||||
): EvolutionSerializerFactory {
|
||||
|
||||
override fun getEvolutionSerializer(remote: RemoteTypeInformation,
|
||||
local: LocalTypeInformation): AMQPSerializer<Any>? =
|
||||
@ -77,7 +86,7 @@ class DefaultEvolutionSerializerFactory(
|
||||
val localClass = localProperty.type.observedType.asClass()
|
||||
val remoteClass = remoteProperty.type.typeIdentifier.getLocalType(classLoader).asClass()
|
||||
|
||||
if (!localClass.isAssignableFrom(remoteClass) && remoteClass != localClass.kotlin.javaPrimitiveType) {
|
||||
if (!localClass.isAssignableFrom(remoteClass) && remoteClass != primitiveTypes[localClass]) {
|
||||
throw EvolutionSerializationException(this,
|
||||
"Local type $localClass of property $name is not assignable from remote type $remoteClass")
|
||||
}
|
||||
|
@ -6,10 +6,13 @@ import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.serialization.internal.model.*
|
||||
import net.corda.serialization.internal.model.TypeIdentifier.*
|
||||
import net.corda.serialization.internal.model.TypeIdentifier.Companion.classLoaderFor
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
import java.util.*
|
||||
import java.util.function.Function
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
@ -87,6 +90,7 @@ class DefaultLocalSerializerFactory(
|
||||
private val fingerPrinter: FingerPrinter,
|
||||
override val classloader: ClassLoader,
|
||||
private val descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry,
|
||||
private val primitiveSerializerFactory: Function<Class<*>, AMQPSerializer<Any>>,
|
||||
private val customSerializerRegistry: CustomSerializerRegistry,
|
||||
private val onlyCustomSerializers: Boolean)
|
||||
: LocalSerializerFactory {
|
||||
@ -137,9 +141,18 @@ class DefaultLocalSerializerFactory(
|
||||
serializersByTypeId.getOrPut(localTypeInformation.typeIdentifier) {
|
||||
val declaredClass = declaredType.asClass()
|
||||
|
||||
// Any Custom Serializer cached for a ParameterizedType can only be
|
||||
// found by searching for that exact same type. Searching for its raw
|
||||
// class will not work!
|
||||
val declaredGenericType = if (declaredType !is ParameterizedType && localTypeInformation.typeIdentifier is Parameterised) {
|
||||
localTypeInformation.typeIdentifier.getLocalType(classLoaderFor(declaredClass))
|
||||
} else {
|
||||
declaredType
|
||||
}
|
||||
|
||||
// can be useful to enable but will be *extremely* chatty if you do
|
||||
logger.trace { "Get Serializer for $declaredClass ${declaredType.typeName}" }
|
||||
customSerializerRegistry.findCustomSerializer(declaredClass, declaredType)?.apply { return@get this }
|
||||
logger.trace { "Get Serializer for $declaredClass ${declaredGenericType.typeName}" }
|
||||
customSerializerRegistry.findCustomSerializer(declaredClass, declaredGenericType)?.apply { return@get this }
|
||||
|
||||
return when (localTypeInformation) {
|
||||
is LocalTypeInformation.ACollection -> makeDeclaredCollection(localTypeInformation)
|
||||
@ -226,7 +239,7 @@ class DefaultLocalSerializerFactory(
|
||||
throw AMQPNotSerializableException(
|
||||
type,
|
||||
"Serializer does not support synthetic classes")
|
||||
AMQPTypeIdentifiers.isPrimitive(typeInformation.typeIdentifier) -> AMQPPrimitiveSerializer(clazz)
|
||||
AMQPTypeIdentifiers.isPrimitive(typeInformation.typeIdentifier) -> primitiveSerializerFactory.apply(clazz)
|
||||
else -> makeNonCustomSerializer(type, typeInformation, clazz)
|
||||
}
|
||||
}
|
||||
@ -250,4 +263,4 @@ class DefaultLocalSerializerFactory(
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package net.corda.serialization.internal.amqp
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.serialization.internal.model.*
|
||||
import org.hibernate.type.descriptor.java.ByteTypeDescriptor
|
||||
import java.io.NotSerializableException
|
||||
|
||||
/**
|
||||
|
@ -3,18 +3,46 @@ package net.corda.serialization.internal.amqp
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.serialization.internal.CordaSerializationMagic
|
||||
import org.apache.qpid.proton.amqp.DescribedType
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import org.apache.qpid.proton.amqp.UnsignedInteger
|
||||
import org.apache.qpid.proton.amqp.UnsignedLong
|
||||
import net.corda.serialization.internal.amqp.AMQPTypeIdentifiers.isPrimitive
|
||||
import net.corda.serialization.internal.model.TypeIdentifier
|
||||
import net.corda.serialization.internal.model.TypeIdentifier.TopType
|
||||
import net.corda.serialization.internal.model.TypeIdentifier.Companion.forGenericType
|
||||
import org.apache.qpid.proton.amqp.*
|
||||
import org.apache.qpid.proton.codec.DescribedTypeConstructor
|
||||
import java.io.NotSerializableException
|
||||
import net.corda.serialization.internal.carpenter.Field as CarpenterField
|
||||
import net.corda.serialization.internal.carpenter.Schema as CarpenterSchema
|
||||
import java.lang.reflect.Type
|
||||
|
||||
const val DESCRIPTOR_DOMAIN: String = "net.corda"
|
||||
val amqpMagic = CordaSerializationMagic("corda".toByteArray() + byteArrayOf(1, 0))
|
||||
|
||||
fun typeDescriptorFor(typeId: TypeIdentifier): Symbol = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${AMQPTypeIdentifiers.nameForType(typeId)}")
|
||||
fun typeDescriptorFor(type: Type): Symbol = typeDescriptorFor(forGenericType(type))
|
||||
|
||||
/**
|
||||
* Repackages a naked, non-primitive [obj] as a [DescribedType]. If [obj] is primitive, [Binary] or already
|
||||
* an instance of [DescribedType]] then it is returned unchanged. This allows Corda to search for a serializer
|
||||
* capable of handling instances of [type].
|
||||
*/
|
||||
fun redescribe(obj: Any?, type: Type): Any? {
|
||||
return if (obj == null || obj is DescribedType || obj is Binary || forGenericType(type).run { isPrimitive(this) || this == TopType }) {
|
||||
obj
|
||||
} else {
|
||||
/**
|
||||
* This must be a primitive [obj] that has a non-primitive [type].
|
||||
* Rewrap it with the required descriptor for further deserialization.
|
||||
*/
|
||||
RedescribedType(typeDescriptorFor(type), obj)
|
||||
}
|
||||
}
|
||||
|
||||
private class RedescribedType(
|
||||
private val descriptor: Symbol,
|
||||
private val described: Any?
|
||||
) : DescribedType {
|
||||
override fun getDescriptor(): Symbol = descriptor
|
||||
override fun getDescribed(): Any? = described
|
||||
}
|
||||
|
||||
/**
|
||||
* This and the classes below are OO representations of the AMQP XML schema described in the specification. Their
|
||||
* [toString] representations generate the associated XML form.
|
||||
|
@ -7,9 +7,28 @@ import net.corda.serialization.internal.carpenter.ClassCarpenter
|
||||
import net.corda.serialization.internal.carpenter.ClassCarpenterImpl
|
||||
import net.corda.serialization.internal.model.*
|
||||
import java.io.NotSerializableException
|
||||
import java.util.Collections.unmodifiableMap
|
||||
import java.util.function.Function
|
||||
|
||||
@KeepForDJVM
|
||||
object SerializerFactoryBuilder {
|
||||
/**
|
||||
* The standard mapping of Java object types to Java primitive types.
|
||||
* The DJVM will need to override these, but probably not anyone else.
|
||||
*/
|
||||
@Suppress("unchecked_cast")
|
||||
private val javaPrimitiveTypes: Map<Class<*>, Class<*>> = unmodifiableMap(listOf(
|
||||
Boolean::class,
|
||||
Byte::class,
|
||||
Char::class,
|
||||
Double::class,
|
||||
Float::class,
|
||||
Int::class,
|
||||
Long::class,
|
||||
Short::class
|
||||
).associate {
|
||||
klazz -> klazz.javaObjectType to klazz.javaPrimitiveType
|
||||
}) as Map<Class<*>, Class<*>>
|
||||
|
||||
@JvmStatic
|
||||
fun build(whitelist: ClassWhitelist, classCarpenter: ClassCarpenter): SerializerFactory {
|
||||
@ -89,17 +108,19 @@ object SerializerFactoryBuilder {
|
||||
fingerPrinter,
|
||||
classCarpenter.classloader,
|
||||
descriptorBasedSerializerRegistry,
|
||||
Function { clazz -> AMQPPrimitiveSerializer(clazz) },
|
||||
customSerializerRegistry,
|
||||
onlyCustomSerializers)
|
||||
|
||||
val typeLoader = ClassCarpentingTypeLoader(
|
||||
val typeLoader: TypeLoader = ClassCarpentingTypeLoader(
|
||||
SchemaBuildingRemoteTypeCarpenter(classCarpenter),
|
||||
classCarpenter.classloader)
|
||||
|
||||
val evolutionSerializerFactory = if (allowEvolution) DefaultEvolutionSerializerFactory(
|
||||
localSerializerFactory,
|
||||
classCarpenter.classloader,
|
||||
mustPreserveDataWhenEvolving
|
||||
mustPreserveDataWhenEvolving,
|
||||
javaPrimitiveTypes
|
||||
) else NoEvolutionSerializerFactory
|
||||
|
||||
val remoteSerializerFactory = DefaultRemoteSerializerFactory(
|
||||
@ -116,15 +137,17 @@ object SerializerFactoryBuilder {
|
||||
}
|
||||
|
||||
object NoEvolutionSerializerFactory : EvolutionSerializerFactory {
|
||||
override fun getEvolutionSerializer(remoteTypeInformation: RemoteTypeInformation, localTypeInformation: LocalTypeInformation): AMQPSerializer<Any> {
|
||||
override fun getEvolutionSerializer(remote: RemoteTypeInformation, local: LocalTypeInformation): AMQPSerializer<Any> {
|
||||
throw NotSerializableException("""
|
||||
Evolution not permitted.
|
||||
|
||||
Remote:
|
||||
${remoteTypeInformation.prettyPrint(false)}
|
||||
${remote.prettyPrint(false)}
|
||||
|
||||
Local:
|
||||
${localTypeInformation.prettyPrint(false)}
|
||||
${local.prettyPrint(false)}
|
||||
""")
|
||||
}
|
||||
|
||||
override val primitiveTypes: Map<Class<*>, Class<*>> = emptyMap()
|
||||
}
|
@ -1,7 +1,5 @@
|
||||
package net.corda.serialization.internal.model
|
||||
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.serialization.internal.amqp.*
|
||||
import java.lang.reflect.*
|
||||
|
||||
/**
|
||||
@ -54,7 +52,7 @@ class ConfigurableLocalTypeModel(private val typeModelConfiguration: LocalTypeMo
|
||||
private val typeInformationCache = DefaultCacheProvider.createCache<TypeIdentifier, LocalTypeInformation>()
|
||||
|
||||
/**
|
||||
* We need to provide the [TypeInformationBuilder] with a temporary local cache, so that it doesn't leak
|
||||
* We need to provide the [LocalTypeInformationBuilder] with a temporary local cache, so that it doesn't leak
|
||||
* [LocalTypeInformation] with unpatched cycles into the global cache where other threads can access them
|
||||
* before we've patched the cycles up.
|
||||
*/
|
||||
|
@ -63,6 +63,8 @@ sealed class TypeIdentifier {
|
||||
// This method has locking. So we memo the value here.
|
||||
private val systemClassLoader: ClassLoader = ClassLoader.getSystemClassLoader()
|
||||
|
||||
fun classLoaderFor(clazz: Class<*>): ClassLoader = clazz.classLoader ?: systemClassLoader
|
||||
|
||||
/**
|
||||
* Obtain the [TypeIdentifier] for an erased Java class.
|
||||
*
|
||||
@ -206,7 +208,11 @@ sealed class TypeIdentifier {
|
||||
|
||||
override fun toString() = "Parameterised(${prettyPrint()})"
|
||||
override fun getLocalType(classLoader: ClassLoader): Type {
|
||||
val rawType = Class.forName(name, false, classLoader)
|
||||
// We need to invoke ClassLoader.loadClass() directly, because
|
||||
// the JVM will complain if Class.forName() returns a class
|
||||
// that has a name other than the requested one. This will happen
|
||||
// for "transformative" class loaders, i.e. `A` -> `sandbox.A`.
|
||||
val rawType = classLoader.loadClass(name)
|
||||
if (rawType.typeParameters.size != parameters.size) {
|
||||
throw IncompatibleTypeIdentifierException(
|
||||
"Class $rawType expects ${rawType.typeParameters.size} type arguments, " +
|
||||
|
@ -4,7 +4,9 @@ import com.google.common.hash.Hashing
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.toBase64
|
||||
import net.corda.serialization.internal.amqp.*
|
||||
import java.io.NotSerializableException
|
||||
import net.corda.serialization.internal.model.TypeIdentifier.*
|
||||
import net.corda.serialization.internal.model.TypeIdentifier.Companion.classLoaderFor
|
||||
import java.lang.reflect.ParameterizedType
|
||||
|
||||
/**
|
||||
* A fingerprinter that fingerprints [LocalTypeInformation].
|
||||
@ -34,11 +36,15 @@ class TypeModellingFingerPrinter(
|
||||
private val cache: MutableMap<TypeIdentifier, String> = DefaultCacheProvider.createCache()
|
||||
|
||||
override fun fingerprint(typeInformation: LocalTypeInformation): String =
|
||||
cache.computeIfAbsent(typeInformation.typeIdentifier) {
|
||||
FingerPrintingState(
|
||||
customTypeDescriptorLookup,
|
||||
FingerprintWriter(debugEnabled)).fingerprint(typeInformation)
|
||||
}
|
||||
/*
|
||||
* We cannot use ConcurrentMap.computeIfAbsent() here because it requires
|
||||
* that the map not be re-entered during the computation function. And
|
||||
* the Fingerprinter cannot guarantee that.
|
||||
*/
|
||||
cache.getOrPut(typeInformation.typeIdentifier) {
|
||||
FingerPrintingState(customTypeDescriptorLookup, FingerprintWriter(debugEnabled))
|
||||
.fingerprint(typeInformation)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -224,7 +230,22 @@ private class FingerPrintingState(
|
||||
|
||||
// Give any custom serializers loaded into the factory the chance to supply their own type-descriptors
|
||||
private fun fingerprintWithCustomSerializerOrElse(type: LocalTypeInformation, defaultAction: () -> Unit) {
|
||||
val customTypeDescriptor = customSerializerRegistry.findCustomSerializer(type.observedType.asClass(), type.observedType)?.typeDescriptor?.toString()
|
||||
val observedType = type.observedType
|
||||
val observedClass = observedType.asClass()
|
||||
|
||||
// Any Custom Serializer cached for a ParameterizedType can only be
|
||||
// found by searching for that exact same type. Searching for its raw
|
||||
// class will not work!
|
||||
val observedGenericType = if (observedType !is ParameterizedType && type.typeIdentifier is Parameterised) {
|
||||
type.typeIdentifier.getLocalType(classLoaderFor(observedClass))
|
||||
} else {
|
||||
observedType
|
||||
}
|
||||
|
||||
val customTypeDescriptor = customSerializerRegistry.findCustomSerializer(
|
||||
clazz = observedClass,
|
||||
declaredType = observedGenericType
|
||||
)?.typeDescriptor?.toString()
|
||||
if (customTypeDescriptor != null) writer.write(customTypeDescriptor)
|
||||
else defaultAction()
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package net.corda.tools.shell
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory
|
||||
import com.google.common.io.Files
|
||||
import com.jcraft.jsch.ChannelExec
|
||||
import com.jcraft.jsch.JSch
|
||||
import com.nhaarman.mockito_kotlin.any
|
||||
@ -11,17 +10,25 @@ import com.nhaarman.mockito_kotlin.doAnswer
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import net.corda.client.jackson.JacksonSupport
|
||||
import net.corda.client.rpc.RPCException
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.inputStream
|
||||
import net.corda.core.internal.list
|
||||
import net.corda.core.internal.messaging.InternalCordaRPCOps
|
||||
import net.corda.core.messaging.ClientRpcSslOptions
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.internal.NodeStartup
|
||||
import net.corda.node.services.Permissions
|
||||
import net.corda.node.services.Permissions.Companion.all
|
||||
@ -34,6 +41,7 @@ import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.NodeHandle
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.driver.internal.NodeHandleInternal
|
||||
import net.corda.testing.internal.useSslRpcOverrides
|
||||
@ -49,9 +57,10 @@ import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.util.zip.ZipFile
|
||||
import java.util.*
|
||||
import java.util.zip.ZipInputStream
|
||||
import javax.security.auth.x500.X500Principal
|
||||
import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@ -73,14 +82,8 @@ class InteractiveShellIntegrationTest {
|
||||
fun `shell should not log in with invalid credentials`() {
|
||||
val user = User("u", "p", setOf())
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
|
||||
val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
|
||||
val node = nodeFuture.getOrThrow()
|
||||
|
||||
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
|
||||
user = "fake", password = "fake",
|
||||
hostAndPort = node.rpcAddress)
|
||||
InteractiveShell.startShell(conf)
|
||||
|
||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||
startShell("fake", "fake", node.rpcAddress)
|
||||
assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(ActiveMQSecurityException::class.java)
|
||||
}
|
||||
}
|
||||
@ -88,15 +91,9 @@ class InteractiveShellIntegrationTest {
|
||||
@Test
|
||||
fun `shell should log in with valid credentials`() {
|
||||
val user = User("u", "p", setOf())
|
||||
driver(DriverParameters(notarySpecs = emptyList())) {
|
||||
val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
|
||||
val node = nodeFuture.getOrThrow()
|
||||
|
||||
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
|
||||
user = user.username, password = user.password,
|
||||
hostAndPort = node.rpcAddress)
|
||||
|
||||
InteractiveShell.startShell(conf)
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
|
||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||
startShell(node)
|
||||
InteractiveShell.nodeInfo()
|
||||
}
|
||||
}
|
||||
@ -104,7 +101,6 @@ class InteractiveShellIntegrationTest {
|
||||
@Test
|
||||
fun `shell should log in with ssl`() {
|
||||
val user = User("mark", "dadada", setOf(all()))
|
||||
var successful = false
|
||||
|
||||
val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(testName)
|
||||
val keyStorePath = saveToKeyStore(tempFolder.root.toPath() / "keystore.jks", keyPair, cert)
|
||||
@ -114,20 +110,10 @@ class InteractiveShellIntegrationTest {
|
||||
val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password")
|
||||
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
|
||||
startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow().use { node ->
|
||||
|
||||
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
|
||||
user = user.username, password = user.password,
|
||||
hostAndPort = node.rpcAddress,
|
||||
ssl = clientSslOptions)
|
||||
|
||||
InteractiveShell.startShell(conf)
|
||||
|
||||
InteractiveShell.nodeInfo()
|
||||
successful = true
|
||||
}
|
||||
val node = startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow()
|
||||
startShell(node, clientSslOptions)
|
||||
InteractiveShell.nodeInfo()
|
||||
}
|
||||
assertThat(successful).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -142,47 +128,33 @@ class InteractiveShellIntegrationTest {
|
||||
val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password")
|
||||
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
|
||||
startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow().use { node ->
|
||||
|
||||
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
|
||||
user = user.username, password = user.password,
|
||||
hostAndPort = node.rpcAddress,
|
||||
ssl = clientSslOptions)
|
||||
|
||||
InteractiveShell.startShell(conf)
|
||||
|
||||
assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(RPCException::class.java)
|
||||
}
|
||||
val node = startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow()
|
||||
startShell(node, clientSslOptions)
|
||||
assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(RPCException::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `internal shell user should not be able to connect if node started with devMode=false`() {
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
|
||||
startNode().getOrThrow().use { node ->
|
||||
val conf = (node as NodeHandleInternal).configuration.toShellConfig()
|
||||
InteractiveShell.startShell(conf)
|
||||
assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(ActiveMQSecurityException::class.java)
|
||||
}
|
||||
val node = startNode().getOrThrow()
|
||||
val conf = (node as NodeHandleInternal).configuration.toShellConfig()
|
||||
InteractiveShell.startShell(conf)
|
||||
assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(ActiveMQSecurityException::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
fun `ssh runs flows via standalone shell`() {
|
||||
val user = User("u", "p", setOf(Permissions.startFlow<FlowICanRun>(),
|
||||
val user = User("u", "p", setOf(
|
||||
Permissions.startFlow<FlowICanRun>(),
|
||||
Permissions.invokeRpc(CordaRPCOps::registeredFlows),
|
||||
Permissions.invokeRpc(CordaRPCOps::nodeInfo)))
|
||||
driver(DriverParameters(notarySpecs = emptyList())) {
|
||||
val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
|
||||
val node = nodeFuture.getOrThrow()
|
||||
|
||||
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
|
||||
user = user.username, password = user.password,
|
||||
hostAndPort = node.rpcAddress,
|
||||
sshdPort = 2224)
|
||||
|
||||
InteractiveShell.startShell(conf)
|
||||
Permissions.invokeRpc(CordaRPCOps::nodeInfo)
|
||||
))
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
|
||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||
startShell(node, sshdPort = 2224)
|
||||
InteractiveShell.nodeInfo()
|
||||
|
||||
val session = JSch().getSession("u", "localhost", 2224)
|
||||
@ -200,7 +172,7 @@ class InteractiveShellIntegrationTest {
|
||||
|
||||
val response = String(Streams.readAll(channel.inputStream))
|
||||
|
||||
val linesWithDoneCount = response.lines().filter { line -> line.contains("Done") }
|
||||
val linesWithDoneCount = response.lines().filter { line -> "Done" in line }
|
||||
|
||||
channel.disconnect()
|
||||
session.disconnect()
|
||||
@ -213,9 +185,11 @@ class InteractiveShellIntegrationTest {
|
||||
@Ignore
|
||||
@Test
|
||||
fun `ssh run flows via standalone shell over ssl to node`() {
|
||||
val user = User("mark", "dadada", setOf(Permissions.startFlow<FlowICanRun>(),
|
||||
val user = User("mark", "dadada", setOf(
|
||||
Permissions.startFlow<FlowICanRun>(),
|
||||
Permissions.invokeRpc(CordaRPCOps::registeredFlows),
|
||||
Permissions.invokeRpc(CordaRPCOps::nodeInfo)/*all()*/))
|
||||
Permissions.invokeRpc(CordaRPCOps::nodeInfo)/*all()*/
|
||||
))
|
||||
|
||||
val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(testName)
|
||||
val keyStorePath = saveToKeyStore(tempFolder.root.toPath() / "keystore.jks", keyPair, cert)
|
||||
@ -226,14 +200,7 @@ class InteractiveShellIntegrationTest {
|
||||
var successful = false
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
|
||||
startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow().use { node ->
|
||||
|
||||
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
|
||||
user = user.username, password = user.password,
|
||||
hostAndPort = node.rpcAddress,
|
||||
ssl = clientSslOptions,
|
||||
sshdPort = 2223)
|
||||
|
||||
InteractiveShell.startShell(conf)
|
||||
startShell(node, clientSslOptions, sshdPort = 2223)
|
||||
InteractiveShell.nodeInfo()
|
||||
|
||||
val session = JSch().getSession("mark", "localhost", 2223)
|
||||
@ -251,7 +218,7 @@ class InteractiveShellIntegrationTest {
|
||||
|
||||
val response = String(Streams.readAll(channel.inputStream))
|
||||
|
||||
val linesWithDoneCount = response.lines().filter { line -> line.contains("Done") }
|
||||
val linesWithDoneCount = response.lines().filter { line -> "Done" in line }
|
||||
|
||||
channel.disconnect()
|
||||
session.disconnect() // TODO Simon make sure to close them
|
||||
@ -263,174 +230,136 @@ class InteractiveShellIntegrationTest {
|
||||
}
|
||||
|
||||
assertThat(successful).isTrue()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shell should start flow with fully qualified class name`() {
|
||||
val user = User("u", "p", setOf(all()))
|
||||
var successful = false
|
||||
driver(DriverParameters(notarySpecs = emptyList())) {
|
||||
val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
|
||||
val node = nodeFuture.getOrThrow()
|
||||
|
||||
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
|
||||
user = user.username, password = user.password,
|
||||
hostAndPort = node.rpcAddress)
|
||||
InteractiveShell.startShell(conf)
|
||||
|
||||
// setup and configure some mocks required by InteractiveShell.runFlowByNameFragment()
|
||||
val output = mock<RenderPrintWriter> {
|
||||
on { println(any<String>()) } doAnswer {
|
||||
val line = it.arguments[0]
|
||||
println("$line")
|
||||
if ((line is String) && (line.startsWith("Flow completed with result:")))
|
||||
successful = true
|
||||
}
|
||||
}
|
||||
val ansiProgressRenderer = mock<ANSIProgressRenderer> {
|
||||
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
|
||||
}
|
||||
InteractiveShell.runFlowByNameFragment(
|
||||
"NoOpFlow",
|
||||
"", output, node.rpc, ansiProgressRenderer)
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
|
||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||
startShell(node)
|
||||
val (output, lines) = mockRenderPrintWriter()
|
||||
InteractiveShell.runFlowByNameFragment(NoOpFlow::class.java.name, "", output, node.rpc, mockAnsiProgressRenderer())
|
||||
assertThat(lines.last()).startsWith("Flow completed with result:")
|
||||
}
|
||||
assertThat(successful).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shell should start flow with unique un-qualified class name`() {
|
||||
val user = User("u", "p", setOf(all()))
|
||||
var successful = false
|
||||
driver(DriverParameters(notarySpecs = emptyList())) {
|
||||
val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
|
||||
val node = nodeFuture.getOrThrow()
|
||||
|
||||
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
|
||||
user = user.username, password = user.password,
|
||||
hostAndPort = node.rpcAddress)
|
||||
InteractiveShell.startShell(conf)
|
||||
|
||||
// setup and configure some mocks required by InteractiveShell.runFlowByNameFragment()
|
||||
val output = mock<RenderPrintWriter> {
|
||||
on { println(any<String>()) } doAnswer {
|
||||
val line = it.arguments[0]
|
||||
println("$line")
|
||||
if ((line is String) && (line.startsWith("Flow completed with result:")))
|
||||
successful = true
|
||||
}
|
||||
}
|
||||
val ansiProgressRenderer = mock<ANSIProgressRenderer> {
|
||||
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
|
||||
}
|
||||
InteractiveShell.runFlowByNameFragment(
|
||||
"NoOpFlowA",
|
||||
"", output, node.rpc, ansiProgressRenderer)
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
|
||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||
startShell(node)
|
||||
val (output, lines) = mockRenderPrintWriter()
|
||||
InteractiveShell.runFlowByNameFragment("NoOpFlowA", "", output, node.rpc, mockAnsiProgressRenderer())
|
||||
assertThat(lines.last()).startsWith("Flow completed with result:")
|
||||
}
|
||||
assertThat(successful).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shell should fail to start flow with ambiguous class name`() {
|
||||
val user = User("u", "p", setOf(all()))
|
||||
var successful = false
|
||||
driver(DriverParameters(notarySpecs = emptyList())) {
|
||||
val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
|
||||
val node = nodeFuture.getOrThrow()
|
||||
|
||||
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
|
||||
user = user.username, password = user.password,
|
||||
hostAndPort = node.rpcAddress)
|
||||
InteractiveShell.startShell(conf)
|
||||
|
||||
// setup and configure some mocks required by InteractiveShell.runFlowByNameFragment()
|
||||
val output = mock<RenderPrintWriter> {
|
||||
on { println(any<String>()) } doAnswer {
|
||||
val line = it.arguments[0]
|
||||
println("$line")
|
||||
if ((line is String) && (line.startsWith("Ambiguous name provided, please be more specific.")))
|
||||
successful = true
|
||||
}
|
||||
}
|
||||
val ansiProgressRenderer = mock<ANSIProgressRenderer> {
|
||||
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
|
||||
}
|
||||
InteractiveShell.runFlowByNameFragment(
|
||||
"NoOpFlo",
|
||||
"", output, node.rpc, ansiProgressRenderer)
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
|
||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||
startShell(node)
|
||||
val (output, lines) = mockRenderPrintWriter()
|
||||
InteractiveShell.runFlowByNameFragment("NoOpFlo", "", output, node.rpc, mockAnsiProgressRenderer())
|
||||
assertThat(lines.any { it.startsWith("Ambiguous name provided, please be more specific.") }).isTrue()
|
||||
}
|
||||
assertThat(successful).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shell should start flow with partially matching class name`() {
|
||||
val user = User("u", "p", setOf(all()))
|
||||
var successful = false
|
||||
driver(DriverParameters(notarySpecs = emptyList())) {
|
||||
val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
|
||||
val node = nodeFuture.getOrThrow()
|
||||
|
||||
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
|
||||
user = user.username, password = user.password,
|
||||
hostAndPort = node.rpcAddress)
|
||||
InteractiveShell.startShell(conf)
|
||||
|
||||
// setup and configure some mocks required by InteractiveShell.runFlowByNameFragment()
|
||||
val output = mock<RenderPrintWriter> {
|
||||
on { println(any<String>()) } doAnswer {
|
||||
val line = it.arguments[0]
|
||||
println("$line")
|
||||
if ((line is String) && (line.startsWith("Flow completed with result")))
|
||||
successful = true
|
||||
}
|
||||
}
|
||||
val ansiProgressRenderer = mock<ANSIProgressRenderer> {
|
||||
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
|
||||
}
|
||||
InteractiveShell.runFlowByNameFragment(
|
||||
"Burble",
|
||||
"", output, node.rpc, ansiProgressRenderer)
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
|
||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||
startShell(node)
|
||||
val (output, lines) = mockRenderPrintWriter()
|
||||
InteractiveShell.runFlowByNameFragment("Burble", "", output, node.rpc, mockAnsiProgressRenderer())
|
||||
assertThat(lines.last()).startsWith("Flow completed with result")
|
||||
}
|
||||
assertThat(successful).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dumpCheckpoints creates zip with json file for suspended flow`() {
|
||||
val user = User("u", "p", setOf(all()))
|
||||
driver(DriverParameters(notarySpecs = emptyList())) {
|
||||
val aliceNode = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true).getOrThrow()
|
||||
val bobNode = startNode(providedName = BOB_NAME, rpcUsers = listOf(user), startInSameProcess = true).getOrThrow()
|
||||
driver(DriverParameters(startNodesInProcess = true)) {
|
||||
val aliceNode = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||
val bobNode = startNode(providedName = BOB_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||
bobNode.stop()
|
||||
|
||||
// create logs directory since the driver is not creating it
|
||||
(aliceNode.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).toFile().mkdir()
|
||||
// Create logs directory since the driver is not creating it
|
||||
(aliceNode.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).createDirectories()
|
||||
|
||||
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
|
||||
user = user.username, password = user.password,
|
||||
hostAndPort = aliceNode.rpcAddress)
|
||||
InteractiveShell.startShell(conf)
|
||||
// setup and configure some mocks required by InteractiveShell.runFlowByNameFragment()
|
||||
val output = mock<RenderPrintWriter> {
|
||||
on { println(any<String>()) } doAnswer {
|
||||
val line = it.arguments[0]
|
||||
assertNotEquals("Please try 'man run' to learn what syntax is acceptable", line)
|
||||
}
|
||||
startShell(aliceNode)
|
||||
|
||||
val linearId = UniqueIdentifier(id = UUID.fromString("7c0719f0-e489-46e8-bf3b-ee203156fc7c"))
|
||||
aliceNode.rpc.startFlow(
|
||||
::FlowForCheckpointDumping,
|
||||
MyState(
|
||||
"some random string",
|
||||
linearId,
|
||||
listOf(aliceNode.nodeInfo.singleIdentity(), bobNode.nodeInfo.singleIdentity())
|
||||
),
|
||||
bobNode.nodeInfo.singleIdentity()
|
||||
)
|
||||
|
||||
Thread.sleep(5000)
|
||||
|
||||
val (output) = mockRenderPrintWriter()
|
||||
InteractiveShell.runRPCFromString(listOf("dumpCheckpoints"), output, mock(), aliceNode.rpc as InternalCordaRPCOps, inputObjectMapper)
|
||||
|
||||
val zipFile = (aliceNode.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).list().first { "checkpoints_dump-" in it.toString() }
|
||||
val json = ZipInputStream(zipFile.inputStream()).use { zip ->
|
||||
zip.nextEntry
|
||||
ObjectMapper().readTree(zip)
|
||||
}
|
||||
|
||||
aliceNode.rpc.startFlow(::SendFlow, bobNode.nodeInfo.singleIdentity())
|
||||
assertNotNull(json["flowId"].asText())
|
||||
assertEquals(FlowForCheckpointDumping::class.java.name, json["topLevelFlowClass"].asText())
|
||||
assertEquals(linearId.id.toString(), json["topLevelFlowLogic"]["myState"]["linearId"]["id"].asText())
|
||||
assertEquals(4, json["flowCallStackSummary"].size())
|
||||
assertEquals(4, json["flowCallStack"].size())
|
||||
val sendAndReceiveJson = json["suspendedOn"]["sendAndReceive"][0]
|
||||
assertEquals(bobNode.nodeInfo.singleIdentity().toString(), sendAndReceiveJson["session"]["peer"].asText())
|
||||
assertEquals(SignedTransaction::class.qualifiedName, sendAndReceiveJson["sentPayloadType"].asText())
|
||||
}
|
||||
}
|
||||
|
||||
InteractiveShell.runRPCFromString(
|
||||
listOf("dumpCheckpoints"), output, mock(), aliceNode.rpc as InternalCordaRPCOps, inputObjectMapper)
|
||||
private fun startShell(node: NodeHandle, ssl: ClientRpcSslOptions? = null, sshdPort: Int? = null) {
|
||||
val user = node.rpcUsers[0]
|
||||
startShell(user.username, user.password, node.rpcAddress, ssl, sshdPort)
|
||||
}
|
||||
|
||||
// assert that the checkpoint dump zip has been created
|
||||
val zip = (aliceNode.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).list()
|
||||
.find { it.toString().contains("checkpoints_dump-") }
|
||||
assertNotNull(zip)
|
||||
// assert that a json file has been created for the suspended flow
|
||||
val json = ZipFile((zip!!).toFile()).entries().asSequence()
|
||||
.find { it.name.contains(SendFlow::class.simpleName!!) }
|
||||
assertNotNull(json)
|
||||
private fun startShell(user: String, password: String, address: NetworkHostAndPort, ssl: ClientRpcSslOptions? = null, sshdPort: Int? = null) {
|
||||
val conf = ShellConfiguration(
|
||||
commandsDirectory = tempFolder.newFolder().toPath(),
|
||||
user = user,
|
||||
password = password,
|
||||
hostAndPort = address,
|
||||
ssl = ssl,
|
||||
sshdPort = sshdPort
|
||||
)
|
||||
InteractiveShell.startShell(conf)
|
||||
}
|
||||
|
||||
private fun mockRenderPrintWriter(): Pair<RenderPrintWriter, List<String>> {
|
||||
val lines = ArrayList<String>()
|
||||
val writer = mock<RenderPrintWriter> {
|
||||
on { println(any<String>()) } doAnswer {
|
||||
val line = it.getArgument(0, String::class.java)
|
||||
println(">>> $line")
|
||||
lines += line
|
||||
Unit
|
||||
}
|
||||
}
|
||||
return Pair(writer, lines)
|
||||
}
|
||||
|
||||
private fun mockAnsiProgressRenderer(): ANSIProgressRenderer {
|
||||
return mock {
|
||||
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
|
||||
}
|
||||
}
|
||||
|
||||
@ -438,7 +367,6 @@ class InteractiveShellIntegrationTest {
|
||||
val objectMapper = JacksonSupport.createNonRpcMapper()
|
||||
val tf = TypeFactory.defaultInstance().withClassLoader(classLoader)
|
||||
objectMapper.typeFactory = tf
|
||||
|
||||
return objectMapper
|
||||
}
|
||||
}
|
||||
@ -470,23 +398,47 @@ class BurbleFlow : FlowLogic<Unit>() {
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class SendFlow(private val party: Party) : FlowLogic<Unit>() {
|
||||
override val progressTracker = ProgressTracker()
|
||||
@StartableByRPC
|
||||
class FlowForCheckpointDumping(private val myState: MyState, private val party: Party): FlowLogic<Unit>() {
|
||||
// Make sure any SerializeAsToken instances are not serialised
|
||||
private var services: ServiceHub? = null
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
initiateFlow(party).sendAndReceive<String>("hi").unwrap { it }
|
||||
services = serviceHub
|
||||
val tx = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()).apply {
|
||||
addOutputState(myState)
|
||||
addCommand(MyContract.Create(), listOf(ourIdentity, party).map(Party::owningKey))
|
||||
}
|
||||
val sessions = listOf(initiateFlow(party))
|
||||
val stx = serviceHub.signInitialTransaction(tx)
|
||||
subFlow(CollectSignaturesFlow(stx, sessions))
|
||||
throw IllegalStateException("The test should not get here")
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(SendFlow::class)
|
||||
class ReceiveFlow(private val session: FlowSession) : FlowLogic<Unit>() {
|
||||
override val progressTracker = ProgressTracker()
|
||||
@Suspendable
|
||||
@InitiatedBy(FlowForCheckpointDumping::class)
|
||||
class FlowForCheckpointDumpingResponder(private val session: FlowSession): FlowLogic<Unit>() {
|
||||
override fun call() {
|
||||
session.receive<String>().unwrap { it }
|
||||
session.send("hi")
|
||||
val signTxFlow = object : SignTransactionFlow(session) {
|
||||
override fun checkTransaction(stx: SignedTransaction) {
|
||||
|
||||
}
|
||||
}
|
||||
subFlow(signTxFlow)
|
||||
throw IllegalStateException("The test should not get here")
|
||||
}
|
||||
}
|
||||
|
||||
class MyContract : Contract {
|
||||
class Create : CommandData
|
||||
override fun verify(tx: LedgerTransaction) {}
|
||||
}
|
||||
|
||||
@BelongsToContract(MyContract::class)
|
||||
data class MyState(
|
||||
val data: String,
|
||||
override val linearId: UniqueIdentifier,
|
||||
override val participants: List<AbstractParty>
|
||||
) : LinearState
|
||||
|
Loading…
Reference in New Issue
Block a user