CORDA-553 - First steps towards evolvability

Define the two transforms that will be useful for enum evolvability (see
design document for more details).

Furthermore, define the generic mechanism by which transform annotations
on classes are encoded into the AMQP envelope

With nothing to check for these annotations at either end, this is
mostly a no op, but an important step toward getting evolvability in
place
This commit is contained in:
Katelyn Baker 2017-10-10 23:09:25 +01:00
parent 01f80fb187
commit 3633624dc6
13 changed files with 928 additions and 72 deletions

1
.gitignore vendored
View File

@ -36,6 +36,7 @@ lib/quasar.jar
.idea/dataSources
.idea/markdown-navigator
.idea/runConfigurations
.idea/dictionaries
/gradle-plugins/.idea/
# Include the -parameters compiler option by default in IntelliJ required for serialization.

View File

@ -0,0 +1,18 @@
package net.corda.core.serialization
/**
*
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class CordaSerializationTransformEnumDefaults(vararg val value: CordaSerializationTransformEnumDefault)
/**
*
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
// When Kotlin starts writing 1.8 class files enable this, it removes the need for the wrapping annotation
//@Repeatable
annotation class CordaSerializationTransformEnumDefault(val new: String, val old: String)

View File

@ -0,0 +1,17 @@
package net.corda.core.serialization
/**
*
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class CordaSerializationTransformRenames(vararg val value: CordaSerializationTransformRename)
/**
*
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
// When Kotlin starts writing 1.8 class files enable this, it removes the need for the wrapping annotation
//@Repeatable
annotation class CordaSerializationTransformRename(val to: String, val from: String)

View File

@ -0,0 +1,31 @@
package net.corda.nodeapi.internal.serialization.amqp
import org.apache.qpid.proton.amqp.UnsignedLong
/**
* R3 AMQP assigned enterprise number
*
* see [here](https://www.iana.org/assignments/enterprise-numbers/enterprise-numbers)
*
* Repeated here for brevity:
* 50530 - R3 - Mike Hearn - mike&r3.com
*/
const val DESCRIPTOR_TOP_32BITS: Long = 0xc5620000
enum class AMQPDescriptorRegistry(val id: Long) {
ENVELOPE(1),
SCHEMA(2),
OBJECT_DESCRIPTOR(3),
FIELD(4),
COMPOSITE_TYPE(5),
RESTRICTED_TYPE(6),
CHOICE(7),
REFERENCED_OBJECT(8),
TRANSFORM_SCHEMA(9),
TRANSFORM_ELEMENT(10),
TRANSFORM_ELEMENT_KEY(11)
;
val amqpDescriptor = UnsignedLong(id or DESCRIPTOR_TOP_32BITS)
}

View File

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

View File

@ -10,7 +10,6 @@ import org.apache.qpid.proton.amqp.DescribedType
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.amqp.UnsignedInteger
import org.apache.qpid.proton.amqp.UnsignedLong
import org.apache.qpid.proton.codec.Data
import org.apache.qpid.proton.codec.DescribedTypeConstructor
import java.io.NotSerializableException
import java.lang.reflect.*
@ -18,68 +17,10 @@ import java.util.*
import net.corda.nodeapi.internal.serialization.carpenter.Field as CarpenterField
import net.corda.nodeapi.internal.serialization.carpenter.Schema as CarpenterSchema
/**
* R3 AMQP assigned enterprise number
*
* see [here](https://www.iana.org/assignments/enterprise-numbers/enterprise-numbers)
*
* Repeated here for brevity:
* 50530 - R3 - Mike Hearn - mike&r3.com
*/
const val DESCRIPTOR_TOP_32BITS: Long = 0xc5620000
const val DESCRIPTOR_DOMAIN: String = "net.corda"
// "corda" + majorVersionByte + minorVersionMSB + minorVersionLSB
val AmqpHeaderV1_0: OpaqueBytes = OpaqueBytes("corda\u0001\u0000\u0000".toByteArray())
private enum class DescriptorRegistry(val id: Long) {
ENVELOPE(1),
SCHEMA(2),
OBJECT_DESCRIPTOR(3),
FIELD(4),
COMPOSITE_TYPE(5),
RESTRICTED_TYPE(6),
CHOICE(7),
REFERENCED_OBJECT(8),
;
val amqpDescriptor = UnsignedLong(id or DESCRIPTOR_TOP_32BITS)
}
/**
* This class wraps all serialized data, so that the schema can be carried along with it. We will provide various internal utilities
* to decompose and recompose with/without schema etc so that e.g. we can store objects with a (relationally) normalised out schema to
* avoid excessive duplication.
*/
// TODO: make the schema parsing lazy since mostly schemas will have been seen before and we only need it if we don't recognise a type descriptor.
data class Envelope(val obj: Any?, val schema: Schema) : DescribedType {
companion object : DescribedTypeConstructor<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
@ -87,7 +28,7 @@ data class Envelope(val obj: Any?, val schema: Schema) : DescribedType {
*/
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

View File

@ -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()

View File

@ -0,0 +1,66 @@
package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.serialization.CordaSerializationTransformEnumDefaults
import net.corda.core.serialization.CordaSerializationTransformEnumDefault
import net.corda.core.serialization.CordaSerializationTransformRenames
import net.corda.core.serialization.CordaSerializationTransformRename
/**
* Utility class that defines an instance of a transform we support
*
* @property type The transform annotation
* @property enum Maps the annotaiton onto a transform type, we expect there are multiple annotations that
* would map to a single transform type
* @property f Anonymous function that should return a list of Annotations encapsualted by the parent annotation
* that reference the transform. Notionally this allows the code that extracts transforms to work on single instances
* of a transform or a meta list of them
*/
data class SupportedTransform(
val type: Class<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) }
/**
* Utility list of all transforms we support that simplifies our generator
*
* NOTE: We have to support single instances of the transform annotations as well as the wrapping annotation
* when many instances are repeated
*/
val supportedTransforms = listOf(
SupportedTransform(
CordaSerializationTransformEnumDefaults::class.java,
TransformTypes.EnumDefault,
wrapperExtract
),
SupportedTransform(
CordaSerializationTransformEnumDefault::class.java,
TransformTypes.EnumDefault,
singleExtract
),
SupportedTransform(
CordaSerializationTransformRenames::class.java,
TransformTypes.Rename,
wrapperExtract
),
SupportedTransform(
CordaSerializationTransformRename::class.java,
TransformTypes.Rename,
singleExtract
)
)

View File

@ -0,0 +1,55 @@
package net.corda.nodeapi.internal.serialization.amqp
import org.apache.qpid.proton.amqp.DescribedType
import net.corda.core.serialization.CordaSerializationTransformEnumDefault
import net.corda.core.serialization.CordaSerializationTransformEnumDefaults
import net.corda.core.serialization.CordaSerializationTransformRename
import org.apache.qpid.proton.codec.DescribedTypeConstructor
import java.io.NotSerializableException
/**
* Enumerated type that represents each transform that can be applied to a class. Used as the key type in
* the [TransformsSchema] map for each class.
*
* @property build should be a function that takes a transform [Annotation] (currently one of
* [CordaSerializationTransformRename] or [CordaSerializationTransformEnumDefaults])
* and constructs an instance of the corresponding [Transform] type
*/
// TODO: it would be awesome to auto build this list by scanning for transform annotations themselves
// TODO: annotated with some annotation
enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType {
EnumDefault({ a -> EnumDefaultSchemeTransform((a as CordaSerializationTransformEnumDefault).old, a.new) }) {
override fun getDescriptor(): Any = DESCRIPTOR
override fun getDescribed(): Any = ordinal
},
Rename({ a -> RenameSchemaTransform((a as CordaSerializationTransformRename).from, a.to) }) {
override fun getDescriptor(): Any = DESCRIPTOR
override fun getDescribed(): Any = ordinal
};
companion object : DescribedTypeConstructor<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}.")
}
try {
return values()[describedType.described as Int]
} catch (e: IndexOutOfBoundsException) {
throw NotSerializableException("Bad ordinal value ${describedType.described}.")
}
}
override fun getTypeClass(): Class<*> = TransformTypes::class.java
}
}

View File

@ -0,0 +1,268 @@
package net.corda.nodeapi.internal.serialization.amqp
import java.util.*
import net.corda.core.serialization.CordaSerializationTransformEnumDefault
import net.corda.core.serialization.CordaSerializationTransformRename
import org.apache.qpid.proton.amqp.DescribedType
import org.apache.qpid.proton.codec.DescribedTypeConstructor
import java.io.NotSerializableException
// NOTE: We are effectively going to replicate the annotations, we need to do this because
// we can't instantiate instances of those annotation classes and this code needs to
// work at the de-serialising end
/**
* Base class for representations of specific types of transforms as applied to a type within the
* Corda serialisation framework
*/
sealed class Transform : DescribedType {
companion object : DescribedTypeConstructor<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
*
* @param obj: a serialized instance of a described type, should be one of the
* descendants of this class
*/
override fun newInstance(obj: Any?): Transform {
val described = Transform.checkDescribed(obj) as List<*>
return when (described[0]) {
EnumDefaultSchemeTransform.typeName -> EnumDefaultSchemeTransform.newInstance(described)
RenameSchemaTransform.typeName -> RenameSchemaTransform.newInstance(described)
else -> throw NotSerializableException("Unexpected transform type ${described[0]}")
}
}
override fun getTypeClass(): Class<*> = Transform::class.java
}
override fun getDescriptor(): Any = DESCRIPTOR
/**
* Return a string representation of a transform in terms of key / value pairs, used
* by the serializer to encode arbitrary transforms
*/
abstract fun params(): String
abstract val name: String
}
/**
* Transform to be used on an Enumerated Type whenever a new element is added
*
* @property old The value the [new] instance should default to when not available
* @property new the value (as a String) that has been added
*/
class EnumDefaultSchemeTransform(val old: String, val new: String) : Transform() {
companion object : DescribedTypeConstructor<EnumDefaultSchemeTransform> {
/**
* Value encoded into the schema that identifies a transform as this type
*/
val typeName = "EnumDefault"
override fun newInstance(obj: Any?): EnumDefaultSchemeTransform {
val described = obj as List<*>
val old = described[1] as? String ?: throw IllegalStateException("Was expecting \"old\" as a String")
val new = described[2] as? String ?: throw IllegalStateException("Was expecting \"new\" as a String")
return EnumDefaultSchemeTransform(old, new)
}
override fun getTypeClass(): Class<*> = EnumDefaultSchemeTransform::class.java
}
@Suppress("UNUSED")
constructor (annotation: CordaSerializationTransformEnumDefault) : this(annotation.old, annotation.new)
override fun getDescribed(): Any = listOf(name, old, new)
override fun params() = "old=${old.esc()} new=${new.esc()}"
override fun equals(other: Any?): Boolean {
val o = other as? EnumDefaultSchemeTransform ?: return super.equals(other)
return o.new == new && o.old == old
}
override val name: String get() = typeName
}
/**
* Transform applied to either a class or enum where a property is renamed
*
* @property from the name at time of change of the property
* @property to the new name of the property
*/
class RenameSchemaTransform(val from: String, val to: String) : Transform() {
companion object : DescribedTypeConstructor<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?): Boolean {
val o = other as? RenameSchemaTransform ?: return super.equals(other)
return o.from == from && o.to == to
}
override val name: String get() = typeName
}
/**
* @property types is a list of serialised types that have transforms, each list element is a
*/
data class TransformsSchema(val types: Map<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 ->
val clazz = try {
sf.classloader.loadClass(type.name)
} catch (e: ClassNotFoundException) {
return@forEach
}
supportedTransforms.forEach { transform ->
clazz.getAnnotation(transform.type)?.let { list ->
transform.getAnnotations(list).forEach {
val t = transform.enum.build(it)
val m = rtn.computeIfAbsent(type.name) {
EnumMap<TransformTypes, MutableList<Transform>>(TransformTypes::class.java)
}
// we're explicitly rejecting repeated annotations, whilst it's fine and we'd just
// ignore them it feels like a good thing to alert the user to since this is
// more than likely a typo in their code so best make it an actual error
if (m.computeIfAbsent(transform.enum) { mutableListOf() }.filter { it == t }.isNotEmpty()) {
throw NotSerializableException(
"Repeated unique transformation annotation of type ${t.name}")
}
m[transform.enum]!!.add(t)
}
}
}
}
return TransformsSchema(rtn)
}
override fun getTypeClass(): Class<*> = TransformsSchema::class.java
/**
* Constructs an instance of the object from the serialised form of an instance
* of this object
*/
override fun newInstance(described: Any?): TransformsSchema {
val rtn = mutableMapOf<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\""

View File

@ -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()
}

View File

@ -0,0 +1,380 @@
package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.serialization.CordaSerializationTransformEnumDefault
import net.corda.core.serialization.CordaSerializationTransformEnumDefaults
import net.corda.core.serialization.CordaSerializationTransformRename
import net.corda.core.serialization.CordaSerializationTransformRenames
import org.assertj.core.api.Assertions
import org.junit.Test
import java.io.NotSerializableException
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class EnumEvolvabilityTests {
companion object {
val VERBOSE = false
}
enum class NotAnnotated {
A, B, C, D
}
@CordaSerializationTransformEnumDefaults()
enum class MissingDefaults {
A, B, C, D
}
@CordaSerializationTransformRenames()
enum class MissingRenames {
A, B, C, D
}
@CordaSerializationTransformEnumDefault("D", "A")
enum class AnnotatedEnumOnce {
A, B, C, D
}
@CordaSerializationTransformEnumDefaults(
CordaSerializationTransformEnumDefault("E", "D"),
CordaSerializationTransformEnumDefault("D", "A"))
enum class AnnotatedEnumTwice {
A, B, C, D, E
}
@CordaSerializationTransformRename("E", "D")
enum class RenameEnumOnce {
A, B, C, E
}
@CordaSerializationTransformRenames(
CordaSerializationTransformRename("E", "C"),
CordaSerializationTransformRename("F", "D"))
enum class RenameEnumTwice {
A, B, E, F
}
@Test
fun noAnnotation() {
data class C (val n: NotAnnotated)
val sf = testDefaultFactory()
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(NotAnnotated.A))
assertEquals(2, bAndS.schema.types.size)
assertEquals(0, bAndS.transformsSchema.types.size)
}
@Test
fun missingDefaults() {
data class C (val m: MissingDefaults)
val sf = testDefaultFactory()
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(MissingDefaults.A))
assertEquals(2, bAndS.schema.types.size)
assertEquals(0, bAndS.transformsSchema.types.size)
}
@Test
fun missingRenames() {
data class C (val m: MissingRenames)
val sf = testDefaultFactory()
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(MissingRenames.A))
assertEquals(2, bAndS.schema.types.size)
assertEquals(0, bAndS.transformsSchema.types.size)
}
@Test
fun defaultAnnotationIsAddedToEnvelope() {
data class C (val annotatedEnum: AnnotatedEnumOnce)
val sf = testDefaultFactory()
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(AnnotatedEnumOnce.D))
// only the enum is decorated so schema sizes should be different (2 objects, only one evolved)
assertEquals(2, bAndS.schema.types.size)
assertEquals(1, bAndS.transformsSchema.types.size)
assertEquals (AnnotatedEnumOnce::class.java.name, bAndS.transformsSchema.types.keys.first())
val schema = bAndS.transformsSchema.types.values.first()
assertEquals(1, schema.size)
assertTrue (schema.keys.contains(TransformTypes.EnumDefault))
assertEquals (1, schema[TransformTypes.EnumDefault]!!.size)
assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemeTransform)
assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).new)
assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).old)
}
@Test
fun doubleDefaultAnnotationIsAddedToEnvelope() {
data class C (val annotatedEnum: AnnotatedEnumTwice)
val sf = testDefaultFactory()
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(AnnotatedEnumTwice.E))
assertEquals(2, bAndS.schema.types.size)
assertEquals(1, bAndS.transformsSchema.types.size)
assertEquals (AnnotatedEnumTwice::class.java.name, bAndS.transformsSchema.types.keys.first())
val schema = bAndS.transformsSchema.types.values.first()
assertEquals(1, schema.size)
assertTrue (schema.keys.contains(TransformTypes.EnumDefault))
assertEquals (2, schema[TransformTypes.EnumDefault]!!.size)
assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemeTransform)
assertEquals ("E", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).new)
assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).old)
assertTrue (schema[TransformTypes.EnumDefault]!![1] is EnumDefaultSchemeTransform)
assertEquals ("D", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemeTransform).new)
assertEquals ("A", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemeTransform).old)
}
@Test
fun defaultAnnotationIsAddedToEnvelopeAndDeserialised() {
data class C (val annotatedEnum: AnnotatedEnumOnce)
val sf = testDefaultFactory()
val sb = TestSerializationOutput(VERBOSE, sf).serialize(C(AnnotatedEnumOnce.D))
val db = DeserializationInput(sf).deserializeAndReturnEnvelope(sb)
// as with the serialisation stage, de-serialising the object we should see two
// types described in the header with one of those having transforms
assertEquals(2, db.envelope.schema.types.size)
assertEquals(1, db.envelope.transformsSchema.types.size)
val eName = AnnotatedEnumOnce::class.java.name
val types = db.envelope.schema.types
val transforms = db.envelope.transformsSchema.types
assertEquals(1, types.filter { it.name == eName }.size)
assertTrue(eName in transforms)
val schema = transforms[eName]
assertTrue (schema!!.keys.contains(TransformTypes.EnumDefault))
assertEquals (1, schema[TransformTypes.EnumDefault]!!.size)
assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemeTransform)
assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).new)
assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).old)
}
@Test
fun doubleDefaultAnnotationIsAddedToEnvelopeAndDeserialised() {
data class C(val annotatedEnum: AnnotatedEnumTwice)
val sf = testDefaultFactory()
val sb = TestSerializationOutput(VERBOSE, sf).serialize(C(AnnotatedEnumTwice.E))
val db = DeserializationInput(sf).deserializeAndReturnEnvelope(sb)
// as with the serialisation stage, de-serialising the object we should see two
// types described in the header with one of those having transforms
assertEquals(2, db.envelope.schema.types.size)
assertEquals(1, db.envelope.transformsSchema.types.size)
val transforms = db.envelope.transformsSchema.types
assertTrue (transforms.contains(AnnotatedEnumTwice::class.java.name))
assertTrue (transforms[AnnotatedEnumTwice::class.java.name]!!.contains(TransformTypes.EnumDefault))
assertEquals (2, transforms[AnnotatedEnumTwice::class.java.name]!![TransformTypes.EnumDefault]!!.size)
val enumDefaults = transforms[AnnotatedEnumTwice::class.java.name]!![TransformTypes.EnumDefault]!!
assertEquals("E", (enumDefaults[0] as EnumDefaultSchemeTransform).new)
assertEquals("D", (enumDefaults[0] as EnumDefaultSchemeTransform).old)
assertEquals("D", (enumDefaults[1] as EnumDefaultSchemeTransform).new)
assertEquals("A", (enumDefaults[1] as EnumDefaultSchemeTransform).old)
}
@Test
fun renameAnnotationIsAdded() {
data class C (val annotatedEnum: RenameEnumOnce)
val sf = testDefaultFactory()
// Serialise the object
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(RenameEnumOnce.E))
assertEquals(2, bAndS.schema.types.size)
assertEquals(1, bAndS.transformsSchema.types.size)
assertEquals (RenameEnumOnce::class.java.name, bAndS.transformsSchema.types.keys.first())
val serialisedSchema = bAndS.transformsSchema.types[RenameEnumOnce::class.java.name]!!
assertEquals(1, serialisedSchema.size)
assertTrue(serialisedSchema.containsKey(TransformTypes.Rename))
assertEquals(1, serialisedSchema[TransformTypes.Rename]!!.size)
assertEquals("D", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).from)
assertEquals("E", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).to)
// Now de-serialise the blob
val cAndS = DeserializationInput(sf).deserializeAndReturnEnvelope(bAndS.obj)
assertEquals(2, cAndS.envelope.schema.types.size)
assertEquals(1, cAndS.envelope.transformsSchema.types.size)
assertEquals (RenameEnumOnce::class.java.name, cAndS.envelope.transformsSchema.types.keys.first())
val deserialisedSchema = cAndS.envelope.transformsSchema.types[RenameEnumOnce::class.java.name]!!
assertEquals(1, deserialisedSchema.size)
assertTrue(deserialisedSchema.containsKey(TransformTypes.Rename))
assertEquals(1, deserialisedSchema[TransformTypes.Rename]!!.size)
assertEquals("D", (deserialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).from)
assertEquals("E", (deserialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).to)
}
@Test
fun doubleRenameAnnotationIsAdded() {
data class C (val annotatedEnum: RenameEnumTwice)
val sf = testDefaultFactory()
// Serialise the object
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(RenameEnumTwice.F))
assertEquals(2, bAndS.schema.types.size)
assertEquals(1, bAndS.transformsSchema.types.size)
assertEquals (RenameEnumTwice::class.java.name, bAndS.transformsSchema.types.keys.first())
val serialisedSchema = bAndS.transformsSchema.types[RenameEnumTwice::class.java.name]!!
assertEquals(1, serialisedSchema.size)
assertTrue(serialisedSchema.containsKey(TransformTypes.Rename))
assertEquals(2, serialisedSchema[TransformTypes.Rename]!!.size)
assertEquals("C", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).from)
assertEquals("E", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).to)
assertEquals("D", (serialisedSchema[TransformTypes.Rename]!![1] as RenameSchemaTransform).from)
assertEquals("F", (serialisedSchema[TransformTypes.Rename]!![1] as RenameSchemaTransform).to)
// Now de-serialise the blob
val cAndS = DeserializationInput(sf).deserializeAndReturnEnvelope(bAndS.obj)
assertEquals(2, cAndS.envelope.schema.types.size)
assertEquals(1, cAndS.envelope.transformsSchema.types.size)
assertEquals (RenameEnumTwice::class.java.name, cAndS.envelope.transformsSchema.types.keys.first())
val deserialisedSchema = cAndS.envelope.transformsSchema.types[RenameEnumTwice::class.java.name]!!
assertEquals(1, deserialisedSchema.size)
assertTrue(deserialisedSchema.containsKey(TransformTypes.Rename))
assertEquals(2, deserialisedSchema[TransformTypes.Rename]!!.size)
assertEquals("C", (deserialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).from)
assertEquals("E", (deserialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).to)
assertEquals("D", (deserialisedSchema[TransformTypes.Rename]!![1] as RenameSchemaTransform).from)
assertEquals("F", (deserialisedSchema[TransformTypes.Rename]!![1] as RenameSchemaTransform).to)
}
@CordaSerializationTransformRename(from="A", to="X")
@CordaSerializationTransformEnumDefault(old = "X", new="E")
enum class RenameAndExtendEnum {
X, B, C, D, E
}
@Test
fun bothAnnotationTypes() {
data class C (val annotatedEnum: RenameAndExtendEnum)
val sf = testDefaultFactory()
// Serialise the object
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(RenameAndExtendEnum.X))
assertEquals(2, bAndS.schema.types.size)
assertEquals(1, bAndS.transformsSchema.types.size)
assertEquals (RenameAndExtendEnum::class.java.name, bAndS.transformsSchema.types.keys.first())
val serialisedSchema = bAndS.transformsSchema.types[RenameAndExtendEnum::class.java.name]!!
// This time there should be two distinct transform types (all previous tests have had only
// a single type
assertEquals(2, serialisedSchema.size)
assertTrue (serialisedSchema.containsKey(TransformTypes.Rename))
assertTrue (serialisedSchema.containsKey(TransformTypes.EnumDefault))
assertEquals(1, serialisedSchema[TransformTypes.Rename]!!.size)
assertEquals("A", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).from)
assertEquals("X", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).to)
assertEquals(1, serialisedSchema[TransformTypes.EnumDefault]!!.size)
assertEquals("E", (serialisedSchema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).new)
assertEquals("X", (serialisedSchema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).old)
}
@CordaSerializationTransformEnumDefaults (
CordaSerializationTransformEnumDefault("D", "A"),
CordaSerializationTransformEnumDefault("D", "A"))
enum class RepeatedAnnotation {
A, B, C, D, E
}
@Test
fun repeatedAnnotation() {
data class C (val a: RepeatedAnnotation)
val sf = testDefaultFactory()
Assertions.assertThatThrownBy {
TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(RepeatedAnnotation.A))
}.isInstanceOf(NotSerializableException::class.java)
}
@CordaSerializationTransformEnumDefault("D", "A")
enum class E1 {
A, B, C, D
}
@CordaSerializationTransformEnumDefaults (
CordaSerializationTransformEnumDefault("D", "A"),
CordaSerializationTransformEnumDefault("E", "A"))
enum class E2 {
A, B, C, D, E
}
@CordaSerializationTransformEnumDefaults (CordaSerializationTransformEnumDefault("D", "A"))
enum class E3 {
A, B, C, D
}
@Test
fun multiEnums() {
data class A (val a: E1, val b: E2)
data class B (val a: E3, val b: A, val c: E1)
data class C (val a: B, val b: E2, val c: E3)
val c = C(B(E3.A,A(E1.A,E2.B),E1.C),E2.B,E3.A)
val sf = testDefaultFactory()
// Serialise the object
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(c)
println (bAndS.transformsSchema)
// we have six types and three of those, the enums, should have transforms
assertEquals(6, bAndS.schema.types.size)
assertEquals(3, bAndS.transformsSchema.types.size)
assertTrue (E1::class.java.name in bAndS.transformsSchema.types)
assertTrue (E2::class.java.name in bAndS.transformsSchema.types)
assertTrue (E3::class.java.name in bAndS.transformsSchema.types)
val e1S = bAndS.transformsSchema.types[E1::class.java.name]!!
val e2S = bAndS.transformsSchema.types[E2::class.java.name]!!
val e3S = bAndS.transformsSchema.types[E3::class.java.name]!!
assertEquals(1, e1S.size)
assertEquals(1, e2S.size)
assertEquals(1, e3S.size)
assertTrue(TransformTypes.EnumDefault in e1S)
assertTrue(TransformTypes.EnumDefault in e2S)
assertTrue(TransformTypes.EnumDefault in e3S)
assertEquals(1, e1S[TransformTypes.EnumDefault]!!.size)
assertEquals(2, e2S[TransformTypes.EnumDefault]!!.size)
assertEquals(1, e3S[TransformTypes.EnumDefault]!!.size)
}
}

View File

@ -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))