mirror of
https://github.com/corda/corda.git
synced 2025-04-11 13:21:26 +00:00
Merge pull request #1904 from corda/kat/feature/enumEvolver
CORDA-553 - First steps towards evolvability
This commit is contained in:
commit
a74154a126
.ci
core/src/main/kotlin/net/corda/core/serialization
node-api/src
main/kotlin/net/corda/nodeapi/internal/serialization/amqp
AMQPDescriptorRegistry.ktEnvelope.ktSchema.ktSerializationOutput.ktSerializerFactory.ktSupportedTransforms.ktTansformTypes.ktTransformsSchema.kt
test
java/net/corda/nodeapi/internal/serialization/amqp
kotlin/net/corda/nodeapi/internal/serialization/amqp
resources/net/corda/nodeapi/internal/serialization/amqp
EnumEvolvabilityTests.testUnknownTransformEvolvabilityTests.addAdditionalParamNotMandatoryEvolvabilityTests.addAndRemoveParametersEvolvabilityTests.addMandatoryFieldWithAltConstructorEvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorEvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemovalEvolvabilityTests.changeSubTypeEvolvabilityTests.multiVersion.1EvolvabilityTests.multiVersion.2EvolvabilityTests.multiVersion.3EvolvabilityTests.multiVersionWithRemoval.1EvolvabilityTests.multiVersionWithRemoval.2EvolvabilityTests.multiVersionWithRemoval.3EvolvabilityTests.removeParametersEvolvabilityTests.simpleOrderSwapDifferentTypeEvolvabilityTests.simpleOrderSwapSameType
@ -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()
|
||||
##
|
||||
|
93
core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformEnumDefault.kt
Normal file
93
core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformEnumDefault.kt
Normal file
@ -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)
|
||||
|
35
core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformRename.kt
Normal file
35
core/src/main/kotlin/net/corda/core/serialization/CordaSerializationTransformRename.kt
Normal file
@ -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)
|
38
node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPDescriptorRegistry.kt
Normal file
38
node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPDescriptorRegistry.kt
Normal file
@ -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
|
||||
|
76
node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt
Normal file
76
node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt
Normal file
@ -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)
|
||||
)
|
72
node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt
Normal file
72
node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt
Normal file
@ -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
|
||||
}
|
||||
}
|
317
node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt
Normal file
317
node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt
Normal file
@ -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()
|
||||
}
|
||||
|
436
node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt
Normal file
436
node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt
Normal file
@ -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))
|
||||
|
||||
|
2
node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt
2
node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt
@ -164,6 +164,8 @@ class SerializationOutputTests {
|
||||
this.register(Choice.DESCRIPTOR, Choice.Companion)
|
||||
this.register(RestrictedType.DESCRIPTOR, RestrictedType.Companion)
|
||||
this.register(ReferencedObject.DESCRIPTOR, ReferencedObject.Companion)
|
||||
this.register(TransformsSchema.DESCRIPTOR, TransformsSchema.Companion)
|
||||
this.register(TransformTypes.DESCRIPTOR, TransformTypes.Companion)
|
||||
}
|
||||
EncoderImpl(decoder)
|
||||
decoder.setByteBuffer(ByteBuffer.wrap(bytes.bytes, 8, bytes.size - 8))
|
||||
|
BIN
node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.testUnknownTransform
Normal file
BIN
node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.testUnknownTransform
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user