mirror of
https://github.com/corda/corda.git
synced 2025-01-31 00:24:59 +00:00
Fix evolver to work on nested schema types
Dumb assumption in the initial implementation meant it could only evolve a top level type in the schema
This commit is contained in:
parent
6bcfb2eddf
commit
7894e723e8
@ -5,12 +5,11 @@ import org.apache.qpid.proton.codec.Data
|
||||
import java.lang.reflect.Type
|
||||
import java.io.NotSerializableException
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
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,
|
||||
@ -22,8 +21,13 @@ class EvolutionSerializer(
|
||||
override val propertySerializers: Collection<PropertySerializer> = listOf()
|
||||
|
||||
/**
|
||||
* represents a paramter as would be passed to the constructor of the class as it was
|
||||
* 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)
|
||||
|
||||
@ -64,10 +68,10 @@ class EvolutionSerializer(
|
||||
* @param new is the Serializer built for the Class as it exists now, not
|
||||
* how it was serialised and persisted.
|
||||
*/
|
||||
fun make (old: schemaAndDescriptor, new: ObjectSerializer,
|
||||
fun make (old: CompositeType, new: ObjectSerializer,
|
||||
factory: SerializerFactory) : AMQPSerializer<Any> {
|
||||
|
||||
val oldFieldToType = (old.schema.types.first() as CompositeType).fields.map {
|
||||
val oldFieldToType = old.fields.map {
|
||||
it.name as String? to it.getTypeAsClass(factory.classloader) as Type
|
||||
}.toMap()
|
||||
|
||||
@ -77,7 +81,7 @@ class EvolutionSerializer(
|
||||
|
||||
val oldArgs = mutableMapOf<String, oldParam>()
|
||||
var idx = 0
|
||||
(old.schema.types.first() as CompositeType).fields.forEach {
|
||||
old.fields.forEach {
|
||||
val returnType = it.getTypeAsClass(factory.classloader)
|
||||
oldArgs[it.name] = oldParam(
|
||||
returnType, idx++, PropertySerializer.make(it.name, null, returnType, factory))
|
||||
@ -91,6 +95,13 @@ class EvolutionSerializer(
|
||||
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
|
||||
*/
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
|
||||
if (obj !is List<*>) throw NotSerializableException("Body of described type is unexpected $obj")
|
||||
|
||||
|
@ -30,9 +30,6 @@ data class schemaAndDescriptor (val schema: Schema, val typeDescriptor: Any)
|
||||
// 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.
|
||||
@ -50,9 +47,9 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
|
||||
val classloader : ClassLoader
|
||||
get() = classCarpenter.classloader
|
||||
|
||||
fun getEvolutionSerializer(schema: schemaAndDescriptor, newSerializer: ObjectSerializer) : AMQPSerializer<Any> {
|
||||
return serializersByDescriptor.computeIfAbsent(schema.typeDescriptor) {
|
||||
EvolutionSerializer.make(schema, newSerializer, this)
|
||||
fun getEvolutionSerializer(typeNotation: TypeNotation, newSerializer: ObjectSerializer) : AMQPSerializer<Any> {
|
||||
return serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) {
|
||||
EvolutionSerializer.make(typeNotation as CompositeType, newSerializer, this)
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,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.")
|
||||
}()
|
||||
}
|
||||
|
||||
@ -196,8 +193,8 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
|
||||
// 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 an older
|
||||
// instance of the class, as such we need to build an evolverSerilaiser
|
||||
if (serialiser.typeDescriptor != schema.typeDescriptor) {
|
||||
getEvolutionSerializer(schema, serialiser as ObjectSerializer)
|
||||
if (serialiser.typeDescriptor != typeNotation.descriptor.name) {
|
||||
getEvolutionSerializer(typeNotation, serialiser as ObjectSerializer)
|
||||
}
|
||||
} catch (e: ClassNotFoundException) {
|
||||
if (sentinel || (typeNotation !is CompositeType)) throw e
|
||||
|
@ -188,4 +188,31 @@ class EvolvabilityTests {
|
||||
assertEquals ("hello", deserializedCC.b)
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user