Merge pull request from corda/kat/feature/enumEvolver

CORDA-553 - First steps towards evolvability
This commit is contained in:
Katelyn Baker 2017-10-30 14:08:09 +00:00 committed by GitHub
commit a74154a126
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1253 additions and 161 deletions

@ -2436,6 +2436,20 @@ public interface net.corda.core.serialization.ClassWhitelist
##
public @interface net.corda.core.serialization.CordaSerializable
##
public @interface net.corda.core.serialization.CordaSerializationTransformEnumDefault
public abstract String new()
public abstract String old()
##
public @interface net.corda.core.serialization.CordaSerializationTransformEnumDefaults
public abstract net.corda.core.serialization.CordaSerializationTransformEnumDefault[] value()
##
public @interface net.corda.core.serialization.CordaSerializationTransformRename
public abstract String from()
public abstract String to()
##
public @interface net.corda.core.serialization.CordaSerializationTransformRenames
public abstract net.corda.core.serialization.CordaSerializationTransformRename[] value()
##
public @interface net.corda.core.serialization.DeprecatedConstructorForDeserialization
public abstract int version()
##

@ -0,0 +1,93 @@
package net.corda.core.serialization
/**
* This annotation is used to mark an enumerated type as having had multiple members added, It acts
* as a container annotation for instances of [CordaSerializationTransformEnumDefault], each of which
* details individual additions.
*
* @property value an array of [CordaSerializationTransformEnumDefault].
*
* NOTE: Order is important, new values should always be added before any others
*
* ```
* // initial implementation
* enum class ExampleEnum {
* A, B, C
* }
*
* // First alteration
* @CordaSerializationTransformEnumDefaults(
* CordaSerializationTransformEnumDefault("D", "C"))
* enum class ExampleEnum {
* A, B, C, D
* }
*
* // Second alteration, new transform is placed at the head of the list
* @CordaSerializationTransformEnumDefaults(
* CordaSerializationTransformEnumDefault("E", "C"),
* CordaSerializationTransformEnumDefault("D", "C"))
* enum class ExampleEnum {
* A, B, C, D, E
* }
* ```
*
* IMPORTANT - Once added (and in production) do NOT remove old annotations. See documentation for
* more discussion on this point!.
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class CordaSerializationTransformEnumDefaults(vararg val value: CordaSerializationTransformEnumDefault)
/**
* This annotation is used to mark an enumerated type as having had a new constant appended to it. For
* each additional constant added a new annotation should be appended to the class. If more than one
* is required the wrapper annotation [CordaSerializationTransformEnumDefaults] should be used to
* encapsulate them
*
* @property new [String] equivalent of the value of the new constant
* @property old [String] equivalent of the value of the existing constant that deserialisers should
* favour when de-serialising a value they have no corresponding value for
*
* For Example
*
* Enum before modification:
* ```
* enum class ExampleEnum {
* A, B, C
* }
* ```
*
* Assuming at some point a new constant is added it is required we have some mechanism by which to tell
* nodes with an older version of the class on their Class Path what to do if they attempt to deserialize
* an example of the class with that new value
*
* ```
* @CordaSerializationTransformEnumDefault("D", "C")
* enum class ExampleEnum {
* A, B, C, D
* }
* ```
*
* So, on deserialisation treat any instance of the enum that is encoded as D as C
*
* Adding a second new constant requires the wrapper annotation [CordaSerializationTransformEnumDefaults]
*
* ```
* @CordaSerializationTransformEnumDefaults(
* CordaSerializationTransformEnumDefault("E", "D"),
* CordaSerializationTransformEnumDefault("D", "C"))
* enum class ExampleEnum {
* A, B, C, D, E
* }
* ```
*
* It's fine to assign the second new value a default that may not be present in all versions as in this
* case it will work down the transform hierarchy until it finds a value it can apply, in this case it would
* try E -> D -> C (when E -> D fails)
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
// When Kotlin starts writing 1.8 class files enable this, it removes the need for the wrapping annotation
//@Repeatable
annotation class CordaSerializationTransformEnumDefault(val new: String, val old: String)

@ -0,0 +1,35 @@
package net.corda.core.serialization
/**
* This annotation is used to mark a class as having had multiple elements renamed as a container annotation for
* instances of [CordaSerializationTransformRename], each of which details an individual rename.
*
* @property value an array of [CordaSerializationTransformRename]
*
* NOTE: Order is important, new values should always be added before existing
*
* IMPORTANT - Once added (and in production) do NOT remove old annotations. See documentation for
* more discussion on this point!.
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class CordaSerializationTransformRenames(vararg val value: CordaSerializationTransformRename)
// TODO When we have class renaming update the docs
/**
* This annotation is used to mark a class has having had a property element. It is used by the
* AMQP deserialiser to allow instances with different versions of the class on their Class Path
* to successfully deserialize the object
*
* NOTE: Renaming of the class itself is not be done with this annotation. For class renaming
* see ???
*
* @property to [String] representation of the properties new name
* @property from [String] representation of the properties old new
*
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
// When Kotlin starts writing 1.8 class files enable this, it removes the need for the wrapping annotation
//@Repeatable
annotation class CordaSerializationTransformRename(val to: String, val from: String)

@ -0,0 +1,38 @@
package net.corda.nodeapi.internal.serialization.amqp
import org.apache.qpid.proton.amqp.UnsignedLong
/**
* R3 AMQP assigned enterprise number
*
* see [here](https://www.iana.org/assignments/enterprise-numbers/enterprise-numbers)
*
* Repeated here for brevity:
* 50530 - R3 - Mike Hearn - mike&r3.com
*/
const val DESCRIPTOR_TOP_32BITS: Long = 0xc562L shl(32 + 16)
/**
* AMQP desriptor ID's for our custom types.
*
* NEVER DELETE OR CHANGE THE ID ASSOCIATED WITH A TYPE
*
* these are encoded as part of a serialised blob and doing so would render us unable to
* de-serialise that blob!!!
*/
enum class AMQPDescriptorRegistry(val id: Long) {
ENVELOPE(1),
SCHEMA(2),
OBJECT_DESCRIPTOR(3),
FIELD(4),
COMPOSITE_TYPE(5),
RESTRICTED_TYPE(6),
CHOICE(7),
REFERENCED_OBJECT(8),
TRANSFORM_SCHEMA(9),
TRANSFORM_ELEMENT(10),
TRANSFORM_ELEMENT_KEY(11)
;
val amqpDescriptor = UnsignedLong(id or DESCRIPTOR_TOP_32BITS)
}

@ -0,0 +1,70 @@
package net.corda.nodeapi.internal.serialization.amqp
import org.apache.qpid.proton.amqp.DescribedType
import org.apache.qpid.proton.codec.Data
import org.apache.qpid.proton.codec.DescribedTypeConstructor
import java.io.NotSerializableException
/**
* This class wraps all serialized data, so that the schema can be carried along with it. We will provide various
* internal utilities to decompose and recompose with/without schema etc so that e.g. we can store objects with a
* (relationally) normalised out schema to avoid excessive duplication.
*/
// TODO: make the schema parsing lazy since mostly schemas will have been seen before and we only need it if we
// TODO: don't recognise a type descriptor.
data class Envelope(val obj: Any?, val schema: Schema, val transformsSchema: TransformsSchema) : DescribedType {
companion object : DescribedTypeConstructor<Envelope> {
val DESCRIPTOR = AMQPDescriptorRegistry.ENVELOPE.amqpDescriptor
val DESCRIPTOR_OBJECT = Descriptor(null, DESCRIPTOR)
// described list should either be two or three elements long
private const val ENVELOPE_WITHOUT_TRANSFORMS = 2
private const val ENVELOPE_WITH_TRANSFORMS = 3
private const val BLOB_IDX = 0
private const val SCHEMA_IDX = 1
private const val TRANSFORMS_SCHEMA_IDX = 2
fun get(data: Data): Envelope {
val describedType = data.`object` as DescribedType
if (describedType.descriptor != DESCRIPTOR) {
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
}
val list = describedType.described as List<*>
// We need to cope with objects serialised without the transforms header element in the
// envelope
val transformSchema: Any? = when (list.size) {
ENVELOPE_WITHOUT_TRANSFORMS -> null
ENVELOPE_WITH_TRANSFORMS -> list[TRANSFORMS_SCHEMA_IDX]
else -> throw NotSerializableException("Malformed list, bad length of ${list.size} (should be 2 or 3)")
}
return newInstance(listOf(list[BLOB_IDX], Schema.get(list[SCHEMA_IDX]!!),
TransformsSchema.newInstance(transformSchema)))
}
// This separation of functions is needed as this will be the entry point for the default
// AMQP decoder if one is used (see the unit tests).
override fun newInstance(described: Any?): Envelope {
val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list")
// We need to cope with objects serialised without the transforms header element in the
// envelope
val transformSchema = when (list.size) {
ENVELOPE_WITHOUT_TRANSFORMS -> TransformsSchema.newInstance(null)
ENVELOPE_WITH_TRANSFORMS -> list[TRANSFORMS_SCHEMA_IDX] as TransformsSchema
else -> throw NotSerializableException("Malformed list, bad length of ${list.size} (should be 2 or 3)")
}
return Envelope(list[BLOB_IDX], list[SCHEMA_IDX] as Schema, transformSchema)
}
override fun getTypeClass(): Class<*> = Envelope::class.java
}
override fun getDescriptor(): Any = DESCRIPTOR
override fun getDescribed(): Any = listOf(obj, schema, transformsSchema)
}

@ -10,7 +10,6 @@ import org.apache.qpid.proton.amqp.DescribedType
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.amqp.UnsignedInteger
import org.apache.qpid.proton.amqp.UnsignedLong
import org.apache.qpid.proton.codec.Data
import org.apache.qpid.proton.codec.DescribedTypeConstructor
import java.io.NotSerializableException
import java.lang.reflect.*
@ -18,76 +17,18 @@ import java.util.*
import net.corda.nodeapi.internal.serialization.carpenter.Field as CarpenterField
import net.corda.nodeapi.internal.serialization.carpenter.Schema as CarpenterSchema
/**
* R3 AMQP assigned enterprise number
*
* see [here](https://www.iana.org/assignments/enterprise-numbers/enterprise-numbers)
*
* Repeated here for brevity:
* 50530 - R3 - Mike Hearn - mike&r3.com
*/
const val DESCRIPTOR_TOP_32BITS: Long = 0xc5620000
const val DESCRIPTOR_DOMAIN: String = "net.corda"
// "corda" + majorVersionByte + minorVersionMSB + minorVersionLSB
val AmqpHeaderV1_0: OpaqueBytes = OpaqueBytes("corda\u0001\u0000\u0000".toByteArray())
private enum class DescriptorRegistry(val id: Long) {
ENVELOPE(1),
SCHEMA(2),
OBJECT_DESCRIPTOR(3),
FIELD(4),
COMPOSITE_TYPE(5),
RESTRICTED_TYPE(6),
CHOICE(7),
REFERENCED_OBJECT(8),
;
val amqpDescriptor = UnsignedLong(id or DESCRIPTOR_TOP_32BITS)
}
/**
* This class wraps all serialized data, so that the schema can be carried along with it. We will provide various internal utilities
* to decompose and recompose with/without schema etc so that e.g. we can store objects with a (relationally) normalised out schema to
* avoid excessive duplication.
*/
// TODO: make the schema parsing lazy since mostly schemas will have been seen before and we only need it if we don't recognise a type descriptor.
data class Envelope(val obj: Any?, val schema: Schema) : DescribedType {
companion object : DescribedTypeConstructor<Envelope> {
val DESCRIPTOR = DescriptorRegistry.ENVELOPE.amqpDescriptor
val DESCRIPTOR_OBJECT = Descriptor(null, DESCRIPTOR)
fun get(data: Data): Envelope {
val describedType = data.`object` as DescribedType
if (describedType.descriptor != DESCRIPTOR) {
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
}
val list = describedType.described as List<*>
return newInstance(listOf(list[0], Schema.get(list[1]!!)))
}
override fun getTypeClass(): Class<*> = Envelope::class.java
override fun newInstance(described: Any?): Envelope {
val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list")
return Envelope(list[0], list[1] as Schema)
}
}
override fun getDescriptor(): Any = DESCRIPTOR
override fun getDescribed(): Any = listOf(obj, schema)
}
/**
* This and the classes below are OO representations of the AMQP XML schema described in the specification. Their
* [toString] representations generate the associated XML form.
*/
data class Schema(val types: List<TypeNotation>) : DescribedType {
companion object : DescribedTypeConstructor<Schema> {
val DESCRIPTOR = DescriptorRegistry.SCHEMA.amqpDescriptor
val DESCRIPTOR = AMQPDescriptorRegistry.SCHEMA.amqpDescriptor
fun get(obj: Any): Schema {
val describedType = obj as DescribedType
@ -117,7 +58,7 @@ data class Descriptor(val name: Symbol?, val code: UnsignedLong? = null) : Descr
constructor(name: String?) : this(Symbol.valueOf(name))
companion object : DescribedTypeConstructor<Descriptor> {
val DESCRIPTOR = DescriptorRegistry.OBJECT_DESCRIPTOR.amqpDescriptor
val DESCRIPTOR = AMQPDescriptorRegistry.OBJECT_DESCRIPTOR.amqpDescriptor
fun get(obj: Any): Descriptor {
val describedType = obj as DescribedType
@ -155,7 +96,7 @@ data class Descriptor(val name: Symbol?, val code: UnsignedLong? = null) : Descr
data class Field(val name: String, val type: String, val requires: List<String>, val default: String?, val label: String?, val mandatory: Boolean, val multiple: Boolean) : DescribedType {
companion object : DescribedTypeConstructor<Field> {
val DESCRIPTOR = DescriptorRegistry.FIELD.amqpDescriptor
val DESCRIPTOR = AMQPDescriptorRegistry.FIELD.amqpDescriptor
fun get(obj: Any): Field {
val describedType = obj as DescribedType
@ -215,7 +156,7 @@ sealed class TypeNotation : DescribedType {
data class CompositeType(override val name: String, override val label: String?, override val provides: List<String>, override val descriptor: Descriptor, val fields: List<Field>) : TypeNotation() {
companion object : DescribedTypeConstructor<CompositeType> {
val DESCRIPTOR = DescriptorRegistry.COMPOSITE_TYPE.amqpDescriptor
val DESCRIPTOR = AMQPDescriptorRegistry.COMPOSITE_TYPE.amqpDescriptor
fun get(describedType: DescribedType): CompositeType {
if (describedType.descriptor != DESCRIPTOR) {
@ -264,7 +205,7 @@ data class RestrictedType(override val name: String,
override val descriptor: Descriptor,
val choices: List<Choice>) : TypeNotation() {
companion object : DescribedTypeConstructor<RestrictedType> {
val DESCRIPTOR = DescriptorRegistry.RESTRICTED_TYPE.amqpDescriptor
val DESCRIPTOR = AMQPDescriptorRegistry.RESTRICTED_TYPE.amqpDescriptor
fun get(describedType: DescribedType): RestrictedType {
if (describedType.descriptor != DESCRIPTOR) {
@ -309,7 +250,7 @@ data class RestrictedType(override val name: String,
data class Choice(val name: String, val value: String) : DescribedType {
companion object : DescribedTypeConstructor<Choice> {
val DESCRIPTOR = DescriptorRegistry.CHOICE.amqpDescriptor
val DESCRIPTOR = AMQPDescriptorRegistry.CHOICE.amqpDescriptor
fun get(obj: Any): Choice {
val describedType = obj as DescribedType
@ -338,7 +279,7 @@ data class Choice(val name: String, val value: String) : DescribedType {
data class ReferencedObject(private val refCounter: Int) : DescribedType {
companion object : DescribedTypeConstructor<ReferencedObject> {
val DESCRIPTOR = DescriptorRegistry.REFERENCED_OBJECT.amqpDescriptor
val DESCRIPTOR = AMQPDescriptorRegistry.REFERENCED_OBJECT.amqpDescriptor
fun get(obj: Any): ReferencedObject {
val describedType = obj as DescribedType

@ -45,10 +45,10 @@ open class SerializationOutput(internal val serializerFactory: SerializerFactory
val data = Data.Factory.create()
data.withDescribed(Envelope.DESCRIPTOR_OBJECT) {
withList {
// Our object
writeObject(obj, this)
// The schema
writeSchema(Schema(schemaHistory.toList()), this)
val schema = Schema(schemaHistory.toList())
writeSchema(schema, this)
writeTransformSchema(TransformsSchema.build(schema, serializerFactory), this)
}
}
val bytes = ByteArray(data.encodedSize().toInt() + 8)
@ -66,6 +66,10 @@ open class SerializationOutput(internal val serializerFactory: SerializerFactory
data.putObject(schema)
}
open fun writeTransformSchema(transformsSchema: TransformsSchema, data: Data) {
data.putObject(transformsSchema)
}
internal fun writeObjectOrNull(obj: Any?, data: Data, type: Type) {
if (obj == null) {
data.putNull()

@ -34,6 +34,8 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
private val serializersByType = ConcurrentHashMap<Type, AMQPSerializer<Any>>()
private val serializersByDescriptor = ConcurrentHashMap<Any, AMQPSerializer<Any>>()
private val customSerializers = CopyOnWriteArrayList<CustomSerializer<out Any>>()
val transformsCache = ConcurrentHashMap<String, EnumMap<TransformTypes, MutableList<Transform>>>()
open val classCarpenter = ClassCarpenter(cl, whitelist)
val classloader: ClassLoader
get() = classCarpenter.classloader

@ -0,0 +1,76 @@
package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.serialization.CordaSerializationTransformEnumDefault
import net.corda.core.serialization.CordaSerializationTransformEnumDefaults
import net.corda.core.serialization.CordaSerializationTransformRename
import net.corda.core.serialization.CordaSerializationTransformRenames
/**
* Utility class that defines an instance of a transform we support.
*
* @property type The transform annotation.
* @property enum Maps the annotaiton onto a transform type, we expect there are multiple annotations that
* would map to a single transform type.
* @property getAnnotations Anonymous function that should return a list of Annotations encapsualted by the parent annotation
* that reference the transform. Notionally this allows the code that extracts transforms to work on single instances
* of a transform or a meta list of them.
*/
data class SupportedTransform(
val type: Class<out Annotation>,
val enum: TransformTypes,
val getAnnotations: (Annotation) -> List<Annotation>)
/**
* Extract from an annotated class the list of annotations that refer to a particular
* transformation type when that class has multiple transforms wrapped in an
* outer annotation
*/
@Suppress("UNCHECKED_CAST")
private val wrapperExtract = { x: Annotation ->
(x::class.java.getDeclaredMethod("value").invoke(x) as Array<Annotation>).toList()
}
/**
* Extract from an annotated class the list of annotations that refer to a particular
* transformation type when that class has a single decorator applied
*/
private val singleExtract = { x: Annotation -> listOf(x) }
// Transform annotation used to test the handling of transforms the de-serialising node doesn't understand. At
// some point test cases will have been created with this transform applied.
// @Target(AnnotationTarget.CLASS)
// @Retention(AnnotationRetention.RUNTIME)
// annotation class UnknownTransformAnnotation(val a: Int, val b: Int, val c: Int)
/**
* Utility list of all transforms we support that simplifies our generation code.
*
* NOTE: We have to support single instances of the transform annotations as well as the wrapping annotation
* when many instances are repeated.
*/
val supportedTransforms = listOf(
SupportedTransform(
CordaSerializationTransformEnumDefaults::class.java,
TransformTypes.EnumDefault,
wrapperExtract
),
SupportedTransform(
CordaSerializationTransformEnumDefault::class.java,
TransformTypes.EnumDefault,
singleExtract
),
SupportedTransform(
CordaSerializationTransformRenames::class.java,
TransformTypes.Rename,
wrapperExtract
),
SupportedTransform(
CordaSerializationTransformRename::class.java,
TransformTypes.Rename,
singleExtract
)
//,SupportedTransform(
// UnknownTransformAnnotation::class.java,
// TransformTypes.UnknownTest,
// singleExtract)
)

@ -0,0 +1,72 @@
package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.serialization.CordaSerializationTransformEnumDefault
import net.corda.core.serialization.CordaSerializationTransformEnumDefaults
import net.corda.core.serialization.CordaSerializationTransformRename
import org.apache.qpid.proton.amqp.DescribedType
import org.apache.qpid.proton.codec.DescribedTypeConstructor
import java.io.NotSerializableException
/**
* Enumerated type that represents each transform that can be applied to a class. Used as the key type in
* the [TransformsSchema] map for each class.
*
* @property build should be a function that takes a transform [Annotation] (currently one of
* [CordaSerializationTransformRename] or [CordaSerializationTransformEnumDefaults])
* and constructs an instance of the corresponding [Transform] type
*
* DO NOT REORDER THE CONSTANTS!!! Please append any new entries to the end
*/
// TODO: it would be awesome to auto build this list by scanning for transform annotations themselves
// TODO: annotated with some annotation
enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType {
/**
* Placeholder entry for future transforms where a node receives a transform we've subsequently
* added and thus the de-serialising node doesn't know about that transform.
*/
Unknown({ UnknownTransform() }) {
override fun getDescriptor(): Any = DESCRIPTOR
override fun getDescribed(): Any = ordinal
},
EnumDefault({ a -> EnumDefaultSchemaTransform((a as CordaSerializationTransformEnumDefault).old, a.new) }) {
override fun getDescriptor(): Any = DESCRIPTOR
override fun getDescribed(): Any = ordinal
},
Rename({ a -> RenameSchemaTransform((a as CordaSerializationTransformRename).from, a.to) }) {
override fun getDescriptor(): Any = DESCRIPTOR
override fun getDescribed(): Any = ordinal
}
// Transform used to test the unknown handler, leave this at as the final constant, uncomment
// when regenerating test cases - if Java had a pre-processor this would be much neater
//
//,UnknownTest({ a -> UnknownTestTransform((a as UnknownTransformAnnotation).a, a.b, a.c)}) {
// override fun getDescriptor(): Any = DESCRIPTOR
// override fun getDescribed(): Any = ordinal
//}
;
companion object : DescribedTypeConstructor<TransformTypes> {
val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_ELEMENT_KEY.amqpDescriptor
/**
* Used to construct an instance of the object from the serialised bytes
*
* @param obj the serialised byte object from the AMQP serialised stream
*/
override fun newInstance(obj: Any?): TransformTypes {
val describedType = obj as DescribedType
if (describedType.descriptor != DESCRIPTOR) {
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
}
return try {
values()[describedType.described as Int]
} catch (e: IndexOutOfBoundsException) {
values()[0]
}
}
override fun getTypeClass(): Class<*> = TransformTypes::class.java
}
}

@ -0,0 +1,317 @@
package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.serialization.CordaSerializationTransformEnumDefault
import net.corda.core.serialization.CordaSerializationTransformRename
import org.apache.qpid.proton.amqp.DescribedType
import org.apache.qpid.proton.codec.DescribedTypeConstructor
import java.io.NotSerializableException
import java.util.*
// NOTE: We are effectively going to replicate the annotations, we need to do this because
// we can't instantiate instances of those annotation classes and this code needs to
// work at the de-serialising end
/**
* Base class for representations of specific types of transforms as applied to a type within the
* Corda serialisation framework
*/
abstract class Transform : DescribedType {
companion object : DescribedTypeConstructor<Transform> {
val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_ELEMENT.amqpDescriptor
/**
* @param obj: a serialized instance of a described type, should be one of the
* descendants of this class
*/
private fun checkDescribed(obj: Any?): Any? {
val describedType = obj as DescribedType
if (describedType.descriptor != DESCRIPTOR) {
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
}
return describedType.described
}
/**
* From an encoded descendant return an instance of the specific type. Transforms are encoded into
* the schema as a list of class name and parameters.Using the class name (list element 0)
* create the appropriate class instance
*
* For future proofing any unknown transform types are not treated as errors, rather we
* simply create a placeholder object so we can ignore it
*
* @param obj: a serialized instance of a described type, should be one of the
* descendants of this class
*/
override fun newInstance(obj: Any?): Transform {
val described = Transform.checkDescribed(obj) as List<*>
return when (described[0]) {
EnumDefaultSchemaTransform.typeName -> EnumDefaultSchemaTransform.newInstance(described)
RenameSchemaTransform.typeName -> RenameSchemaTransform.newInstance(described)
else -> UnknownTransform()
}
}
override fun getTypeClass(): Class<*> = Transform::class.java
}
override fun getDescriptor(): Any = DESCRIPTOR
/**
* Return a string representation of a transform in terms of key / value pairs, used
* by the serializer to encode arbitrary transforms
*/
abstract fun params(): String
abstract val name: String
}
/**
* Transform type placeholder that allows for backward compatibility. Should a noce recieve
* a transform type it doesn't recognise, we can will use this as a placeholder
*/
class UnknownTransform : Transform() {
companion object : DescribedTypeConstructor<UnknownTransform> {
val typeName = "UnknownTransform"
override fun newInstance(obj: Any?) = UnknownTransform()
override fun getTypeClass(): Class<*> = UnknownTransform::class.java
}
override fun getDescribed(): Any = emptyList<Any>()
override fun params() = ""
override val name: String get() = typeName
}
class UnknownTestTransform(val a: Int, val b: Int, val c: Int) : Transform() {
companion object : DescribedTypeConstructor<UnknownTestTransform> {
val typeName = "UnknownTest"
override fun newInstance(obj: Any?) : UnknownTestTransform {
val described = obj as List<*>
return UnknownTestTransform(described[1] as Int, described[2] as Int, described[3] as Int)
}
override fun getTypeClass(): Class<*> = UnknownTransform::class.java
}
override fun getDescribed(): Any = listOf(name, a, b, c)
override fun params() = ""
override val name: String get() = typeName
}
/**
* Transform to be used on an Enumerated Type whenever a new element is added
*
* @property old The value the [new] instance should default to when not available
* @property new the value (as a String) that has been added
*/
class EnumDefaultSchemaTransform(val old: String, val new: String) : Transform() {
companion object : DescribedTypeConstructor<EnumDefaultSchemaTransform> {
/**
* Value encoded into the schema that identifies a transform as this type
*/
val typeName = "EnumDefault"
override fun newInstance(obj: Any?): EnumDefaultSchemaTransform {
val described = obj as List<*>
val old = described[1] as? String ?: throw IllegalStateException("Was expecting \"old\" as a String")
val new = described[2] as? String ?: throw IllegalStateException("Was expecting \"new\" as a String")
return EnumDefaultSchemaTransform(old, new)
}
override fun getTypeClass(): Class<*> = EnumDefaultSchemaTransform::class.java
}
@Suppress("UNUSED")
constructor (annotation: CordaSerializationTransformEnumDefault) : this(annotation.old, annotation.new)
override fun getDescribed(): Any = listOf(name, old, new)
override fun params() = "old=${old.esc()} new=${new.esc()}"
override fun equals(other: Any?) = (
(other is EnumDefaultSchemaTransform && other.new == new && other.old == old) || super.equals(other))
override fun hashCode() = (17 * new.hashCode()) + old.hashCode()
override val name: String get() = typeName
}
/**
* Transform applied to either a class or enum where a property is renamed
*
* @property from the name at time of change of the property
* @property to the new name of the property
*/
class RenameSchemaTransform(val from: String, val to: String) : Transform() {
companion object : DescribedTypeConstructor<RenameSchemaTransform> {
/**
* Value encoded into the schema that identifies a transform as this type
*/
val typeName = "Rename"
override fun newInstance(obj: Any?): RenameSchemaTransform {
val described = obj as List<*>
val from = described[1] as? String ?: throw IllegalStateException("Was expecting \"from\" as a String")
val to = described[2] as? String ?: throw IllegalStateException("Was expecting \"to\" as a String")
return RenameSchemaTransform(from, to)
}
override fun getTypeClass(): Class<*> = RenameSchemaTransform::class.java
}
@Suppress("UNUSED")
constructor (annotation: CordaSerializationTransformRename) : this(annotation.from, annotation.to)
override fun getDescribed(): Any = listOf(name, from, to)
override fun params() = "from=${from.esc()} to=${to.esc()}"
override fun equals(other: Any?) = (
(other is RenameSchemaTransform && other.from == from && other.to == to) || super.equals(other))
override fun hashCode() = (11 * from.hashCode()) + to.hashCode()
override val name: String get() = typeName
}
/**
* Represents the set of all transforms that can be a applied to all classes represented as part of
* an AMQP schema. It forms a part of the AMQP envelope alongside the [Schema] and the serialized bytes
*
* @property types maps class names to a map of transformation types. In turn those transformation types
* are each a list of instances o that transform.
*/
data class TransformsSchema(val types: Map<String, EnumMap<TransformTypes, MutableList<Transform>>>) : DescribedType {
companion object : DescribedTypeConstructor<TransformsSchema> {
val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_SCHEMA.amqpDescriptor
/**
* Prepare a schema for encoding, takes all of the types being transmitted and inspects each
* one for any transform annotations. If there are any build up a set that can be
* encoded into the AMQP [Envelope]
*
* @param schema should be a [Schema] generated for a serialised data structure
* @param sf should be provided by the same serialization context that generated the schema
*/
fun build(schema: Schema, sf: SerializerFactory): TransformsSchema {
val rtn = mutableMapOf<String, EnumMap<TransformTypes, MutableList<Transform>>>()
schema.types.forEach { type ->
sf.transformsCache.computeIfAbsent(type.name) {
val transforms = EnumMap<TransformTypes, MutableList<Transform>>(TransformTypes::class.java)
try {
val clazz = sf.classloader.loadClass(type.name)
supportedTransforms.forEach { transform ->
clazz.getAnnotation(transform.type)?.let { list ->
transform.getAnnotations(list).forEach { annotation ->
val t = transform.enum.build(annotation)
// we're explicitly rejecting repeated annotations, whilst it's fine and we'd just
// ignore them it feels like a good thing to alert the user to since this is
// more than likely a typo in their code so best make it an actual error
if (transforms.computeIfAbsent(transform.enum) { mutableListOf() }
.filter { t == it }.isNotEmpty()) {
throw NotSerializableException(
"Repeated unique transformation annotation of type ${t.name}")
}
transforms[transform.enum]!!.add(t)
}
}
}
} catch (_: ClassNotFoundException) {
// if we can't load the class we'll end up caching an empty list which is fine as that
// list, on lookup, won't be included in the schema because it's empty
}
transforms
}.apply {
if (isNotEmpty()) {
rtn[type.name] = this
}
}
}
return TransformsSchema(rtn)
}
override fun getTypeClass(): Class<*> = TransformsSchema::class.java
/**
* Constructs an instance of the object from the serialised form of an instance
* of this object
*/
override fun newInstance(described: Any?): TransformsSchema {
val rtn = mutableMapOf<String, EnumMap<TransformTypes, MutableList<Transform>>>()
val describedType = described as? DescribedType ?: return TransformsSchema(rtn)
if (describedType.descriptor != DESCRIPTOR) {
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
}
val map = describedType.described as? Map<*, *> ?:
throw NotSerializableException("Transform schema must be encoded as a map")
map.forEach { type ->
val fingerprint = type.key as? String ?:
throw NotSerializableException("Fingerprint must be encoded as a string")
rtn[fingerprint] = EnumMap<TransformTypes, MutableList<Transform>>(TransformTypes::class.java)
(type.value as Map<*, *>).forEach { transformType, transforms ->
val transform = TransformTypes.newInstance(transformType)
rtn[fingerprint]!![transform] = mutableListOf()
(transforms as List<*>).forEach {
rtn[fingerprint]!![TransformTypes.newInstance(transformType)]?.add(Transform.newInstance(it)) ?:
throw NotSerializableException("De-serialization error with transform for class "
+ "${type.key} ${transform.name}")
}
}
}
return TransformsSchema(rtn)
}
}
override fun getDescriptor(): Any = DESCRIPTOR
override fun getDescribed(): Any = types
override fun toString(): String {
data class Indent(val indent: String) {
@Suppress("UNUSED") constructor(i: Indent) : this(" ${i.indent}")
override fun toString() = indent
}
val sb = StringBuilder("")
val indent = Indent("")
sb.appendln("$indent<type-transforms>")
types.forEach { type ->
val indent = Indent(indent)
sb.appendln("$indent<type name=${type.key.esc()}>")
type.value.forEach { transform ->
val indent = Indent(indent)
sb.appendln("$indent<transforms type=${transform.key.name.esc()}>")
transform.value.forEach {
val indent = Indent(indent)
sb.appendln("$indent<transform ${it.params()} />")
}
sb.appendln("$indent</transforms>")
}
sb.appendln("$indent</type>")
}
sb.appendln("$indent</type-transforms>")
return sb.toString()
}
}
private fun String.esc() = "\"$this\""

@ -186,6 +186,8 @@ public class JavaSerializationOutputTests {
decoder.register(CompositeType.Companion.getDESCRIPTOR(), CompositeType.Companion);
decoder.register(Choice.Companion.getDESCRIPTOR(), Choice.Companion);
decoder.register(RestrictedType.Companion.getDESCRIPTOR(), RestrictedType.Companion);
decoder.register(Transform.Companion.getDESCRIPTOR(), Transform.Companion);
decoder.register(TransformsSchema.Companion.getDESCRIPTOR(), TransformsSchema.Companion);
new EncoderImpl(decoder);
decoder.setByteBuffer(ByteBuffer.wrap(bytes.getBytes(), 8, bytes.getSize() - 8));

@ -18,18 +18,31 @@ class TestSerializationOutput(
if (verbose) println(schema)
super.writeSchema(schema, data)
}
override fun writeTransformSchema(transformsSchema: TransformsSchema, data: Data) {
if(verbose) {
println ("Writing Transform Schema")
println (transformsSchema)
}
super.writeTransformSchema(transformsSchema, data)
}
}
fun testName(): String = Thread.currentThread().stackTrace[2].methodName
data class BytesAndSchema<T : Any>(val obj: SerializedBytes<T>, val schema: Schema)
data class BytesAndSchemas<T : Any>(
val obj: SerializedBytes<T>,
val schema: Schema,
val transformsSchema: TransformsSchema)
// Extension for the serialize routine that returns the scheme encoded into the
// bytes as well as the bytes for simple testing
@Throws(NotSerializableException::class)
fun <T : Any> SerializationOutput.serializeAndReturnSchema(obj: T): BytesAndSchema<T> {
fun <T : Any> SerializationOutput.serializeAndReturnSchema(obj: T): BytesAndSchemas<T> {
try {
return BytesAndSchema(_serialize(obj), Schema(schemaHistory.toList()))
val blob = _serialize(obj)
val schema = Schema(schemaHistory.toList())
return BytesAndSchemas(blob, schema, TransformsSchema.build(schema, serializerFactory))
} finally {
andFinally()
}

@ -0,0 +1,436 @@
package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.serialization.*
import org.assertj.core.api.Assertions
import org.junit.Test
import java.io.File
import java.io.NotSerializableException
import java.net.URI
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class EnumEvolvabilityTests {
var localPath = "file:///home/katelyn/srcs/corda/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp"
companion object {
val VERBOSE = false
}
enum class NotAnnotated {
A, B, C, D
}
@CordaSerializationTransformEnumDefaults()
enum class MissingDefaults {
A, B, C, D
}
@CordaSerializationTransformRenames()
enum class MissingRenames {
A, B, C, D
}
@CordaSerializationTransformEnumDefault("D", "A")
enum class AnnotatedEnumOnce {
A, B, C, D
}
@CordaSerializationTransformEnumDefaults(
CordaSerializationTransformEnumDefault("E", "D"),
CordaSerializationTransformEnumDefault("D", "A"))
enum class AnnotatedEnumTwice {
A, B, C, D, E
}
@CordaSerializationTransformRename("E", "D")
enum class RenameEnumOnce {
A, B, C, E
}
@CordaSerializationTransformRenames(
CordaSerializationTransformRename("E", "C"),
CordaSerializationTransformRename("F", "D"))
enum class RenameEnumTwice {
A, B, E, F
}
@Test
fun noAnnotation() {
data class C (val n: NotAnnotated)
val sf = testDefaultFactory()
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(NotAnnotated.A))
assertEquals(2, bAndS.schema.types.size)
assertEquals(0, bAndS.transformsSchema.types.size)
}
@Test
fun missingDefaults() {
data class C (val m: MissingDefaults)
val sf = testDefaultFactory()
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(MissingDefaults.A))
assertEquals(2, bAndS.schema.types.size)
assertEquals(0, bAndS.transformsSchema.types.size)
}
@Test
fun missingRenames() {
data class C (val m: MissingRenames)
val sf = testDefaultFactory()
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(MissingRenames.A))
assertEquals(2, bAndS.schema.types.size)
assertEquals(0, bAndS.transformsSchema.types.size)
}
@Test
fun defaultAnnotationIsAddedToEnvelope() {
data class C (val annotatedEnum: AnnotatedEnumOnce)
val sf = testDefaultFactory()
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(AnnotatedEnumOnce.D))
// only the enum is decorated so schema sizes should be different (2 objects, only one evolved)
assertEquals(2, bAndS.schema.types.size)
assertEquals(1, bAndS.transformsSchema.types.size)
assertEquals (AnnotatedEnumOnce::class.java.name, bAndS.transformsSchema.types.keys.first())
val schema = bAndS.transformsSchema.types.values.first()
assertEquals(1, schema.size)
assertTrue (schema.keys.contains(TransformTypes.EnumDefault))
assertEquals (1, schema[TransformTypes.EnumDefault]!!.size)
assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform)
assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new)
assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old)
}
@Test
fun doubleDefaultAnnotationIsAddedToEnvelope() {
data class C (val annotatedEnum: AnnotatedEnumTwice)
val sf = testDefaultFactory()
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(AnnotatedEnumTwice.E))
assertEquals(2, bAndS.schema.types.size)
assertEquals(1, bAndS.transformsSchema.types.size)
assertEquals (AnnotatedEnumTwice::class.java.name, bAndS.transformsSchema.types.keys.first())
val schema = bAndS.transformsSchema.types.values.first()
assertEquals(1, schema.size)
assertTrue (schema.keys.contains(TransformTypes.EnumDefault))
assertEquals (2, schema[TransformTypes.EnumDefault]!!.size)
assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform)
assertEquals ("E", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new)
assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old)
assertTrue (schema[TransformTypes.EnumDefault]!![1] is EnumDefaultSchemaTransform)
assertEquals ("D", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemaTransform).new)
assertEquals ("A", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemaTransform).old)
}
@Test
fun defaultAnnotationIsAddedToEnvelopeAndDeserialised() {
data class C (val annotatedEnum: AnnotatedEnumOnce)
val sf = testDefaultFactory()
val sb = TestSerializationOutput(VERBOSE, sf).serialize(C(AnnotatedEnumOnce.D))
val db = DeserializationInput(sf).deserializeAndReturnEnvelope(sb)
// as with the serialisation stage, de-serialising the object we should see two
// types described in the header with one of those having transforms
assertEquals(2, db.envelope.schema.types.size)
assertEquals(1, db.envelope.transformsSchema.types.size)
val eName = AnnotatedEnumOnce::class.java.name
val types = db.envelope.schema.types
val transforms = db.envelope.transformsSchema.types
assertEquals(1, types.filter { it.name == eName }.size)
assertTrue(eName in transforms)
val schema = transforms[eName]
assertTrue (schema!!.keys.contains(TransformTypes.EnumDefault))
assertEquals (1, schema[TransformTypes.EnumDefault]!!.size)
assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform)
assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new)
assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old)
}
@Test
fun doubleDefaultAnnotationIsAddedToEnvelopeAndDeserialised() {
data class C(val annotatedEnum: AnnotatedEnumTwice)
val sf = testDefaultFactory()
val sb = TestSerializationOutput(VERBOSE, sf).serialize(C(AnnotatedEnumTwice.E))
val db = DeserializationInput(sf).deserializeAndReturnEnvelope(sb)
// as with the serialisation stage, de-serialising the object we should see two
// types described in the header with one of those having transforms
assertEquals(2, db.envelope.schema.types.size)
assertEquals(1, db.envelope.transformsSchema.types.size)
val transforms = db.envelope.transformsSchema.types
assertTrue (transforms.contains(AnnotatedEnumTwice::class.java.name))
assertTrue (transforms[AnnotatedEnumTwice::class.java.name]!!.contains(TransformTypes.EnumDefault))
assertEquals (2, transforms[AnnotatedEnumTwice::class.java.name]!![TransformTypes.EnumDefault]!!.size)
val enumDefaults = transforms[AnnotatedEnumTwice::class.java.name]!![TransformTypes.EnumDefault]!!
assertEquals("E", (enumDefaults[0] as EnumDefaultSchemaTransform).new)
assertEquals("D", (enumDefaults[0] as EnumDefaultSchemaTransform).old)
assertEquals("D", (enumDefaults[1] as EnumDefaultSchemaTransform).new)
assertEquals("A", (enumDefaults[1] as EnumDefaultSchemaTransform).old)
}
@Test
fun renameAnnotationIsAdded() {
data class C (val annotatedEnum: RenameEnumOnce)
val sf = testDefaultFactory()
// Serialise the object
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(RenameEnumOnce.E))
assertEquals(2, bAndS.schema.types.size)
assertEquals(1, bAndS.transformsSchema.types.size)
assertEquals (RenameEnumOnce::class.java.name, bAndS.transformsSchema.types.keys.first())
val serialisedSchema = bAndS.transformsSchema.types[RenameEnumOnce::class.java.name]!!
assertEquals(1, serialisedSchema.size)
assertTrue(serialisedSchema.containsKey(TransformTypes.Rename))
assertEquals(1, serialisedSchema[TransformTypes.Rename]!!.size)
assertEquals("D", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).from)
assertEquals("E", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).to)
// Now de-serialise the blob
val cAndS = DeserializationInput(sf).deserializeAndReturnEnvelope(bAndS.obj)
assertEquals(2, cAndS.envelope.schema.types.size)
assertEquals(1, cAndS.envelope.transformsSchema.types.size)
assertEquals (RenameEnumOnce::class.java.name, cAndS.envelope.transformsSchema.types.keys.first())
val deserialisedSchema = cAndS.envelope.transformsSchema.types[RenameEnumOnce::class.java.name]!!
assertEquals(1, deserialisedSchema.size)
assertTrue(deserialisedSchema.containsKey(TransformTypes.Rename))
assertEquals(1, deserialisedSchema[TransformTypes.Rename]!!.size)
assertEquals("D", (deserialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).from)
assertEquals("E", (deserialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).to)
}
@Test
fun doubleRenameAnnotationIsAdded() {
data class C (val annotatedEnum: RenameEnumTwice)
val sf = testDefaultFactory()
// Serialise the object
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(RenameEnumTwice.F))
assertEquals(2, bAndS.schema.types.size)
assertEquals(1, bAndS.transformsSchema.types.size)
assertEquals (RenameEnumTwice::class.java.name, bAndS.transformsSchema.types.keys.first())
val serialisedSchema = bAndS.transformsSchema.types[RenameEnumTwice::class.java.name]!!
assertEquals(1, serialisedSchema.size)
assertTrue(serialisedSchema.containsKey(TransformTypes.Rename))
assertEquals(2, serialisedSchema[TransformTypes.Rename]!!.size)
assertEquals("C", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).from)
assertEquals("E", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).to)
assertEquals("D", (serialisedSchema[TransformTypes.Rename]!![1] as RenameSchemaTransform).from)
assertEquals("F", (serialisedSchema[TransformTypes.Rename]!![1] as RenameSchemaTransform).to)
// Now de-serialise the blob
val cAndS = DeserializationInput(sf).deserializeAndReturnEnvelope(bAndS.obj)
assertEquals(2, cAndS.envelope.schema.types.size)
assertEquals(1, cAndS.envelope.transformsSchema.types.size)
assertEquals (RenameEnumTwice::class.java.name, cAndS.envelope.transformsSchema.types.keys.first())
val deserialisedSchema = cAndS.envelope.transformsSchema.types[RenameEnumTwice::class.java.name]!!
assertEquals(1, deserialisedSchema.size)
assertTrue(deserialisedSchema.containsKey(TransformTypes.Rename))
assertEquals(2, deserialisedSchema[TransformTypes.Rename]!!.size)
assertEquals("C", (deserialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).from)
assertEquals("E", (deserialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).to)
assertEquals("D", (deserialisedSchema[TransformTypes.Rename]!![1] as RenameSchemaTransform).from)
assertEquals("F", (deserialisedSchema[TransformTypes.Rename]!![1] as RenameSchemaTransform).to)
}
@CordaSerializationTransformRename(from="A", to="X")
@CordaSerializationTransformEnumDefault(old = "X", new="E")
enum class RenameAndExtendEnum {
X, B, C, D, E
}
@Test
fun bothAnnotationTypes() {
data class C (val annotatedEnum: RenameAndExtendEnum)
val sf = testDefaultFactory()
// Serialise the object
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(RenameAndExtendEnum.X))
assertEquals(2, bAndS.schema.types.size)
assertEquals(1, bAndS.transformsSchema.types.size)
assertEquals (RenameAndExtendEnum::class.java.name, bAndS.transformsSchema.types.keys.first())
val serialisedSchema = bAndS.transformsSchema.types[RenameAndExtendEnum::class.java.name]!!
// This time there should be two distinct transform types (all previous tests have had only
// a single type
assertEquals(2, serialisedSchema.size)
assertTrue (serialisedSchema.containsKey(TransformTypes.Rename))
assertTrue (serialisedSchema.containsKey(TransformTypes.EnumDefault))
assertEquals(1, serialisedSchema[TransformTypes.Rename]!!.size)
assertEquals("A", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).from)
assertEquals("X", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).to)
assertEquals(1, serialisedSchema[TransformTypes.EnumDefault]!!.size)
assertEquals("E", (serialisedSchema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new)
assertEquals("X", (serialisedSchema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old)
}
@CordaSerializationTransformEnumDefaults (
CordaSerializationTransformEnumDefault("D", "A"),
CordaSerializationTransformEnumDefault("D", "A"))
enum class RepeatedAnnotation {
A, B, C, D, E
}
@Test
fun repeatedAnnotation() {
data class C (val a: RepeatedAnnotation)
val sf = testDefaultFactory()
Assertions.assertThatThrownBy {
TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(RepeatedAnnotation.A))
}.isInstanceOf(NotSerializableException::class.java)
}
@CordaSerializationTransformEnumDefault("D", "A")
enum class E1 {
A, B, C, D
}
@CordaSerializationTransformEnumDefaults (
CordaSerializationTransformEnumDefault("D", "A"),
CordaSerializationTransformEnumDefault("E", "A"))
enum class E2 {
A, B, C, D, E
}
@CordaSerializationTransformEnumDefaults (CordaSerializationTransformEnumDefault("D", "A"))
enum class E3 {
A, B, C, D
}
@Test
fun multiEnums() {
data class A (val a: E1, val b: E2)
data class B (val a: E3, val b: A, val c: E1)
data class C (val a: B, val b: E2, val c: E3)
val c = C(B(E3.A,A(E1.A,E2.B),E1.C),E2.B,E3.A)
val sf = testDefaultFactory()
// Serialise the object
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(c)
println (bAndS.transformsSchema)
// we have six types and three of those, the enums, should have transforms
assertEquals(6, bAndS.schema.types.size)
assertEquals(3, bAndS.transformsSchema.types.size)
assertTrue (E1::class.java.name in bAndS.transformsSchema.types)
assertTrue (E2::class.java.name in bAndS.transformsSchema.types)
assertTrue (E3::class.java.name in bAndS.transformsSchema.types)
val e1S = bAndS.transformsSchema.types[E1::class.java.name]!!
val e2S = bAndS.transformsSchema.types[E2::class.java.name]!!
val e3S = bAndS.transformsSchema.types[E3::class.java.name]!!
assertEquals(1, e1S.size)
assertEquals(1, e2S.size)
assertEquals(1, e3S.size)
assertTrue(TransformTypes.EnumDefault in e1S)
assertTrue(TransformTypes.EnumDefault in e2S)
assertTrue(TransformTypes.EnumDefault in e3S)
assertEquals(1, e1S[TransformTypes.EnumDefault]!!.size)
assertEquals(2, e2S[TransformTypes.EnumDefault]!!.size)
assertEquals(1, e3S[TransformTypes.EnumDefault]!!.size)
}
@Test
fun testCache() {
data class C2(val annotatedEnum: AnnotatedEnumOnce)
data class C1(val annotatedEnum: AnnotatedEnumOnce)
val sf = testDefaultFactory()
assertEquals(0, sf.transformsCache.size)
val sb1 = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C1(AnnotatedEnumOnce.D))
assertEquals(2, sf.transformsCache.size)
assertTrue(sf.transformsCache.containsKey(C1::class.java.name))
assertTrue(sf.transformsCache.containsKey(AnnotatedEnumOnce::class.java.name))
val sb2 = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C2(AnnotatedEnumOnce.D))
assertEquals(3, sf.transformsCache.size)
assertTrue(sf.transformsCache.containsKey(C1::class.java.name))
assertTrue(sf.transformsCache.containsKey(C2::class.java.name))
assertTrue(sf.transformsCache.containsKey(AnnotatedEnumOnce::class.java.name))
assertEquals (sb1.transformsSchema.types[AnnotatedEnumOnce::class.java.name],
sb2.transformsSchema.types[AnnotatedEnumOnce::class.java.name])
}
//@UnknownTransformAnnotation (10, 20, 30)
enum class WithUnknownTest {
A, B, C, D
}
data class WrapsUnknown(val unknown: WithUnknownTest)
// To regenerate the types for this test uncomment the UnknownTransformAnnotation from
// TransformTypes.kt and SupportedTransforms.kt
// ALSO: remember to re-annotate the enum WithUnkownTest above
@Test
fun testUnknownTransform() {
val resource = "EnumEvolvabilityTests.testUnknownTransform"
val sf = testDefaultFactory()
//File(URI("$localPath/$resource")).writeBytes(
// SerializationOutput(sf).serialize(WrapsUnknown(WithUnknownTest.D)).bytes)
val path = EvolvabilityTests::class.java.getResource(resource)
val sb1 = File(path.toURI()).readBytes()
val envelope = DeserializationInput(sf).deserializeAndReturnEnvelope(SerializedBytes<WrapsUnknown>(sb1)).envelope
assertTrue(envelope.transformsSchema.types.containsKey(WithUnknownTest::class.java.name))
assertTrue(envelope.transformsSchema.types[WithUnknownTest::class.java.name]!!.containsKey(TransformTypes.Unknown))
}
}

@ -5,36 +5,39 @@ import net.corda.core.serialization.SerializedBytes
import org.junit.Test
import java.io.File
import java.io.NotSerializableException
import java.net.URI
import kotlin.test.assertEquals
// To regenerate any of the binary test files do the following
//
// 0. set localPath accordingly
// 1. Uncomment the code where the original form of the class is defined in the test
// 2. Comment out the rest of the test
// 3. Run the test
// 4. Using the printed path copy that file to the resources directory
// 5. Comment back out the generation code and uncomment the actual test
class EvolvabilityTests {
// When regenerating the test files this needs to be set to the file system location of the resource files
var localPath = "file:///<path>/<to>/<toplevel of>/corda/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp"
@Test
fun simpleOrderSwapSameType() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.simpleOrderSwapSameType")
val f = File(path.toURI())
val resource= "EvolvabilityTests.simpleOrderSwapSameType"
val A = 1
val B = 2
// Original version of the class for the serialised version of this class
//
// data class C (val a: Int, val b: Int)
// val sc = SerializationOutput(sf).serialize(C(A, B))
// f.writeBytes(sc.bytes)
// println (path)
// File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(C(A, B)).bytes)
// new version of the class, in this case the order of the parameters has been swapped
data class C(val b: Int, val a: Int)
val path = EvolvabilityTests::class.java.getResource(resource)
val f = File(path.toURI())
val sc2 = f.readBytes()
val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes<C>(sc2))
@ -45,21 +48,19 @@ class EvolvabilityTests {
@Test
fun simpleOrderSwapDifferentType() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.simpleOrderSwapDifferentType")
val f = File(path.toURI())
val A = 1
val B = "two"
val resource = "EvolvabilityTests.simpleOrderSwapDifferentType"
// Original version of the class as it was serialised
//
// data class C (val a: Int, val b: String)
// val sc = SerializationOutput(sf).serialize(C(A, B))
// f.writeBytes(sc.bytes)
// println (path)
// File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(C(A, B)).bytes)
// new version of the class, in this case the order of the parameters has been swapped
data class C(val b: String, val a: Int)
val path = EvolvabilityTests::class.java.getResource(resource)
val f = File(path.toURI())
val sc2 = f.readBytes()
val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes<C>(sc2))
@ -70,24 +71,23 @@ class EvolvabilityTests {
@Test
fun addAdditionalParamNotMandatory() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.addAdditionalParamNotMandatory")
val f = File(path.toURI())
val A = 1
val resource = "EvolvabilityTests.addAdditionalParamNotMandatory"
// Original version of the class as it was serialised
//
// data class C(val a: Int)
// val sc = SerializationOutput(sf).serialize(C(A))
// f.writeBytes(sc.bytes)
// println ("Path = $path")
// File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(C(A)).bytes)
data class C(val a: Int, val b: Int?)
val path = EvolvabilityTests::class.java.getResource(resource)
val f = File(path.toURI())
val sc2 = f.readBytes()
val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes<C>(sc2))
assertEquals(A, deserializedC.a)
assertEquals(null, deserializedC.b)
}
}
@Test(expected = NotSerializableException::class)
fun addAdditionalParam() {
@ -119,22 +119,20 @@ class EvolvabilityTests {
@Test
fun removeParameters() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.removeParameters")
val f = File(path.toURI())
val resource = "EvolvabilityTests.removeParameters"
val A = 1
val B = "two"
val C = "three"
val D = 4
// Original version of the class as it was serialised
//
// data class CC(val a: Int, val b: String, val c: String, val d: Int)
// val scc = SerializationOutput(sf).serialize(CC(A, B, C, D))
// f.writeBytes(scc.bytes)
// println ("Path = $path")
// File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(CC(A, B, C, D)).bytes)
data class CC(val b: String, val d: Int)
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.removeParameters")
val f = File(path.toURI())
val sc2 = f.readBytes()
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
@ -146,23 +144,22 @@ class EvolvabilityTests {
@Test
fun addAndRemoveParameters() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.addAndRemoveParameters")
val f = File(path.toURI())
val A = 1
val B = "two"
val C = "three"
val D = 4
val E = null
val resource = "EvolvabilityTests.addAndRemoveParameters"
// Original version of the class as it was serialised
//
// data class CC(val a: Int, val b: String, val c: String, val d: Int)
// val scc = SerializationOutput(sf).serialize(CC(A, B, C, D))
// f.writeBytes(scc.bytes)
// println ("Path = $path")
// File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(CC(A, B, C, D)).bytes)
data class CC(val a: Int, val e: Boolean?, val d: Int)
val path = EvolvabilityTests::class.java.getResource(resource)
val f = File(path.toURI())
val sc2 = f.readBytes()
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
@ -174,16 +171,12 @@ class EvolvabilityTests {
@Test
fun addMandatoryFieldWithAltConstructor() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.addMandatoryFieldWithAltConstructor")
val f = File(path.toURI())
val A = 1
val resource = "EvolvabilityTests.addMandatoryFieldWithAltConstructor"
// Original version of the class as it was serialised
//
// data class CC(val a: Int)
// val scc = SerializationOutput(sf).serialize(CC(A))
// f.writeBytes(scc.bytes)
// println ("Path = $path")
// File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(CC(A)).bytes)
@Suppress("UNUSED")
data class CC(val a: Int, val b: String) {
@ -191,6 +184,8 @@ class EvolvabilityTests {
constructor (a: Int) : this(a, "hello")
}
val path = EvolvabilityTests::class.java.getResource(resource)
val f = File(path.toURI())
val sc2 = f.readBytes()
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
@ -228,19 +223,14 @@ class EvolvabilityTests {
@Test
fun addMandatoryFieldWithAltReorderedConstructor() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource(
"EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor")
val f = File(path.toURI())
val resource = "EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor"
val A = 1
val B = 100
val C = "This is not a banana"
// Original version of the class as it was serialised
//
// data class CC(val a: Int, val b: Int, val c: String)
// val scc = SerializationOutput(sf).serialize(CC(A, B, C))
// f.writeBytes(scc.bytes)
// println ("Path = $path")
// File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(CC(A, B, C)).bytes)
@Suppress("UNUSED")
data class CC(val a: Int, val b: Int, val c: String, val d: String) {
@ -250,6 +240,8 @@ class EvolvabilityTests {
constructor (c: String, a: Int, b: Int) : this(a, b, c, "wibble")
}
val path = EvolvabilityTests::class.java.getResource(resource)
val f = File(path.toURI())
val sc2 = f.readBytes()
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
@ -262,20 +254,15 @@ class EvolvabilityTests {
@Test
fun addMandatoryFieldWithAltReorderedConstructorAndRemoval() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource(
"EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval")
val f = File(path.toURI())
val resource = "EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval"
val A = 1
@Suppress("UNUSED_VARIABLE")
val B = 100
val C = "This is not a banana"
// Original version of the class as it was serialised
//
// data class CC(val a: Int, val b: Int, val c: String)
// val scc = SerializationOutput(sf).serialize(CC(A, B, C))
// f.writeBytes(scc.bytes)
// println ("Path = $path")
// File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(CC(A, B, C)).bytes)
// b is removed, d is added
data class CC(val a: Int, val c: String, val d: String) {
@ -286,6 +273,8 @@ class EvolvabilityTests {
constructor (c: String, a: Int) : this(a, c, "wibble")
}
val path = EvolvabilityTests::class.java.getResource(resource)
val f = File(path.toURI())
val sc2 = f.readBytes()
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
@ -297,9 +286,9 @@ class EvolvabilityTests {
@Test
fun multiVersion() {
val sf = testDefaultFactory()
val path1 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersion.1")
val path2 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersion.2")
val path3 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersion.3")
val resource1 = "EvolvabilityTests.multiVersion.1"
val resource2 = "EvolvabilityTests.multiVersion.2"
val resource3 = "EvolvabilityTests.multiVersion.3"
val a = 100
val b = 200
@ -310,24 +299,15 @@ class EvolvabilityTests {
//
// Version 1:
// data class C (val a: Int, val b: Int)
//
// val scc = SerializationOutput(sf).serialize(C(a, b))
// File(path1.toURI()).writeBytes(scc.bytes)
// println ("Path = $path1")
// File(URI("$localPath/$resource1")).writeBytes(SerializationOutput(sf).serialize(C(a, b)).bytes)
//
// Version 2 - add param c
// data class C (val c: Int, val b: Int, val a: Int)
//
// val scc = SerializationOutput(sf).serialize(C(c, b, a))
// File(path2.toURI()).writeBytes(scc.bytes)
// println ("Path = $path2")
// File(URI("$localPath/$resource2")).writeBytes(SerializationOutput(sf).serialize(C(c, b, a)).bytes)
//
// Version 3 - add param d
// data class C (val b: Int, val c: Int, val d: Int, val a: Int)
//
// val scc = SerializationOutput(sf).serialize(C(b, c, d, a))
// File(path3.toURI()).writeBytes(scc.bytes)
// println ("Path = $path3")
// File(URI("$localPath/$resource3")).writeBytes(SerializationOutput(sf).serialize(C(b, c, d, a)).bytes)
@Suppress("UNUSED")
data class C(val e: Int, val c: Int, val b: Int, val a: Int, val d: Int) {
@ -341,6 +321,10 @@ class EvolvabilityTests {
constructor (a: Int, b: Int, c: Int, d: Int) : this(-1, c, b, a, d)
}
val path1 = EvolvabilityTests::class.java.getResource(resource1)
val path2 = EvolvabilityTests::class.java.getResource(resource2)
val path3 = EvolvabilityTests::class.java.getResource(resource3)
val sb1 = File(path1.toURI()).readBytes()
val db1 = DeserializationInput(sf).deserialize(SerializedBytes<C>(sb1))
@ -372,24 +356,21 @@ class EvolvabilityTests {
@Test
fun changeSubType() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.changeSubType")
val f = File(path.toURI())
val resource = "EvolvabilityTests.changeSubType"
val oa = 100
val ia = 200
// Original version of the class as it was serialised
//
// data class Inner (val a: Int)
// data class Outer (val a: Int, val b: Inner)
// val scc = SerializationOutput(sf).serialize(Outer(oa, Inner (ia)))
// f.writeBytes(scc.bytes)
// println ("Path = $path")
// File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(Outer(oa, Inner (ia))).bytes)
// Add a parameter to inner but keep outer unchanged
data class Inner(val a: Int, val b: String?)
data class Outer(val a: Int, val b: Inner)
val path = EvolvabilityTests::class.java.getResource(resource)
val f = File(path.toURI())
val sc2 = f.readBytes()
val outer = DeserializationInput(sf).deserialize(SerializedBytes<Outer>(sc2))
@ -401,9 +382,10 @@ class EvolvabilityTests {
@Test
fun multiVersionWithRemoval() {
val sf = testDefaultFactory()
val path1 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersionWithRemoval.1")
val path2 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersionWithRemoval.2")
val path3 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersionWithRemoval.3")
val resource1 = "EvolvabilityTests.multiVersionWithRemoval.1"
val resource2 = "EvolvabilityTests.multiVersionWithRemoval.2"
val resource3 = "EvolvabilityTests.multiVersionWithRemoval.3"
@Suppress("UNUSED_VARIABLE")
val a = 100
@ -417,24 +399,15 @@ class EvolvabilityTests {
//
// Version 1:
// data class C (val a: Int, val b: Int, val c: Int)
// File(URI("$localPath/$resource1")).writeBytes(SerializationOutput(sf).serialize(C(a, b, c)).bytes)
//
// val scc = SerializationOutput(sf).serialize(C(a, b, c))
// File(path1.toURI()).writeBytes(scc.bytes)
// println ("Path = $path1")
//
// Version 2 - add param c
// Version 2 - remove property a, add property e
// data class C (val b: Int, val c: Int, val d: Int, val e: Int)
//
// val scc = SerializationOutput(sf).serialize(C(b, c, d, e))
// File(path2.toURI()).writeBytes(scc.bytes)
// println ("Path = $path2")
// File(URI("$localPath/$resource2")).writeBytes(SerializationOutput(sf).serialize(C(b, c, d, e)).bytes)
//
// Version 3 - add param d
// data class C (val b: Int, val c: Int, val d: Int, val e: Int, val f: Int)
//
// val scc = SerializationOutput(sf).serialize(C(b, c, d, e, f))
// File(path3.toURI()).writeBytes(scc.bytes)
// println ("Path = $path3")
// File(URI("$localPath/$resource3")).writeBytes(SerializationOutput(sf).serialize(C(b, c, d, e, f)).bytes)
@Suppress("UNUSED")
data class C(val b: Int, val c: Int, val d: Int, val e: Int, val f: Int, val g: Int) {
@ -451,6 +424,10 @@ class EvolvabilityTests {
constructor (b: Int, c: Int, d: Int, e: Int, f: Int) : this(b, c, d, e, f, -1)
}
val path1 = EvolvabilityTests::class.java.getResource(resource1)
val path2 = EvolvabilityTests::class.java.getResource(resource2)
val path3 = EvolvabilityTests::class.java.getResource(resource3)
val sb1 = File(path1.toURI()).readBytes()
val db1 = DeserializationInput(sf).deserialize(SerializedBytes<C>(sb1))

@ -164,6 +164,8 @@ class SerializationOutputTests {
this.register(Choice.DESCRIPTOR, Choice.Companion)
this.register(RestrictedType.DESCRIPTOR, RestrictedType.Companion)
this.register(ReferencedObject.DESCRIPTOR, ReferencedObject.Companion)
this.register(TransformsSchema.DESCRIPTOR, TransformsSchema.Companion)
this.register(TransformTypes.DESCRIPTOR, TransformTypes.Companion)
}
EncoderImpl(decoder)
decoder.setByteBuffer(ByteBuffer.wrap(bytes.bytes, 8, bytes.size - 8))