From e8822ce391e496a7b00add041725bda0c1ad5d21 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Mon, 27 Nov 2017 13:43:30 +0000 Subject: [PATCH 1/7] CORDA-553 - Plumb the transform schema into the AMQP serialisation framework This change doesn't enable anything, it just changes the code to pass around both relevant schemas instead of a single one from the AMQP envelope. The actual evolver will build ontop of this --- .../nodeapi/internal/serialization/amqp/EnumEvolveTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.kt index 265f4b9a0c..23637f825d 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.kt @@ -40,4 +40,4 @@ class EnumEvolveTests { File(EvolvabilityTests::class.java.getResource(resource).toURI()).readBytes())) }.isInstanceOf(NotSerializableException::class.java) } -} \ No newline at end of file +} From 6fc736a5f5d8cdf0a004698a179ee0d2ce5a91be Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Mon, 27 Nov 2017 19:21:27 +0000 Subject: [PATCH 2/7] CORDA-553 - Enable Enum Evolution --- .../serialization/amqp/AMQPSerializer.kt | 2 +- .../amqp/EnumEvolutionSerializer.kt | 89 +++++ .../serialization/amqp/SerializerFactory.kt | 2 +- .../serialization/amqp/TansformTypes.kt | 50 +++ .../serialization/amqp/TransformsSchema.kt | 104 +++--- .../amqp/EnumEvolvabilityTests.kt | 124 ++++++- .../serialization/amqp/EnumEvolveTests.kt | 323 +++++++++++++++++- .../serialization/amqp/EvolvabilityTests.kt | 4 +- ...volveTests.deserialiseNewerSetToUnknown2.C | Bin 0 -> 873 bytes ...volveTests.deserialiseNewerSetToUnknown2.D | Bin 0 -> 873 bytes ...volveTests.deserialiseNewerSetToUnknown2.E | Bin 0 -> 873 bytes ...EnumEvolveTests.deserialiseNewerWithNoRule | Bin 0 -> 670 bytes ...EnumEvolveTests.deserializeWithRename.1.AA | Bin 0 -> 763 bytes .../EnumEvolveTests.deserializeWithRename.1.B | Bin 0 -> 762 bytes .../EnumEvolveTests.deserializeWithRename.1.C | Bin 0 -> 762 bytes ...EnumEvolveTests.deserializeWithRename.2.AA | Bin 0 -> 792 bytes ...EnumEvolveTests.deserializeWithRename.2.BB | Bin 0 -> 792 bytes .../EnumEvolveTests.deserializeWithRename.2.C | Bin 0 -> 791 bytes ...EnumEvolveTests.deserializeWithRename.3.AA | Bin 0 -> 821 bytes .../EnumEvolveTests.deserializeWithRename.3.C | Bin 0 -> 820 bytes ...EnumEvolveTests.deserializeWithRename.3.XX | Bin 0 -> 821 bytes .../amqp/EnumEvolveTests.multiOperations.1.A | Bin 0 -> 760 bytes .../amqp/EnumEvolveTests.multiOperations.1.B | Bin 0 -> 760 bytes .../amqp/EnumEvolveTests.multiOperations.1.C | Bin 0 -> 760 bytes .../amqp/EnumEvolveTests.multiOperations.1.D | Bin 0 -> 760 bytes .../amqp/EnumEvolveTests.multiOperations.2.A | Bin 0 -> 811 bytes .../amqp/EnumEvolveTests.multiOperations.2.B | Bin 0 -> 811 bytes .../amqp/EnumEvolveTests.multiOperations.2.C | Bin 0 -> 811 bytes .../amqp/EnumEvolveTests.multiOperations.2.D | Bin 0 -> 811 bytes .../amqp/EnumEvolveTests.multiOperations.2.E | Bin 0 -> 811 bytes .../amqp/EnumEvolveTests.multiOperations.3.A | Bin 0 -> 857 bytes .../amqp/EnumEvolveTests.multiOperations.3.B | Bin 0 -> 857 bytes .../EnumEvolveTests.multiOperations.3.BOB | Bin 0 -> 859 bytes .../amqp/EnumEvolveTests.multiOperations.3.C | Bin 0 -> 857 bytes .../amqp/EnumEvolveTests.multiOperations.3.D | Bin 0 -> 857 bytes .../amqp/EnumEvolveTests.multiOperations.4.A | Bin 0 -> 1004 bytes .../amqp/EnumEvolveTests.multiOperations.4.B | Bin 0 -> 1004 bytes .../EnumEvolveTests.multiOperations.4.BOB | Bin 0 -> 1006 bytes .../EnumEvolveTests.multiOperations.4.CAT | Bin 0 -> 1006 bytes .../amqp/EnumEvolveTests.multiOperations.4.D | Bin 0 -> 1004 bytes .../amqp/EnumEvolveTests.multiOperations.4.F | Bin 0 -> 1004 bytes .../amqp/EnumEvolveTests.multiOperations.4.G | Bin 0 -> 1004 bytes .../EnumEvolveTests.multiOperations.5.APPLE | Bin 0 -> 1115 bytes .../amqp/EnumEvolveTests.multiOperations.5.B | Bin 0 -> 1111 bytes .../EnumEvolveTests.multiOperations.5.BBB | Bin 0 -> 1113 bytes .../EnumEvolveTests.multiOperations.5.CAT | Bin 0 -> 1113 bytes .../amqp/EnumEvolveTests.multiOperations.5.D | Bin 0 -> 1111 bytes .../EnumEvolveTests.multiOperations.5.FLUMP | Bin 0 -> 1115 bytes .../amqp/EnumEvolveTests.multiOperations.5.G | Bin 0 -> 1111 bytes 49 files changed, 635 insertions(+), 63 deletions(-) create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserialiseNewerSetToUnknown2.C create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserialiseNewerSetToUnknown2.D create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserialiseNewerSetToUnknown2.E create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserialiseNewerWithNoRule create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserializeWithRename.1.AA create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserializeWithRename.1.B create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserializeWithRename.1.C create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserializeWithRename.2.AA create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserializeWithRename.2.BB create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserializeWithRename.2.C create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserializeWithRename.3.AA create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserializeWithRename.3.C create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserializeWithRename.3.XX create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.1.A create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.1.B create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.1.C create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.1.D create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.2.A create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.2.B create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.2.C create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.2.D create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.2.E create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.3.A create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.3.B create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.3.BOB create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.3.C create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.3.D create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.4.A create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.4.B create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.4.BOB create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.4.CAT create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.4.D create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.4.F create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.4.G create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.5.APPLE create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.5.B create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.5.BBB create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.5.CAT create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.5.D create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.5.FLUMP create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.5.G diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPSerializer.kt index d4596c5ec0..e70b55d8fc 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPSerializer.kt @@ -35,5 +35,5 @@ interface AMQPSerializer { /** * Read the given object from the input. The envelope is provided in case the schema is required. */ - fun readObject(obj: Any, schema: SerializationSchemas, input: DeserializationInput): T + fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): T } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt new file mode 100644 index 0000000000..f8579fc21e --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt @@ -0,0 +1,89 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import org.apache.qpid.proton.amqp.Symbol +import org.apache.qpid.proton.codec.Data +import java.io.NotSerializableException +import java.lang.reflect.Type +import java.util.* + +/** + * @property transforms + * + */ +class EnumEvolutionSerializer( + clazz: Type, + factory: SerializerFactory, + private val conversions : Map, + private val ordinals : Map) : AMQPSerializer { + override val type: Type = clazz + override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")!! + + companion object { + fun MutableMap.mapInPlace(f : (String)->String) { + val i = this.iterator() + while(i.hasNext()) { + val curr = (i.next()) + curr.setValue(f(curr.value)) + } + } + + /** + * @param old + * @param new + */ + fun make(old: RestrictedType, + new: AMQPSerializer, + factory: SerializerFactory, + transformsFromBlob: TransformsSchema): AMQPSerializer { + + val wireTransforms = transformsFromBlob.types[old.name] + val localTransforms = TransformsSchema.get(old.name, factory) + val transforms = if (wireTransforms?.size ?: -1 > localTransforms.size) wireTransforms!! else localTransforms + + // if either of these isn't of the cast type then something has gone terribly wrong + // elsewhere in the code + @Suppress("UNCHECKED_CAST") + val defaultRules = transforms[TransformTypes.EnumDefault] as? List + @Suppress("UNCHECKED_CAST") + val renameRules = transforms[TransformTypes.Rename] as? List + + // What values exist on the enum as it exists on the class path + val localVals = new.type.asClass()!!.enumConstants.map { it.toString() } + + var conversions : MutableMap = new.type.asClass()!!.enumConstants.map { it.toString() } + .union(defaultRules?.map { it.new }?.toSet() ?: emptySet()) + .union(renameRules?.map { it.to } ?: emptySet()) + .associateBy({ it }, { it }) + .toMutableMap() + + val rules : MutableMap = mutableMapOf() + rules.putAll(defaultRules?.associateBy({ it.new }, { it.old }) ?: emptyMap()) + rules.putAll(renameRules?.associateBy({ it.to }, { it.from }) ?: emptyMap()) + + while (conversions.filter { it.value !in localVals }.isNotEmpty()) { + conversions.mapInPlace { rules[it] ?: it } + } + + var idx = 0 + return EnumEvolutionSerializer(new.type, factory, conversions, localVals.associateBy( {it}, { idx++ })) + } + } + + override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any { + var enumName = (obj as List<*>)[0] as String + + if (enumName !in conversions) { + throw NotSerializableException ("No rule to evolve enum constant $type::$enumName") + } + + return type.asClass()!!.enumConstants[ordinals[conversions[enumName]]!!] + } + + override fun writeClassInfo(output: SerializationOutput) { + throw IllegalAccessException("It should be impossible to write an evolution serializer") + } + + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) { + throw IllegalAccessException("It should be impossible to write an evolution serializer") + } +} \ No newline at end of file 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 d310f5a6bb..764f0b5458 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 @@ -50,7 +50,7 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { return serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) { when (typeNotation) { is CompositeType -> EvolutionSerializer.make(typeNotation, newSerializer as ObjectSerializer, this) - is RestrictedType -> throw NotSerializableException("Enum evolution is not currently supported") + is RestrictedType -> EnumEvolutionSerializer.make(typeNotation, newSerializer, this, transforms) } } } 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 5bb6fc05d5..6fd1e4a95f 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 @@ -27,14 +27,62 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType Unknown({ UnknownTransform() }) { override fun getDescriptor(): Any = DESCRIPTOR override fun getDescribed(): Any = ordinal + override fun validate(l : List, constants: Set) { } }, EnumDefault({ a -> EnumDefaultSchemaTransform((a as CordaSerializationTransformEnumDefault).old, a.new) }) { override fun getDescriptor(): Any = DESCRIPTOR override fun getDescribed(): Any = ordinal + + /** + * Validates a list of constant additions to an enumerated types, to be valid a default (the value + * that should be used when we cannot use the new value) must refer to a constant that exists in the + * enum class as it exists now and it cannot refer to itself. + * + * @param l The list of transforms representing new constants and the mapping from that constant to an + * existing value + * @param constants The list of enum constants on the type the transforms are being applied to + */ + override fun validate(l : List, constants: Set) { + @Suppress("UNCHECKED_CAST") (l as List).forEach { + if (!constants.contains(it.old)) { + throw NotSerializableException( + "Enum extension defaults must be to a valid constant: ${it.new} -> ${it.old}. ${it.old} " + + "doesn't exist in constant set $constants") + } + + if (it.old == it.new) { + throw NotSerializableException("Enum extension ${it.new} cannot default to itself") + } + } + } }, Rename({ a -> RenameSchemaTransform((a as CordaSerializationTransformRename).from, a.to) }) { override fun getDescriptor(): Any = DESCRIPTOR override fun getDescribed(): Any = ordinal + + /** + * Validates a list of rename transforms is valid. Such a list isn't valid if we detect a cyclic chain, + * that is a constant is renamed to something that used to exist in the enum. We do this for both + * the same constant (i.e. C -> D -> C) and multiple constants (C->D, B->C) + * + * @param l The list of transforms representing the renamed constants and the mapping between their new + * and old values + * @param constants The list of enum constants on the type the transforms are being applied to + */ + override fun validate(l : List, constants: Set) { + object : Any() { + val from : MutableSet = mutableSetOf() + val to : MutableSet = mutableSetOf() }.apply { + @Suppress("UNCHECKED_CAST") (l as List).forEach { rename -> + if (rename.to in this.to || rename.from in this.from) { + throw NotSerializableException("Cyclic renames are not allowed (${rename.to})") + } + + this.to.add(rename.from) + this.from.add(rename.to) + } + } + } } // 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 @@ -45,6 +93,8 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType //} ; + abstract fun validate(l: List, constants: Set) + companion object : DescribedTypeConstructor { val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_ELEMENT_KEY.amqpDescriptor 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 974de134b1..a8409a64c3 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 @@ -148,6 +148,8 @@ class EnumDefaultSchemaTransform(val old: String, val new: String) : Transform() * * @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 { @@ -192,6 +194,61 @@ data class TransformsSchema(val types: Map { val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_SCHEMA.amqpDescriptor + /** + * Takes a class name and either returns a cached instance of the TransformSet for it or, on a cache miss, + * instantiates the transform set before inserting into the cache and returning it. + * + * @param name fully qualified class name to lookup transforms for + * @param sf the [SerializerFactory] building this transform set. Needed as each can define it's own + * class loader and this dictates which classes we can and cannot see + */ + fun get(name: String, sf: SerializerFactory) = sf.transformsCache.computeIfAbsent(name) { + val transforms = EnumMap>(TransformTypes::class.java) + try { + val clazz = sf.classloader.loadClass(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) + } + + transform.enum.validate( + transforms[transform.enum] ?: emptyList(), + clazz.enumConstants.map { it.toString() }.toSet()) + } + } + } 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 + } + + private fun getAndAdd( + type: String, + sf: SerializerFactory, + map: MutableMap>>) { + get(type, sf).apply { + if (isNotEmpty()) { + map[type] = this + } + } + } + /** * 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 @@ -200,48 +257,10 @@ data class TransformsSchema(val types: Map>>() - - 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) - } + fun build(schema: Schema, sf: SerializerFactory) = TransformsSchema( + mutableMapOf>>().apply { + schema.types.forEach { type -> getAndAdd(type.name, sf, this) } + }) override fun getTypeClass(): Class<*> = TransformsSchema::class.java @@ -286,6 +305,7 @@ data class TransformsSchema(val types: Map(File(path.toURI()).readBytes())) + + assertEquals (DeserializeNewerSetToUnknown.C, obj.e) + } + + // Version of the class as it was serialised + // + // @CordaSerializationTransformEnumDefaults ( + // CordaSerializationTransformEnumDefault("D", "C"), + // CordaSerializationTransformEnumDefault("E", "D")) + // enum class DeserializeNewerSetToUnknown2 { A, B, C, D, E } + // + // Version of the class as it's used in the test + enum class DeserializeNewerSetToUnknown2 { A, B, C } + + @Test + fun deserialiseNewerSetToUnknown2() { + val resource = "${this.javaClass.simpleName}.${testName()}" + val sf = testDefaultFactory() + + data class C(val e: DeserializeNewerSetToUnknown2) + + // Uncomment to re-generate test files + // val so = SerializationOutput(sf) + // File(URI("$localPath/$resource.C")).writeBytes(so.serialize(C(DeserializeNewerSetToUnknown2.C)).bytes) + // File(URI("$localPath/$resource.D")).writeBytes(so.serialize(C(DeserializeNewerSetToUnknown2.D)).bytes) + // File(URI("$localPath/$resource.E")).writeBytes(so.serialize(C(DeserializeNewerSetToUnknown2.E)).bytes) + + val path1 = EvolvabilityTests::class.java.getResource("$resource.C") + val path2 = EvolvabilityTests::class.java.getResource("$resource.D") + val path3 = EvolvabilityTests::class.java.getResource("$resource.E") + + // C will just work + val obj1 = DeserializationInput(sf).deserialize(SerializedBytes(File(path1.toURI()).readBytes())) + // D will transform directly to C + val obj2 = DeserializationInput(sf).deserialize(SerializedBytes(File(path2.toURI()).readBytes())) + // E will have to transform from E -> D -> C to work, so this should exercise that part + // of the evolution code + val obj3 = DeserializationInput(sf).deserialize(SerializedBytes(File(path3.toURI()).readBytes())) + + assertEquals (DeserializeNewerSetToUnknown2.C, obj1.e) + assertEquals (DeserializeNewerSetToUnknown2.C, obj2.e) + assertEquals (DeserializeNewerSetToUnknown2.C, obj3.e) + } + + + // Version of the class as it was serialised, evolve rule purposfuly not included to + // test failure conditions + // + // enum class DeserializeNewerWithNoRule { A, B, C, D } + // + // Class as it exists for the test + enum class DeserializeNewerWithNoRule { A, B, C } + + // Lets test to see if they forgot to provide an upgrade rule + @Test + fun deserialiseNewerWithNoRule() { + val resource = "${this.javaClass.simpleName}.${testName()}" + val sf = testDefaultFactory() + + data class C(val e: DeserializeNewerWithNoRule) + + // Uncomment to re-generate test files + // val so = SerializationOutput(sf) + // File(URI("$localPath/$resource")).writeBytes(so.serialize(C(DeserializeNewerWithNoRule.D)).bytes) + + val path = EvolvabilityTests::class.java.getResource(resource) + Assertions.assertThatThrownBy { - DeserializationInput(sf).deserialize(SerializedBytes( - File(EvolvabilityTests::class.java.getResource(resource).toURI()).readBytes())) + DeserializationInput(sf).deserialize(SerializedBytes(File(path.toURI()).readBytes())) }.isInstanceOf(NotSerializableException::class.java) } + + // Version of class as it was serialized, at some point in the "future" several + // values have been renamed + // + // First Change + // A -> AA + // @CordaSerializationTransformRenames ( + // CordaSerializationTransformRename(from ="A", to = "AA") + // ) + // enum class DeserializeWithRename { AA, B, C } + // + // Second Change + // B -> BB + // @CordaSerializationTransformRenames ( + // CordaSerializationTransformRename(from = "B", to = "BB"), + // CordaSerializationTransformRename(from = "A", to = "AA") + // ) + // enum class DeserializeWithRename { AA, BB, C } + // + // Third Change + // BB -> XX + // @CordaSerializationTransformRenames ( + // CordaSerializationTransformRename(from = "B", to = "BB"), + // CordaSerializationTransformRename(from = "BB", to = "XX"), + // CordaSerializationTransformRename(from = "A", to = "AA") + // ) + // enum class DeserializeWithRename { AA, XX, C } + // + // Finally, the version we're using to test with + enum class DeserializeWithRename { A, B, C } + + @Test + fun deserializeWithRename() { + val resource = "${this.javaClass.simpleName}.${testName()}" + val sf = testDefaultFactory() + + data class C(val e: DeserializeWithRename) + + // Uncomment to re-generate test files, needs to be done in three stages + val so = SerializationOutput(sf) + // First change + // File(URI("$localPath/$resource.1.AA")).writeBytes(so.serialize(C(DeserializeWithRename.AA)).bytes) + // File(URI("$localPath/$resource.1.B")).writeBytes(so.serialize(C(DeserializeWithRename.B)).bytes) + // File(URI("$localPath/$resource.1.C")).writeBytes(so.serialize(C(DeserializeWithRename.C)).bytes) + // Second change + // File(URI("$localPath/$resource.2.AA")).writeBytes(so.serialize(C(DeserializeWithRename.AA)).bytes) + // File(URI("$localPath/$resource.2.BB")).writeBytes(so.serialize(C(DeserializeWithRename.BB)).bytes) + // File(URI("$localPath/$resource.2.C")).writeBytes(so.serialize(C(DeserializeWithRename.C)).bytes) + // Third change + // File(URI("$localPath/$resource.3.AA")).writeBytes(so.serialize(C(DeserializeWithRename.AA)).bytes) + // File(URI("$localPath/$resource.3.XX")).writeBytes(so.serialize(C(DeserializeWithRename.XX)).bytes) + // File(URI("$localPath/$resource.3.C")).writeBytes(so.serialize(C(DeserializeWithRename.C)).bytes) + + // + // Test we can deserialize instances of the class after its first transformation + // + val path1_AA = EvolvabilityTests::class.java.getResource("$resource.1.AA") + val path1_B = EvolvabilityTests::class.java.getResource("$resource.1.B") + val path1_C = EvolvabilityTests::class.java.getResource("$resource.1.C") + + val obj1_AA = DeserializationInput(sf).deserialize(SerializedBytes(File(path1_AA.toURI()).readBytes())) + val obj1_B = DeserializationInput(sf).deserialize(SerializedBytes(File(path1_B.toURI()).readBytes())) + val obj1_C = DeserializationInput(sf).deserialize(SerializedBytes(File(path1_C.toURI()).readBytes())) + + assertEquals(DeserializeWithRename.A, obj1_AA.e) + assertEquals(DeserializeWithRename.B, obj1_B.e) + assertEquals(DeserializeWithRename.C, obj1_C.e) + + // + // Test we can deserialize instances of the class after its second transformation + // + val path2_AA = EvolvabilityTests::class.java.getResource("$resource.2.AA") + val path2_BB = EvolvabilityTests::class.java.getResource("$resource.2.BB") + val path2_C = EvolvabilityTests::class.java.getResource("$resource.2.C") + + val obj2_AA = DeserializationInput(sf).deserialize(SerializedBytes(File(path2_AA.toURI()).readBytes())) + val obj2_BB = DeserializationInput(sf).deserialize(SerializedBytes(File(path2_BB.toURI()).readBytes())) + val obj2_C = DeserializationInput(sf).deserialize(SerializedBytes(File(path2_C.toURI()).readBytes())) + + assertEquals(DeserializeWithRename.A, obj2_AA.e) + assertEquals(DeserializeWithRename.B, obj2_BB.e) + assertEquals(DeserializeWithRename.C, obj2_C.e) + + // + // Test we can deserialize instances of the class after its third transformation + // + val path3_AA = EvolvabilityTests::class.java.getResource("$resource.3.AA") + val path3_XX = EvolvabilityTests::class.java.getResource("$resource.3.XX") + val path3_C = EvolvabilityTests::class.java.getResource("$resource.3.C") + + val obj3_AA = DeserializationInput(sf).deserialize(SerializedBytes(File(path3_AA.toURI()).readBytes())) + val obj3_XX = DeserializationInput(sf).deserialize(SerializedBytes(File(path3_XX.toURI()).readBytes())) + val obj3_C = DeserializationInput(sf).deserialize(SerializedBytes(File(path3_C.toURI()).readBytes())) + + assertEquals(DeserializeWithRename.A, obj3_AA.e) + assertEquals(DeserializeWithRename.B, obj3_XX.e) + assertEquals(DeserializeWithRename.C, obj3_C.e) + } + + // The origional version of the enum, what we'll be eventually deserialising into + // enum class MultiOperations { A, B, C } + // + // First alteration, add D + // @CordaSerializationTransformEnumDefault(old = "C", new = "D") + // enum class MultiOperations { A, B, C, D } + // + // Second, add E + // @CordaSerializationTransformEnumDefaults( + // CordaSerializationTransformEnumDefault(old = "C", new = "D"), + // CordaSerializationTransformEnumDefault(old = "D", new = "E") + // ) + // enum class MultiOperations { A, B, C, D, E } + // + // Third, Rename E to BOB + // @CordaSerializationTransformEnumDefaults( + // CordaSerializationTransformEnumDefault(old = "C", new = "D"), + // CordaSerializationTransformEnumDefault(old = "D", new = "E") + // ) + // @CordaSerializationTransformRename(to = "BOB", from = "E") + // enum class MultiOperations { A, B, C, D, BOB } + // + // Fourth, Rename C to CAT, ADD F and G + // @CordaSerializationTransformEnumDefaults( + // CordaSerializationTransformEnumDefault(old = "F", new = "G"), + // CordaSerializationTransformEnumDefault(old = "BOB", new = "F"), + // CordaSerializationTransformEnumDefault(old = "D", new = "E"), + // CordaSerializationTransformEnumDefault(old = "C", new = "D") + // ) + // @CordaSerializationTransformRenames ( + // CordaSerializationTransformRename(to = "CAT", from = "C"), + // CordaSerializationTransformRename(to = "BOB", from = "E") + // ) + // enum class MultiOperations { A, B, CAT, D, BOB, F, G} + // + // Fifth, Rename F to FLUMP, Rename BOB to BBB, Rename A to APPLE + // @CordaSerializationTransformEnumDefaults( + // CordaSerializationTransformEnumDefault(old = "F", new = "G"), + // CordaSerializationTransformEnumDefault(old = "BOB", new = "F"), + // CordaSerializationTransformEnumDefault(old = "D", new = "E"), + // CordaSerializationTransformEnumDefault(old = "C", new = "D") + // ) + // @CordaSerializationTransformRenames ( + // CordaSerializationTransformRename(to = "APPLE", from = "A"), + // CordaSerializationTransformRename(to = "BBB", from = "BOB"), + // CordaSerializationTransformRename(to = "FLUMP", from = "F"), + // CordaSerializationTransformRename(to = "CAT", from = "C"), + // CordaSerializationTransformRename(to = "BOB", from = "E") + // ) + // enum class MultiOperations { APPLE, B, CAT, D, BBB, FLUMP, G} + // + // Finally, the original version of teh class that we're going to be testing with + enum class MultiOperations { A, B, C } + + @Test + fun multiOperations() { + val resource = "${this.javaClass.simpleName}.${testName()}" + val sf = testDefaultFactory() + + data class C(val e: MultiOperations) + + // Uncomment to re-generate test files, needs to be done in three stages + val so = SerializationOutput(sf) + // First change + // File(URI("$localPath/$resource.1.A")).writeBytes(so.serialize(C(MultiOperations.A)).bytes) + // File(URI("$localPath/$resource.1.B")).writeBytes(so.serialize(C(MultiOperations.B)).bytes) + // File(URI("$localPath/$resource.1.C")).writeBytes(so.serialize(C(MultiOperations.C)).bytes) + // File(URI("$localPath/$resource.1.D")).writeBytes(so.serialize(C(MultiOperations.D)).bytes) + // Second change + // File(URI("$localPath/$resource.2.A")).writeBytes(so.serialize(C(MultiOperations.A)).bytes) + // File(URI("$localPath/$resource.2.B")).writeBytes(so.serialize(C(MultiOperations.B)).bytes) + // File(URI("$localPath/$resource.2.C")).writeBytes(so.serialize(C(MultiOperations.C)).bytes) + // File(URI("$localPath/$resource.2.D")).writeBytes(so.serialize(C(MultiOperations.D)).bytes) + // File(URI("$localPath/$resource.2.E")).writeBytes(so.serialize(C(MultiOperations.E)).bytes) + // Third change + // File(URI("$localPath/$resource.3.A")).writeBytes(so.serialize(C(MultiOperations.A)).bytes) + // File(URI("$localPath/$resource.3.B")).writeBytes(so.serialize(C(MultiOperations.B)).bytes) + // File(URI("$localPath/$resource.3.C")).writeBytes(so.serialize(C(MultiOperations.C)).bytes) + // File(URI("$localPath/$resource.3.D")).writeBytes(so.serialize(C(MultiOperations.D)).bytes) + // File(URI("$localPath/$resource.3.BOB")).writeBytes(so.serialize(C(MultiOperations.BOB)).bytes) + // Fourth change + // File(URI("$localPath/$resource.4.A")).writeBytes(so.serialize(C(MultiOperations.A)).bytes) + // File(URI("$localPath/$resource.4.B")).writeBytes(so.serialize(C(MultiOperations.B)).bytes) + // File(URI("$localPath/$resource.4.CAT")).writeBytes(so.serialize(C(MultiOperations.CAT)).bytes) + // File(URI("$localPath/$resource.4.D")).writeBytes(so.serialize(C(MultiOperations.D)).bytes) + // File(URI("$localPath/$resource.4.BOB")).writeBytes(so.serialize(C(MultiOperations.BOB)).bytes) + // File(URI("$localPath/$resource.4.F")).writeBytes(so.serialize(C(MultiOperations.F)).bytes) + // File(URI("$localPath/$resource.4.G")).writeBytes(so.serialize(C(MultiOperations.G)).bytes) + // Fifth change - { APPLE, B, CAT, D, BBB, FLUMP, G} + // File(URI("$localPath/$resource.5.APPLE")).writeBytes(so.serialize(C(MultiOperations.APPLE)).bytes) + // File(URI("$localPath/$resource.5.B")).writeBytes(so.serialize(C(MultiOperations.B)).bytes) + // File(URI("$localPath/$resource.5.CAT")).writeBytes(so.serialize(C(MultiOperations.CAT)).bytes) + // File(URI("$localPath/$resource.5.D")).writeBytes(so.serialize(C(MultiOperations.D)).bytes) + // File(URI("$localPath/$resource.5.BBB")).writeBytes(so.serialize(C(MultiOperations.BBB)).bytes) + // File(URI("$localPath/$resource.5.FLUMP")).writeBytes(so.serialize(C(MultiOperations.FLUMP)).bytes) + // File(URI("$localPath/$resource.5.G")).writeBytes(so.serialize(C(MultiOperations.G)).bytes) + + val stage1Resources = listOf( + Pair("$resource.1.A", MultiOperations.A), + Pair("$resource.1.B", MultiOperations.B), + Pair("$resource.1.C", MultiOperations.C), + Pair("$resource.1.D", MultiOperations.C)) + + val stage2Resources = listOf( + Pair("$resource.2.A", MultiOperations.A), + Pair("$resource.2.B", MultiOperations.B), + Pair("$resource.2.C", MultiOperations.C), + Pair("$resource.2.D", MultiOperations.C), + Pair("$resource.2.E", MultiOperations.C)) + + val stage3Resources = listOf( + Pair("$resource.3.A", MultiOperations.A), + Pair("$resource.3.B", MultiOperations.B), + Pair("$resource.3.C", MultiOperations.C), + Pair("$resource.3.D", MultiOperations.C), + Pair("$resource.3.BOB", MultiOperations.C)) + + val stage4Resources = listOf( + Pair("$resource.4.A", MultiOperations.A), + Pair("$resource.4.B", MultiOperations.B), + Pair("$resource.4.CAT", MultiOperations.C), + Pair("$resource.4.D", MultiOperations.C), + Pair("$resource.4.BOB", MultiOperations.C), + Pair("$resource.4.F", MultiOperations.C), + Pair("$resource.4.G", MultiOperations.C)) + + val stage5Resources = listOf( + Pair("$resource.5.APPLE", MultiOperations.A), + Pair("$resource.5.B", MultiOperations.B), + Pair("$resource.5.CAT", MultiOperations.C), + Pair("$resource.5.D", MultiOperations.C), + Pair("$resource.5.BBB", MultiOperations.C), + Pair("$resource.5.FLUMP", MultiOperations.C), + Pair("$resource.5.G", MultiOperations.C)) + + fun load(l: List>) = l.map { + Pair (DeserializationInput(sf).deserialize(SerializedBytes( + File(EvolvabilityTests::class.java.getResource(it.first).toURI()).readBytes())), it.second) + } + + load (stage1Resources).forEach { assertEquals(it.second, it.first.e) } + load (stage2Resources).forEach { assertEquals(it.second, it.first.e) } + load (stage3Resources).forEach { assertEquals(it.second, it.first.e) } + load (stage4Resources).forEach { assertEquals(it.second, it.first.e) } + load (stage5Resources).forEach { assertEquals(it.second, it.first.e) } + } } 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 173e7fd87c..10a59e1414 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 @@ -2,6 +2,7 @@ package net.corda.nodeapi.internal.serialization.amqp import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.serialization.SerializedBytes +import net.corda.testing.common.internal.ProjectStructure.projectRootDir import org.junit.Test import java.io.File import java.io.NotSerializableException @@ -18,7 +19,8 @@ import kotlin.test.assertEquals // 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" + var localPath = projectRootDir.toUri().resolve( + "node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp") @Test fun simpleOrderSwapSameType() { diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserialiseNewerSetToUnknown2.C b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserialiseNewerSetToUnknown2.C new file mode 100644 index 0000000000000000000000000000000000000000..cced4f8de74148db910f0c9c62fb86c1a99ba837 GIT binary patch literal 873 zcmcIi-AV#M7@gVmhqB9_Agmi+(UW1yv= zv9E$vF0FEKM?q9$YU|(jQA*`+#|j>Be54UM1s)}VKxr%B=jfLrBNK^ z`*cZ)Xp>GUzoM}lcGwdOr>vB(TUh%s<$>{+hAkOEU6hi8tbkP9{LiKH?Q*upCAKWP zrWG^tGqX`S3yYNRMX^>)4(7KFTkGT6)H8SHzJL&)oCeS1_@|8hSot!2jQh4 z+E)S6xwOi`9R;Put+s%9&}Ve$VwcA(Y|h!WCdj6=6^2jZUEG$yK7sXmJIhfxzY{|Q{F3QTR+P5v>VFIcNc$9$Z0+texBVaiJVF8ad sv^9S-Hadm*)5T2Aj8YxHepN{r0p^oa}{KBvb)%27--YkW`Ed~cRok(MdF3` zK76^8x_h0dSs8Q2{Tff=Lt~9GCN`ONMgP zUUsLgXg8jw7qUC@dp(WdH(!Tbhq65k{K3_2bH5wRmg%OmH9pT#us^9-j&tCg9D#LH zIJQ?cJ?I&!s^&HV64`>$6-Qi1mJ6Dog5{hhd`~6M1fuEV3K6kR$tFuSY{CjzG#2d3 zRIrF`Sw7?PAlL&^u8wO8GIjU2i2Y;4eK{h**bgku(;wdd vu`p3?!V76}J5sQIW`3St zVs2r9o@-udu4`F-PFZS5YH>-iN=hnBQEGT*Nk&j=USe*linD_&+!p2oYD~oV(cwTg zBU}^9fjIVsjHwHQDRZ3*HrKg2I5;}NZDKocmW?LXIJhok$;m7(f!l;05V#`I;ea(W zTod~N4set%WHdk$14c8W(?Ui=G$H4Oj7D%F&VwyXH1XI$9Yhl14q-YVi-s%ci9G&3KV>@t`jV9JOxGrSL$t*5`+k_qvxZ=>^fHgB* z6Z-)UriDz7jtdzLki^(QvAmGc5KYK=A)^soi1T0z6HPpJPzRBOxI>r@$ReVR>wp0B OLN-VuSjgxIayS6udG(_J literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserializeWithRename.1.C b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserializeWithRename.1.C new file mode 100644 index 0000000000000000000000000000000000000000..33425ddb685acc272b5aefd28e6be315eb10e98a GIT binary patch literal 762 zcmYe!FG@*dWME)uIGO|`85kHZFfcGZ1~Ql#7AxhYmgpseR9IEHI$F9Ur)!&gmAd8_ z2Icz)_yifI+u9z`V?cU{kT<3z#b*>H$j!tl!*bba!qlq;Rt_xXmGK)*#HlYUut~hi!V9gBI z#D0K-X(5xN<3dIQBr$eiEITb^G(;0}UdU(!7venF!bB5~9n?W2A?^^S1G0!{<2oR~ PypRo&2o^Frf*cM2=aKcL literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserializeWithRename.2.AA b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserializeWithRename.2.AA new file mode 100644 index 0000000000000000000000000000000000000000..4f75773756defb3f45cade9196fe2f1e9d44f46a GIT binary patch literal 792 zcmYe!FG@*dWME)uIGO|`85kHZFfcHK1eh5XE9Iq@=p}dq60Deo8lQBTw&=7Qpe7;kjc?81Yr>q&?3e+ zP#YN^0~t(kN3b6F$+|EUZo6JyeoAU$L8e}2UP)?EUSf`3acWU!VoqjNVo7Fxo?c>Z zVS%1&UTLmtS$wDojyocxFjPP-#Q4$SKsF;> z6U%`(_Jxe83xg?hoeMVCxjHyFI>BvXJ8+JTCe}E(E@a8cEG~iDgdPyMBGKW14KrL5 z`vDGclrCg6KofIvTF7XKBnFInM(2f$MsOj{gA15w;!LWDG`oFgcq0sueZ{`mj^ literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserializeWithRename.2.BB b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserializeWithRename.2.BB new file mode 100644 index 0000000000000000000000000000000000000000..521bf9ec2fb5712e1369d09c9d110e1c35e348e4 GIT binary patch literal 792 zcmYe!FG@*dWME)uIGO|`85kHZFfcHK1eh5XE9Iq@=p}dq60Deo8lQBTw&=7Qpe7;kjcp@gb{8L6VM{Y zH&7cH9|IXoa5>fkKUo)s!fn^f%TGy7EXdT$%qvMP%1g}AD^4xSOw7rwN-W9D&(llH zEiBM;%`44yEz8d-OASdaE-6+?Nrfp&4bLpe2ujUM%uQ8sc5sE;!hArDi5NdR9LPpE zo8>?p`$ERlg~61$&IOz6Tpb)7o#4)6J8+JTCe}E(E@a8cEG~iDgdPyMBGKW14KrL5 z`vDGMlsY;tWHdk%b8=e9Xow^RjCw}rg^WgUAw%xF3q#?S>*eLAq$U<*>SgAYq!#5R=I9lt7G);pWL71XWaj7TCFT|u z=(*;V=DL>U=ai*}q!yPHtE8mD6s3k|mShB_<|XE)syI8i!d<|8K#hqQFFG8^W`t{E zIS|LbkTG>(FlDZD!R9(w2M0$dxJ_&a&au(N8VA>fEIFCQC2*V20|HkZIvlWJhHGL! zz`?YT$Yg2waiqaKMHc zu8I8s2RKR>G8&+XMMNxQG(-{uMm?kRLPjIF5a+?YOf>P?!Bj*V;tpXt5QcCr*8u_M kg=~;iu#nMdA(Im#q`3|VBIJM)3z;G!;EqMr=m<*10AE!Fs{jB1 literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserializeWithRename.3.C b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.deserializeWithRename.3.C new file mode 100644 index 0000000000000000000000000000000000000000..5e90893cb41986fddc5bc11db303ce07eaa9c5ac GIT binary patch literal 820 zcmYe!FG@*dWME)uIGO|`85kHZFfcI70vXH@LYnJw%xF3q#?y>*eLAq$U<*>SgAYq!#5R=I9lt7G);pWL71XWaj7T zCFT|u=(*;V=DL>U=ai*}q!yPHtE8mD6s3k|mShB_<|XE)syI8i!fjzbpvFXu9~};4 zBb?20AdY<@W9q_S%3SAy&2_F04vtQ6=dm3)$3_!t99$Q&Yg2waiqaKMHc zu8I8s2QW$<9Tzehpov99EMzo95(7p(qw_*WBe)Rf!M#j0@!7#tL>l4_VLA|oa4y#Y k0p^8lkW{dc(P<%*6C$L!4hSOTfD)jT3U@53Mn{mB0parn+5i9m literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.1.A b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.1.A new file mode 100644 index 0000000000000000000000000000000000000000..c28d4809779174a1c05252023cecf33dea2f7a9c GIT binary patch literal 760 zcmb`F%Sr<=6ozxAH?&({Kq+p7Sgj}uh3Ryn6dj8)cv+@xl+?L(W?G~x@lAp+k}ll4 zb*WqN9ZW@1Iv@yk6AnNB$@ia}bi=qu0U`A8+)*b&@Pd%>rivVTS_@c;yHm%(#c6Bj z-imAelg>W%{Pv|y&ic3Yy2LQ&hUp6c^F{o4Fu)`t@9`wn zjO_5gZOoKTUN@CwX}UiF)r@2b*vd#v0oxhL7EsgD(&W)t@Q{=0?DLdekgKz`Ez6ah RC{KTf&2FfAd;uz;{RJ)J^H=}? literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.1.B b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.1.B new file mode 100644 index 0000000000000000000000000000000000000000..4f5e9ab81cbc80e43fdeef5fdfaae81b06e039e2 GIT binary patch literal 760 zcmb_a%Sr<=6wRH!&~E(!rMMAdwW25#rqhX1bS%o?W0|&5Qs>o~X_2nPKMDRwx^VB- zrEbN4FcnGZfFRgSxSV^Eb8gP6ESiD8b%ulF{_=Ol9CXm|5ybm$!4wcLi{6%7HF3tGoeI`Hu$ z2=A&ebgv*k3!<&XaS-+xjX35(%Hn`}IAJlT9v{<`hXJPkAi^XV`eYP(Bj&OsO)CD- zOL;qD@i!-_G)U7BGp(dpt?C zMt1n$5;LWf*OyANr1EPBsAeEbz*Yuo3fRs-wt$+3mL`wJf`^<`XUkJ|L9WhXTb3(1 RQJ!vx&2Fgrd;!X!{RJ}i^I8A^ literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.1.C b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.1.C new file mode 100644 index 0000000000000000000000000000000000000000..6fd1917bebb74f5d04da06c2a5de7470f5adec12 GIT binary patch literal 760 zcmb`F%}N6?5XUpyFSJ)*Kq+2?Sgj}uh3$5u6kUt5_;F0zD5?9^-EEPc#5W1PNP6(@ z)uUd;cQ6%6>4G5GOBnw1AAU2L>4tHS0z&BFxuaf$-~}P$O$9mhv=*=wcc+eni__N5 zy%pE`C!Kxj`Rz-aob_+(b%|ll$FKJ`#^)q*;%ImCXmsct-?iL^@)ZpMP1n#ILvi5a z2MF&fnB@xcvmn|Uj)SnrXv8rOQWgi)!wHKy_4t^kJPa`P2N5R0&?lqN8!?w9X;Sfr zUdr1Mi@!ZdrD+n)&dGJ-FT*k3pb^IQM` literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.1.D b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.1.D new file mode 100644 index 0000000000000000000000000000000000000000..2ca115091f61d9c20857ac53a92cc78618764afb GIT binary patch literal 760 zcmb`F%}N6?5XUpS{Xl#51(f1Nh}DXsP}sH`rRZ9e#gAj!MoHbT?rw|pB)&=TMbd+J zuO9U(zJsYqN*4sdUc&ItfAX8jOgD^s6c9oW&mDCl1TP2~Zz?d+(^|k%+?_fOE>2rJ z_l>yPKk4jK&u?GaIHJ2PumK>fwaNoO*mrQyvDG`hy6QVCa)k=#7}mk~As% zLoem+h{fNYq};TKe&3Yq#$TGVBmSXRBE83p-TSTMjMBIofW z)r@TOzfH`NPEIEkWl_zqE})W;8UnU5Qd7WoMzRG|wX`&OG#1?Cq%vEck_&QmHnwHC SVv5pqJ8X7C)#nRP1=?Rd9`jxR literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.2.A b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.2.A new file mode 100644 index 0000000000000000000000000000000000000000..434b43c42af94e004f5fe4dda370934f73682195 GIT binary patch literal 811 zcmb_a%T59@6z!ei5oOC47~-<=K|(YknE|@dAPhmm(lQijnCalmz`~WkQvH##aPNoc z(yg%=Q^G_O6WKJkr|0&ZlY9Lr9#cRFJv@)}i4eRXWS4beqo)JGQtYo8=fiL~oB30> z7LVH9GjG7-+VsM!*A<2>UT?xCSIe+>c^&(ss^|%i1%q47D_9cT{zk{rI`Hu=2=BVE zjEB5>%gc@t$08atI_H>+l*NJuIAJlT0l%jyj|8US-5e9K2+1-EmaNZ`G^vD(AmyDo zi@!KYrRfm!zpW0fzl3uX1JjgK!=eNx+yC>`-u6Wt$88vmf_g3NSim7N7w{xC8rkr_ zP0Ud@dDB!>#r6Co9A%(}gyRg Xmevc?ViT(8Awhd?88<(?>j2~vLyZGk literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.2.B b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.2.B new file mode 100644 index 0000000000000000000000000000000000000000..f216584cdf0704080d97b6b7d079c90cbf993ddc GIT binary patch literal 811 zcmb_a%T59@6z!ei5oOC47~-<=K|(YknE|@dAPhmm(lQijnCalmz`~WkQvH##aPNoc z(yg%=Q^G_O6WKJk=brYQlY9Lr9#cRFJv@)}i4eRXWS4beqo)JGQtYo8=fiL~oB30> z7LVH9GjG7-+VsM!*A<2>UT?xCSIe+>c^&(ss^|%i1%q47D_9a5eK0zf)`5?2L3r1N zWqNt_mX{qPjzu(Pbj~psDT@USaKd6v1Ab3a9tljtyE!Ie5t3yTELoo=X;KLnLCQOG z7JqS)O4A|ce_I_|e+lO(wlSPj!=eNx+yC>`-u6Wt$88v61@&6kv4BHlF5pROG_v7; zo0y|+^5#)d74`gF2}c>IA>lX!H6@&6pq7Nw3`8VU4Yap~Ql2t7QH^otO@AY_+yV56r4!BXt6E6#`Ea5nR& zZY>_QyJy~j$F=E&SFbA!TXnq&n_Ml!-sN@dkE)_4JQfUYbzQ-d(Ck}=V`&b2d;{TK z2b)}8z2#-cz_EzNjLtdcB4x3l0Zv%VX~6Gk$|HekcsIvHEJCu3f+g#-Buy&eB1m~> z&f>3{q|$VVVcY7^`inV7F)&RzH7rVCvi-lg+S_vx$8j5mQBbdi9RnO9a{*6M!^oQd zZDWqQ$(yF4D(d~Y5{@!bL&9-JYDzfCNG%Dc8Hq@!8fkC!Xzkeds=K+TQXiDN`R=Q- YElcZzX|V~_`_Pvup=GA|A+HUPPfmdYUH||9 literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.2.D b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.2.D new file mode 100644 index 0000000000000000000000000000000000000000..71b890bdef56632d7dd9f24f3f714cd157a523f4 GIT binary patch literal 811 zcmb_a%T59@6z!ei5oOC47~-<=K|(YknStp>gFJ$SrDZ76Fw?=AfrTr7rTQaf;oc9? zrCVb$ri6(mCbDU6&pquqC-?eMJf?sUdUzh`6Crp($S&)`Mo$NVrPyCH&WGV}HuI-$ zEgrQxXWaphYtxHvy{<59@p=)S@h(mL?*EeP+r zuuLzn-tw|-#IcCRjLtdcB4x3l0Zv%VX~6Gk$|HekcsIvHEJCu3f+g#-Buy&eB1n0A z&f+glQfWHG{BNs6>o4IP#lSqusbNt9ldb>xYHj->j^j3rMnSz6b}Zl!nG1N58jWoD z-zMg$o4k2cR7E{MSHe*SYDhTFKurlJ8OW1xnt_Ofs)6=akJgTJuR5E3D)m9Ro9n(R Y+p@G?7>`Y;o`(eO*)wi_=<5LF6I1&GU;qFB literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.2.E b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.2.E new file mode 100644 index 0000000000000000000000000000000000000000..cf15290a05c7320a732ff2f571a2126f75a08ed0 GIT binary patch literal 811 zcmb_a%T59@6z!ei5oOC47~-<=K|(YknE|@dAPhmm(lQijnCalmz`~WkQvH##aPNoc z(yg%=Q^G_O6WKJk=brYQlY9Lr9#cRFJv@)}i4eRXWS4beqo)JGQtYo8=fiL~oB30> z7LVH9GjG7-+VsM!*A<2>UT?xCSIe+>c^&(ss^|%i1%q47D_9apKWB6-tpgw5g7B^j z%k=WtLIBvse6x3^B#{v$Kxqv6B(a47X zZDNkP$(u(-Rn+ryB^+g-hJ@n`)Rb_Nfm#wyGZ2wbHPGJb(b{qDRd=&br9LQkbKO^E YTb9-f(_#~<=OICRZW%W}^mPF830ZXmVgLXD literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.3.A b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.3.A new file mode 100644 index 0000000000000000000000000000000000000000..26cbcc8f4ece2138fa2e3359f4e550e669740d4b GIT binary patch literal 857 zcmb_a%SyvQ6rGv0DQ#DRYZp@76k>g#K`7+WY>KsFqqv!jes1_i-xhRd0A?>UEi$J5xw5CHJ_?C=)=_zHkz@InGln;}V+an3k8bk%6o z&rCHto!m^K{+)9@Q}u3_DR7DB9FIJLshxIkKF)bEqENRB9mI9emmvJ$ z6%rn*?5#?NLR<-_F2RwfcwtK75c^6(Vh{V?Jx;x8sNmo>Qmk+mSlQIih)t3-X$3Ps z^@b6N7o4PJYL=KUu`TqMa2g{}G*#GTjY2{P|MN9i_GM|BZU_yTy~`^W&@5Wg#K`7+WY>KsFqqvtEwOc!p& zAJMH_rv)cACr3tHz^# zZmRj|j;Le|`RKj?>$4ZM;^ec3iuk^1 zL#7u8)?n)rZO%`oN3Jm62!-5rPtbqp^9aQOHEqN;iqmMYHHpe+B*X#Xv_-g2Z+Z*1 literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.3.BOB b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.3.BOB new file mode 100644 index 0000000000000000000000000000000000000000..87a4636599cc079fdec3c77f650aa529d428d2ca GIT binary patch literal 859 zcmb_a%SyvQ6rGv$VcV79+JzK1g;*bG5DH0}Y>KsFqqvtEwOc!p& zAJMH_rxhnQCX2T@%1?B-P%_ZMeB4}8!qtdqv5f?zh#|wJ+I}MO1tHsjb^+RL?wNoTP2}{xDxu3 zgzvmUqE}|GWjYY@au7NMN3QG!DTxE@$q9*F?74S1b;Cf${!JvC!NfPS(94KLk~C@h z6EAfK5sBxVq}f$X@xR12(O=e68G&M`#4f57659Wtul}MhQ&lxxXjIr+Wyu1nNlTua zq(Y;({BJi?nOzn`RaQk5Y818$kWS%t0qRn?Q-BN#cfUck&oofDC&1eD87+D9>3yEI zh6S0H2dvK4B-)ro#vqrNUxY&L+7NDj=<^lw05vSc*7Io8*{Vbg#K`7+WY>KsFqqvtEwOc!p& zAJMH_rv)cACr3tHz^# zZmRj|g#K`7+8Db|XO;&#%EG@6%58bn<9D}z5WUAPs0 zM7M687M$9kAlS`tIrrQ-_i)cBPCX0(0FTcue*u8607wQeB=EEukz5(g7)OVmnhbk| zsTQZ>n{m>+bFU|=-t964F7e#s;YoO~?Od2a(DrP#(+Zw3Ag+VH1mOp- zkmyy}Ta^xkxDv%4!HKWM?bTo2%hojA5E?Rjmsc#H*|ZY)SuQln z=6}1G#@wnHs<9fspH5+?1Q`_WlpvGB-4bL`xc3dJBPtmKKDsZ!`t${@*!lFKBED}r zkm-wnHQ2gDo3qGRS7|bSZL)()ef>)_tgwSfORw*QD(;`?yC1B|-wyOm4XR=L;C&8O4JVa4m@+5@Jbf~_psqYBn@N6 z;q>6NUmw*^dRk?2t~))|_O}(K(I667;6+glx%vPNCVh2qjQaatcUrxoyc~~#V$6Mb z>cdN4hWsX7@T`U`dCy{=9ax#H{i87=wVgPaXe|2>i>(XRdQJF`Mh62`U z7P%$c$QsVrR*0BWI)dN&CX;)Si9{mCUBsZAf>aDD2`JBFp{W=c>?FWg+QMKr0mVF3 zr{Zb?jCpMg_EJy>gBpYKFEpoeJTptq>G3VYOo|2|ck|x@Sru?G9gwSfORw*QD(;`?yC1B_-wxa~{sL3=ft^{}f#o&*O z3-`Kk)z&&hu5J`iW zaX39VHS5FrNl&Xx&ULq^I-_kxX*7rg=6F$5L!RD8{fVjekCD0Wcc;}W%FFQ>s0Ls< zJk{Z)FF}5jZg>{UlJ_junYiTnLuid`$@UNoJj;D=I52M@<3i z)QjAbWn>L!96LlzY0-k;`X-ZmktK;lq+L`nC@(-N29*?)=dsXK3=DQsV4T{*U^fND zJXNR3)f5=>+8FFDKphNf49dSypUUyfEZL{Ww+s_08i3r*js>zR;9@*xEqH0sE+%Dm z7`;H&7UG|8Yo{3BZlH$D#_|V+CiRTv7Y&{q&3;LNtOz)F20hD#7&H^h8FxOYB|d)u DK!z}6 literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.4.BOB b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.4.BOB new file mode 100644 index 0000000000000000000000000000000000000000..b741fceac9f439ca428245dc442d4307deabd70d GIT binary patch literal 1006 zcmb_b%SyvQ6wM@k*mfm|;8v;|A+%Z_RSHSkvZ;5hE~>4L5t2c9`jc%>VGdsufN5(g3E zaC&fR)Q9zxo?4xpYfewG{ViE;G)M)^@y6plYvUi^~eH|U0Eu`GGdVvWg*t~Z3HZ;6(RVBngz7{b6ZZR^HFmgkD5bM1?&J95;q zXOE!{Lll;sk&P_Phe6B<%S}mT_X}i`|I70v0s%|P5F?KP#NGeZ)t#?Pl_XhV8aZkT zSfgI#7AzyFIAdEOVoGTXe(Rh>?(-}uPx3qlG6t0tq+n1@Kn2?ErmSPIodBcM76v;B zDB{tnz@U}@BVHSW-4xWpV2?qC7wS_bnwbUr^!PTL(2@?w-RxK-D*`S>W7dL~ChcNe zW{1^OB&(_T=iAx|#R&*MXugMy=hnJouKZ4*Pd0bDQ|_xf+`yZ z&y;wo#EV~&{1)9ru{8NeW1Y#1t~Y?@&=M^d!I5j)q7O%wX|B?bn(CIe1CaaZm?vujE(BxNfLA8%VpwL2 z(eq?I5ud-U8Do6Aff^ExgwSfORw?Asv#6JF^?;2t(Sh@?Tp zIGi1v_Ugm>Nms8-&keV$I-_kxX*7rg7I4s;qEP2mjgNaL?KZMrEmTV8fv1d6_0LQlF*f$okeNVF7>ye~;6ITy? zCxj*pP*8Fw4zi6A9LJoXq{%wFUm#ojUmhnD2v|~v^h69G?)v8ug6j7fqfV&3{ROtOz)F20hD#7-+HOjJp`rW1l|& DPJS?G literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.4.F b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.4.F new file mode 100644 index 0000000000000000000000000000000000000000..b9bedc2502364bada4459213896dd518397395df GIT binary patch literal 1004 zcmb_b%SyvQ6wM@ULfe%ff?KI>gwSfORw*QD(;`?yC1B_-wxa~{GMPSbCAjl127hE+ zxYva%zrlZSQe~P}1;K8HnRCybbLZSU{U93HfaAE^rwh7r9C+q9;gxO#?qS1+NbV<$ z!^y#^RU6b!x_WthZg^eI4YyUbUMDh`v(?@|j zf+mbnT=a%6a*Pm0Q%+oLC_1}eAY1%j9;XlpSW<>q5(W@={#RFLzAjx+RE=qf)D*Bz zy%3iyqv$x}Ix%8Oix&LWH-+3wEJ-2~?V^f7X#vtOD5sztkA;S6Vz83}lhh^#yD2E) zX$DQMq`-vN!eDO!YGY7kQ0|5LR7z%M$v!>4Wtd381mtdZ%#&3C7m_h+!b_WWaav}Z z(eq?&A^!QcR*LcM25QJ`EPqgFP|sL?!Q{!&?3d)pih#v4=-VE|pfRgwSfORw*QD(;`?yC1B_-wxa~{vY9?`CAjl127hE+ zxYva%zrlZS5@niJ1;K8HnRCybbLZSU{a`q-0mpH-PZxCMIPlDI!Ykbf+{1ot2U^eboKJ&-0-@Z8*Zy=y-s8>#~Y8U$TxbZH?g$dF|zi9&a`qxc_NQN+Z34& zPjz_tOOW5B8=l3ofo$=Ad7MHZU`ZKbNf z^+H^-jH2U=>qLktEn4tf-xP8$u_TE|w2LYRr3FaCpqzqoJQftrX+i4b+g?SpJ~Upq{b(g2|Jk*)PeH6#E^K_ju8ap!}2;`0Y% CaxiWH literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.5.APPLE b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.5.APPLE new file mode 100644 index 0000000000000000000000000000000000000000..95b55e36de930ecf79bed3e3565b6439581c852e GIT binary patch literal 1115 zcmb_bO;6iE5cRI}A(9iK;@%2zL6|nB5u!r!N1T92WQ)30#cdmwV%2d%Y!bn#QvU&5 zdL8wT?1el05dH(Rfo>KEQl)aScHg|2H}huQ?GFzaF%0A9D1rY`QeT7y;iNuzHe=b8hOCu``KXqOSl_2?%GNg9v#-5xHU#z zg+kE@!?qtywJ2)Q`n@P!#RFA+6va&u0@Z+(_76B4#8#ZXKCUxxZvmOoOj7^= literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.5.B b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.5.B new file mode 100644 index 0000000000000000000000000000000000000000..f6aaf7d2ff87ef338bbd9666a6e5fddc2f78ed1c GIT binary patch literal 1111 zcmb_aO;6iE5cRI}A(B(os&^{X3&K!JqY8xN51fEVWQ!VvxGiBR7LJp~CQ&&d@ekm_ zb;KXp3wQV-{0C+O+AK6kNaeEHee-7C%$sd@`v;5|hH-YYkEdafTf->WcrlIZFDcKh zwqz_f*N3z7jp47q{?CnLZ;)>_7TfIi>Z-8FGalc|dP_UuX5hHG*?gGT;%<@@%GalH_sFIAPq)So8SWHF9h+11iun8@Zn^fgc5cRC{L6Z{#aWAU4RJf&-2!Sfu>pB6E$W}23aa+=Dv2dI;b`!w~iGKhW zt|R`)Ubw>#;Xg1Ns?D~f5)!?vcHg|2H~VIWe?zC^zz@f%ak{w9x7&rR zyrE`1>OYxpZuRER-$xIhz6)p7L2qX$j`#M|Hk{+h*X3mQW%4Wz_||5byglj!S#M6f zYYbh^d0I0{jFedCOp@KD3p260`c=o+$a}^3P>MbvR=`5VYR+`lQnxO|T^`dfyB6cG3S`Z4NmnR@Cx6_x!q=_h;bO9v1S1+}%zp!>cT1xH literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.5.CAT b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.5.CAT new file mode 100644 index 0000000000000000000000000000000000000000..27d9d766f71d37be47212de7082a1dd244c50e7b GIT binary patch literal 1113 zcmb_bO;6iE5cRI}QIZp)>YWO4L72}*s3?*@Z~|%~Ti~LqwE*?zd`hCiC zt0Ng7Tf3vBmFDQ+r@ys#>J9V#=6Z*HtJlRUdBo#iv;O8`_$hGQm8C5Feo}Md<_LLj zD43oTMN}zKRHC&9NxF=ONvtUT6d_RZR@yt}tRGu(n)5-*5-a0_m?iNqmdCx+V%;Bo z%TLd`{;-z}dBn3kTj-u8c^vflV9d!DJlj{=rdXse8P67+sHB4UQU=1I{eOM6ANS?k zw&N;|5}HEh(Tmb6%h)~?k~qti(o+kb+ng;f$||WWsuYS&7`A*Ms~TLUKwKbe8Z>ji zaUkm&D0w?THZ(}`To;o!HBiblTA$k?kPjLx)0;qQ3Yxh?zb@t5d1b$3-EFK}P*T;1 zi1W#{BHo%%m5*+RpD^4hkOnrV=oRtqN!)Y;oU)-Y{;7c0OdjbJ1rOa*`PGPutD?GA r#GDDGeV(!|2jV=eX?l3vigbN?7$>=RUklG_#l7*r{7lftbq4NVN#g!hc{kpk;v|A(hK&_syGmGjF!j8yqlV7{PW_BYkRb?)Es^N<8SO7d&7LMxzb_3wzh;tp78jOtiSdx+zlLeX(0=b4r@-_93ihn zfz%^Y=}^?6wFg1Eh!?72MR8k%K#5yv?|`#@Y{hBL2PsRej1OX##D7^H_fm^>fA=jv zJ?Z+xUNYnn&+=@(dy?dF(C33OC!6DdYosQF>A?MSDva literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.5.FLUMP b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.multiOperations.5.FLUMP new file mode 100644 index 0000000000000000000000000000000000000000..86b4ed8e6a5d0a910440392e45bb15de5edb975f GIT binary patch literal 1115 zcmb_bO>fgc5cRI}p(ZB;;!aidQsI^mB2-k#A8`U|B3r>!6}KhbwpJY{jon1$gv399 z3)c~UWG~$559xpCY#MDgrIC=@i?x39X5Q?Z+5T{R#E4-S-~N2TuVIkChEZtZ!8Cra zr#!d%lCj%)Ki%4HPd{GzZ$F%Qlf2ii_1VeZo~V%}9{-$;>IdO|;JDjcS$KNfaN_n9 zc@+x9R_9YUjEYK&q84q;Md?dCP!%hR>mmfI0V^FIaW;yrIL-MuWr>yXamCs# zf|9C6M0{WTR>U(Cs&eU?{FvcPfwZtWMX!kGcjBfS;4NDk&0z=1gPF)8*^c-D$^{r)iCfgc5cRGTM@>$s6?dwNON9$*8X-_5f7CG*iEI^vP;c9GOD!BHjon0WLgF9L zORppT$X>XYAHsiNHdNbfNhBn4S?#`gGjHb24n~s`MhwIF^5Ym!!ywm&QE~8M8s9f_ zUfKi6c+q<`+ujLhZ$1Uj4?g*yIk69~I-)yZCkFdOO=ie0JJ)({P47 zivkG~OX*P5q3s(%`UEdj#j4_06(S{W=c5zO#;Kj=C76*rjlx+L80o#aW4S!RD)}{To1@I4O*jM z`fdWGsezKW4`f$^B+v6Oxutw`gogZd`}~yQPJwi>IYqCFhxg*97vYp$jqz6nwC47cE>Q5%J(b@`sJN=C sXI-qDU>)+D4LK0^W=)I3^H!wmi^Dj{ANRHJ1Fg6}-|UiLfa{FB-(2xa9smFU literal 0 HcmV?d00001 From 541207738a8174d128b1a17381d3cb217bcfa562 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Thu, 30 Nov 2017 13:31:33 +0000 Subject: [PATCH 3/7] CORDA-553 - Documentation --- docs/source/release-notes.rst | 5 +++ .../amqp/EnumEvolutionSerializer.kt | 32 +++++++++++++------ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 14d9a494cc..79263cfd6e 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -6,6 +6,11 @@ Here are release notes for each snapshot release from M9 onwards. Unreleased ---------- +** Enum Class Evolution + With the addition of AMQP serialization Corda now supports enum constant evolution, that is the abilit to alter + an enum constant and, as long as certain rules ar followed and the correct annotations applied, have older and + newer instances of that enumeration be understood. + Release 2.0 ---------- Following quickly on the heels of the release of Corda 1.0, Corda version 2.0 consolidates diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt index f8579fc21e..221954371b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt @@ -7,7 +7,12 @@ import java.lang.reflect.Type import java.util.* /** - * @property transforms + * @property clazz The enum as it exists now, not as it did when it was seialised (either in the past + * or future). + * @property factory the [SerializerFactory] that is building this serialization object. + * @property conversions A mapping between all potential enum constants that could've been assigned as + * existed at serialisation and those that exist now + * @property ordinals Convenience mapping of constant to ordinality * */ class EnumEvolutionSerializer( @@ -19,7 +24,7 @@ class EnumEvolutionSerializer( override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")!! companion object { - fun MutableMap.mapInPlace(f : (String)->String) { + private fun MutableMap.mapInPlace(f : (String)->String) { val i = this.iterator() while(i.hasNext()) { val curr = (i.next()) @@ -28,8 +33,14 @@ class EnumEvolutionSerializer( } /** - * @param old - * @param new + * Builds an Enum Evolver serializer. + * + * @param old The description of the enum as it existed at the time of serialisation take from the + * received AMQP header + * @param new The Serializer object we built based on the current state of the enum class on our classpath + * @param factory the [SerializerFactory] that is building this serialization object. + * @param transformsFromBlob the transforms attached to the class in the AMQP header, i.e. the transforms + * known at serialization time */ fun make(old: RestrictedType, new: AMQPSerializer, @@ -48,9 +59,9 @@ class EnumEvolutionSerializer( val renameRules = transforms[TransformTypes.Rename] as? List // What values exist on the enum as it exists on the class path - val localVals = new.type.asClass()!!.enumConstants.map { it.toString() } + val localValues = new.type.asClass()!!.enumConstants.map { it.toString() } - var conversions : MutableMap = new.type.asClass()!!.enumConstants.map { it.toString() } + val conversions : MutableMap = new.type.asClass()!!.enumConstants.map { it.toString() } .union(defaultRules?.map { it.new }?.toSet() ?: emptySet()) .union(renameRules?.map { it.to } ?: emptySet()) .associateBy({ it }, { it }) @@ -60,17 +71,20 @@ class EnumEvolutionSerializer( rules.putAll(defaultRules?.associateBy({ it.new }, { it.old }) ?: emptyMap()) rules.putAll(renameRules?.associateBy({ it.to }, { it.from }) ?: emptyMap()) - while (conversions.filter { it.value !in localVals }.isNotEmpty()) { + while (conversions.filter { it.value !in localValues }.isNotEmpty()) { conversions.mapInPlace { rules[it] ?: it } } + // you'd think this was overkill to get access to the ordinal values for each constant but it's actually + // rather tricky when you don't have access to the actual type, so this is a nice way to be able + // to precompute and pass to the actual object var idx = 0 - return EnumEvolutionSerializer(new.type, factory, conversions, localVals.associateBy( {it}, { idx++ })) + return EnumEvolutionSerializer(new.type, factory, conversions, localValues.associateBy( {it}, { idx++ })) } } override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any { - var enumName = (obj as List<*>)[0] as String + val enumName = (obj as List<*>)[0] as String if (enumName !in conversions) { throw NotSerializableException ("No rule to evolve enum constant $type::$enumName") From 32ebd2cc8b7e3e138645f5c3c672ea94e5c29432 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Fri, 1 Dec 2017 16:59:19 +0000 Subject: [PATCH 4/7] CORDA-553 - Review comments --- docs/source/release-notes.rst | 9 ++-- .../amqp/EnumEvolutionSerializer.kt | 42 +++++++++---------- .../serialization/amqp/EvolutionSerializer.kt | 2 +- .../serialization/amqp/TansformTypes.kt | 3 +- .../serialization/amqp/TransformsSchema.kt | 6 +-- .../serialization/amqp/EnumEvolveTests.kt | 10 ++--- 6 files changed, 35 insertions(+), 37 deletions(-) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 79263cfd6e..36aeb6c6c5 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -6,10 +6,11 @@ Here are release notes for each snapshot release from M9 onwards. Unreleased ---------- -** Enum Class Evolution - With the addition of AMQP serialization Corda now supports enum constant evolution, that is the abilit to alter - an enum constant and, as long as certain rules ar followed and the correct annotations applied, have older and - newer instances of that enumeration be understood. +* **Enum Class Evolution** + With the addition of AMQP serialization Corda now supports enum constant evolution. + + That is the ability to alter an enum constant and, as long as certain rules are followed and the correct + annotations applied, have older and newer instances of that enumeration be understood. Release 2.0 ---------- diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt index 221954371b..701df3732d 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt @@ -1,33 +1,33 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.internal.uncheckedCast import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data import java.io.NotSerializableException +import java.lang.UnsupportedOperationException import java.lang.reflect.Type -import java.util.* /** - * @property clazz The enum as it exists now, not as it did when it was seialised (either in the past + * @property clazz The enum as it exists now, not as it did when it was serialized (either in the past * or future). * @property factory the [SerializerFactory] that is building this serialization object. - * @property conversions A mapping between all potential enum constants that could've been assigned as - * existed at serialisation and those that exist now + * @property conversions A mapping between all potential enum constants that could've been assigned to + * an instance of the enum as it existed at serialisation and those that exist now * @property ordinals Convenience mapping of constant to ordinality - * */ class EnumEvolutionSerializer( clazz: Type, factory: SerializerFactory, - private val conversions : Map, - private val ordinals : Map) : AMQPSerializer { + private val conversions: Map, + private val ordinals: Map) : AMQPSerializer { override val type: Type = clazz override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")!! companion object { - private fun MutableMap.mapInPlace(f : (String)->String) { - val i = this.iterator() - while(i.hasNext()) { - val curr = (i.next()) + private fun MutableMap.mapInPlace(f: (String) -> String) { + val i = iterator() + while (i.hasNext()) { + val curr = i.next() curr.setValue(f(curr.value)) } } @@ -35,7 +35,7 @@ class EnumEvolutionSerializer( /** * Builds an Enum Evolver serializer. * - * @param old The description of the enum as it existed at the time of serialisation take from the + * @param old The description of the enum as it existed at the time of serialisation taken from the * received AMQP header * @param new The Serializer object we built based on the current state of the enum class on our classpath * @param factory the [SerializerFactory] that is building this serialization object. @@ -53,21 +53,19 @@ class EnumEvolutionSerializer( // if either of these isn't of the cast type then something has gone terribly wrong // elsewhere in the code - @Suppress("UNCHECKED_CAST") - val defaultRules = transforms[TransformTypes.EnumDefault] as? List - @Suppress("UNCHECKED_CAST") - val renameRules = transforms[TransformTypes.Rename] as? List + val defaultRules: List? = uncheckedCast(transforms[TransformTypes.EnumDefault]) + val renameRules: List? = uncheckedCast(transforms[TransformTypes.Rename]) // What values exist on the enum as it exists on the class path val localValues = new.type.asClass()!!.enumConstants.map { it.toString() } - val conversions : MutableMap = new.type.asClass()!!.enumConstants.map { it.toString() } + val conversions: MutableMap = localValues .union(defaultRules?.map { it.new }?.toSet() ?: emptySet()) .union(renameRules?.map { it.to } ?: emptySet()) .associateBy({ it }, { it }) .toMutableMap() - val rules : MutableMap = mutableMapOf() + val rules: MutableMap = mutableMapOf() rules.putAll(defaultRules?.associateBy({ it.new }, { it.old }) ?: emptyMap()) rules.putAll(renameRules?.associateBy({ it.to }, { it.from }) ?: emptyMap()) @@ -79,7 +77,7 @@ class EnumEvolutionSerializer( // rather tricky when you don't have access to the actual type, so this is a nice way to be able // to precompute and pass to the actual object var idx = 0 - return EnumEvolutionSerializer(new.type, factory, conversions, localValues.associateBy( {it}, { idx++ })) + return EnumEvolutionSerializer(new.type, factory, conversions, localValues.associateBy({ it }, { idx++ })) } } @@ -87,17 +85,17 @@ class EnumEvolutionSerializer( val enumName = (obj as List<*>)[0] as String if (enumName !in conversions) { - throw NotSerializableException ("No rule to evolve enum constant $type::$enumName") + throw NotSerializableException("No rule to evolve enum constant $type::$enumName") } return type.asClass()!!.enumConstants[ordinals[conversions[enumName]]!!] } override fun writeClassInfo(output: SerializationOutput) { - throw IllegalAccessException("It should be impossible to write an evolution serializer") + throw UnsupportedOperationException("It should be impossible to write an evolution serializer") } override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) { - throw IllegalAccessException("It should be impossible to write an evolution serializer") + throw UnsupportedOperationException("It should be impossible to write an evolution serializer") } } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolutionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolutionSerializer.kt index 59f22598a0..411e405aad 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolutionSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolutionSerializer.kt @@ -109,7 +109,7 @@ class EvolutionSerializer( } override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) { - throw IllegalAccessException("It should be impossible to write an evolution serializer") + throw UnsupportedOperationException("It should be impossible to write an evolution serializer") } /** 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 6fd1e4a95f..e21498f7b0 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,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializationTransformEnumDefault import net.corda.core.serialization.CordaSerializationTransformEnumDefaults import net.corda.core.serialization.CordaSerializationTransformRename @@ -43,7 +44,7 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType * @param constants The list of enum constants on the type the transforms are being applied to */ override fun validate(l : List, constants: Set) { - @Suppress("UNCHECKED_CAST") (l as List).forEach { + uncheckedCast, List>(l).forEach { if (!constants.contains(it.old)) { throw NotSerializableException( "Enum extension defaults must be to a valid constant: ${it.new} -> ${it.old}. ${it.old} " + 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 a8409a64c3..c88addacaa 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 @@ -146,10 +146,8 @@ class EnumDefaultSchemaTransform(val old: String, val new: String) : Transform() /** * 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 - * - * + * @property from the name of the property or constant prior to being changed, i.e. what it was + * @property to the new name of the property or constant after the change has been made, i.e. what it is now */ class RenameSchemaTransform(val from: String, val to: String) : Transform() { companion object : DescribedTypeConstructor { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.kt index c28ec8d95f..2b7930c707 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.kt @@ -27,7 +27,7 @@ class EnumEvolveTests { @Test fun deserialiseNewerSetToUnknown() { - val resource = "${this.javaClass.simpleName}.${testName()}" + val resource = "${javaClass.simpleName}.${testName()}" val sf = testDefaultFactory() data class C (val e : DeserializeNewerSetToUnknown) @@ -55,7 +55,7 @@ class EnumEvolveTests { @Test fun deserialiseNewerSetToUnknown2() { - val resource = "${this.javaClass.simpleName}.${testName()}" + val resource = "${javaClass.simpleName}.${testName()}" val sf = testDefaultFactory() data class C(val e: DeserializeNewerSetToUnknown2) @@ -95,7 +95,7 @@ class EnumEvolveTests { // Lets test to see if they forgot to provide an upgrade rule @Test fun deserialiseNewerWithNoRule() { - val resource = "${this.javaClass.simpleName}.${testName()}" + val resource = "${javaClass.simpleName}.${testName()}" val sf = testDefaultFactory() data class C(val e: DeserializeNewerWithNoRule) @@ -143,7 +143,7 @@ class EnumEvolveTests { @Test fun deserializeWithRename() { - val resource = "${this.javaClass.simpleName}.${testName()}" + val resource = "${javaClass.simpleName}.${testName()}" val sf = testDefaultFactory() data class C(val e: DeserializeWithRename) @@ -265,7 +265,7 @@ class EnumEvolveTests { @Test fun multiOperations() { - val resource = "${this.javaClass.simpleName}.${testName()}" + val resource = "${javaClass.simpleName}.${testName()}" val sf = testDefaultFactory() data class C(val e: MultiOperations) From 7e044e1124537ebb0450355153de7b6c4ae71fc0 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Fri, 1 Dec 2017 19:15:14 +0000 Subject: [PATCH 5/7] CORDA-553 - Review comments --- .../amqp/EnumEvolutionSerializer.kt | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt index 701df3732d..d5690d5f63 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt @@ -8,11 +8,29 @@ import java.lang.UnsupportedOperationException import java.lang.reflect.Type /** + * Used whenever a deserialized enums fingerprint doesn't match the fingerprint of the generated + * serializer object. I.e. the deserializing code has a different version of the code either newer or + * older). The changes will have been documented using the transformation annotations, a copy of which + * are encoded as part of the AMQP envelope. + * + * This function ascertains which version of the enumeration is newer by comparing the length of the + * transformations list. Since transformation annotations should only ever be added, never removed even + * when seemingly unneeded (such as repeated renaming of a single constant), the longer list will dictate + * which is more up to date. + * + * The list of transforms come from two places, the class as it exists on the current class path and the + * class as it exists as it was serialized. In the case of the former we can build the list by using + * reflection on the class. In the case of the latter the transforms are retrieved from the AMQP envelope. + * + * With a set of transforms chosen we calculate the set of all possible constants, then using the + * transformation rules we create a mapping between those values and the values that exist on the + * current class + * * @property clazz The enum as it exists now, not as it did when it was serialized (either in the past * or future). * @property factory the [SerializerFactory] that is building this serialization object. * @property conversions A mapping between all potential enum constants that could've been assigned to - * an instance of the enum as it existed at serialisation and those that exist now + * an instance of the enum as it existed at time of serialisation and those that exist now * @property ordinals Convenience mapping of constant to ordinality */ class EnumEvolutionSerializer( @@ -76,8 +94,8 @@ class EnumEvolutionSerializer( // you'd think this was overkill to get access to the ordinal values for each constant but it's actually // rather tricky when you don't have access to the actual type, so this is a nice way to be able // to precompute and pass to the actual object - var idx = 0 - return EnumEvolutionSerializer(new.type, factory, conversions, localValues.associateBy({ it }, { idx++ })) + return EnumEvolutionSerializer(new.type, factory, conversions, + localValues.mapIndexed { i, s -> Pair (s, i)}.toMap()) } } From e257872445913c072a22a59436aefc731e39dc2b Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Mon, 4 Dec 2017 09:39:16 +0000 Subject: [PATCH 6/7] CORDA-553 - Review Comments --- .../internal/serialization/amqp/EnumEvolutionSerializer.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt index d5690d5f63..25413d650c 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt @@ -34,11 +34,10 @@ import java.lang.reflect.Type * @property ordinals Convenience mapping of constant to ordinality */ class EnumEvolutionSerializer( - clazz: Type, + override val type: Type, factory: SerializerFactory, private val conversions: Map, private val ordinals: Map) : AMQPSerializer { - override val type: Type = clazz override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")!! companion object { @@ -87,7 +86,7 @@ class EnumEvolutionSerializer( rules.putAll(defaultRules?.associateBy({ it.new }, { it.old }) ?: emptyMap()) rules.putAll(renameRules?.associateBy({ it.to }, { it.from }) ?: emptyMap()) - while (conversions.filter { it.value !in localValues }.isNotEmpty()) { + while (conversions.filterNot { it.value in localValues }.isNotEmpty()) { conversions.mapInPlace { rules[it] ?: it } } From 1ff0d881b356cd9746d05b074f8fbf9a28f51b24 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Mon, 4 Dec 2017 15:45:52 +0000 Subject: [PATCH 7/7] CORDA-553 - Better tests for rule breaking changes to enum constants --- .../amqp/EnumEvolutionSerializer.kt | 32 +++++++--- .../serialization/amqp/SerializerFactory.kt | 6 +- .../serialization/amqp/TansformTypes.kt | 22 +++++-- .../serialization/amqp/TransformsSchema.kt | 2 +- .../serialization/amqp/EnumEvolveTests.kt | 56 ++++++++++++++++++ .../amqp/EnumEvolveTests.changedOrdinality | Bin 0 -> 618 bytes 6 files changed, 101 insertions(+), 17 deletions(-) create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.changedOrdinality diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt index 25413d650c..ec8a61c793 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt @@ -6,6 +6,7 @@ import org.apache.qpid.proton.codec.Data import java.io.NotSerializableException import java.lang.UnsupportedOperationException import java.lang.reflect.Type +import java.util.* /** * Used whenever a deserialized enums fingerprint doesn't match the fingerprint of the generated @@ -62,11 +63,13 @@ class EnumEvolutionSerializer( fun make(old: RestrictedType, new: AMQPSerializer, factory: SerializerFactory, - transformsFromBlob: TransformsSchema): AMQPSerializer { - - val wireTransforms = transformsFromBlob.types[old.name] + schemas: SerializationSchemas): AMQPSerializer { + val wireTransforms = schemas.transforms.types[old.name] ?: EnumMap>(TransformTypes::class.java) val localTransforms = TransformsSchema.get(old.name, factory) - val transforms = if (wireTransforms?.size ?: -1 > localTransforms.size) wireTransforms!! else localTransforms + + // remember, the longer the list the newer we're assuming the transform set it as we assume + // evolution annotations are never removed, only added to + val transforms = if (wireTransforms.size > localTransforms.size) wireTransforms else localTransforms // if either of these isn't of the cast type then something has gone terribly wrong // elsewhere in the code @@ -84,8 +87,12 @@ class EnumEvolutionSerializer( val rules: MutableMap = mutableMapOf() rules.putAll(defaultRules?.associateBy({ it.new }, { it.old }) ?: emptyMap()) - rules.putAll(renameRules?.associateBy({ it.to }, { it.from }) ?: emptyMap()) + val renameRulesMap = renameRules?.associateBy({ it.to }, { it.from }) ?: emptyMap() + rules.putAll(renameRulesMap) + // take out set of all possible constants and build a map from those to the + // existing constants applying the rename and defaulting rules as defined + // in the schema while (conversions.filterNot { it.value in localValues }.isNotEmpty()) { conversions.mapInPlace { rules[it] ?: it } } @@ -93,8 +100,19 @@ class EnumEvolutionSerializer( // you'd think this was overkill to get access to the ordinal values for each constant but it's actually // rather tricky when you don't have access to the actual type, so this is a nice way to be able // to precompute and pass to the actual object - return EnumEvolutionSerializer(new.type, factory, conversions, - localValues.mapIndexed { i, s -> Pair (s, i)}.toMap()) + val ordinals = localValues.mapIndexed { i, s -> Pair(s, i) }.toMap() + + // create a mapping between the ordinal value and the name as it was serialised converted + // to the name as it exists. We want to test any new constants have been added to the end + // of the enum class + val serialisedOrds = ((schemas.schema.types.find { it.name == old.name } as RestrictedType).choices + .associateBy ({ it.value.toInt() }, { conversions[it.name] })) + + if (ordinals.filterNot { serialisedOrds[it.value] == it.key }.isNotEmpty()) { + throw NotSerializableException("Constants have been reordered, additions must be appended to the end") + } + + return EnumEvolutionSerializer(new.type, factory, conversions, ordinals) } } 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 764f0b5458..39abcc58f5 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 @@ -46,11 +46,11 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { private fun getEvolutionSerializer( typeNotation: TypeNotation, newSerializer: AMQPSerializer, - transforms: TransformsSchema): AMQPSerializer { + schemas: SerializationSchemas): AMQPSerializer { return serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) { when (typeNotation) { is CompositeType -> EvolutionSerializer.make(typeNotation, newSerializer as ObjectSerializer, this) - is RestrictedType -> EnumEvolutionSerializer.make(typeNotation, newSerializer, this, transforms) + is RestrictedType -> EnumEvolutionSerializer.make(typeNotation, newSerializer, this, schemas) } } } @@ -210,7 +210,7 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { // doesn't match that of the serialised object then we are dealing with different // instance of the class, as such we need to build an EvolutionSerialiser if (serialiser.typeDescriptor != typeNotation.descriptor.name) { - getEvolutionSerializer(typeNotation, serialiser, schemaAndDescriptor.schemas.transforms) + getEvolutionSerializer(typeNotation, serialiser, schemaAndDescriptor.schemas) } } catch (e: ClassNotFoundException) { if (sentinel) throw e 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 e21498f7b0..0003a48ba2 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 @@ -28,14 +28,14 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType Unknown({ UnknownTransform() }) { override fun getDescriptor(): Any = DESCRIPTOR override fun getDescribed(): Any = ordinal - override fun validate(l : List, constants: Set) { } + override fun validate(l : List, constants: Map) { } }, EnumDefault({ a -> EnumDefaultSchemaTransform((a as CordaSerializationTransformEnumDefault).old, a.new) }) { override fun getDescriptor(): Any = DESCRIPTOR override fun getDescribed(): Any = ordinal /** - * Validates a list of constant additions to an enumerated types, to be valid a default (the value + * Validates a list of constant additions to an enumerated type. To be valid a default (the value * that should be used when we cannot use the new value) must refer to a constant that exists in the * enum class as it exists now and it cannot refer to itself. * @@ -43,8 +43,12 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType * existing value * @param constants The list of enum constants on the type the transforms are being applied to */ - override fun validate(l : List, constants: Set) { - uncheckedCast, List>(l).forEach { + override fun validate(list : List, constants: Map) { + uncheckedCast, List>(list).forEach { + if (!constants.contains(it.new)) { + throw NotSerializableException("Unknown enum constant ${it.new}") + } + if (!constants.contains(it.old)) { throw NotSerializableException( "Enum extension defaults must be to a valid constant: ${it.new} -> ${it.old}. ${it.old} " + @@ -54,6 +58,12 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType if (it.old == it.new) { throw NotSerializableException("Enum extension ${it.new} cannot default to itself") } + + if (constants[it.old]!! >= constants[it.new]!!) { + throw NotSerializableException( + "Enum extensions must default to older constants. ${it.new}[${constants[it.new]}] " + + "defaults to ${it.old}[${constants[it.old]}] which is greater") + } } } }, @@ -70,7 +80,7 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType * and old values * @param constants The list of enum constants on the type the transforms are being applied to */ - override fun validate(l : List, constants: Set) { + override fun validate(l : List, constants: Map) { object : Any() { val from : MutableSet = mutableSetOf() val to : MutableSet = mutableSetOf() }.apply { @@ -94,7 +104,7 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType //} ; - abstract fun validate(l: List, constants: Set) + abstract fun validate(l: List, constants: Map) companion object : DescribedTypeConstructor { val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_ELEMENT_KEY.amqpDescriptor 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 c88addacaa..378675b84e 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 @@ -225,7 +225,7 @@ data class TransformsSchema(val types: Map Pair(s.toString(), i) }.toMap()) } } } catch (_: ClassNotFoundException) { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.kt index 2b7930c707..b2939d7fa9 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.kt @@ -355,4 +355,60 @@ class EnumEvolveTests { load (stage4Resources).forEach { assertEquals(it.second, it.first.e) } load (stage5Resources).forEach { assertEquals(it.second, it.first.e) } } + + @CordaSerializationTransformEnumDefault(old = "A", new = "F") + enum class BadNewValue { A, B, C, D } + + @Test + fun badNewValue() { + val sf = testDefaultFactory() + + data class C (val e : BadNewValue) + + Assertions.assertThatThrownBy { + SerializationOutput(sf).serialize(C(BadNewValue.A)) + }.isInstanceOf(NotSerializableException::class.java) + } + + @CordaSerializationTransformEnumDefaults( + CordaSerializationTransformEnumDefault(new = "D", old = "E"), + CordaSerializationTransformEnumDefault(new = "E", old = "A") + ) + enum class OutOfOrder { A, B, C, D, E} + + @Test + fun outOfOrder() { + val sf = testDefaultFactory() + + data class C (val e : OutOfOrder) + + Assertions.assertThatThrownBy { + SerializationOutput(sf).serialize(C(OutOfOrder.A)) + }.isInstanceOf(NotSerializableException::class.java) + } + + // class as it existed as it was serialized + // + // enum class ChangedOrdinality { A, B, C } + // + // class as it exists for the tests + @CordaSerializationTransformEnumDefault("D", "A") + enum class ChangedOrdinality { A, B, D, C } + + @Test + fun changedOrdinality() { + val resource = "${javaClass.simpleName}.${testName()}" + val sf = testDefaultFactory() + + data class C(val e: ChangedOrdinality) + + // Uncomment to re-generate test files, needs to be done in three stages + // File(URI("$localPath/$resource")).writeBytes( + // SerializationOutput(sf).serialize(C(ChangedOrdinality.A)).bytes) + + Assertions.assertThatThrownBy { + DeserializationInput(sf).deserialize(SerializedBytes( + File(EvolvabilityTests::class.java.getResource(resource).toURI()).readBytes())) + }.isInstanceOf(NotSerializableException::class.java) + } } diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.changedOrdinality b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.changedOrdinality new file mode 100644 index 0000000000000000000000000000000000000000..af084de97a4905407e842a4687e7f13323a45017 GIT binary patch literal 618 zcmb`EPfNov7{-%!Yw>Hy@EF__{|p9QOJR7h=*kYerA_FfUD~y=W1jX^f?p&a1dm?4 z`W^fRW*`v@f}of1^6)(To;Pn=R0Kl+!1gK8F97fb0BfoT8$9fDDp0y}49WRCVYiFx z*&y9a#Psyyh^NVVG*SqD^PTx8@uXj`^DJ65H|1sHACp<8eU2sJIR4^TngicHf$&ui zmg&${?_GIi;D{H5;*uf81+6&FQB5m`b9RpfD>%Z-Rf$4=w+x#iZ)i+wQTNgX<{2eX zMVL-7vFQa~Xb84Cuzs_ir!r_l9W{3)q%!(jv!LB9^t`ceD2{q|{vF|kvXiq~7)tvC ge!82dhPDa0%H3U2!o!y2OW1EofrJAi?L9*90ks*$9RL6T literal 0 HcmV?d00001