From 3633624dc63ab1fa26bdba876ace1dd79bbd2a10 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Tue, 10 Oct 2017 23:09:25 +0100 Subject: [PATCH 1/5] CORDA-553 - First steps towards evolvability Define the two transforms that will be useful for enum evolvability (see design document for more details). Furthermore, define the generic mechanism by which transform annotations on classes are encoded into the AMQP envelope With nothing to check for these annotations at either end, this is mostly a no op, but an important step toward getting evolvability in place --- .gitignore | 1 + .../CordaSerializationTransformEnumDefault.kt | 18 + .../CordaSerializationTransformRename.kt | 17 + .../amqp/AMQPDescriptorRegistry.kt | 31 ++ .../internal/serialization/amqp/Envelope.kt | 60 +++ .../internal/serialization/amqp/Schema.kt | 73 +--- .../serialization/amqp/SerializationOutput.kt | 10 +- .../serialization/amqp/SupportedTransforms.kt | 66 +++ .../serialization/amqp/TansformTypes.kt | 55 +++ .../serialization/amqp/TransformsSchema.kt | 268 ++++++++++++ .../serialization/amqp/AMQPTestUtils.kt | 19 +- .../amqp/EnumEvolvabilityTests.kt | 380 ++++++++++++++++++ .../amqp/SerializationOutputTests.kt | 2 + 13 files changed, 928 insertions(+), 72 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformEnumDefault.kt create mode 100644 core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformRename.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPDescriptorRegistry.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Envelope.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt create mode 100644 node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt diff --git a/.gitignore b/.gitignore index 1eb2b271b4..2bc98f9dfd 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ lib/quasar.jar .idea/dataSources .idea/markdown-navigator .idea/runConfigurations +.idea/dictionaries /gradle-plugins/.idea/ # Include the -parameters compiler option by default in IntelliJ required for serialization. diff --git a/core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformEnumDefault.kt b/core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformEnumDefault.kt new file mode 100644 index 0000000000..8cce317ed4 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformEnumDefault.kt @@ -0,0 +1,18 @@ +package net.corda.core.serialization + +/** + * + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class CordaSerializationTransformEnumDefaults(vararg val value: CordaSerializationTransformEnumDefault) + +/** + * + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +// When Kotlin starts writing 1.8 class files enable this, it removes the need for the wrapping annotation +//@Repeatable +annotation class CordaSerializationTransformEnumDefault(val new: String, val old: String) + diff --git a/core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformRename.kt b/core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformRename.kt new file mode 100644 index 0000000000..379505bf1b --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformRename.kt @@ -0,0 +1,17 @@ +package net.corda.core.serialization + +/** + * + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class CordaSerializationTransformRenames(vararg val value: CordaSerializationTransformRename) + +/** + * + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +// When Kotlin starts writing 1.8 class files enable this, it removes the need for the wrapping annotation +//@Repeatable +annotation class CordaSerializationTransformRename(val to: String, val from: String) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPDescriptorRegistry.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPDescriptorRegistry.kt new file mode 100644 index 0000000000..4ffeb57764 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPDescriptorRegistry.kt @@ -0,0 +1,31 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import org.apache.qpid.proton.amqp.UnsignedLong + +/** + * R3 AMQP assigned enterprise number + * + * see [here](https://www.iana.org/assignments/enterprise-numbers/enterprise-numbers) + * + * Repeated here for brevity: + * 50530 - R3 - Mike Hearn - mike&r3.com + */ +const val DESCRIPTOR_TOP_32BITS: Long = 0xc5620000 + +enum class AMQPDescriptorRegistry(val id: Long) { + + ENVELOPE(1), + SCHEMA(2), + OBJECT_DESCRIPTOR(3), + FIELD(4), + COMPOSITE_TYPE(5), + RESTRICTED_TYPE(6), + CHOICE(7), + REFERENCED_OBJECT(8), + TRANSFORM_SCHEMA(9), + TRANSFORM_ELEMENT(10), + TRANSFORM_ELEMENT_KEY(11) + ; + + val amqpDescriptor = UnsignedLong(id or DESCRIPTOR_TOP_32BITS) +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Envelope.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Envelope.kt new file mode 100644 index 0000000000..d2f73e0575 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Envelope.kt @@ -0,0 +1,60 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import org.apache.qpid.proton.amqp.DescribedType +import org.apache.qpid.proton.codec.Data +import org.apache.qpid.proton.codec.DescribedTypeConstructor + +import java.io.NotSerializableException + +/** + * This class wraps all serialized data, so that the schema can be carried along with it. We will provide various + * internal utilities to decompose and recompose with/without schema etc so that e.g. we can store objects with a + * (relationally) normalised out schema to avoid excessive duplication. + */ +// TODO: make the schema parsing lazy since mostly schemas will have been seen before and we only need it if we +// TODO: don't recognise a type descriptor. +data class Envelope(val obj: Any?, val schema: Schema, val transformsSchema: TransformsSchema) : DescribedType { + companion object : DescribedTypeConstructor { + val DESCRIPTOR = AMQPDescriptorRegistry.ENVELOPE.amqpDescriptor + val DESCRIPTOR_OBJECT = Descriptor(null, DESCRIPTOR) + + fun get(data: Data): Envelope { + val describedType = data.`object` as DescribedType + if (describedType.descriptor != DESCRIPTOR) { + throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.") + } + val list = describedType.described as List<*> + + // We need to cope with objects serialised without the transforms header element in the + // envelope + val transformSchema : Any? = when (list.size) { + 2 -> null + 3 -> list[2] + else -> throw NotSerializableException("Malformed list, bad length of ${list.size} (should be 2 or 3)") + } + + return newInstance(listOf(list[0], Schema.get(list[1]!!), TransformsSchema.newInstance(transformSchema))) + } + + // This seperation of functions is needed as this will be the entry point for the default + // AMQP decoder if one is used (see the unit tests) + override fun newInstance(described: Any?): Envelope { + val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list") + + // We need to cope with objects serialised without the transforms header element in the + // envelope + val transformSchema = when (list.size) { + 2 -> TransformsSchema.newInstance(null) + 3 -> list[2] as TransformsSchema + else -> throw NotSerializableException("Malformed list, bad length of ${list.size} (should be 2 or 3)") + } + return Envelope(list[0], list[1] as Schema, transformSchema) + } + + override fun getTypeClass(): Class<*> = Envelope::class.java + } + + override fun getDescriptor(): Any = DESCRIPTOR + + override fun getDescribed(): Any = listOf(obj, schema, transformsSchema) +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt index cfb505ab6c..0d20ca0822 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt @@ -10,7 +10,6 @@ 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 org.apache.qpid.proton.codec.Data import org.apache.qpid.proton.codec.DescribedTypeConstructor import java.io.NotSerializableException import java.lang.reflect.* @@ -18,68 +17,10 @@ import java.util.* import net.corda.nodeapi.internal.serialization.carpenter.Field as CarpenterField import net.corda.nodeapi.internal.serialization.carpenter.Schema as CarpenterSchema -/** - * R3 AMQP assigned enterprise number - * - * see [here](https://www.iana.org/assignments/enterprise-numbers/enterprise-numbers) - * - * Repeated here for brevity: - * 50530 - R3 - Mike Hearn - mike&r3.com - */ -const val DESCRIPTOR_TOP_32BITS: Long = 0xc5620000 - -const val DESCRIPTOR_DOMAIN: String = "net.corda" // "corda" + majorVersionByte + minorVersionMSB + minorVersionLSB val AmqpHeaderV1_0: OpaqueBytes = OpaqueBytes("corda\u0001\u0000\u0000".toByteArray()) -private enum class DescriptorRegistry(val id: Long) { - - ENVELOPE(1), - SCHEMA(2), - OBJECT_DESCRIPTOR(3), - FIELD(4), - COMPOSITE_TYPE(5), - RESTRICTED_TYPE(6), - CHOICE(7), - REFERENCED_OBJECT(8), - ; - - val amqpDescriptor = UnsignedLong(id or DESCRIPTOR_TOP_32BITS) -} - -/** - * This class wraps all serialized data, so that the schema can be carried along with it. We will provide various internal utilities - * to decompose and recompose with/without schema etc so that e.g. we can store objects with a (relationally) normalised out schema to - * avoid excessive duplication. - */ -// TODO: make the schema parsing lazy since mostly schemas will have been seen before and we only need it if we don't recognise a type descriptor. -data class Envelope(val obj: Any?, val schema: Schema) : DescribedType { - companion object : DescribedTypeConstructor { - val DESCRIPTOR = DescriptorRegistry.ENVELOPE.amqpDescriptor - val DESCRIPTOR_OBJECT = Descriptor(null, DESCRIPTOR) - - fun get(data: Data): Envelope { - val describedType = data.`object` as DescribedType - if (describedType.descriptor != DESCRIPTOR) { - throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.") - } - val list = describedType.described as List<*> - return newInstance(listOf(list[0], Schema.get(list[1]!!))) - } - - override fun getTypeClass(): Class<*> = Envelope::class.java - - override fun newInstance(described: Any?): Envelope { - val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list") - return Envelope(list[0], list[1] as Schema) - } - } - - override fun getDescriptor(): Any = DESCRIPTOR - - override fun getDescribed(): Any = listOf(obj, schema) -} /** * This and the classes below are OO representations of the AMQP XML schema described in the specification. Their @@ -87,7 +28,7 @@ data class Envelope(val obj: Any?, val schema: Schema) : DescribedType { */ data class Schema(val types: List) : DescribedType { companion object : DescribedTypeConstructor { - val DESCRIPTOR = DescriptorRegistry.SCHEMA.amqpDescriptor + val DESCRIPTOR = AMQPDescriptorRegistry.SCHEMA.amqpDescriptor fun get(obj: Any): Schema { val describedType = obj as DescribedType @@ -117,7 +58,7 @@ data class Descriptor(val name: Symbol?, val code: UnsignedLong? = null) : Descr constructor(name: String?) : this(Symbol.valueOf(name)) companion object : DescribedTypeConstructor { - val DESCRIPTOR = DescriptorRegistry.OBJECT_DESCRIPTOR.amqpDescriptor + val DESCRIPTOR = AMQPDescriptorRegistry.OBJECT_DESCRIPTOR.amqpDescriptor fun get(obj: Any): Descriptor { val describedType = obj as DescribedType @@ -155,7 +96,7 @@ data class Descriptor(val name: Symbol?, val code: UnsignedLong? = null) : Descr data class Field(val name: String, val type: String, val requires: List, val default: String?, val label: String?, val mandatory: Boolean, val multiple: Boolean) : DescribedType { companion object : DescribedTypeConstructor { - val DESCRIPTOR = DescriptorRegistry.FIELD.amqpDescriptor + val DESCRIPTOR = AMQPDescriptorRegistry.FIELD.amqpDescriptor fun get(obj: Any): Field { val describedType = obj as DescribedType @@ -215,7 +156,7 @@ sealed class TypeNotation : DescribedType { data class CompositeType(override val name: String, override val label: String?, override val provides: List, override val descriptor: Descriptor, val fields: List) : TypeNotation() { companion object : DescribedTypeConstructor { - val DESCRIPTOR = DescriptorRegistry.COMPOSITE_TYPE.amqpDescriptor + val DESCRIPTOR = AMQPDescriptorRegistry.COMPOSITE_TYPE.amqpDescriptor fun get(describedType: DescribedType): CompositeType { if (describedType.descriptor != DESCRIPTOR) { @@ -264,7 +205,7 @@ data class RestrictedType(override val name: String, override val descriptor: Descriptor, val choices: List) : TypeNotation() { companion object : DescribedTypeConstructor { - val DESCRIPTOR = DescriptorRegistry.RESTRICTED_TYPE.amqpDescriptor + val DESCRIPTOR = AMQPDescriptorRegistry.RESTRICTED_TYPE.amqpDescriptor fun get(describedType: DescribedType): RestrictedType { if (describedType.descriptor != DESCRIPTOR) { @@ -309,7 +250,7 @@ data class RestrictedType(override val name: String, data class Choice(val name: String, val value: String) : DescribedType { companion object : DescribedTypeConstructor { - val DESCRIPTOR = DescriptorRegistry.CHOICE.amqpDescriptor + val DESCRIPTOR = AMQPDescriptorRegistry.CHOICE.amqpDescriptor fun get(obj: Any): Choice { val describedType = obj as DescribedType @@ -338,7 +279,7 @@ data class Choice(val name: String, val value: String) : DescribedType { data class ReferencedObject(private val refCounter: Int) : DescribedType { companion object : DescribedTypeConstructor { - val DESCRIPTOR = DescriptorRegistry.REFERENCED_OBJECT.amqpDescriptor + val DESCRIPTOR = AMQPDescriptorRegistry.REFERENCED_OBJECT.amqpDescriptor fun get(obj: Any): ReferencedObject { val describedType = obj as DescribedType diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt index 87a19376c9..2c0c4ece9f 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt @@ -45,10 +45,10 @@ open class SerializationOutput(internal val serializerFactory: SerializerFactory val data = Data.Factory.create() data.withDescribed(Envelope.DESCRIPTOR_OBJECT) { withList { - // Our object writeObject(obj, this) - // The schema - writeSchema(Schema(schemaHistory.toList()), this) + val schema = Schema(schemaHistory.toList()) + writeSchema(schema, this) + writeTransformSchema(TransformsSchema.build(schema, serializerFactory), this) } } val bytes = ByteArray(data.encodedSize().toInt() + 8) @@ -66,6 +66,10 @@ open class SerializationOutput(internal val serializerFactory: SerializerFactory data.putObject(schema) } + open fun writeTransformSchema(transformsSchema: TransformsSchema, data: Data) { + data.putObject(transformsSchema) + } + internal fun writeObjectOrNull(obj: Any?, data: Data, type: Type) { if (obj == null) { data.putNull() diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt new file mode 100644 index 0000000000..3b8f3b843a --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt @@ -0,0 +1,66 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import net.corda.core.serialization.CordaSerializationTransformEnumDefaults +import net.corda.core.serialization.CordaSerializationTransformEnumDefault +import net.corda.core.serialization.CordaSerializationTransformRenames +import net.corda.core.serialization.CordaSerializationTransformRename + +/** + * Utility class that defines an instance of a transform we support + * + * @property type The transform annotation + * @property enum Maps the annotaiton onto a transform type, we expect there are multiple annotations that + * would map to a single transform type + * @property f Anonymous function that should return a list of Annotations encapsualted by the parent annotation + * that reference the transform. Notionally this allows the code that extracts transforms to work on single instances + * of a transform or a meta list of them + */ +data class SupportedTransform( + val type: Class, + val enum: TransformTypes, + val getAnnotations: (Annotation) -> List) + +/** + * Extract from an annotated class the list of annotations that refer to a particular + * transformation type when that class has multiple transforms wrapped in an + * outer annotation + */ +@Suppress("UNCHECKED_CAST") +private val wrapperExtract = { x: Annotation -> + (x::class.java.getDeclaredMethod("value").invoke(x) as Array).toList() +} + +/** + * Extract from an annotated class the list of annotations that refer to a particular + * transformation type when that class has a single decorator applied + */ +private val singleExtract = { x: Annotation -> listOf(x) } + +/** + * Utility list of all transforms we support that simplifies our generator + * + * NOTE: We have to support single instances of the transform annotations as well as the wrapping annotation + * when many instances are repeated + */ +val supportedTransforms = listOf( + SupportedTransform( + CordaSerializationTransformEnumDefaults::class.java, + TransformTypes.EnumDefault, + wrapperExtract + ), + SupportedTransform( + CordaSerializationTransformEnumDefault::class.java, + TransformTypes.EnumDefault, + singleExtract + ), + SupportedTransform( + CordaSerializationTransformRenames::class.java, + TransformTypes.Rename, + wrapperExtract + ), + SupportedTransform( + CordaSerializationTransformRename::class.java, + TransformTypes.Rename, + singleExtract + ) +) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt new file mode 100644 index 0000000000..65e49c14de --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt @@ -0,0 +1,55 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import org.apache.qpid.proton.amqp.DescribedType + +import net.corda.core.serialization.CordaSerializationTransformEnumDefault +import net.corda.core.serialization.CordaSerializationTransformEnumDefaults +import net.corda.core.serialization.CordaSerializationTransformRename +import org.apache.qpid.proton.codec.DescribedTypeConstructor +import java.io.NotSerializableException + +/** + * Enumerated type that represents each transform that can be applied to a class. Used as the key type in + * the [TransformsSchema] map for each class. + * + * @property build should be a function that takes a transform [Annotation] (currently one of + * [CordaSerializationTransformRename] or [CordaSerializationTransformEnumDefaults]) + * and constructs an instance of the corresponding [Transform] type + */ +// TODO: it would be awesome to auto build this list by scanning for transform annotations themselves +// TODO: annotated with some annotation +enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType { + EnumDefault({ a -> EnumDefaultSchemeTransform((a as CordaSerializationTransformEnumDefault).old, a.new) }) { + override fun getDescriptor(): Any = DESCRIPTOR + override fun getDescribed(): Any = ordinal + }, + Rename({ a -> RenameSchemaTransform((a as CordaSerializationTransformRename).from, a.to) }) { + override fun getDescriptor(): Any = DESCRIPTOR + override fun getDescribed(): Any = ordinal + }; + + companion object : DescribedTypeConstructor { + val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_ELEMENT_KEY.amqpDescriptor + + /** + * Used to construct an instance of the object from the serialised bytes + * + * @param obj the serialised byte object from the AMQP serialised stream + */ + override fun newInstance(obj: Any?): TransformTypes { + val describedType = obj as DescribedType + + if (describedType.descriptor != DESCRIPTOR) { + throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.") + } + + try { + return values()[describedType.described as Int] + } catch (e: IndexOutOfBoundsException) { + throw NotSerializableException("Bad ordinal value ${describedType.described}.") + } + } + + override fun getTypeClass(): Class<*> = TransformTypes::class.java + } +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt new file mode 100644 index 0000000000..d7f959e1f9 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt @@ -0,0 +1,268 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import java.util.* +import net.corda.core.serialization.CordaSerializationTransformEnumDefault +import net.corda.core.serialization.CordaSerializationTransformRename +import org.apache.qpid.proton.amqp.DescribedType +import org.apache.qpid.proton.codec.DescribedTypeConstructor +import java.io.NotSerializableException + +// NOTE: We are effectively going to replicate the annotations, we need to do this because +// we can't instantiate instances of those annotation classes and this code needs to +// work at the de-serialising end +/** + * Base class for representations of specific types of transforms as applied to a type within the + * Corda serialisation framework + */ +sealed class Transform : DescribedType { + companion object : DescribedTypeConstructor { + val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_ELEMENT.amqpDescriptor + + /** + * @param obj: a serialized instance of a described type, should be one of the + * descendants of this class + */ + private fun checkDescribed(obj: Any?): Any? { + val describedType = obj as DescribedType + + if (describedType.descriptor != DESCRIPTOR) { + throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.") + } + + return describedType.described + } + + /** + * From an encoded descendant return an instance of the specific type. Transforms are encoded into + * the schema as a list of class name and parameters.Using the class name (list element 0) + * create the appropriate class instance + * + * @param obj: a serialized instance of a described type, should be one of the + * descendants of this class + */ + override fun newInstance(obj: Any?): Transform { + val described = Transform.checkDescribed(obj) as List<*> + return when (described[0]) { + EnumDefaultSchemeTransform.typeName -> EnumDefaultSchemeTransform.newInstance(described) + RenameSchemaTransform.typeName -> RenameSchemaTransform.newInstance(described) + else -> throw NotSerializableException("Unexpected transform type ${described[0]}") + } + } + + override fun getTypeClass(): Class<*> = Transform::class.java + } + + override fun getDescriptor(): Any = DESCRIPTOR + + /** + * Return a string representation of a transform in terms of key / value pairs, used + * by the serializer to encode arbitrary transforms + */ + abstract fun params(): String + + abstract val name: String +} + +/** + * Transform to be used on an Enumerated Type whenever a new element is added + * + * @property old The value the [new] instance should default to when not available + * @property new the value (as a String) that has been added + */ +class EnumDefaultSchemeTransform(val old: String, val new: String) : Transform() { + companion object : DescribedTypeConstructor { + /** + * Value encoded into the schema that identifies a transform as this type + */ + val typeName = "EnumDefault" + + override fun newInstance(obj: Any?): EnumDefaultSchemeTransform { + val described = obj as List<*> + val old = described[1] as? String ?: throw IllegalStateException("Was expecting \"old\" as a String") + val new = described[2] as? String ?: throw IllegalStateException("Was expecting \"new\" as a String") + return EnumDefaultSchemeTransform(old, new) + } + + override fun getTypeClass(): Class<*> = EnumDefaultSchemeTransform::class.java + } + + @Suppress("UNUSED") + constructor (annotation: CordaSerializationTransformEnumDefault) : this(annotation.old, annotation.new) + + override fun getDescribed(): Any = listOf(name, old, new) + override fun params() = "old=${old.esc()} new=${new.esc()}" + + override fun equals(other: Any?): Boolean { + val o = other as? EnumDefaultSchemeTransform ?: return super.equals(other) + return o.new == new && o.old == old + } + + override val name: String get() = typeName +} + +/** + * Transform applied to either a class or enum where a property is renamed + * + * @property from the name at time of change of the property + * @property to the new name of the property + */ +class RenameSchemaTransform(val from: String, val to: String) : Transform() { + companion object : DescribedTypeConstructor { + /** + * Value encoded into the schema that identifies a transform as this type + */ + val typeName = "Rename" + + override fun newInstance(obj: Any?): RenameSchemaTransform { + val described = obj as List<*> + val from = described[1] as? String ?: throw IllegalStateException("Was expecting \"from\" as a String") + val to = described[2] as? String ?: throw IllegalStateException("Was expecting \"to\" as a String") + return RenameSchemaTransform(from, to) + } + + override fun getTypeClass(): Class<*> = RenameSchemaTransform::class.java + } + + @Suppress("UNUSED") + constructor (annotation: CordaSerializationTransformRename) : this(annotation.from, annotation.to) + + override fun getDescribed(): Any = listOf(name, from, to) + + override fun params() = "from=${from.esc()} to=${to.esc()}" + + override fun equals(other: Any?): Boolean { + val o = other as? RenameSchemaTransform ?: return super.equals(other) + return o.from == from && o.to == to + } + + override val name: String get() = typeName +} + + +/** + * @property types is a list of serialised types that have transforms, each list element is a + */ +data class TransformsSchema(val types: Map>>) : DescribedType { + companion object : DescribedTypeConstructor { + val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_SCHEMA.amqpDescriptor + + /** + * Prepare a schema for encoding, takes all of the types being transmitted and inspects each + * one for any transform annotations. If there are any build up a set that can be + * encoded into the AMQP [Envelope] + * + * @param schema should be a [Schema] generated for a serialised data structure + * @param sf should be provided by the same serialization context that generated the schema + */ + fun build(schema: Schema, sf: SerializerFactory): TransformsSchema { + val rtn = mutableMapOf>>() + + schema.types.forEach { type -> + val clazz = try { + sf.classloader.loadClass(type.name) + } catch (e: ClassNotFoundException) { + return@forEach + } + + supportedTransforms.forEach { transform -> + clazz.getAnnotation(transform.type)?.let { list -> + transform.getAnnotations(list).forEach { + val t = transform.enum.build(it) + + val m = rtn.computeIfAbsent(type.name) { + EnumMap>(TransformTypes::class.java) + } + + // we're explicitly rejecting repeated annotations, whilst it's fine and we'd just + // ignore them it feels like a good thing to alert the user to since this is + // more than likely a typo in their code so best make it an actual error + if (m.computeIfAbsent(transform.enum) { mutableListOf() }.filter { it == t }.isNotEmpty()) { + throw NotSerializableException( + "Repeated unique transformation annotation of type ${t.name}") + } + + m[transform.enum]!!.add(t) + } + } + } + } + + return TransformsSchema(rtn) + } + + override fun getTypeClass(): Class<*> = TransformsSchema::class.java + + /** + * Constructs an instance of the object from the serialised form of an instance + * of this object + */ + override fun newInstance(described: Any?): TransformsSchema { + val rtn = mutableMapOf>>() + + val describedType = described as? DescribedType ?: return TransformsSchema(rtn) + + if (describedType.descriptor != DESCRIPTOR) { + throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.") + } + + val map = describedType.described as? Map<*, *> ?: + throw NotSerializableException("Transform schema must be encoded as a map") + + + map.forEach { type -> + val fingerprint = type.key as? String ?: + throw NotSerializableException("Fingerprint must be encoded as a string") + + rtn[fingerprint] = EnumMap>(TransformTypes::class.java) + + (type.value as Map<*, *>).forEach { transformType, transforms -> + val transform = TransformTypes.newInstance(transformType) + + rtn[fingerprint]!![transform] = mutableListOf() + (transforms as List<*>).forEach { + rtn[fingerprint]!![TransformTypes.newInstance(transformType)]?.add(Transform.newInstance(it)) ?: + throw NotSerializableException("De-serialization error with transform for class " + + "${type.key} ${transform.name}") + } + } + } + + return TransformsSchema(rtn) + } + } + + override fun getDescriptor(): Any = DESCRIPTOR + + override fun getDescribed(): Any = types + + override fun toString(): String { + data class Indent(val indent: String) { + @Suppress("UNUSED") constructor(i: Indent) : this(" ${i.indent}") + override fun toString() = indent + } + + val sb = StringBuilder("") + val indent = Indent("") + + sb.appendln("$indent") + types.forEach { type -> + val indent = Indent(indent) + sb.appendln("$indent") + type.value.forEach { transform -> + val indent = Indent(indent) + sb.appendln("$indent") + transform.value.forEach { + val indent = Indent(indent) + sb.appendln("$indent") + } + sb.appendln("$indent") + } + sb.appendln("$indent") + } + sb.appendln("$indent") + + return sb.toString() + } +} + +private fun String.esc() = "\"$this\"" diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt index 4f9b7b6872..43fe82a6ee 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt @@ -18,18 +18,31 @@ class TestSerializationOutput( if (verbose) println(schema) super.writeSchema(schema, data) } + + override fun writeTransformSchema(transformsSchema: TransformsSchema, data: Data) { + if(verbose) { + println ("Writing Transform Schema") + println (transformsSchema) + } + super.writeTransformSchema(transformsSchema, data) + } } fun testName(): String = Thread.currentThread().stackTrace[2].methodName -data class BytesAndSchema(val obj: SerializedBytes, val schema: Schema) +data class BytesAndSchemas( + val obj: SerializedBytes, + val schema: Schema, + val transformsSchema: TransformsSchema) // Extension for the serialize routine that returns the scheme encoded into the // bytes as well as the bytes for simple testing @Throws(NotSerializableException::class) -fun SerializationOutput.serializeAndReturnSchema(obj: T): BytesAndSchema { +fun SerializationOutput.serializeAndReturnSchema(obj: T): BytesAndSchemas { try { - return BytesAndSchema(_serialize(obj), Schema(schemaHistory.toList())) + val blob = _serialize(obj) + val schema = Schema(schemaHistory.toList()) + return BytesAndSchemas(blob, schema, TransformsSchema.build(schema, serializerFactory)) } finally { andFinally() } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt new file mode 100644 index 0000000000..f2b0d7dec4 --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt @@ -0,0 +1,380 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import net.corda.core.serialization.CordaSerializationTransformEnumDefault +import net.corda.core.serialization.CordaSerializationTransformEnumDefaults +import net.corda.core.serialization.CordaSerializationTransformRename +import net.corda.core.serialization.CordaSerializationTransformRenames +import org.assertj.core.api.Assertions +import org.junit.Test +import java.io.NotSerializableException +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class EnumEvolvabilityTests { + companion object { + val VERBOSE = false + } + + enum class NotAnnotated { + A, B, C, D + } + + @CordaSerializationTransformEnumDefaults() + enum class MissingDefaults { + A, B, C, D + } + + @CordaSerializationTransformRenames() + enum class MissingRenames { + A, B, C, D + } + + @CordaSerializationTransformEnumDefault("D", "A") + enum class AnnotatedEnumOnce { + A, B, C, D + } + + @CordaSerializationTransformEnumDefaults( + CordaSerializationTransformEnumDefault("E", "D"), + CordaSerializationTransformEnumDefault("D", "A")) + enum class AnnotatedEnumTwice { + A, B, C, D, E + } + + @CordaSerializationTransformRename("E", "D") + enum class RenameEnumOnce { + A, B, C, E + } + + @CordaSerializationTransformRenames( + CordaSerializationTransformRename("E", "C"), + CordaSerializationTransformRename("F", "D")) + enum class RenameEnumTwice { + A, B, E, F + } + + @Test + fun noAnnotation() { + data class C (val n: NotAnnotated) + + val sf = testDefaultFactory() + val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(NotAnnotated.A)) + + assertEquals(2, bAndS.schema.types.size) + assertEquals(0, bAndS.transformsSchema.types.size) + } + + @Test + fun missingDefaults() { + data class C (val m: MissingDefaults) + + val sf = testDefaultFactory() + val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(MissingDefaults.A)) + + assertEquals(2, bAndS.schema.types.size) + assertEquals(0, bAndS.transformsSchema.types.size) + } + + @Test + fun missingRenames() { + data class C (val m: MissingRenames) + + val sf = testDefaultFactory() + val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(MissingRenames.A)) + + assertEquals(2, bAndS.schema.types.size) + assertEquals(0, bAndS.transformsSchema.types.size) + + } + + @Test + fun defaultAnnotationIsAddedToEnvelope() { + data class C (val annotatedEnum: AnnotatedEnumOnce) + + val sf = testDefaultFactory() + val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(AnnotatedEnumOnce.D)) + + // only the enum is decorated so schema sizes should be different (2 objects, only one evolved) + assertEquals(2, bAndS.schema.types.size) + assertEquals(1, bAndS.transformsSchema.types.size) + assertEquals (AnnotatedEnumOnce::class.java.name, bAndS.transformsSchema.types.keys.first()) + + val schema = bAndS.transformsSchema.types.values.first() + + assertEquals(1, schema.size) + assertTrue (schema.keys.contains(TransformTypes.EnumDefault)) + assertEquals (1, schema[TransformTypes.EnumDefault]!!.size) + assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemeTransform) + assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).new) + assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).old) + } + + @Test + fun doubleDefaultAnnotationIsAddedToEnvelope() { + data class C (val annotatedEnum: AnnotatedEnumTwice) + + val sf = testDefaultFactory() + val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(AnnotatedEnumTwice.E)) + + assertEquals(2, bAndS.schema.types.size) + assertEquals(1, bAndS.transformsSchema.types.size) + assertEquals (AnnotatedEnumTwice::class.java.name, bAndS.transformsSchema.types.keys.first()) + + val schema = bAndS.transformsSchema.types.values.first() + + assertEquals(1, schema.size) + assertTrue (schema.keys.contains(TransformTypes.EnumDefault)) + assertEquals (2, schema[TransformTypes.EnumDefault]!!.size) + assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemeTransform) + assertEquals ("E", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).new) + assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).old) + assertTrue (schema[TransformTypes.EnumDefault]!![1] is EnumDefaultSchemeTransform) + assertEquals ("D", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemeTransform).new) + assertEquals ("A", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemeTransform).old) + } + + @Test + fun defaultAnnotationIsAddedToEnvelopeAndDeserialised() { + data class C (val annotatedEnum: AnnotatedEnumOnce) + + val sf = testDefaultFactory() + val sb = TestSerializationOutput(VERBOSE, sf).serialize(C(AnnotatedEnumOnce.D)) + val db = DeserializationInput(sf).deserializeAndReturnEnvelope(sb) + + // as with the serialisation stage, de-serialising the object we should see two + // types described in the header with one of those having transforms + assertEquals(2, db.envelope.schema.types.size) + assertEquals(1, db.envelope.transformsSchema.types.size) + + val eName = AnnotatedEnumOnce::class.java.name + val types = db.envelope.schema.types + val transforms = db.envelope.transformsSchema.types + + assertEquals(1, types.filter { it.name == eName }.size) + assertTrue(eName in transforms) + + val schema = transforms[eName] + + assertTrue (schema!!.keys.contains(TransformTypes.EnumDefault)) + assertEquals (1, schema[TransformTypes.EnumDefault]!!.size) + assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemeTransform) + assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).new) + assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).old) + } + + @Test + fun doubleDefaultAnnotationIsAddedToEnvelopeAndDeserialised() { + data class C(val annotatedEnum: AnnotatedEnumTwice) + + val sf = testDefaultFactory() + val sb = TestSerializationOutput(VERBOSE, sf).serialize(C(AnnotatedEnumTwice.E)) + val db = DeserializationInput(sf).deserializeAndReturnEnvelope(sb) + + // as with the serialisation stage, de-serialising the object we should see two + // types described in the header with one of those having transforms + assertEquals(2, db.envelope.schema.types.size) + assertEquals(1, db.envelope.transformsSchema.types.size) + + val transforms = db.envelope.transformsSchema.types + + assertTrue (transforms.contains(AnnotatedEnumTwice::class.java.name)) + assertTrue (transforms[AnnotatedEnumTwice::class.java.name]!!.contains(TransformTypes.EnumDefault)) + assertEquals (2, transforms[AnnotatedEnumTwice::class.java.name]!![TransformTypes.EnumDefault]!!.size) + + val enumDefaults = transforms[AnnotatedEnumTwice::class.java.name]!![TransformTypes.EnumDefault]!! + + assertEquals("E", (enumDefaults[0] as EnumDefaultSchemeTransform).new) + assertEquals("D", (enumDefaults[0] as EnumDefaultSchemeTransform).old) + assertEquals("D", (enumDefaults[1] as EnumDefaultSchemeTransform).new) + assertEquals("A", (enumDefaults[1] as EnumDefaultSchemeTransform).old) + } + + @Test + fun renameAnnotationIsAdded() { + data class C (val annotatedEnum: RenameEnumOnce) + + val sf = testDefaultFactory() + + // Serialise the object + val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(RenameEnumOnce.E)) + + assertEquals(2, bAndS.schema.types.size) + assertEquals(1, bAndS.transformsSchema.types.size) + assertEquals (RenameEnumOnce::class.java.name, bAndS.transformsSchema.types.keys.first()) + + val serialisedSchema = bAndS.transformsSchema.types[RenameEnumOnce::class.java.name]!! + + assertEquals(1, serialisedSchema.size) + assertTrue(serialisedSchema.containsKey(TransformTypes.Rename)) + assertEquals(1, serialisedSchema[TransformTypes.Rename]!!.size) + assertEquals("D", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).from) + assertEquals("E", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).to) + + // Now de-serialise the blob + val cAndS = DeserializationInput(sf).deserializeAndReturnEnvelope(bAndS.obj) + + assertEquals(2, cAndS.envelope.schema.types.size) + assertEquals(1, cAndS.envelope.transformsSchema.types.size) + assertEquals (RenameEnumOnce::class.java.name, cAndS.envelope.transformsSchema.types.keys.first()) + + val deserialisedSchema = cAndS.envelope.transformsSchema.types[RenameEnumOnce::class.java.name]!! + + assertEquals(1, deserialisedSchema.size) + assertTrue(deserialisedSchema.containsKey(TransformTypes.Rename)) + assertEquals(1, deserialisedSchema[TransformTypes.Rename]!!.size) + assertEquals("D", (deserialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).from) + assertEquals("E", (deserialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).to) + } + + @Test + fun doubleRenameAnnotationIsAdded() { + data class C (val annotatedEnum: RenameEnumTwice) + + val sf = testDefaultFactory() + + // Serialise the object + val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(RenameEnumTwice.F)) + + assertEquals(2, bAndS.schema.types.size) + assertEquals(1, bAndS.transformsSchema.types.size) + assertEquals (RenameEnumTwice::class.java.name, bAndS.transformsSchema.types.keys.first()) + + val serialisedSchema = bAndS.transformsSchema.types[RenameEnumTwice::class.java.name]!! + + assertEquals(1, serialisedSchema.size) + assertTrue(serialisedSchema.containsKey(TransformTypes.Rename)) + assertEquals(2, serialisedSchema[TransformTypes.Rename]!!.size) + assertEquals("C", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).from) + assertEquals("E", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).to) + assertEquals("D", (serialisedSchema[TransformTypes.Rename]!![1] as RenameSchemaTransform).from) + assertEquals("F", (serialisedSchema[TransformTypes.Rename]!![1] as RenameSchemaTransform).to) + + // Now de-serialise the blob + val cAndS = DeserializationInput(sf).deserializeAndReturnEnvelope(bAndS.obj) + + assertEquals(2, cAndS.envelope.schema.types.size) + assertEquals(1, cAndS.envelope.transformsSchema.types.size) + assertEquals (RenameEnumTwice::class.java.name, cAndS.envelope.transformsSchema.types.keys.first()) + + val deserialisedSchema = cAndS.envelope.transformsSchema.types[RenameEnumTwice::class.java.name]!! + + assertEquals(1, deserialisedSchema.size) + assertTrue(deserialisedSchema.containsKey(TransformTypes.Rename)) + assertEquals(2, deserialisedSchema[TransformTypes.Rename]!!.size) + assertEquals("C", (deserialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).from) + assertEquals("E", (deserialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).to) + assertEquals("D", (deserialisedSchema[TransformTypes.Rename]!![1] as RenameSchemaTransform).from) + assertEquals("F", (deserialisedSchema[TransformTypes.Rename]!![1] as RenameSchemaTransform).to) + } + + @CordaSerializationTransformRename(from="A", to="X") + @CordaSerializationTransformEnumDefault(old = "X", new="E") + enum class RenameAndExtendEnum { + X, B, C, D, E + } + + @Test + fun bothAnnotationTypes() { + data class C (val annotatedEnum: RenameAndExtendEnum) + + val sf = testDefaultFactory() + + // Serialise the object + val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(RenameAndExtendEnum.X)) + + assertEquals(2, bAndS.schema.types.size) + assertEquals(1, bAndS.transformsSchema.types.size) + assertEquals (RenameAndExtendEnum::class.java.name, bAndS.transformsSchema.types.keys.first()) + + val serialisedSchema = bAndS.transformsSchema.types[RenameAndExtendEnum::class.java.name]!! + + // This time there should be two distinct transform types (all previous tests have had only + // a single type + assertEquals(2, serialisedSchema.size) + assertTrue (serialisedSchema.containsKey(TransformTypes.Rename)) + assertTrue (serialisedSchema.containsKey(TransformTypes.EnumDefault)) + + assertEquals(1, serialisedSchema[TransformTypes.Rename]!!.size) + assertEquals("A", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).from) + assertEquals("X", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).to) + + assertEquals(1, serialisedSchema[TransformTypes.EnumDefault]!!.size) + assertEquals("E", (serialisedSchema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).new) + assertEquals("X", (serialisedSchema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).old) + } + + @CordaSerializationTransformEnumDefaults ( + CordaSerializationTransformEnumDefault("D", "A"), + CordaSerializationTransformEnumDefault("D", "A")) + enum class RepeatedAnnotation { + A, B, C, D, E + } + + @Test + fun repeatedAnnotation() { + data class C (val a: RepeatedAnnotation) + + val sf = testDefaultFactory() + + Assertions.assertThatThrownBy { + TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(RepeatedAnnotation.A)) + }.isInstanceOf(NotSerializableException::class.java) + } + + @CordaSerializationTransformEnumDefault("D", "A") + enum class E1 { + A, B, C, D + } + + @CordaSerializationTransformEnumDefaults ( + CordaSerializationTransformEnumDefault("D", "A"), + CordaSerializationTransformEnumDefault("E", "A")) + enum class E2 { + A, B, C, D, E + } + + @CordaSerializationTransformEnumDefaults (CordaSerializationTransformEnumDefault("D", "A")) + enum class E3 { + A, B, C, D + } + + @Test + fun multiEnums() { + data class A (val a: E1, val b: E2) + data class B (val a: E3, val b: A, val c: E1) + data class C (val a: B, val b: E2, val c: E3) + + val c = C(B(E3.A,A(E1.A,E2.B),E1.C),E2.B,E3.A) + + val sf = testDefaultFactory() + + // Serialise the object + val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(c) + + println (bAndS.transformsSchema) + + // we have six types and three of those, the enums, should have transforms + assertEquals(6, bAndS.schema.types.size) + assertEquals(3, bAndS.transformsSchema.types.size) + + assertTrue (E1::class.java.name in bAndS.transformsSchema.types) + assertTrue (E2::class.java.name in bAndS.transformsSchema.types) + assertTrue (E3::class.java.name in bAndS.transformsSchema.types) + + val e1S = bAndS.transformsSchema.types[E1::class.java.name]!! + val e2S = bAndS.transformsSchema.types[E2::class.java.name]!! + val e3S = bAndS.transformsSchema.types[E3::class.java.name]!! + + assertEquals(1, e1S.size) + assertEquals(1, e2S.size) + assertEquals(1, e3S.size) + + assertTrue(TransformTypes.EnumDefault in e1S) + assertTrue(TransformTypes.EnumDefault in e2S) + assertTrue(TransformTypes.EnumDefault in e3S) + + assertEquals(1, e1S[TransformTypes.EnumDefault]!!.size) + assertEquals(2, e2S[TransformTypes.EnumDefault]!!.size) + assertEquals(1, e3S[TransformTypes.EnumDefault]!!.size) + } +} diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt index 31136f0723..038234548f 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt @@ -164,6 +164,8 @@ class SerializationOutputTests { this.register(Choice.DESCRIPTOR, Choice.Companion) this.register(RestrictedType.DESCRIPTOR, RestrictedType.Companion) this.register(ReferencedObject.DESCRIPTOR, ReferencedObject.Companion) + this.register(TransformsSchema.DESCRIPTOR, TransformsSchema.Companion) + this.register(TransformTypes.DESCRIPTOR, TransformTypes.Companion) } EncoderImpl(decoder) decoder.setByteBuffer(ByteBuffer.wrap(bytes.bytes, 8, bytes.size - 8)) From bc12f87a2448c88165e2eac25040de9127cf022c Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Thu, 19 Oct 2017 16:17:35 +0100 Subject: [PATCH 2/5] CORDA-553 - Review Comments --- .../CordaSerializationTransformEnumDefault.kt | 75 ++++++++++++++++++ .../CordaSerializationTransformRename.kt | 18 +++++ .../amqp/AMQPDescriptorRegistry.kt | 11 ++- .../internal/serialization/amqp/Envelope.kt | 27 ++++--- .../internal/serialization/amqp/Schema.kt | 2 +- .../serialization/amqp/SerializerFactory.kt | 2 + .../serialization/amqp/SupportedTransforms.kt | 16 ++-- .../serialization/amqp/TansformTypes.kt | 3 +- .../serialization/amqp/TransformsSchema.kt | 78 +++++++++++-------- .../amqp/JavaSerializationOutputTests.java | 2 + .../amqp/EnumEvolvabilityTests.kt | 27 +++++++ 11 files changed, 205 insertions(+), 56 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformEnumDefault.kt b/core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformEnumDefault.kt index 8cce317ed4..aa1e9c7154 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformEnumDefault.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformEnumDefault.kt @@ -1,14 +1,89 @@ package net.corda.core.serialization /** + * This annotation is used to mark an enumerated type as having had multiple members added, It acts + * as a container annotation for instances of [CordaSerializationTransformEnumDefault], each of which + * details individual additions. * + * @property value an array of [CordaSerializationTransformEnumDefault]. + * + * NOTE: Order is important, new values should always be added before any others + * + * ``` + * // initial implementation + * enum class ExampleEnum { + * A, B, C + * } + * + * // First alteration + * @CordaSerializationTransformEnumDefaults( + * CordaSerializationTransformEnumDefault("D", "C")) + * enum class ExampleEnum { + * A, B, C, D + * } + * + * // Second alteration, new transform is placed at the head of the list + * @CordaSerializationTransformEnumDefaults( + * CordaSerializationTransformEnumDefault("E", "C"), + * CordaSerializationTransformEnumDefault("D", "C")) + * enum class ExampleEnum { + * A, B, C, D, E + * } + * ``` + * + * IMPORTANT - Once added (and in production) do NOT remove old annotations. See documentation for + * more discussion on this point!. */ @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) annotation class CordaSerializationTransformEnumDefaults(vararg val value: CordaSerializationTransformEnumDefault) /** + * This annotation is used to mark an enumerated type as having had a new constant appended to it. For + * each additional constant added a new annotation should be appended to the class. If more than one + * is required the wrapper annotation [CordaSerializationTransformEnumDefaults] should be used to + * encapsulate them * + * @property new [String] equivalent of the value of the new constant + * @property old [String] equivalent of the value of the existing constant that deserialisers should + * favour when de-serialising a value they have no corresponding value for + * + * For Example + * + * Enum before modification: + * ``` + * enum class ExampleEnum { + * A, B, C + * } + * ``` + * + * Assuming at some point a new constant is added it is required we have some mechanism by which to tell + * nodes with an older version of the class on their Class Path what to do if they attempt to deserialize + * an example of the class with that new value + * + * ``` + * @CordaSerializationTransformEnumDefault("D", "C") + * enum class ExampleEnum { + * A, B, C, D + * } + * ``` + * + * So, on deserialisation treat any instance of the enum that is encoded as D as C + * + * Adding a second new constant requires the wrapper annotation [CordaSerializationTransformEnumDefaults] + * + * ``` + * @CordaSerializationTransformEnumDefaults( + * CordaSerializationTransformEnumDefault("E", "D"), + * CordaSerializationTransformEnumDefault("D", "C")) + * enum class ExampleEnum { + * A, B, C, D, E + * } + * ``` + * + * It's fine to assign the second new value a default that may not be present in all versions as in this + * case it will work down the transform hierarchy until it finds a value it can apply, in this case it would + * try E -> D -> C (when E -> D fails) */ @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) diff --git a/core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformRename.kt b/core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformRename.kt index 379505bf1b..c8ae7e2aec 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformRename.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformRename.kt @@ -1,13 +1,31 @@ package net.corda.core.serialization /** + * This annotation is used to mark a class as having had multiple elements renamed as a container annotation for + * instances of [CordaSerializationTransformRename], each of which details an individual rename. * + * @property value an array of [CordaSerializationTransformRename] + * + * NOTE: Order is important, new values should always be added before existing + * + * IMPORTANT - Once added (and in production) do NOT remove old annotations. See documentation for + * more discussion on this point!. */ @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) annotation class CordaSerializationTransformRenames(vararg val value: CordaSerializationTransformRename) +// TODO When we have class renaming update the docs /** + * This annotation is used to mark a class has having had a property element. It is used by the + * AMQP deserialiser to allow instances with different versions of the class on their Class Path + * to successfully deserialize the object + * + * NOTE: Renaming of the class itself is not be done with this annotation. For class renaming + * see ??? + * + * @property to [String] representation of the properties new name + * @property from [String] representation of the properties old new * */ @Target(AnnotationTarget.CLASS) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPDescriptorRegistry.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPDescriptorRegistry.kt index 4ffeb57764..0c9a06b8fd 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPDescriptorRegistry.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPDescriptorRegistry.kt @@ -10,10 +10,17 @@ import org.apache.qpid.proton.amqp.UnsignedLong * Repeated here for brevity: * 50530 - R3 - Mike Hearn - mike&r3.com */ -const val DESCRIPTOR_TOP_32BITS: Long = 0xc5620000 +const val DESCRIPTOR_TOP_32BITS: Long = 0xc562 shl(32 + 16) +/** + * AMQP desriptor ID's for our custom types. + * + * NEVER DELETE OR CHANGE THE ID ASSOCIATED WITH A TYPE + * + * these are encoded as part of a serialised blob and doing so would render us unable to + * de-serialise that blob!!! + */ enum class AMQPDescriptorRegistry(val id: Long) { - ENVELOPE(1), SCHEMA(2), OBJECT_DESCRIPTOR(3), diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Envelope.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Envelope.kt index d2f73e0575..358cecbb48 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Envelope.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Envelope.kt @@ -18,6 +18,14 @@ data class Envelope(val obj: Any?, val schema: Schema, val transformsSchema: Tra val DESCRIPTOR = AMQPDescriptorRegistry.ENVELOPE.amqpDescriptor val DESCRIPTOR_OBJECT = Descriptor(null, DESCRIPTOR) + // described list should either be two or three elements long + private const val ENVELOPE_WITHOUT_TRANSFORMS = 2 + private const val ENVELOPE_WITH_TRANSFORMS = 3 + + private const val BLOB_IDX = 0 + private const val SCHEMA_IDX = 1 + private const val TRANSFORMS_SCHEMA_IDX = 2 + fun get(data: Data): Envelope { val describedType = data.`object` as DescribedType if (describedType.descriptor != DESCRIPTOR) { @@ -27,28 +35,29 @@ data class Envelope(val obj: Any?, val schema: Schema, val transformsSchema: Tra // We need to cope with objects serialised without the transforms header element in the // envelope - val transformSchema : Any? = when (list.size) { - 2 -> null - 3 -> list[2] + val transformSchema: Any? = when (list.size) { + ENVELOPE_WITHOUT_TRANSFORMS -> null + ENVELOPE_WITH_TRANSFORMS -> list[TRANSFORMS_SCHEMA_IDX] else -> throw NotSerializableException("Malformed list, bad length of ${list.size} (should be 2 or 3)") } - return newInstance(listOf(list[0], Schema.get(list[1]!!), TransformsSchema.newInstance(transformSchema))) + return newInstance(listOf(list[BLOB_IDX], Schema.get(list[SCHEMA_IDX]!!), + TransformsSchema.newInstance(transformSchema))) } - // This seperation of functions is needed as this will be the entry point for the default - // AMQP decoder if one is used (see the unit tests) + // This separation of functions is needed as this will be the entry point for the default + // AMQP decoder if one is used (see the unit tests). override fun newInstance(described: Any?): Envelope { val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list") // We need to cope with objects serialised without the transforms header element in the // envelope val transformSchema = when (list.size) { - 2 -> TransformsSchema.newInstance(null) - 3 -> list[2] as TransformsSchema + ENVELOPE_WITHOUT_TRANSFORMS -> TransformsSchema.newInstance(null) + ENVELOPE_WITH_TRANSFORMS -> list[TRANSFORMS_SCHEMA_IDX] as TransformsSchema else -> throw NotSerializableException("Malformed list, bad length of ${list.size} (should be 2 or 3)") } - return Envelope(list[0], list[1] as Schema, transformSchema) + return Envelope(list[BLOB_IDX], list[SCHEMA_IDX] as Schema, transformSchema) } override fun getTypeClass(): Class<*> = Envelope::class.java diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt index 0d20ca0822..bb38933ed0 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt @@ -17,11 +17,11 @@ import java.util.* import net.corda.nodeapi.internal.serialization.carpenter.Field as CarpenterField import net.corda.nodeapi.internal.serialization.carpenter.Schema as CarpenterSchema +const val DESCRIPTOR_DOMAIN: String = "net.corda" // "corda" + majorVersionByte + minorVersionMSB + minorVersionLSB val AmqpHeaderV1_0: OpaqueBytes = OpaqueBytes("corda\u0001\u0000\u0000".toByteArray()) - /** * 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. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt index da8ae1afc6..5075593ace 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt @@ -34,6 +34,8 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { private val serializersByType = ConcurrentHashMap>() private val serializersByDescriptor = ConcurrentHashMap>() private val customSerializers = CopyOnWriteArrayList>() + val transformsCache = ConcurrentHashMap>>() + open val classCarpenter = ClassCarpenter(cl, whitelist) val classloader: ClassLoader get() = classCarpenter.classloader diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt index 3b8f3b843a..90ef03d79b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt @@ -1,19 +1,19 @@ package net.corda.nodeapi.internal.serialization.amqp -import net.corda.core.serialization.CordaSerializationTransformEnumDefaults import net.corda.core.serialization.CordaSerializationTransformEnumDefault -import net.corda.core.serialization.CordaSerializationTransformRenames +import net.corda.core.serialization.CordaSerializationTransformEnumDefaults import net.corda.core.serialization.CordaSerializationTransformRename +import net.corda.core.serialization.CordaSerializationTransformRenames /** - * Utility class that defines an instance of a transform we support + * Utility class that defines an instance of a transform we support. * - * @property type The transform annotation + * @property type The transform annotation. * @property enum Maps the annotaiton onto a transform type, we expect there are multiple annotations that - * would map to a single transform type - * @property f Anonymous function that should return a list of Annotations encapsualted by the parent annotation + * would map to a single transform type. + * @property getAnnotations Anonymous function that should return a list of Annotations encapsualted by the parent annotation * that reference the transform. Notionally this allows the code that extracts transforms to work on single instances - * of a transform or a meta list of them + * of a transform or a meta list of them. */ data class SupportedTransform( val type: Class, @@ -37,7 +37,7 @@ private val wrapperExtract = { x: Annotation -> private val singleExtract = { x: Annotation -> listOf(x) } /** - * Utility list of all transforms we support that simplifies our generator + * Utility list of all transforms we support that simplifies our generation code * * NOTE: We have to support single instances of the transform annotations as well as the wrapping annotation * when many instances are repeated diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt index 65e49c14de..28f5b7e27c 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt @@ -1,10 +1,9 @@ package net.corda.nodeapi.internal.serialization.amqp -import org.apache.qpid.proton.amqp.DescribedType - import net.corda.core.serialization.CordaSerializationTransformEnumDefault import net.corda.core.serialization.CordaSerializationTransformEnumDefaults import net.corda.core.serialization.CordaSerializationTransformRename +import org.apache.qpid.proton.amqp.DescribedType import org.apache.qpid.proton.codec.DescribedTypeConstructor import java.io.NotSerializableException diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt index d7f959e1f9..54836bc158 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt @@ -1,11 +1,11 @@ package net.corda.nodeapi.internal.serialization.amqp -import java.util.* import net.corda.core.serialization.CordaSerializationTransformEnumDefault import net.corda.core.serialization.CordaSerializationTransformRename import org.apache.qpid.proton.amqp.DescribedType import org.apache.qpid.proton.codec.DescribedTypeConstructor import java.io.NotSerializableException +import java.util.* // NOTE: We are effectively going to replicate the annotations, we need to do this because // we can't instantiate instances of those annotation classes and this code needs to @@ -14,7 +14,7 @@ import java.io.NotSerializableException * Base class for representations of specific types of transforms as applied to a type within the * Corda serialisation framework */ -sealed class Transform : DescribedType { +abstract class Transform : DescribedType { companion object : DescribedTypeConstructor { val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_ELEMENT.amqpDescriptor @@ -92,10 +92,10 @@ class EnumDefaultSchemeTransform(val old: String, val new: String) : Transform() override fun getDescribed(): Any = listOf(name, old, new) override fun params() = "old=${old.esc()} new=${new.esc()}" - override fun equals(other: Any?): Boolean { - val o = other as? EnumDefaultSchemeTransform ?: return super.equals(other) - return o.new == new && o.old == old - } + override fun equals(other: Any?) = ( + (other is EnumDefaultSchemeTransform && other.new == new && other.old == old) || super.equals(other)) + + override fun hashCode() = (17 * new.hashCode()) + old.hashCode() override val name: String get() = typeName } @@ -130,17 +130,21 @@ class RenameSchemaTransform(val from: String, val to: String) : Transform() { override fun params() = "from=${from.esc()} to=${to.esc()}" - override fun equals(other: Any?): Boolean { - val o = other as? RenameSchemaTransform ?: return super.equals(other) - return o.from == from && o.to == to - } + override fun equals(other: Any?) = ( + (other is RenameSchemaTransform && other.from == from && other.to == to) || super.equals(other)) + + override fun hashCode() = (11 * from.hashCode()) + to.hashCode() override val name: String get() = typeName } /** - * @property types is a list of serialised types that have transforms, each list element is a + * Represents the set of all transforms that can be a applied to all classes represented as part of + * an AMQP schema. It forms a part of the AMQP envelope alongside the [Schema] and the serialized bytes + * + * @property types maps class names to a map of transformation types. In turn those transformation types + * are each a list of instances o that transform. */ data class TransformsSchema(val types: Map>>) : DescribedType { companion object : DescribedTypeConstructor { @@ -158,31 +162,38 @@ data class TransformsSchema(val types: Map>>() schema.types.forEach { type -> - val clazz = try { - sf.classloader.loadClass(type.name) - } catch (e: ClassNotFoundException) { - return@forEach - } + sf.transformsCache.computeIfAbsent(type.name) { + val transforms = EnumMap>(TransformTypes::class.java) + try { + val clazz = sf.classloader.loadClass(type.name) - supportedTransforms.forEach { transform -> - clazz.getAnnotation(transform.type)?.let { list -> - transform.getAnnotations(list).forEach { - val t = transform.enum.build(it) + supportedTransforms.forEach { transform -> + clazz.getAnnotation(transform.type)?.let { list -> + transform.getAnnotations(list).forEach { annotation -> + val t = transform.enum.build(annotation) - val m = rtn.computeIfAbsent(type.name) { - EnumMap>(TransformTypes::class.java) + // we're explicitly rejecting repeated annotations, whilst it's fine and we'd just + // ignore them it feels like a good thing to alert the user to since this is + // more than likely a typo in their code so best make it an actual error + if (transforms.computeIfAbsent(transform.enum) { mutableListOf() } + .filter { t == it }.isNotEmpty()) { + throw NotSerializableException( + "Repeated unique transformation annotation of type ${t.name}") + } + + transforms[transform.enum]!!.add(t) + } } - - // we're explicitly rejecting repeated annotations, whilst it's fine and we'd just - // ignore them it feels like a good thing to alert the user to since this is - // more than likely a typo in their code so best make it an actual error - if (m.computeIfAbsent(transform.enum) { mutableListOf() }.filter { it == t }.isNotEmpty()) { - throw NotSerializableException( - "Repeated unique transformation annotation of type ${t.name}") - } - - m[transform.enum]!!.add(t) } + } catch (_: ClassNotFoundException) { + // if we can't load the class we'll end up caching an empty list which is fine as that + // list, on lookup, won't be included in the schema because it's empty + } + + transforms + }.apply { + if (isNotEmpty()) { + rtn[type.name] = this } } } @@ -198,7 +209,6 @@ data class TransformsSchema(val types: Map>>() - val describedType = described as? DescribedType ?: return TransformsSchema(rtn) if (describedType.descriptor != DESCRIPTOR) { @@ -208,7 +218,6 @@ data class TransformsSchema(val types: Map ?: throw NotSerializableException("Transform schema must be encoded as a map") - map.forEach { type -> val fingerprint = type.key as? String ?: throw NotSerializableException("Fingerprint must be encoded as a string") @@ -238,6 +247,7 @@ data class TransformsSchema(val types: Map Date: Thu, 26 Oct 2017 16:54:36 +0100 Subject: [PATCH 3/5] CORDA-553 - Cope with future transforms --- .../internal/serialization/amqp/Envelope.kt | 1 + .../serialization/amqp/SupportedTransforms.kt | 14 +- .../serialization/amqp/TansformTypes.kt | 25 ++- .../serialization/amqp/TransformsSchema.kt | 57 +++++-- .../amqp/EnumEvolvabilityTests.kt | 73 ++++++--- .../serialization/amqp/EvolvabilityTests.kt | 146 ++++++++---------- ...EnumEvolvabilityTests.testUnknownTransform | Bin 0 -> 785 bytes ...bilityTests.addAdditionalParamNotMandatory | Bin 273 -> 292 bytes .../EvolvabilityTests.addAndRemoveParameters | Bin 390 -> 403 bytes ...yTests.addMandatoryFieldWithAltConstructor | Bin 285 -> 298 bytes ...dMandatoryFieldWithAltReorderedConstructor | Bin 387 -> 400 bytes ...FieldWithAltReorderedConstructorAndRemoval | Bin 403 -> 416 bytes .../amqp/EvolvabilityTests.changeSubType | Bin 616 -> 629 bytes .../amqp/EvolvabilityTests.multiVersion.1 | Bin 294 -> 307 bytes .../amqp/EvolvabilityTests.multiVersion.2 | Bin 327 -> 340 bytes .../amqp/EvolvabilityTests.multiVersion.3 | Bin 372 -> 385 bytes ...volvabilityTests.multiVersionWithRemoval.1 | Bin 338 -> 351 bytes ...volvabilityTests.multiVersionWithRemoval.2 | Bin 392 -> 405 bytes ...volvabilityTests.multiVersionWithRemoval.3 | Bin 425 -> 438 bytes .../amqp/EvolvabilityTests.removeParameters | Bin 378 -> 391 bytes ...vabilityTests.simpleOrderSwapDifferentType | Bin 311 -> 324 bytes .../EvolvabilityTests.simpleOrderSwapSameType | Bin 302 -> 315 bytes 22 files changed, 198 insertions(+), 118 deletions(-) create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.testUnknownTransform diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Envelope.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Envelope.kt index 358cecbb48..5b489c5d84 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Envelope.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Envelope.kt @@ -57,6 +57,7 @@ data class Envelope(val obj: Any?, val schema: Schema, val transformsSchema: Tra ENVELOPE_WITH_TRANSFORMS -> list[TRANSFORMS_SCHEMA_IDX] as TransformsSchema else -> throw NotSerializableException("Malformed list, bad length of ${list.size} (should be 2 or 3)") } + return Envelope(list[BLOB_IDX], list[SCHEMA_IDX] as Schema, transformSchema) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt index 90ef03d79b..df3f4f8d45 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt @@ -36,11 +36,17 @@ private val wrapperExtract = { x: Annotation -> */ private val singleExtract = { x: Annotation -> listOf(x) } +// Transform annotation used to test the handling of transforms the de-serialising node doesn't understand. At +// some point test cases will have been created with this transform applied. +//@Target(AnnotationTarget.CLASS) +//@Retention(AnnotationRetention.RUNTIME) +//annotation class UnknownTransformAnnotation(val a: Int, val b: Int, val c: Int) + /** - * Utility list of all transforms we support that simplifies our generation code + * Utility list of all transforms we support that simplifies our generation code. * * NOTE: We have to support single instances of the transform annotations as well as the wrapping annotation - * when many instances are repeated + * when many instances are repeated. */ val supportedTransforms = listOf( SupportedTransform( @@ -63,4 +69,8 @@ val supportedTransforms = listOf( TransformTypes.Rename, singleExtract ) + //,SupportedTransform( + // UnknownTransformAnnotation::class.java, + // TransformTypes.UnknownTest, + // singleExtract) ) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt index 28f5b7e27c..de4f2d8703 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt @@ -14,11 +14,21 @@ import java.io.NotSerializableException * @property build should be a function that takes a transform [Annotation] (currently one of * [CordaSerializationTransformRename] or [CordaSerializationTransformEnumDefaults]) * and constructs an instance of the corresponding [Transform] type + * + * DO NOT REORDER THE CONSTANTS!!! Please append any new entries to the end */ // TODO: it would be awesome to auto build this list by scanning for transform annotations themselves // TODO: annotated with some annotation enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType { - EnumDefault({ a -> EnumDefaultSchemeTransform((a as CordaSerializationTransformEnumDefault).old, a.new) }) { + /** + * Placeholder entry for future transforms where a node receives a transform we've subsequently + * added and thus the de-serialising node doesn't know about that transform. + */ + Unknown({ UnknownTransform() }) { + override fun getDescriptor(): Any = DESCRIPTOR + override fun getDescribed(): Any = ordinal + }, + EnumDefault({ a -> EnumDefaultSchemaTransform((a as CordaSerializationTransformEnumDefault).old, a.new) }) { override fun getDescriptor(): Any = DESCRIPTOR override fun getDescribed(): Any = ordinal }, @@ -26,6 +36,13 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType override fun getDescriptor(): Any = DESCRIPTOR override fun getDescribed(): Any = ordinal }; + // Transform used to test the unknown handler, leave this at as the final constant, uncomment + // when regenerating test cases - if Java had a pre-processor this would be much neater + // + //UnknownTest({ a -> UnknownTestTransform((a as UnknownTransformAnnotation).a, a.b, a.c)}) { + // override fun getDescriptor(): Any = DESCRIPTOR + // override fun getDescribed(): Any = ordinal + //}; companion object : DescribedTypeConstructor { val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_ELEMENT_KEY.amqpDescriptor @@ -42,10 +59,10 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.") } - try { - return values()[describedType.described as Int] + return try { + values()[describedType.described as Int] } catch (e: IndexOutOfBoundsException) { - throw NotSerializableException("Bad ordinal value ${describedType.described}.") + values()[0] } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt index 54836bc158..2bde766c2c 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt @@ -37,15 +37,18 @@ abstract class Transform : DescribedType { * the schema as a list of class name and parameters.Using the class name (list element 0) * create the appropriate class instance * + * For future proofing any unknown transform types are not treated as errors, rather we + * simply create a placeholder object so we can ignore it + * * @param obj: a serialized instance of a described type, should be one of the * descendants of this class */ override fun newInstance(obj: Any?): Transform { val described = Transform.checkDescribed(obj) as List<*> return when (described[0]) { - EnumDefaultSchemeTransform.typeName -> EnumDefaultSchemeTransform.newInstance(described) + EnumDefaultSchemaTransform.typeName -> EnumDefaultSchemaTransform.newInstance(described) RenameSchemaTransform.typeName -> RenameSchemaTransform.newInstance(described) - else -> throw NotSerializableException("Unexpected transform type ${described[0]}") + else -> UnknownTransform() } } @@ -63,27 +66,64 @@ abstract class Transform : DescribedType { abstract val name: String } +/** + * Transform type placeholder that allows for backward compatibility. Should a noce recieve + * a transform type it doesn't recognise, we can will use this as a placeholder + */ +class UnknownTransform : Transform() { + companion object : DescribedTypeConstructor { + val typeName = "UnknownTransform" + + override fun newInstance(obj: Any?) = UnknownTransform() + + override fun getTypeClass(): Class<*> = UnknownTransform::class.java + } + + override fun getDescribed(): Any = emptyList() + override fun params() = "" + + override val name: String get() = typeName +} + +class UnknownTestTransform(val a: Int, val b: Int, val c: Int) : Transform() { + companion object : DescribedTypeConstructor { + val typeName = "UnknownTest" + + override fun newInstance(obj: Any?) : UnknownTestTransform { + val described = obj as List<*> + return UnknownTestTransform(described[1] as Int, described[2] as Int, described[3] as Int) + } + + override fun getTypeClass(): Class<*> = UnknownTransform::class.java + } + + override fun getDescribed(): Any = listOf(name, a, b, c) + override fun params() = "" + + override val name: String get() = typeName +} + /** * Transform to be used on an Enumerated Type whenever a new element is added * * @property old The value the [new] instance should default to when not available * @property new the value (as a String) that has been added */ -class EnumDefaultSchemeTransform(val old: String, val new: String) : Transform() { - companion object : DescribedTypeConstructor { +class EnumDefaultSchemaTransform(val old: String, val new: String) : Transform() { + companion object : DescribedTypeConstructor { /** * Value encoded into the schema that identifies a transform as this type */ val typeName = "EnumDefault" - override fun newInstance(obj: Any?): EnumDefaultSchemeTransform { + override fun newInstance(obj: Any?): EnumDefaultSchemaTransform { val described = obj as List<*> val old = described[1] as? String ?: throw IllegalStateException("Was expecting \"old\" as a String") val new = described[2] as? String ?: throw IllegalStateException("Was expecting \"new\" as a String") - return EnumDefaultSchemeTransform(old, new) + return EnumDefaultSchemaTransform(old, new) } - override fun getTypeClass(): Class<*> = EnumDefaultSchemeTransform::class.java + override fun getTypeClass(): Class<*> = EnumDefaultSchemaTransform::class.java } @Suppress("UNUSED") @@ -93,7 +133,7 @@ class EnumDefaultSchemeTransform(val old: String, val new: String) : Transform() override fun params() = "old=${old.esc()} new=${new.esc()}" override fun equals(other: Any?) = ( - (other is EnumDefaultSchemeTransform && other.new == new && other.old == old) || super.equals(other)) + (other is EnumDefaultSchemaTransform && other.new == new && other.old == old) || super.equals(other)) override fun hashCode() = (17 * new.hashCode()) + old.hashCode() @@ -138,7 +178,6 @@ class RenameSchemaTransform(val from: String, val to: String) : Transform() { override val name: String get() = typeName } - /** * Represents the set of all transforms that can be a applied to all classes represented as part of * an AMQP schema. It forms a part of the AMQP envelope alongside the [Schema] and the serialized bytes diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt index 2e333e15e7..2fffc13b28 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt @@ -1,16 +1,19 @@ package net.corda.nodeapi.internal.serialization.amqp -import net.corda.core.serialization.CordaSerializationTransformEnumDefault -import net.corda.core.serialization.CordaSerializationTransformEnumDefaults -import net.corda.core.serialization.CordaSerializationTransformRename -import net.corda.core.serialization.CordaSerializationTransformRenames +import net.corda.core.serialization.* import org.assertj.core.api.Assertions import org.junit.Test +import java.io.File import java.io.NotSerializableException +import java.net.URI import kotlin.test.assertEquals import kotlin.test.assertTrue class EnumEvolvabilityTests { +// var localPath = "file:///Users/katelynbaker/srcs-ide/corda/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp" + var localPath = "file:///home/katelyn/srcs/corda/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp" + + companion object { val VERBOSE = false } @@ -104,9 +107,9 @@ class EnumEvolvabilityTests { assertEquals(1, schema.size) assertTrue (schema.keys.contains(TransformTypes.EnumDefault)) assertEquals (1, schema[TransformTypes.EnumDefault]!!.size) - assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemeTransform) - assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).new) - assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).old) + assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform) + assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new) + assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old) } @Test @@ -125,12 +128,12 @@ class EnumEvolvabilityTests { assertEquals(1, schema.size) assertTrue (schema.keys.contains(TransformTypes.EnumDefault)) assertEquals (2, schema[TransformTypes.EnumDefault]!!.size) - assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemeTransform) - assertEquals ("E", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).new) - assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).old) - assertTrue (schema[TransformTypes.EnumDefault]!![1] is EnumDefaultSchemeTransform) - assertEquals ("D", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemeTransform).new) - assertEquals ("A", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemeTransform).old) + assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform) + assertEquals ("E", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new) + assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old) + assertTrue (schema[TransformTypes.EnumDefault]!![1] is EnumDefaultSchemaTransform) + assertEquals ("D", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemaTransform).new) + assertEquals ("A", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemaTransform).old) } @Test @@ -157,9 +160,9 @@ class EnumEvolvabilityTests { assertTrue (schema!!.keys.contains(TransformTypes.EnumDefault)) assertEquals (1, schema[TransformTypes.EnumDefault]!!.size) - assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemeTransform) - assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).new) - assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).old) + assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform) + assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new) + assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old) } @Test @@ -183,10 +186,10 @@ class EnumEvolvabilityTests { val enumDefaults = transforms[AnnotatedEnumTwice::class.java.name]!![TransformTypes.EnumDefault]!! - assertEquals("E", (enumDefaults[0] as EnumDefaultSchemeTransform).new) - assertEquals("D", (enumDefaults[0] as EnumDefaultSchemeTransform).old) - assertEquals("D", (enumDefaults[1] as EnumDefaultSchemeTransform).new) - assertEquals("A", (enumDefaults[1] as EnumDefaultSchemeTransform).old) + assertEquals("E", (enumDefaults[0] as EnumDefaultSchemaTransform).new) + assertEquals("D", (enumDefaults[0] as EnumDefaultSchemaTransform).old) + assertEquals("D", (enumDefaults[1] as EnumDefaultSchemaTransform).new) + assertEquals("A", (enumDefaults[1] as EnumDefaultSchemaTransform).old) } @Test @@ -299,8 +302,8 @@ class EnumEvolvabilityTests { assertEquals("X", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).to) assertEquals(1, serialisedSchema[TransformTypes.EnumDefault]!!.size) - assertEquals("E", (serialisedSchema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).new) - assertEquals("X", (serialisedSchema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).old) + assertEquals("E", (serialisedSchema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new) + assertEquals("X", (serialisedSchema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old) } @CordaSerializationTransformEnumDefaults ( @@ -402,6 +405,32 @@ class EnumEvolvabilityTests { assertEquals (sb1.transformsSchema.types[AnnotatedEnumOnce::class.java.name], sb2.transformsSchema.types[AnnotatedEnumOnce::class.java.name]) + } + + // @UnknownTransformAnnotation (10, 20, 30) + enum class WithUnknownTest { + A, B, C, D + } + + data class WrapsUnknown(val unknown: WithUnknownTest) + + // To regenerate the types for this test uncomment the UnknownTransformAnnotation from + // TransformTypes.kt and SupportedTransforms.kt + @Test + fun testUnknownTransform() { + val resource = "EnumEvolvabilityTests.testUnknownTransform" + val sf = testDefaultFactory() + + //File(URI("$localPath/$resource")).writeBytes( + // SerializationOutput(sf).serialize(WrapsUnknown(WithUnknownTest.D)).bytes) + + val path = EvolvabilityTests::class.java.getResource(resource) + val sb1 = File(path.toURI()).readBytes() + + val envelope = DeserializationInput(sf).deserializeAndReturnEnvelope(SerializedBytes(sb1)).envelope + + assertTrue(envelope.transformsSchema.types.containsKey(WithUnknownTest::class.java.name)) + assertTrue(envelope.transformsSchema.types[WithUnknownTest::class.java.name]!!.containsKey(TransformTypes.Unknown)) } } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt index 9c4a949384..f5ab608e44 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt @@ -5,36 +5,40 @@ import net.corda.core.serialization.SerializedBytes import org.junit.Test import java.io.File import java.io.NotSerializableException +import java.net.URI import kotlin.test.assertEquals // To regenerate any of the binary test files do the following // +// 0. set localPath accordingly // 1. Uncomment the code where the original form of the class is defined in the test // 2. Comment out the rest of the test // 3. Run the test // 4. Using the printed path copy that file to the resources directory // 5. Comment back out the generation code and uncomment the actual test class EvolvabilityTests { + // When regenerating the test files this needs to be set to the file system location of the resource files + var localPath = "file://////corda/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp" @Test fun simpleOrderSwapSameType() { val sf = testDefaultFactory() - val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.simpleOrderSwapSameType") - val f = File(path.toURI()) + val resource= "EvolvabilityTests.simpleOrderSwapSameType" val A = 1 val B = 2 // Original version of the class for the serialised version of this class - // // data class C (val a: Int, val b: Int) // val sc = SerializationOutput(sf).serialize(C(A, B)) - // f.writeBytes(sc.bytes) - // println (path) + // File(URI("$localPath/$resource")).writeBytes(sc.bytes) // new version of the class, in this case the order of the parameters has been swapped data class C(val b: Int, val a: Int) + val path = EvolvabilityTests::class.java.getResource(resource) + val f = File(path.toURI()) + val sc2 = f.readBytes() val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -45,21 +49,20 @@ class EvolvabilityTests { @Test fun simpleOrderSwapDifferentType() { val sf = testDefaultFactory() - val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.simpleOrderSwapDifferentType") - val f = File(path.toURI()) val A = 1 val B = "two" + val resource = "EvolvabilityTests.simpleOrderSwapDifferentType" // Original version of the class as it was serialised - // // data class C (val a: Int, val b: String) // val sc = SerializationOutput(sf).serialize(C(A, B)) - // f.writeBytes(sc.bytes) - // println (path) + // File(URI("$localPath/$resource")).writeBytes(sc.bytes) // new version of the class, in this case the order of the parameters has been swapped data class C(val b: String, val a: Int) + val path = EvolvabilityTests::class.java.getResource(resource) + val f = File(path.toURI()) val sc2 = f.readBytes() val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -70,18 +73,18 @@ class EvolvabilityTests { @Test fun addAdditionalParamNotMandatory() { val sf = testDefaultFactory() - val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.addAdditionalParamNotMandatory") - val f = File(path.toURI()) val A = 1 + val resource = "EvolvabilityTests.addAdditionalParamNotMandatory" // Original version of the class as it was serialised - // // data class C(val a: Int) // val sc = SerializationOutput(sf).serialize(C(A)) - // f.writeBytes(sc.bytes) - // println ("Path = $path") + // File(URI("$localPath/$resource")).writeBytes(sc.bytes) + data class C(val a: Int, val b: Int?) + val path = EvolvabilityTests::class.java.getResource(resource) + val f = File(path.toURI()) val sc2 = f.readBytes() val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -119,22 +122,21 @@ class EvolvabilityTests { @Test fun removeParameters() { val sf = testDefaultFactory() - val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.removeParameters") - val f = File(path.toURI()) + val resource = "EvolvabilityTests.removeParameters" val A = 1 val B = "two" val C = "three" val D = 4 // Original version of the class as it was serialised - // // data class CC(val a: Int, val b: String, val c: String, val d: Int) // val scc = SerializationOutput(sf).serialize(CC(A, B, C, D)) - // f.writeBytes(scc.bytes) - // println ("Path = $path") + // File(URI("$localPath/$resource")).writeBytes(scc.bytes) data class CC(val b: String, val d: Int) + val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.removeParameters") + val f = File(path.toURI()) val sc2 = f.readBytes() val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -146,23 +148,23 @@ class EvolvabilityTests { @Test fun addAndRemoveParameters() { val sf = testDefaultFactory() - val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.addAndRemoveParameters") - val f = File(path.toURI()) val A = 1 val B = "two" val C = "three" val D = 4 val E = null + val resource = "EvolvabilityTests.addAndRemoveParameters" + // Original version of the class as it was serialised - // // data class CC(val a: Int, val b: String, val c: String, val d: Int) // val scc = SerializationOutput(sf).serialize(CC(A, B, C, D)) - // f.writeBytes(scc.bytes) - // println ("Path = $path") + // File(URI("$localPath/$resource")).writeBytes(scc.bytes) data class CC(val a: Int, val e: Boolean?, val d: Int) + val path = EvolvabilityTests::class.java.getResource(resource) + val f = File(path.toURI()) val sc2 = f.readBytes() val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -174,16 +176,13 @@ class EvolvabilityTests { @Test fun addMandatoryFieldWithAltConstructor() { val sf = testDefaultFactory() - val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.addMandatoryFieldWithAltConstructor") - val f = File(path.toURI()) val A = 1 + val resource = "EvolvabilityTests.addMandatoryFieldWithAltConstructor" // Original version of the class as it was serialised - // // data class CC(val a: Int) // val scc = SerializationOutput(sf).serialize(CC(A)) - // f.writeBytes(scc.bytes) - // println ("Path = $path") + // File(URI("$localPath/$resource")).writeBytes(scc.bytes) @Suppress("UNUSED") data class CC(val a: Int, val b: String) { @@ -191,6 +190,8 @@ class EvolvabilityTests { constructor (a: Int) : this(a, "hello") } + val path = EvolvabilityTests::class.java.getResource(resource) + val f = File(path.toURI()) val sc2 = f.readBytes() val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -228,19 +229,15 @@ class EvolvabilityTests { @Test fun addMandatoryFieldWithAltReorderedConstructor() { val sf = testDefaultFactory() - val path = EvolvabilityTests::class.java.getResource( - "EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor") - val f = File(path.toURI()) + val resource = "EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor" val A = 1 val B = 100 val C = "This is not a banana" // Original version of the class as it was serialised - // // data class CC(val a: Int, val b: Int, val c: String) // val scc = SerializationOutput(sf).serialize(CC(A, B, C)) - // f.writeBytes(scc.bytes) - // println ("Path = $path") + // File(URI("$localPath/$resource")).writeBytes(scc.bytes) @Suppress("UNUSED") data class CC(val a: Int, val b: Int, val c: String, val d: String) { @@ -250,6 +247,8 @@ class EvolvabilityTests { constructor (c: String, a: Int, b: Int) : this(a, b, c, "wibble") } + val path = EvolvabilityTests::class.java.getResource(resource) + val f = File(path.toURI()) val sc2 = f.readBytes() val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -262,20 +261,15 @@ class EvolvabilityTests { @Test fun addMandatoryFieldWithAltReorderedConstructorAndRemoval() { val sf = testDefaultFactory() - val path = EvolvabilityTests::class.java.getResource( - "EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval") - val f = File(path.toURI()) + val resource = "EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval" val A = 1 @Suppress("UNUSED_VARIABLE") val B = 100 val C = "This is not a banana" // Original version of the class as it was serialised - // // data class CC(val a: Int, val b: Int, val c: String) - // val scc = SerializationOutput(sf).serialize(CC(A, B, C)) - // f.writeBytes(scc.bytes) - // println ("Path = $path") + // File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(CC(A, B, C)).bytes) // b is removed, d is added data class CC(val a: Int, val c: String, val d: String) { @@ -286,6 +280,8 @@ class EvolvabilityTests { constructor (c: String, a: Int) : this(a, c, "wibble") } + val path = EvolvabilityTests::class.java.getResource(resource) + val f = File(path.toURI()) val sc2 = f.readBytes() val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -297,9 +293,9 @@ class EvolvabilityTests { @Test fun multiVersion() { val sf = testDefaultFactory() - val path1 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersion.1") - val path2 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersion.2") - val path3 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersion.3") + val resource1 = "EvolvabilityTests.multiVersion.1" + val resource2 = "EvolvabilityTests.multiVersion.2" + val resource3 = "EvolvabilityTests.multiVersion.3" val a = 100 val b = 200 @@ -310,24 +306,15 @@ class EvolvabilityTests { // // Version 1: // data class C (val a: Int, val b: Int) - // - // val scc = SerializationOutput(sf).serialize(C(a, b)) - // File(path1.toURI()).writeBytes(scc.bytes) - // println ("Path = $path1") + // File(URI("$localPath/$resource1")).writeBytes(SerializationOutput(sf).serialize(C(a, b)).bytes) // // Version 2 - add param c // data class C (val c: Int, val b: Int, val a: Int) - // - // val scc = SerializationOutput(sf).serialize(C(c, b, a)) - // File(path2.toURI()).writeBytes(scc.bytes) - // println ("Path = $path2") + // File(URI("$localPath/$resource2")).writeBytes(SerializationOutput(sf).serialize(C(c, b, a)).bytes) // // Version 3 - add param d // data class C (val b: Int, val c: Int, val d: Int, val a: Int) - // - // val scc = SerializationOutput(sf).serialize(C(b, c, d, a)) - // File(path3.toURI()).writeBytes(scc.bytes) - // println ("Path = $path3") + // File(URI("$localPath/$resource3")).writeBytes(SerializationOutput(sf).serialize(C(b, c, d, a)).bytes) @Suppress("UNUSED") data class C(val e: Int, val c: Int, val b: Int, val a: Int, val d: Int) { @@ -341,6 +328,10 @@ class EvolvabilityTests { constructor (a: Int, b: Int, c: Int, d: Int) : this(-1, c, b, a, d) } + val path1 = EvolvabilityTests::class.java.getResource(resource1) + val path2 = EvolvabilityTests::class.java.getResource(resource2) + val path3 = EvolvabilityTests::class.java.getResource(resource3) + val sb1 = File(path1.toURI()).readBytes() val db1 = DeserializationInput(sf).deserialize(SerializedBytes(sb1)) @@ -372,24 +363,21 @@ class EvolvabilityTests { @Test fun changeSubType() { val sf = testDefaultFactory() - val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.changeSubType") - val f = File(path.toURI()) + val resource = "EvolvabilityTests.changeSubType" val oa = 100 val ia = 200 // Original version of the class as it was serialised - // // data class Inner (val a: Int) // data class Outer (val a: Int, val b: Inner) - // val scc = SerializationOutput(sf).serialize(Outer(oa, Inner (ia))) - // f.writeBytes(scc.bytes) - // println ("Path = $path") + // File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(Outer(oa, Inner (ia))).bytes) // Add a parameter to inner but keep outer unchanged data class Inner(val a: Int, val b: String?) - data class Outer(val a: Int, val b: Inner) + val path = EvolvabilityTests::class.java.getResource(resource) + val f = File(path.toURI()) val sc2 = f.readBytes() val outer = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -401,9 +389,10 @@ class EvolvabilityTests { @Test fun multiVersionWithRemoval() { val sf = testDefaultFactory() - val path1 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersionWithRemoval.1") - val path2 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersionWithRemoval.2") - val path3 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersionWithRemoval.3") + + val resource1 = "EvolvabilityTests.multiVersionWithRemoval.1" + val resource2 = "EvolvabilityTests.multiVersionWithRemoval.2" + val resource3 = "EvolvabilityTests.multiVersionWithRemoval.3" @Suppress("UNUSED_VARIABLE") val a = 100 @@ -417,24 +406,15 @@ class EvolvabilityTests { // // Version 1: // data class C (val a: Int, val b: Int, val c: Int) + // File(URI("$localPath/$resource1")).writeBytes(SerializationOutput(sf).serialize(C(a, b, c)).bytes) // - // val scc = SerializationOutput(sf).serialize(C(a, b, c)) - // File(path1.toURI()).writeBytes(scc.bytes) - // println ("Path = $path1") - // - // Version 2 - add param c + // Version 2 - remove property a, add property e // data class C (val b: Int, val c: Int, val d: Int, val e: Int) - // - // val scc = SerializationOutput(sf).serialize(C(b, c, d, e)) - // File(path2.toURI()).writeBytes(scc.bytes) - // println ("Path = $path2") + // File(URI("$localPath/$resource2")).writeBytes(SerializationOutput(sf).serialize(C(b, c, d, e)).bytes) // // Version 3 - add param d // data class C (val b: Int, val c: Int, val d: Int, val e: Int, val f: Int) - // - // val scc = SerializationOutput(sf).serialize(C(b, c, d, e, f)) - // File(path3.toURI()).writeBytes(scc.bytes) - // println ("Path = $path3") + // File(URI("$localPath/$resource3")).writeBytes(SerializationOutput(sf).serialize(C(b, c, d, e, f)).bytes) @Suppress("UNUSED") data class C(val b: Int, val c: Int, val d: Int, val e: Int, val f: Int, val g: Int) { @@ -451,6 +431,10 @@ class EvolvabilityTests { constructor (b: Int, c: Int, d: Int, e: Int, f: Int) : this(b, c, d, e, f, -1) } + val path1 = EvolvabilityTests::class.java.getResource(resource1) + val path2 = EvolvabilityTests::class.java.getResource(resource2) + val path3 = EvolvabilityTests::class.java.getResource(resource3) + val sb1 = File(path1.toURI()).readBytes() val db1 = DeserializationInput(sf).deserialize(SerializedBytes(sb1)) diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.testUnknownTransform b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.testUnknownTransform new file mode 100644 index 0000000000000000000000000000000000000000..74c557f8841e8ae8104dd447af0628e58fc06bd7 GIT binary patch literal 785 zcmYe!FG@*dWME)u_zwg}lNcB;FfcIv0tzrQELO@(EzwH`sjxCjc1;g+^m46;$_NiH zb9AmUGS5f}w6#5;$B3dXIV#sJr7FnK&@>>l*f=o4(k#*>vK*+6jcFmHO9(UE9wwkY zjQ@c4fGlME3S=AX6_huOzi7FEK~2IJGDuQb=SEI+3#F)1@Av!pU4wYa2MCA=uHpg1%yJ1@UH&%qUL0rLShCSrW& zaG;P8u8HM93j0F#Qix#-gQ?=Y%#sYK>p<>xb#QQWg4@S-;658otZ{H%$dZ#;TmrWV zJveYhqQe1S7Puz%1MI*kbzI13fF|U$kkJrL$ax{75t@+8LPle_5a+=TCYpHlpaCLP bafdJ;P((y0*8vfhh1^J?AHo$P5+Vlx^;tFt literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParamNotMandatory b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParamNotMandatory index 8e37059133d3a9853cee8fff322dc0c207a23114..7892b7f89b2195eab0d703c515fd399b6dbf28f7 100644 GIT binary patch delta 115 zcmbQpw1i0}Ilm|+k&%Icq2WIe98F?iyuiS~$O{x;W|(+ZMfw0UV+bQ$lIg%P#si0; r{E4BtEFeu2j~S{%HM1NLU|-0XxR5zBuf%mBqk)5?6I?InK}H4utUWRT delta 70 zcmZ3&G?7U(Ilm|+k&%Icp#g{)jwUfM9{9^NQCdd?%4IrmjPbx>Mu_yp=v)?%_{1}Y KV0tnmqZj~bYY+MW diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAndRemoveParameters b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAndRemoveParameters index 3943c84020a798e6b7d85324210e8f08669b6fa2..d92fd43a0389b705fb7185aa77bac8f253056354 100644 GIT binary patch delta 188 zcmZo;p3E$roL`iZ$jHFJ(C{A!jwUfMUSMEgtN{uzPn5P&fC@5!1XX~7U|DGp1EOJK zX$}iW&BUjMQBbuk2L#v`GA1r$&de)uUC3zQ;OGQ5oaKNZP%dd9TX9KIW?s6h1Eyp$ S7Rkv4jAFcSLpTpIG5`Q1`AsAM delta 174 zcmbQt+{P@OoL`iZ$jHFJ&;Y~?N0S&BFEB7L<^$PG6Q!*bpn^;wK@}heEGrFSKr~D& z&0ztlnfTN&3Pb}9U^yVbzK}6-A#-M4iR(f}0|!SZxZx}Z1c7o%3)zZGiZb)kT^%qb Mld(unE?^V`02@9gy8r+H diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructor b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructor index a7e207ac7570921b04699d400c231783cff4597f..43d786ee3df01a947174bab0dba886a8c53cc59d 100644 GIT binary patch delta 120 zcmbQsw2DbOIlm|+k&%Icq2WIe98F?iyuiS~C=3)}o+zy&0u^LBaE9@~Nk)jGiP6Q} vP;ur1YD^PlRCFB<$U-H7Ca@e3U|-0XxR5zBuf%mBqk)5?6WnypgNzIS%)~P6 delta 107 zcmZ3*G?z&_Ilm|+k&%Icp#g{)jwUfMUSMEgWCOC9CQ9pwKn0l&oMAk0k`bb4VstS# pRGj&M8q-7>6$OU_vQSB&2`mQ$*cUP;E@aNkD{)=OXyD-J1OViM7p1>@foL`iZ$jHFJ(C{A!jwUfMUSMEgtON=$Pn331fC@5!1jT`ZU|Bv81EOJK zeGLmp&BXtPFtscP1lSicCN5;o%qwwS$Y|i;=mb$aIe<|@18N}40YRXm8=iNRX`_j9%N(y0Mckcc>n+a delta 144 zcmbQh+{`SUoL`iZ$jHFJ&;Y~?N0S&BFEB7L<^b7D6Qx}gpn^;wL2)1lEXxOCKr~FO zuVDeHnfTvO0Yn20U^yVbzK}6-A#-M4iR(f}0|!SZh}y{kj1tmtMS?&@$qU(vONuh{ J(p?>ZssO`u9t;2g diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval index 922eecc33539c7e8c483ad07ad2090138c8a0055..95ada2e128e9730af949662bab91f8e08555ff85 100644 GIT binary patch delta 158 zcmbQtyntCcIlm|+k&%Icq2WIe98F?iyuiS~*a;M1o+$0202O2c3917H!Lo8721LWe z`W_aLn#uf(hA_n}2L#v`GA1r$&de)uUC3zQ;OGQVJUM|;LIY|d%K<^4qU42a#U(|V RdFieWa8*Dza2{l2007o~Kn0mVg6cpHSXK_ifM}Rl z-@^h@Gnt>!PyxgMn!s{EfPEoj;zH)kyb{-ij0O&lP7uYD6Bs3=;fe%-ijo(y6_*rc L=B2wj09641z1ba) diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.changeSubType b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.changeSubType index 96e11c7f09fa2c31a82b92521617dbaca74dd69e..636217e01feac82cc33fbd123917d8c845fd05c7 100644 GIT binary patch delta 183 zcmaFC@|8t8Ilm|+k&%Icq2WIe98F?iyuiS~6bBSwo+uru02O2c3Nk(i3W8)A?*SQ1 z5DgQjq_Ke1O#EjEQ_FHdfPEoj;zH)kyb{-ij0O&lP7t+|{Tb7^pa!xYSUGV);A8vw@1R91vh%$e6s4IWw=sbs?jHgQFANXoy@ArrcyhMloKvGR}jH F3;@tFL{0zz delta 142 zcmcb@beu^#Ilm|+k&%Icp#g{)jwUfMUSMEgGyt-hCQ6%$Kn0l&ykR`>k`bb4Vrn!u yRGj&M8q-7>m5J}!Y@i}62L#v`GA1u%&de)uUC3zQ;OGQ58X}j3DL2`WQ49cI2_fMC diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.3 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.3 index 1e36577ca927ec4b8c0ad0f9d9a5a54794c47338..06a0d6698324959a560fd966846719783fa9199a 100644 GIT binary patch delta 186 zcmeyu)W|HIoL`iZ$jHFJ(C{A!jwUfMUSMEg%mNB9Pn5P+fC@5!1O7%$ui&V!5$ E0EI+Ln*aa+ delta 172 zcmZo<{=y`koL`iZ$jHFJ&;Y~?N0S&BFEB7L#sb+)6Q!*cpn^;wK|vq~EXx66Kr~D& yi)I0-nRwMO0z?B1U^yVbzK}6#A#-M4iR(f}0|!SZxZx1FWIS>ym~xZj7{vg?@g*_< diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.1 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.1 index 9496b5632a3dc176a8e7ea25eaebc5b58dcea412..70e8a1387a17c02b4071e5ba12fe6e174509a351 100644 GIT binary patch delta 144 zcmcb_be~B&Ilm|+k&%Icq2WIe98F?iyuiS~=m8X9o+xc50u^LB@SX9%7eO>Q8K0?pskz-85sa&r9mG6 delta 131 zcmcc5bcsniIlm|+k&%Icp#g{)jwUfMUSMEgv<9-7CQ6%$Kn0l&d}loHg%P4?Vrm)- sNPOaXLj|Zj%K-uQg^Y;{nKScBTo*DLI5;{%)J|4ll#oJEl#Hwh0LqFUvH$=8 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.2 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.2 index 44cd5fb07fa79b1ab257e7ffa18f71049dc2fa9d..3d4e9e897fbc360041e72ce9ab513da7c1d32108 100644 GIT binary patch delta 168 zcmeBRp2{qfoL`iZ$jHFJ(C{A!jwUfMUSMEgtOE)#Gfe!TGSNYj1tc*sRsup-=0NC| yhG2TK9HT@$NC8kM%K-uQg^bAynKScBTo*DLI5;}NO@+v%;E_wkD#v+{kpTd!*GcpM delta 155 zcmbQr+`%l9oL`iZ$jHFJ&;Y~?N0S&BFEB7L76RE!3===7OmvWB0ZB}Zm4MKdIS~4# tA();l$0!j5QUKJ+azKE6A!G7F=FGej*M*D*4vtO?4JdLcc;r%%D@60`#ff@MuX42Xt_ zbvY~`H51<%!ql=H5MW=(n6!{NGq1#TA)|qVqZ35!WP3)5c&LF8MJbqyfG&W@rQ(rG L!z#ylkdXlZfhJcS delta 199 zcmdnSypmZuIlm|+k&%Icp#g{)jwUfMUSMEgoCIVuO_X+4fC@5!1nqzvu&gPF0nsqA zE{6rAX5u?T1rQB1faQPy`$EQ~h0K|GC9Vq@4ICVuAZjPuGfD)ZC`!Rp1UDcRk6aqE F8~_y2EVuvw diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.removeParameters b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.removeParameters index 65d11e24e38b90de2ec4778b916be5447c96fed3..926661d87b79097940bd60201fd8b13576e1b4c0 100644 GIT binary patch delta 190 zcmeyx)XprEoL`iZ$jHFJ(C{A!jwUfMUSMEgEC32HGfaG-GSNX)0V=@+l8^vOfK~8= z7!XYpOA}Z?Y9`(^1k;oG7$p*+nph49vM*#zTF6#hQk0pO?&{#+=mfV4BAJX!QUItk UWg&BBUWw~MMgwGxoCg^h07cwR1^@s6 delta 177 zcmZo?{>3DdoL`iZ$jHFJ&;Y~?N0S&BFEB7LCIi__3=CQ2KM0BMjQ(}9PK2ktRK6irOX pW&w#$Ja4D~qJi=(2L#v`GA1r$&de)uUC3zQ;OGQVJ6VBI901B`7exR7 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapSameType b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapSameType index b96b0ce60ecd349703faf4b3e207ec9d54d41f87..377e82d50e84774271bdecccc9fd376139dd9ea2 100644 GIT binary patch delta 119 zcmZ3-w3|sLIlm|+k&%Icq2WIe98F?iyuiS~r~(vVW|(+RWugP82vmaUz-`6@Hy9xb uC&s3+fW#*rH-ylCB-EkmSPlrVFJw$w$efv1;<}L0z`@Z8ZaC*bMg{GZF delta 106 zcmdnZw2nz8Ilm|+k&%Icp#g{)jwUfMUSMEg6bG`I7$%-mndrbN0+nDoaGUYK4MvE< oiLq%cAn}RE4I%U&2`Q*LmIDIp3mKCZGH2$MxGrQgaBy@200yQQ=l}o! From ecbb4330da9d9cc087abbdb204b2d136618b7d28 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Mon, 30 Oct 2017 11:15:05 +0000 Subject: [PATCH 4/5] CORDA-553 - Review comments --- .../amqp/AMQPDescriptorRegistry.kt | 2 +- .../serialization/amqp/SupportedTransforms.kt | 6 ++--- .../serialization/amqp/TansformTypes.kt | 7 +++--- .../amqp/EnumEvolvabilityTests.kt | 4 +-- .../serialization/amqp/EvolvabilityTests.kt | 23 ++++++------------ ...EnumEvolvabilityTests.testUnknownTransform | Bin 785 -> 785 bytes ...bilityTests.addAdditionalParamNotMandatory | Bin 292 -> 292 bytes .../EvolvabilityTests.addAndRemoveParameters | Bin 403 -> 403 bytes ...yTests.addMandatoryFieldWithAltConstructor | Bin 298 -> 298 bytes ...dMandatoryFieldWithAltReorderedConstructor | Bin 400 -> 400 bytes ...FieldWithAltReorderedConstructorAndRemoval | Bin 416 -> 416 bytes .../amqp/EvolvabilityTests.changeSubType | Bin 629 -> 629 bytes .../amqp/EvolvabilityTests.multiVersion.1 | Bin 307 -> 307 bytes .../amqp/EvolvabilityTests.multiVersion.2 | Bin 340 -> 340 bytes .../amqp/EvolvabilityTests.multiVersion.3 | Bin 385 -> 385 bytes ...volvabilityTests.multiVersionWithRemoval.1 | Bin 351 -> 351 bytes ...volvabilityTests.multiVersionWithRemoval.2 | Bin 405 -> 405 bytes ...volvabilityTests.multiVersionWithRemoval.3 | Bin 438 -> 438 bytes .../amqp/EvolvabilityTests.removeParameters | Bin 391 -> 391 bytes ...vabilityTests.simpleOrderSwapDifferentType | Bin 324 -> 324 bytes .../EvolvabilityTests.simpleOrderSwapSameType | Bin 315 -> 315 bytes 21 files changed, 18 insertions(+), 24 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPDescriptorRegistry.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPDescriptorRegistry.kt index 0c9a06b8fd..78ef9dc52a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPDescriptorRegistry.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPDescriptorRegistry.kt @@ -10,7 +10,7 @@ import org.apache.qpid.proton.amqp.UnsignedLong * Repeated here for brevity: * 50530 - R3 - Mike Hearn - mike&r3.com */ -const val DESCRIPTOR_TOP_32BITS: Long = 0xc562 shl(32 + 16) +const val DESCRIPTOR_TOP_32BITS: Long = 0xc562L shl(32 + 16) /** * AMQP desriptor ID's for our custom types. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt index df3f4f8d45..b785d0e8c6 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt @@ -38,9 +38,9 @@ private val singleExtract = { x: Annotation -> listOf(x) } // Transform annotation used to test the handling of transforms the de-serialising node doesn't understand. At // some point test cases will have been created with this transform applied. -//@Target(AnnotationTarget.CLASS) -//@Retention(AnnotationRetention.RUNTIME) -//annotation class UnknownTransformAnnotation(val a: Int, val b: Int, val c: Int) +// @Target(AnnotationTarget.CLASS) +// @Retention(AnnotationRetention.RUNTIME) +// annotation class UnknownTransformAnnotation(val a: Int, val b: Int, val c: Int) /** * Utility list of all transforms we support that simplifies our generation code. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt index de4f2d8703..5bb6fc05d5 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt @@ -35,14 +35,15 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType Rename({ a -> RenameSchemaTransform((a as CordaSerializationTransformRename).from, a.to) }) { override fun getDescriptor(): Any = DESCRIPTOR override fun getDescribed(): Any = ordinal - }; + } // Transform used to test the unknown handler, leave this at as the final constant, uncomment // when regenerating test cases - if Java had a pre-processor this would be much neater // - //UnknownTest({ a -> UnknownTestTransform((a as UnknownTransformAnnotation).a, a.b, a.c)}) { + //,UnknownTest({ a -> UnknownTestTransform((a as UnknownTransformAnnotation).a, a.b, a.c)}) { // override fun getDescriptor(): Any = DESCRIPTOR // override fun getDescribed(): Any = ordinal - //}; + //} + ; companion object : DescribedTypeConstructor { val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_ELEMENT_KEY.amqpDescriptor diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt index 2fffc13b28..6626f558b9 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt @@ -10,7 +10,6 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue class EnumEvolvabilityTests { -// var localPath = "file:///Users/katelynbaker/srcs-ide/corda/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp" var localPath = "file:///home/katelyn/srcs/corda/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp" @@ -408,7 +407,7 @@ class EnumEvolvabilityTests { } - // @UnknownTransformAnnotation (10, 20, 30) + //@UnknownTransformAnnotation (10, 20, 30) enum class WithUnknownTest { A, B, C, D } @@ -417,6 +416,7 @@ class EnumEvolvabilityTests { // To regenerate the types for this test uncomment the UnknownTransformAnnotation from // TransformTypes.kt and SupportedTransforms.kt + // ALSO: remember to re-annotate the enum WithUnkownTest above @Test fun testUnknownTransform() { val resource = "EnumEvolvabilityTests.testUnknownTransform" diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt index f5ab608e44..173e7fd87c 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt @@ -30,8 +30,7 @@ class EvolvabilityTests { // Original version of the class for the serialised version of this class // data class C (val a: Int, val b: Int) - // val sc = SerializationOutput(sf).serialize(C(A, B)) - // File(URI("$localPath/$resource")).writeBytes(sc.bytes) + // File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(C(A, B)).bytes) // new version of the class, in this case the order of the parameters has been swapped data class C(val b: Int, val a: Int) @@ -55,8 +54,7 @@ class EvolvabilityTests { // Original version of the class as it was serialised // data class C (val a: Int, val b: String) - // val sc = SerializationOutput(sf).serialize(C(A, B)) - // File(URI("$localPath/$resource")).writeBytes(sc.bytes) + // File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(C(A, B)).bytes) // new version of the class, in this case the order of the parameters has been swapped data class C(val b: String, val a: Int) @@ -78,8 +76,7 @@ class EvolvabilityTests { // Original version of the class as it was serialised // data class C(val a: Int) - // val sc = SerializationOutput(sf).serialize(C(A)) - // File(URI("$localPath/$resource")).writeBytes(sc.bytes) + // File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(C(A)).bytes) data class C(val a: Int, val b: Int?) @@ -90,7 +87,7 @@ class EvolvabilityTests { assertEquals(A, deserializedC.a) assertEquals(null, deserializedC.b) - } + } @Test(expected = NotSerializableException::class) fun addAdditionalParam() { @@ -130,8 +127,7 @@ class EvolvabilityTests { // Original version of the class as it was serialised // data class CC(val a: Int, val b: String, val c: String, val d: Int) - // val scc = SerializationOutput(sf).serialize(CC(A, B, C, D)) - // File(URI("$localPath/$resource")).writeBytes(scc.bytes) + // File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(CC(A, B, C, D)).bytes) data class CC(val b: String, val d: Int) @@ -158,8 +154,7 @@ class EvolvabilityTests { // Original version of the class as it was serialised // data class CC(val a: Int, val b: String, val c: String, val d: Int) - // val scc = SerializationOutput(sf).serialize(CC(A, B, C, D)) - // File(URI("$localPath/$resource")).writeBytes(scc.bytes) + // File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(CC(A, B, C, D)).bytes) data class CC(val a: Int, val e: Boolean?, val d: Int) @@ -181,8 +176,7 @@ class EvolvabilityTests { // Original version of the class as it was serialised // data class CC(val a: Int) - // val scc = SerializationOutput(sf).serialize(CC(A)) - // File(URI("$localPath/$resource")).writeBytes(scc.bytes) + // File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(CC(A)).bytes) @Suppress("UNUSED") data class CC(val a: Int, val b: String) { @@ -236,8 +230,7 @@ class EvolvabilityTests { // Original version of the class as it was serialised // data class CC(val a: Int, val b: Int, val c: String) - // val scc = SerializationOutput(sf).serialize(CC(A, B, C)) - // File(URI("$localPath/$resource")).writeBytes(scc.bytes) + // File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(CC(A, B, C)).bytes) @Suppress("UNUSED") data class CC(val a: Int, val b: Int, val c: String, val d: String) { diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.testUnknownTransform b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.testUnknownTransform index 74c557f8841e8ae8104dd447af0628e58fc06bd7..2916621d548edd3e0443ad168e191bc6391e2800 100644 GIT binary patch delta 267 zcmbQpHjzyrIlm|+k&%Icq2Xu}kYt!BkfaG=F)&?VU|{?Q6k`N2z5*FcFjWk!2Y#_G z44Ax-(RJbjer~7+<^yU>6J=B;%Q3RCL4+o2F=oM-vlvrgOco~naHt;k1MEx-866ig z8oIv~Qb XkUKOlJ1@UHFC?|NB!nwOBt#AXV7xyD delta 267 zcmbQpHjzyrIlm|+k&%Icq2WIe98H=ikfaG=F)&?VU|{?Qlwt%iz5*FcFjWk!2Y#_G z44Ax-(RJbjer~7+<^yU>6J=B;%Q3RCL4+o2F=oM-vlvrgOco~naHt;k1MEx-866ig z8oIv~Qb XkUKOlJ1@UHFC?|NB!nwOBt#AXpIUa# diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParamNotMandatory b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParamNotMandatory index 7892b7f89b2195eab0d703c515fd399b6dbf28f7..3eb318bb7e9978619d29b517830c28722d805357 100644 GIT binary patch delta 100 zcmZ3&w1i0@Ilm|+k&%Icq2Xu}kYt!BU@it>F)$rC#(3Z`BU~mnn+>YwoW43#4a)%m c_Jxdz3z;+XN?aE*8aOyQ!PGNw9%N(y03zZUQUCw| delta 100 zcmZ3&w1i0@Ilm|+k&%Icq2WIe98H=iU@it>F)$rC#(3Z`BU~mnn+>YwoW43#4a)%m c_Jxdz3z;+XN?aE*8aOyQ!PGNw9%N(y0M?>0IsgCw diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAndRemoveParameters b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAndRemoveParameters index d92fd43a0389b705fb7185aa77bac8f253056354..52f755150520b7ec758078c3a2522f158cf471ce 100644 GIT binary patch delta 198 zcmbQtJegS_Ilm|+k&%Icq2Xu}kYt!B;Hm^+F)&?VU|>`MiZOy1(jW$0Re2U0RQ+@P zTBv%K0|M*|850*WXXcf-E@U)taCCyHXJ9!X2$V}&$W~lZl$n?A>VPSkj7t(^Y6@0U JIS(>2008G^Fc<&; delta 198 zcmbQtJegS_Ilm|+k&%Icq2WIe98H=i;Hm^+F)&?VU|>`MN-=^M(jW$0Re2U0RQ+@P zTBv%K0|M*|850*WXXcf-E@U)taCCyHXJ9!X2$V}&$W~lZl$n?A>VPSkj7t(^Y6@0U JIS(>2006ncQ}h4; diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructor b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructor index 43d786ee3df01a947174bab0dba886a8c53cc59d..f6cb7d74d4c9cabc27c359d6ac9c756992bb8f7f 100644 GIT binary patch delta 111 zcmZ3*w2Da}Ilm|+k&%Icq2Xu}kYt!BU@it>F)$rC!+78%BU~o7kQ*w)d_aw9qKt~J l!vR^iGy}^40rrKAi3^!C^GaM7G8#BII>EFua2{l2001cx8%F>D delta 111 zcmZ3*w2Da}Ilm|+k&%Icq2WIe98H=iU@it>F)$rC!+78%BU~o7kQ*w)d_aw9qKt~J l!vR^iGy}^40rrKAi3^!C^GaM7G8#BII>EFua2{l2004|JGcN!D diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor index b51b42dd55f90f11e5f1313280acb92eb9eeac43..584aef76d73795a61ea36fa961ca83a4b598122d 100644 GIT binary patch delta 152 zcmbQhJb_stIlm|+k&%Icq2Xu}kYt!B;I9N>F)&?VU|{r~Rbv$!MAc+Q wMtwV|29^T?>F)&?VU|{r~Rbv$!MAc+Q wMtwV|29^T?>F)&?VU|>`SiZOy1av%m=Rbv+$MAc+L wMtwV|29^T?>F)&?VU|>`SN-=^Mav%m=Rbv+$MAc+L wMtwV|29^T?>6k`N2?g1H0a8=V%*dVGV zG3vwAvm6j$U&xrakU2B2#C0K~frFzHO#S3Q#uP57v8)GHPFxT;S%8TXYQ$uDCVh3N NBpx#uI1e&1002TADzyLr delta 180 zcmey$@|8s(Ilm|+k&%Icq2WIe98H=ikfH=(F)&?VU|@U>lwt%i?g1H0a8=V%*dVGV zG3vwAvm6j$U&xrakU2B2#C0K~frFzHO#S3Q#uP57v8)GHPFxT;S%8TXYQ$uDCVh3N NBpx#uI1e&1001JpQjGur diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.1 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.1 index 667eb7472c0b559ac22f4b8f5f6ae2ea1e991017..a7da3eeda3a6c6d33c63be16d2a32e1fbc01661f 100644 GIT binary patch delta 128 zcmdnYw3$gDIlm|+k&%Icq2Xu}kYt!BU?T=$F)$stzIlm|+k&%Icq2Xu}kYt!B;K>GM#f!mM)mbpsYkeqdvI3)6JVYS_%K-uQ ig^bAynKScBTo*DLI5;}NjDyOh;E_wkD#v+{kpTdkJ14~e delta 161 zcmbQrJe64>Ilm|+k&%Icq2WIe98H=i;K>GM#f!mM)mbpsYkeqdvI3)6JVYS_%K-uQ ig^bAynKScBTo*DLI5;}NjDyOh;E_wkD#v+{kpTc#kV~Ne diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.3 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.3 index 2c3b1ce9eb0bd50812abb242891940aba8d41857..c00a28213633119c60fb277f46a022fe507f7a9c 100644 GIT binary patch delta 207 zcmdnSyp35PIlm|+k&%Icq2Xu}kYt!B;HLy(F)&?VU|_TZiZOy1rXU7fRYMjVRQ(5i zn0l520_+PJlNK^(=9RcEWHfMabb_g$?8qn<4>cB|ECrh~28diL9=SBEa-0Vl831}| BF-`yg delta 207 zcmdnSyp35PIlm|+k&%Icq2WIe98H=i;HLy(F)&?VU|_TZN-=^MrXU7fRYMjVRQ(5i zn0l520_+PJlNK^(=9RcEWHfMabb_g$?8qn<4>cB|ECrh~28diL9=SBEa-0Vl832#` BStbAg diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.removeParameters b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.removeParameters index 926661d87b79097940bd60201fd8b13576e1b4c0..37c419f0765fcc4bba5b63dc15a500b085729edb 100644 GIT binary patch delta 181 zcmZo?Zf6!q&M!(yWMp7qXgHb#BpD_OxGF(d3``do7#JmhVvHaLKZpTWRUXF%RexI_ z%9<>|D3%D-%5p%EeIaAgLbl?PqRhN>R|f}2CzxIah-5M@NdchFl!eThc_pq384Zv% Javo%4002j8Dop?Y delta 181 zcmZo?Zf6!q&M!(yWMp7qX!s8VN0TNBxGF(d3``do7#JmhQj8!5KZpTWRUXF%RexI_ z%9<>|D3%D-%5p%EeIaAgLbl?PqRhN>R|f}2CzxIah-5M@NdchFl!eThc_pq384Zv% Javo%40074PPALEY diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapDifferentType b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapDifferentType index 61da589b24be6143afd29d2feaf3b3ddbc092d65..cc726a13fd0728a971e5e1e3d13ab3428b19690d 100644 GIT binary patch delta 129 zcmX@Ybc9JDIlm|+k&%Icq2Xu}kYt!BU?T=$F)$r?$avr$BU~malMSloqP`1M4a)%m u_Jxdz3z;+XN?aE*8aOyQ!PGOb91sM`B`sttE-A{)OLuiZmgGFh$N&J*ZXx{u delta 129 zcmX@Ybc9JDIlm|+k&%Icq2WIe98H=iU?T=$F)$r?$avr$BU~malMSloqP`1M4a)%m u_Jxdz3z;+XN?aE*8aOyQ!PGOb91sM`B`sttE-A{)OLuiZmgGFh$N&Ia@IB!G diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapSameType b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapSameType index 377e82d50e84774271bdecccc9fd376139dd9ea2..dc9fae7152dcf5d7e8a9b33918cad39be6c2a4ad 100644 GIT binary patch delta 110 zcmdnZw3|sFIlm|+k&%Icq2Xu}kYt!BU?~P+F)$st&3NDjBU~mvg$=6aq&|%GPfQ)E gn&p51`$EQ~h0K|GC9Vq@4ICVuU^*E%4>B?U0AEoYbpQYW delta 110 zcmdnZw3|sFIlm|+k&%Icq2WIe98H=iU?~P+F)$st&3NDjBU~mvg$=6aq&|%GPfQ)E gn&p51`$EQ~h0K|GC9Vq@4ICVuU^*E%4>B?U0BbckSpWb4 From 851918dd76dfa628fa18701cfe832f816fe14822 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Mon, 30 Oct 2017 11:34:54 +0000 Subject: [PATCH 5/5] CORDA-553 - API standard doc update The evolution annotations are being added to core which is being flagged as a breaking API change. Given these are part of the upcoming introduction of AMQP they're not actually breaking anything we've not enabled yet --- .ci/api-current.txt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index e66520b6f2..a1579cb063 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -2436,6 +2436,20 @@ public interface net.corda.core.serialization.ClassWhitelist ## public @interface net.corda.core.serialization.CordaSerializable ## +public @interface net.corda.core.serialization.CordaSerializationTransformEnumDefault + public abstract String new() + public abstract String old() +## +public @interface net.corda.core.serialization.CordaSerializationTransformEnumDefaults + public abstract net.corda.core.serialization.CordaSerializationTransformEnumDefault[] value() +## +public @interface net.corda.core.serialization.CordaSerializationTransformRename + public abstract String from() + public abstract String to() +## +public @interface net.corda.core.serialization.CordaSerializationTransformRenames + public abstract net.corda.core.serialization.CordaSerializationTransformRename[] value() +## public @interface net.corda.core.serialization.DeprecatedConstructorForDeserialization public abstract int version() ##