mirror of
https://github.com/corda/corda.git
synced 2024-12-24 07:06:44 +00:00
Merge pull request #1302 from corda/feature/kat/evolvability
Add evolvability to the deserializer
This commit is contained in:
commit
2b5e3e39a2
@ -0,0 +1,14 @@
|
||||
package net.corda.core.serialization
|
||||
|
||||
/**
|
||||
* This annotation is a marker to indicate which secondary constructors should be considered, and in which
|
||||
* order, for evolving objects during their deserialisation.
|
||||
*
|
||||
* Versions will be considered in descending order, currently duplicate versions will result in
|
||||
* non deterministic behaviour when deserialising objects
|
||||
*/
|
||||
@Target(AnnotationTarget.CONSTRUCTOR)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class DeprecatedConstructorForDeserialization(val version: Int)
|
||||
|
||||
|
@ -0,0 +1,130 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||
import net.corda.nodeapi.internal.serialization.carpenter.getTypeAsClass
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.lang.reflect.Type
|
||||
import java.io.NotSerializableException
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.jvm.javaType
|
||||
|
||||
/**
|
||||
* Serializer for deserialising objects whose definition has changed since they
|
||||
* were serialised
|
||||
*/
|
||||
class EvolutionSerializer(
|
||||
clazz: Type,
|
||||
factory: SerializerFactory,
|
||||
val readers: List<OldParam?>,
|
||||
override val kotlinConstructor: KFunction<Any>?) : ObjectSerializer(clazz, factory) {
|
||||
|
||||
// explicitly set as empty to indicate it's unused by this type of serializer
|
||||
override val propertySerializers: Collection<PropertySerializer> = emptyList()
|
||||
|
||||
/**
|
||||
* Represents a parameter as would be passed to the constructor of the class as it was
|
||||
* when it was serialised and NOT how that class appears now
|
||||
*
|
||||
* @param type The jvm type of the parameter
|
||||
* @param idx where in the parameter list this parameter falls. Required as the parameter
|
||||
* order may have been changed and we need to know where into the list to look
|
||||
* @param property object to read the actual property value
|
||||
*/
|
||||
data class OldParam(val type: Type, val idx: Int, val property: PropertySerializer) {
|
||||
fun readProperty(paramValues: List<*>, schema: Schema, input: DeserializationInput) =
|
||||
property.readProperty(paramValues[idx], schema, input)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Unlike the generic deserialisation case where we need to locate the primary constructor
|
||||
* for the object (or our best guess) in the case of an object whose structure has changed
|
||||
* since serialisation we need to attempt to locate a constructor that we can use. I.e.
|
||||
* it's parameters match the serialised members and it will initialise any newly added
|
||||
* elements
|
||||
*
|
||||
* TODO: Type evolution
|
||||
* TODO: rename annotation
|
||||
*/
|
||||
internal fun getEvolverConstructor(type: Type, oldArgs: Map<String?, Type>): KFunction<Any>? {
|
||||
val clazz: Class<*> = type.asClass()!!
|
||||
if (!isConcrete(clazz)) return null
|
||||
|
||||
val oldArgumentSet = oldArgs.map { Pair(it.key, it.value) }
|
||||
|
||||
var maxConstructorVersion = Integer.MIN_VALUE
|
||||
var constructor: KFunction<Any>? = null
|
||||
clazz.kotlin.constructors.forEach {
|
||||
val version = it.findAnnotation<DeprecatedConstructorForDeserialization>()?.version ?: Integer.MIN_VALUE
|
||||
if (oldArgumentSet.containsAll(it.parameters.map { v -> Pair(v.name, v.type.javaType) }) &&
|
||||
version > maxConstructorVersion) {
|
||||
constructor = it
|
||||
maxConstructorVersion = version
|
||||
}
|
||||
}
|
||||
|
||||
// if we didn't get an exact match revert to existing behaviour, if the new parameters
|
||||
// are not mandatory (i.e. nullable) things are fine
|
||||
return constructor ?: constructorForDeserialization(type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a serialization object for deserialisation only of objects serialised
|
||||
* as different versions of a class
|
||||
*
|
||||
* @param old is an object holding the schema that represents the object
|
||||
* as it was serialised and the type descriptor of that type
|
||||
* @param new is the Serializer built for the Class as it exists now, not
|
||||
* how it was serialised and persisted.
|
||||
*/
|
||||
fun make(old: CompositeType, new: ObjectSerializer,
|
||||
factory: SerializerFactory): AMQPSerializer<Any> {
|
||||
|
||||
val oldFieldToType = old.fields.map {
|
||||
it.name as String? to it.getTypeAsClass(factory.classloader) as Type
|
||||
}.toMap()
|
||||
|
||||
val constructor = getEvolverConstructor(new.type, oldFieldToType) ?:
|
||||
throw NotSerializableException(
|
||||
"Attempt to deserialize an interface: ${new.type}. Serialized form is invalid.")
|
||||
|
||||
val oldArgs = mutableMapOf<String, OldParam>()
|
||||
var idx = 0
|
||||
old.fields.forEach {
|
||||
val returnType = it.getTypeAsClass(factory.classloader)
|
||||
oldArgs[it.name] = OldParam(
|
||||
returnType, idx++, PropertySerializer.make(it.name, null, returnType, factory))
|
||||
}
|
||||
|
||||
val readers = constructor.parameters.map {
|
||||
oldArgs[it.name!!] ?: if (!it.type.isMarkedNullable) {
|
||||
throw NotSerializableException(
|
||||
"New parameter ${it.name} is mandatory, should be nullable for evolution to worK")
|
||||
} else null
|
||||
}
|
||||
|
||||
return EvolutionSerializer(new.type, factory, readers, constructor)
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
|
||||
throw IllegalAccessException("It should be impossible to write an evolution serializer")
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlike a normal [readObject] call where we simply apply the parameter deserialisers
|
||||
* to the object list of values we need to map that list, which is ordered per the
|
||||
* constructor of the original state of the object, we need to map the new parameter order
|
||||
* of the current constructor onto that list inserting nulls where new parameters are
|
||||
* encountered
|
||||
*
|
||||
* TODO: Object references
|
||||
*/
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
|
||||
if (obj !is List<*>) throw NotSerializableException("Body of described type is unexpected $obj")
|
||||
|
||||
return construct(readers.map { it?.readProperty(obj, schema, input) })
|
||||
}
|
||||
}
|
||||
|
@ -4,23 +4,19 @@ import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion
|
||||
import org.apache.qpid.proton.amqp.UnsignedInteger
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.Constructor
|
||||
import java.lang.reflect.Type
|
||||
import kotlin.reflect.jvm.javaConstructor
|
||||
|
||||
/**
|
||||
* Responsible for serializing and deserializing a regular object instance via a series of properties (matched with a constructor).
|
||||
*/
|
||||
class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerializer<Any> {
|
||||
open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerializer<Any> {
|
||||
override val type: Type get() = clazz
|
||||
private val javaConstructor: Constructor<Any>?
|
||||
internal val propertySerializers: Collection<PropertySerializer>
|
||||
open val kotlinConstructor = constructorForDeserialization(clazz)
|
||||
val javaConstructor by lazy { kotlinConstructor?.javaConstructor }
|
||||
|
||||
init {
|
||||
val kotlinConstructor = constructorForDeserialization(clazz)
|
||||
javaConstructor = kotlinConstructor?.javaConstructor
|
||||
javaConstructor?.isAccessible = true
|
||||
propertySerializers = propertiesForSerialization(kotlinConstructor, clazz, factory)
|
||||
open internal val propertySerializers: Collection<PropertySerializer> by lazy {
|
||||
propertiesForSerialization(kotlinConstructor, clazz, factory)
|
||||
}
|
||||
|
||||
private val typeName = nameForType(clazz)
|
||||
@ -28,7 +24,7 @@ class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerial
|
||||
override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}"
|
||||
private val interfaces = interfacesForSerialization(clazz, factory) // We restrict to only those annotated or whitelisted
|
||||
|
||||
internal val typeNotation: TypeNotation = CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor, null), generateFields())
|
||||
open internal val typeNotation : TypeNotation by lazy {CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor, null), generateFields()) }
|
||||
|
||||
override fun writeClassInfo(output: SerializationOutput) {
|
||||
if (output.writeTypeNotations(typeNotation)) {
|
||||
@ -68,15 +64,8 @@ class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerial
|
||||
return propertySerializers.map { Field(it.name, it.type, it.requires, it.default, null, it.mandatory, false) }
|
||||
}
|
||||
|
||||
private fun generateProvides(): List<String> {
|
||||
return interfaces.map { nameForType(it) }
|
||||
}
|
||||
private fun generateProvides(): List<String> = interfaces.map { nameForType(it) }
|
||||
|
||||
|
||||
fun construct(properties: List<Any?>): Any {
|
||||
if (javaConstructor == null) {
|
||||
fun construct(properties: List<Any?>) = javaConstructor?.newInstance(*properties.toTypedArray()) ?:
|
||||
throw NotSerializableException("Attempt to deserialize an interface: $clazz. Serialized form is invalid.")
|
||||
}
|
||||
return javaConstructor.newInstance(*properties.toTypedArray())
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ import kotlin.reflect.jvm.javaGetter
|
||||
/**
|
||||
* Base class for serialization of a property of an object.
|
||||
*/
|
||||
sealed class PropertySerializer(val name: String, val readMethod: Method, val resolvedType: Type) {
|
||||
sealed class PropertySerializer(val name: String, val readMethod: Method?, val resolvedType: Type) {
|
||||
abstract fun writeClassInfo(output: SerializationOutput)
|
||||
abstract fun writeProperty(obj: Any?, data: Data, output: SerializationOutput)
|
||||
abstract fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any?
|
||||
@ -44,7 +44,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re
|
||||
}
|
||||
|
||||
private fun generateMandatory(): Boolean {
|
||||
return isJVMPrimitive || !readMethod.returnsNullable()
|
||||
return isJVMPrimitive || !(readMethod?.returnsNullable() ?: true)
|
||||
}
|
||||
|
||||
private fun Method.returnsNullable(): Boolean {
|
||||
@ -53,8 +53,8 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun make(name: String, readMethod: Method, resolvedType: Type, factory: SerializerFactory): PropertySerializer {
|
||||
readMethod.isAccessible = true
|
||||
fun make(name: String, readMethod: Method?, resolvedType: Type, factory: SerializerFactory): PropertySerializer {
|
||||
readMethod?.isAccessible = true
|
||||
if (SerializerFactory.isPrimitive(resolvedType)) {
|
||||
return when(resolvedType) {
|
||||
Char::class.java, Character::class.java -> AMQPCharPropertySerializer(name, readMethod)
|
||||
@ -69,7 +69,10 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re
|
||||
/**
|
||||
* A property serializer for a complex type (another object).
|
||||
*/
|
||||
class DescribedTypePropertySerializer(name: String, readMethod: Method, resolvedType: Type, private val lazyTypeSerializer: () -> AMQPSerializer<*>) : PropertySerializer(name, readMethod, resolvedType) {
|
||||
class DescribedTypePropertySerializer(
|
||||
name: String, readMethod: Method?,
|
||||
resolvedType: Type,
|
||||
private val lazyTypeSerializer: () -> AMQPSerializer<*>) : PropertySerializer(name, readMethod, resolvedType) {
|
||||
// This is lazy so we don't get an infinite loop when a method returns an instance of the class.
|
||||
private val typeSerializer: AMQPSerializer<*> by lazy { lazyTypeSerializer() }
|
||||
|
||||
@ -84,14 +87,14 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re
|
||||
}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) {
|
||||
output.writeObjectOrNull(readMethod.invoke(obj), data, resolvedType)
|
||||
output.writeObjectOrNull(readMethod!!.invoke(obj), data, resolvedType)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A property serializer for most AMQP primitive type (Int, String, etc).
|
||||
*/
|
||||
class AMQPPrimitivePropertySerializer(name: String, readMethod: Method, resolvedType: Type) : PropertySerializer(name, readMethod, resolvedType) {
|
||||
class AMQPPrimitivePropertySerializer(name: String, readMethod: Method?, resolvedType: Type) : PropertySerializer(name, readMethod, resolvedType) {
|
||||
override fun writeClassInfo(output: SerializationOutput) {}
|
||||
|
||||
override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? {
|
||||
@ -99,7 +102,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re
|
||||
}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) {
|
||||
val value = readMethod.invoke(obj)
|
||||
val value = readMethod!!.invoke(obj)
|
||||
if (value is ByteArray) {
|
||||
data.putObject(Binary(value))
|
||||
} else {
|
||||
@ -113,7 +116,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re
|
||||
* value of the character is stored in numeric UTF-16 form and on deserialisation requires explicit
|
||||
* casting back to a char otherwise it's treated as an Integer and a TypeMismatch occurs
|
||||
*/
|
||||
class AMQPCharPropertySerializer(name: String, readMethod: Method) :
|
||||
class AMQPCharPropertySerializer(name: String, readMethod: Method?) :
|
||||
PropertySerializer(name, readMethod, Character::class.java) {
|
||||
override fun writeClassInfo(output: SerializationOutput) {}
|
||||
|
||||
@ -122,7 +125,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re
|
||||
}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) {
|
||||
val input = readMethod.invoke(obj)
|
||||
val input = readMethod!!.invoke(obj)
|
||||
if (input != null) data.putShort((input as Char).toShort()) else data.putNull()
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ internal fun <T : Any> propertiesForSerialization(kotlinConstructor: KFunction<T
|
||||
return if (kotlinConstructor != null) propertiesForSerializationFromConstructor(kotlinConstructor, type, factory) else propertiesForSerializationFromAbstract(clazz, type, factory)
|
||||
}
|
||||
|
||||
private fun isConcrete(clazz: Class<*>): Boolean = !(clazz.isInterface || Modifier.isAbstract(clazz.modifiers))
|
||||
fun isConcrete(clazz: Class<*>): Boolean = !(clazz.isInterface || Modifier.isAbstract(clazz.modifiers))
|
||||
|
||||
private fun <T : Any> propertiesForSerializationFromConstructor(kotlinConstructor: KFunction<T>, type: Type, factory: SerializerFactory): Collection<PropertySerializer> {
|
||||
val clazz = (kotlinConstructor.returnType.classifier as KClass<*>).javaObjectType
|
||||
|
@ -16,6 +16,8 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
data class schemaAndDescriptor (val schema: Schema, val typeDescriptor: Any)
|
||||
|
||||
/**
|
||||
* Factory of serializers designed to be shared across threads and invocations.
|
||||
*/
|
||||
@ -28,9 +30,6 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
// TODO: profile for performance in general
|
||||
// TODO: use guava caches etc so not unbounded
|
||||
// TODO: do we need to support a transient annotation to exclude certain properties?
|
||||
// TODO: incorporate the class carpenter for classes not on the classpath.
|
||||
// TODO: apply class loader logic and an "app context" throughout this code.
|
||||
// TODO: schema evolution solution when the fingerprints do not line up.
|
||||
// TODO: allow definition of well known types that are left out of the schema.
|
||||
// TODO: generally map Object to '*' all over the place in the schema and make sure use of '*' amd '?' is consistent and documented in generics.
|
||||
// TODO: found a document that states textual descriptors are Symbols. Adjust schema class appropriately.
|
||||
@ -48,6 +47,12 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
|
||||
val classloader : ClassLoader
|
||||
get() = classCarpenter.classloader
|
||||
|
||||
fun getEvolutionSerializer(typeNotation: TypeNotation, newSerializer: ObjectSerializer) : AMQPSerializer<Any> {
|
||||
return serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) {
|
||||
EvolutionSerializer.make(typeNotation as CompositeType, newSerializer, this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up, and manufacture if necessary, a serializer for the given type.
|
||||
*
|
||||
@ -155,9 +160,9 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
|
||||
@Throws(NotSerializableException::class)
|
||||
fun get(typeDescriptor: Any, schema: Schema): AMQPSerializer<Any> {
|
||||
return serializersByDescriptor[typeDescriptor] ?: {
|
||||
processSchema(schema)
|
||||
serializersByDescriptor[typeDescriptor] ?:
|
||||
throw NotSerializableException("Could not find type matching descriptor $typeDescriptor.")
|
||||
processSchema(schemaAndDescriptor(schema, typeDescriptor))
|
||||
serializersByDescriptor[typeDescriptor] ?: throw NotSerializableException(
|
||||
"Could not find type matching descriptor $typeDescriptor.")
|
||||
}()
|
||||
}
|
||||
|
||||
@ -179,11 +184,18 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
|
||||
* Iterate over an AMQP schema, for each type ascertain weather it's on ClassPath of [classloader] amd
|
||||
* if not use the [ClassCarpenter] to generate a class to use in it's place
|
||||
*/
|
||||
private fun processSchema(schema: Schema, sentinel: Boolean = false) {
|
||||
private fun processSchema(schema: schemaAndDescriptor, sentinel: Boolean = false) {
|
||||
val carpenterSchemas = CarpenterSchemas.newInstance()
|
||||
for (typeNotation in schema.types) {
|
||||
for (typeNotation in schema.schema.types) {
|
||||
try {
|
||||
processSchemaEntry(typeNotation)
|
||||
val serialiser = processSchemaEntry(typeNotation)
|
||||
|
||||
// if we just successfully built a serialiser for the type but the type fingerprint
|
||||
// doesn't match that of the serialised object then we are dealing with different
|
||||
// instance of the class, as such we need to build an EvolutionSerialiser
|
||||
if (serialiser.typeDescriptor != typeNotation.descriptor.name) {
|
||||
getEvolutionSerializer(typeNotation, serialiser as ObjectSerializer)
|
||||
}
|
||||
} catch (e: ClassNotFoundException) {
|
||||
if (sentinel || (typeNotation !is CompositeType)) throw e
|
||||
typeNotation.carpenterSchema(classloader, carpenterSchemas = carpenterSchemas)
|
||||
@ -197,23 +209,21 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun processSchemaEntry(typeNotation: TypeNotation) {
|
||||
when (typeNotation) {
|
||||
private fun processSchemaEntry(typeNotation: TypeNotation) = when (typeNotation) {
|
||||
is CompositeType -> processCompositeType(typeNotation) // java.lang.Class (whether a class or interface)
|
||||
is RestrictedType -> processRestrictedType(typeNotation) // Collection / Map, possibly with generics
|
||||
}
|
||||
}
|
||||
|
||||
private fun processRestrictedType(typeNotation: RestrictedType) {
|
||||
private fun processRestrictedType(typeNotation: RestrictedType): AMQPSerializer<Any> {
|
||||
// TODO: class loader logic, and compare the schema.
|
||||
val type = typeForName(typeNotation.name, classloader)
|
||||
get(null, type)
|
||||
return get(null, type)
|
||||
}
|
||||
|
||||
private fun processCompositeType(typeNotation: CompositeType) {
|
||||
private fun processCompositeType(typeNotation: CompositeType): AMQPSerializer<Any> {
|
||||
// TODO: class loader logic, and compare the schema.
|
||||
val type = typeForName(typeNotation.name, classloader)
|
||||
get(type.asClass() ?: throw NotSerializableException("Unable to build composite type for $type"), type)
|
||||
return get(type.asClass() ?: throw NotSerializableException("Unable to build composite type for $type"), type)
|
||||
}
|
||||
|
||||
private fun makeClassSerializer(clazz: Class<*>, type: Type, declaredType: Type): AMQPSerializer<Any> = serializersByType.computeIfAbsent(type) {
|
||||
|
@ -19,7 +19,7 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<T
|
||||
val constructor = constructorForDeserialization(obj.javaClass)
|
||||
val props = propertiesForSerialization(constructor, obj.javaClass, factory)
|
||||
for (prop in props) {
|
||||
extraProperties[prop.name] = prop.readMethod.invoke(obj)
|
||||
extraProperties[prop.name] = prop.readMethod!!.invoke(obj)
|
||||
}
|
||||
} catch(e: NotSerializableException) {
|
||||
}
|
||||
|
@ -0,0 +1,461 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import java.io.NotSerializableException
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
// To regenerate any of the binary test files do the following
|
||||
//
|
||||
// 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 {
|
||||
|
||||
@Test
|
||||
fun simpleOrderSwapSameType() {
|
||||
val sf = testDefaultFactory()
|
||||
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.simpleOrderSwapSameType")
|
||||
val f = File(path.toURI())
|
||||
|
||||
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)
|
||||
|
||||
// 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 sc2 = f.readBytes()
|
||||
val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes<C>(sc2))
|
||||
|
||||
assertEquals(A, deserializedC.a)
|
||||
assertEquals(B, deserializedC.b)
|
||||
}
|
||||
|
||||
@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"
|
||||
|
||||
// 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)
|
||||
|
||||
// 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 sc2 = f.readBytes()
|
||||
val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes<C>(sc2))
|
||||
|
||||
assertEquals(A, deserializedC.a)
|
||||
assertEquals(B, deserializedC.b)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addAdditionalParamNotMandatory() {
|
||||
val sf = testDefaultFactory()
|
||||
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.addAdditionalParamNotMandatory")
|
||||
val f = File(path.toURI())
|
||||
val A = 1
|
||||
|
||||
// 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")
|
||||
|
||||
data class C (val a: Int, val b: Int?)
|
||||
|
||||
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() {
|
||||
val sf = testDefaultFactory()
|
||||
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.addAdditionalParam")
|
||||
val f = File(path.toURI())
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val A = 1
|
||||
|
||||
// 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")
|
||||
|
||||
// new version of the class, in this case a new parameter has been added (b)
|
||||
data class C (val a: Int, val b: Int)
|
||||
|
||||
val sc2 = f.readBytes()
|
||||
|
||||
// Expected to throw as we can't construct the new type as it contains a newly
|
||||
// added parameter that isn't optional, i.e. not nullable and there isn't
|
||||
// a constructor that takes the old parameters
|
||||
DeserializationInput(sf).deserialize(SerializedBytes<C>(sc2))
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
@Test
|
||||
fun removeParameters() {
|
||||
val sf = testDefaultFactory()
|
||||
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.removeParameters")
|
||||
val f = File(path.toURI())
|
||||
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")
|
||||
|
||||
data class CC (val b: String, val d: Int)
|
||||
|
||||
val sc2 = f.readBytes()
|
||||
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
|
||||
|
||||
assertEquals (B, deserializedCC.b)
|
||||
assertEquals (D, deserializedCC.d)
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
@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
|
||||
|
||||
// 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")
|
||||
|
||||
data class CC(val a: Int, val e: Boolean?, val d: Int)
|
||||
|
||||
val sc2 = f.readBytes()
|
||||
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
|
||||
|
||||
assertEquals(A, deserializedCC.a)
|
||||
assertEquals(E, deserializedCC.e)
|
||||
assertEquals(D, deserializedCC.d)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addMandatoryFieldWithAltConstructor() {
|
||||
val sf = testDefaultFactory()
|
||||
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.addMandatoryFieldWithAltConstructor")
|
||||
val f = File(path.toURI())
|
||||
val A = 1
|
||||
|
||||
// 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")
|
||||
|
||||
@Suppress("UNUSED")
|
||||
data class CC (val a: Int, val b: String) {
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
constructor (a: Int) : this (a, "hello")
|
||||
}
|
||||
|
||||
val sc2 = f.readBytes()
|
||||
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
|
||||
|
||||
assertEquals (A, deserializedCC.a)
|
||||
assertEquals ("hello", deserializedCC.b)
|
||||
}
|
||||
|
||||
@Test(expected = NotSerializableException::class)
|
||||
@Suppress("UNUSED")
|
||||
fun addMandatoryFieldWithAltConstructorUnAnnotated() {
|
||||
val sf = testDefaultFactory()
|
||||
val path = EvolvabilityTests::class.java.getResource(
|
||||
"EvolvabilityTests.addMandatoryFieldWithAltConstructorUnAnnotated")
|
||||
val f = File(path.toURI())
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val A = 1
|
||||
|
||||
// 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")
|
||||
|
||||
data class CC (val a: Int, val b: String) {
|
||||
// constructor annotation purposefully omitted
|
||||
constructor (a: Int) : this (a, "hello")
|
||||
}
|
||||
|
||||
// we expect this to throw as we should not find any constructors
|
||||
// capable of dealing with this
|
||||
DeserializationInput(sf).deserialize(SerializedBytes<CC>(f.readBytes()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addMandatoryFieldWithAltReorderedConstructor() {
|
||||
val sf = testDefaultFactory()
|
||||
val path = EvolvabilityTests::class.java.getResource(
|
||||
"EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor")
|
||||
val f = File(path.toURI())
|
||||
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")
|
||||
|
||||
@Suppress("UNUSED")
|
||||
data class CC (val a: Int, val b: Int, val c: String, val d: String) {
|
||||
// ensure none of the original parameters align with the initial
|
||||
// construction order
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
constructor (c: String, a: Int, b: Int) : this (a, b, c, "wibble")
|
||||
}
|
||||
|
||||
val sc2 = f.readBytes()
|
||||
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
|
||||
|
||||
assertEquals (A, deserializedCC.a)
|
||||
assertEquals (B, deserializedCC.b)
|
||||
assertEquals (C, deserializedCC.c)
|
||||
assertEquals ("wibble", deserializedCC.d)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addMandatoryFieldWithAltReorderedConstructorAndRemoval() {
|
||||
val sf = testDefaultFactory()
|
||||
val path = EvolvabilityTests::class.java.getResource(
|
||||
"EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval")
|
||||
val f = File(path.toURI())
|
||||
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")
|
||||
|
||||
// b is removed, d is added
|
||||
data class CC (val a: Int, val c: String, val d: String) {
|
||||
// ensure none of the original parameters align with the initial
|
||||
// construction order
|
||||
@Suppress("UNUSED")
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
constructor (c: String, a: Int) : this (a, c, "wibble")
|
||||
}
|
||||
|
||||
val sc2 = f.readBytes()
|
||||
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
|
||||
|
||||
assertEquals (A, deserializedCC.a)
|
||||
assertEquals (C, deserializedCC.c)
|
||||
assertEquals ("wibble", deserializedCC.d)
|
||||
}
|
||||
|
||||
@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")
|
||||
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val f = File(path1.toURI())
|
||||
|
||||
val a = 100
|
||||
val b = 200
|
||||
val c = 300
|
||||
val d = 400
|
||||
|
||||
// Original version of the class as it was serialised
|
||||
//
|
||||
// Version 1:
|
||||
// data class C (val a: Int, val b: Int)
|
||||
// Version 2 - add param c
|
||||
// data class C (val c: Int, val b: Int, val a: Int)
|
||||
// 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))
|
||||
// f.writeBytes(scc.bytes)
|
||||
// println ("Path = $path1")
|
||||
|
||||
@Suppress("UNUSED")
|
||||
data class C (val e: Int, val c: Int, val b: Int, val a: Int, val d: Int) {
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
constructor (b: Int, a: Int) : this (-1, -1, b, a, -1)
|
||||
@DeprecatedConstructorForDeserialization(2)
|
||||
constructor (a: Int, c: Int, b: Int) : this (-1, c, b, a, -1)
|
||||
@DeprecatedConstructorForDeserialization(3)
|
||||
constructor (a: Int, b: Int, c: Int, d: Int) : this (-1, c, b, a, d)
|
||||
}
|
||||
|
||||
val sb1 = File(path1.toURI()).readBytes()
|
||||
val db1 = DeserializationInput(sf).deserialize(SerializedBytes<C>(sb1))
|
||||
|
||||
assertEquals(a, db1.a)
|
||||
assertEquals(b, db1.b)
|
||||
assertEquals(-1, db1.c)
|
||||
assertEquals(-1, db1.d)
|
||||
assertEquals(-1, db1.e)
|
||||
|
||||
val sb2 = File(path2.toURI()).readBytes()
|
||||
val db2 = DeserializationInput(sf).deserialize(SerializedBytes<C>(sb2))
|
||||
|
||||
assertEquals(a, db2.a)
|
||||
assertEquals(b, db2.b)
|
||||
assertEquals(c, db2.c)
|
||||
assertEquals(-1, db2.d)
|
||||
assertEquals(-1, db2.e)
|
||||
|
||||
val sb3 = File(path3.toURI()).readBytes()
|
||||
val db3 = DeserializationInput(sf).deserialize(SerializedBytes<C>(sb3))
|
||||
|
||||
assertEquals(a, db3.a)
|
||||
assertEquals(b, db3.b)
|
||||
assertEquals(c, db3.c)
|
||||
assertEquals(d, db3.d)
|
||||
assertEquals(-1, db3.e)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun changeSubType() {
|
||||
val sf = testDefaultFactory()
|
||||
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.changeSubType")
|
||||
val f = File(path.toURI())
|
||||
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")
|
||||
|
||||
// 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 sc2 = f.readBytes()
|
||||
val outer = DeserializationInput(sf).deserialize(SerializedBytes<Outer>(sc2))
|
||||
|
||||
assertEquals (oa, outer.a)
|
||||
assertEquals (ia, outer.b.a)
|
||||
assertEquals (null, outer.b.b)
|
||||
}
|
||||
|
||||
@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")
|
||||
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val a = 100
|
||||
val b = 200
|
||||
val c = 300
|
||||
val d = 400
|
||||
val e = 500
|
||||
val f = 600
|
||||
|
||||
// Original version of the class as it was serialised
|
||||
//
|
||||
// Version 1:
|
||||
// data class C (val a: Int, val b: Int, val c: Int)
|
||||
// Version 2 - add param c
|
||||
// data class C (val b: Int, val c: Int, val d: Int, val e: Int)
|
||||
// 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(path1.toURI()).writeBytes(scc.bytes)
|
||||
// println ("Path = $path1")
|
||||
|
||||
@Suppress("UNUSED")
|
||||
data class C (val b: Int, val c: Int, val d: Int, val e: Int, val f: Int, val g: Int) {
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
constructor (b: Int, c: Int) : this (b, c, -1, -1, -1, -1)
|
||||
@DeprecatedConstructorForDeserialization(2)
|
||||
constructor (b: Int, c: Int, d: Int) : this (b, c, d, -1, -1, -1)
|
||||
@DeprecatedConstructorForDeserialization(3)
|
||||
constructor (b: Int, c: Int, d: Int, e: Int) : this (b, c, d, e, -1, -1)
|
||||
@DeprecatedConstructorForDeserialization(4)
|
||||
constructor (b: Int, c: Int, d: Int, e: Int, f: Int) : this (b, c, d, e, f, -1)
|
||||
}
|
||||
|
||||
val sb1 = File(path1.toURI()).readBytes()
|
||||
val db1 = DeserializationInput(sf).deserialize(SerializedBytes<C>(sb1))
|
||||
|
||||
assertEquals(b, db1.b)
|
||||
assertEquals(c, db1.c)
|
||||
assertEquals(-1, db1.d) // must not be set by calling constructor 2 by mistake
|
||||
assertEquals(-1, db1.e)
|
||||
assertEquals(-1, db1.f)
|
||||
assertEquals(-1, db1.g)
|
||||
|
||||
val sb2 = File(path2.toURI()).readBytes()
|
||||
val db2 = DeserializationInput(sf).deserialize(SerializedBytes<C>(sb2))
|
||||
|
||||
assertEquals(b, db2.b)
|
||||
assertEquals(c, db2.c)
|
||||
assertEquals(d, db2.d)
|
||||
assertEquals(e, db2.e)
|
||||
assertEquals(-1, db2.f)
|
||||
assertEquals(-1, db1.g)
|
||||
|
||||
val sb3 = File(path3.toURI()).readBytes()
|
||||
val db3 = DeserializationInput(sf).deserialize(SerializedBytes<C>(sb3))
|
||||
|
||||
assertEquals(b, db3.b)
|
||||
assertEquals(c, db3.c)
|
||||
assertEquals(d, db3.d)
|
||||
assertEquals(e, db3.e)
|
||||
assertEquals(f, db3.f)
|
||||
assertEquals(-1, db3.g)
|
||||
}
|
||||
}
|
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.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user