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() ## 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..aa1e9c7154 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformEnumDefault.kt @@ -0,0 +1,93 @@ +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) +// 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..c8ae7e2aec --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformRename.kt @@ -0,0 +1,35 @@ +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) +@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..78ef9dc52a --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPDescriptorRegistry.kt @@ -0,0 +1,38 @@ +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 = 0xc562L 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), + 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..5b489c5d84 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Envelope.kt @@ -0,0 +1,70 @@ +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) + + // 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) { + 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) { + 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[BLOB_IDX], Schema.get(list[SCHEMA_IDX]!!), + TransformsSchema.newInstance(transformSchema))) + } + + // 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) { + 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[BLOB_IDX], list[SCHEMA_IDX] 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..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 @@ -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,76 +17,18 @@ 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 * [toString] representations generate the associated XML form. */ 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/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 new file mode 100644 index 0000000000..b785d0e8c6 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt @@ -0,0 +1,76 @@ +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 + +/** + * 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 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. + */ +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) } + +// 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. + * + * 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 + ) + //,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 new file mode 100644 index 0000000000..5bb6fc05d5 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt @@ -0,0 +1,72 @@ +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 org.apache.qpid.proton.amqp.DescribedType +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 + * + * 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 { + /** + * 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 + }, + 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)}) { + // 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}.") + } + + return try { + values()[describedType.described as Int] + } catch (e: IndexOutOfBoundsException) { + values()[0] + } + } + + 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..2bde766c2c --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt @@ -0,0 +1,317 @@ +package net.corda.nodeapi.internal.serialization.amqp + +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 +// 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 + */ +abstract 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 + * + * 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]) { + EnumDefaultSchemaTransform.typeName -> EnumDefaultSchemaTransform.newInstance(described) + RenameSchemaTransform.typeName -> RenameSchemaTransform.newInstance(described) + else -> UnknownTransform() + } + } + + 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 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 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?): 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 EnumDefaultSchemaTransform(old, new) + } + + override fun getTypeClass(): Class<*> = EnumDefaultSchemaTransform::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?) = ( + (other is EnumDefaultSchemaTransform && other.new == new && other.old == old) || super.equals(other)) + + override fun hashCode() = (17 * new.hashCode()) + old.hashCode() + + 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?) = ( + (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 +} + +/** + * 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 { + 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 -> + 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 { annotation -> + val t = transform.enum.build(annotation) + + // 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) + } + } + } + } 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 + } + } + } + + 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/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java index 34ca1abaa6..1743c645b4 100644 --- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java +++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java @@ -186,6 +186,8 @@ public class JavaSerializationOutputTests { decoder.register(CompositeType.Companion.getDESCRIPTOR(), CompositeType.Companion); decoder.register(Choice.Companion.getDESCRIPTOR(), Choice.Companion); decoder.register(RestrictedType.Companion.getDESCRIPTOR(), RestrictedType.Companion); + decoder.register(Transform.Companion.getDESCRIPTOR(), Transform.Companion); + decoder.register(TransformsSchema.Companion.getDESCRIPTOR(), TransformsSchema.Companion); new EncoderImpl(decoder); decoder.setByteBuffer(ByteBuffer.wrap(bytes.getBytes(), 8, bytes.getSize() - 8)); 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..6626f558b9 --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt @@ -0,0 +1,436 @@ +package net.corda.nodeapi.internal.serialization.amqp + +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:///home/katelyn/srcs/corda/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp" + + + 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 EnumDefaultSchemaTransform) + assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new) + assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).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 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 + 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 EnumDefaultSchemaTransform) + assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new) + assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).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 EnumDefaultSchemaTransform).new) + assertEquals("D", (enumDefaults[0] as EnumDefaultSchemaTransform).old) + assertEquals("D", (enumDefaults[1] as EnumDefaultSchemaTransform).new) + assertEquals("A", (enumDefaults[1] as EnumDefaultSchemaTransform).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 EnumDefaultSchemaTransform).new) + assertEquals("X", (serialisedSchema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).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) + } + + @Test + fun testCache() { + data class C2(val annotatedEnum: AnnotatedEnumOnce) + data class C1(val annotatedEnum: AnnotatedEnumOnce) + + val sf = testDefaultFactory() + + assertEquals(0, sf.transformsCache.size) + + val sb1 = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C1(AnnotatedEnumOnce.D)) + + assertEquals(2, sf.transformsCache.size) + assertTrue(sf.transformsCache.containsKey(C1::class.java.name)) + assertTrue(sf.transformsCache.containsKey(AnnotatedEnumOnce::class.java.name)) + + val sb2 = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C2(AnnotatedEnumOnce.D)) + + assertEquals(3, sf.transformsCache.size) + assertTrue(sf.transformsCache.containsKey(C1::class.java.name)) + assertTrue(sf.transformsCache.containsKey(C2::class.java.name)) + assertTrue(sf.transformsCache.containsKey(AnnotatedEnumOnce::class.java.name)) + + 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 + // ALSO: remember to re-annotate the enum WithUnkownTest above + @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..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 @@ -5,36 +5,39 @@ 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(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) + 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 +48,19 @@ 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(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) + val path = EvolvabilityTests::class.java.getResource(resource) + val f = File(path.toURI()) val sc2 = f.readBytes() val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -70,24 +71,23 @@ 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(SerializationOutput(sf).serialize(C(A)).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)) assertEquals(A, deserializedC.a) assertEquals(null, deserializedC.b) - } + } @Test(expected = NotSerializableException::class) fun addAdditionalParam() { @@ -119,22 +119,20 @@ 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(SerializationOutput(sf).serialize(CC(A, B, C, D)).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 +144,22 @@ 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(SerializationOutput(sf).serialize(CC(A, B, C, D)).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 +171,12 @@ 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(SerializationOutput(sf).serialize(CC(A)).bytes) @Suppress("UNUSED") data class CC(val a: Int, val b: String) { @@ -191,6 +184,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 +223,14 @@ 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(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) { @@ -250,6 +240,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 +254,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 +273,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 +286,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 +299,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 +321,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 +356,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 +382,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 +399,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 +424,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/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)) 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 0000000000..2916621d54 Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.testUnknownTransform differ 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 8e37059133..3eb318bb7e 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParamNotMandatory and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParamNotMandatory differ 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 3943c84020..52f7551505 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAndRemoveParameters and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAndRemoveParameters differ 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 a7e207ac75..f6cb7d74d4 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructor and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructor differ 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 87506ae203..584aef76d7 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor differ 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 922eecc335..8a093478bc 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval differ 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 96e11c7f09..b323a3acb3 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.changeSubType and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.changeSubType differ 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 2cca4802ac..a7da3eeda3 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.1 and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.1 differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.2 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.2 index 76818dc697..b00abdaeab 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.2 and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.2 differ 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 1e36577ca9..40ea9d91f6 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.3 and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.3 differ 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 9496b5632a..69a5365b76 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.1 and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.1 differ 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 44cd5fb07f..a3fec70686 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.2 and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.2 differ 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 c1dcb85ab8..c00a282136 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.3 and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.3 differ 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 65d11e24e3..37c419f076 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.removeParameters and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.removeParameters differ 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 6d81269d3e..cc726a13fd 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapDifferentType and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapDifferentType differ 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 b96b0ce60e..dc9fae7152 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapSameType and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapSameType differ