mirror of
https://github.com/corda/corda.git
synced 2025-03-01 04:16:21 +00:00
CORDA-1530 - Generics break default evolver (#3232)
* CORDA-1530 - Generics break default evolver When selecting an annotated constructor for evolving a type make sure we treat generics in the same manner we did when serialized. Effectively throw away the template information and treat lists as lists and maps as maps
This commit is contained in:
parent
76eaff6d88
commit
ba0a94d54d
@ -2,6 +2,8 @@ package net.corda.nodeapi.internal.serialization.amqp
|
|||||||
|
|
||||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||||
import net.corda.nodeapi.internal.serialization.carpenter.getTypeAsClass
|
import net.corda.nodeapi.internal.serialization.carpenter.getTypeAsClass
|
||||||
|
import net.corda.core.utilities.contextLogger
|
||||||
|
import net.corda.core.utilities.debug
|
||||||
import org.apache.qpid.proton.codec.Data
|
import org.apache.qpid.proton.codec.Data
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
import java.io.NotSerializableException
|
import java.io.NotSerializableException
|
||||||
@ -16,10 +18,8 @@ import kotlin.reflect.jvm.javaType
|
|||||||
*
|
*
|
||||||
* @property oldReaders A linked map representing the properties of the object as they were serialized. Note
|
* @property oldReaders A linked map representing the properties of the object as they were serialized. Note
|
||||||
* this may contain properties that are no longer needed by the class. These *must* be read however to ensure
|
* this may contain properties that are no longer needed by the class. These *must* be read however to ensure
|
||||||
* any refferenced objects in the object stream are captured properly
|
* any referenced objects in the object stream are captured properly
|
||||||
* @property kotlinConstructor
|
* @property kotlinConstructor reference to the constructor used to instantiate an instance of the class.
|
||||||
* @property constructorArgs used to hold the properties as sent to the object's constructor. Passed in as a
|
|
||||||
* pre populated array as properties not present on the old constructor must be initialised in the factory
|
|
||||||
*/
|
*/
|
||||||
abstract class EvolutionSerializer(
|
abstract class EvolutionSerializer(
|
||||||
clazz: Type,
|
clazz: Type,
|
||||||
@ -39,15 +39,21 @@ abstract class EvolutionSerializer(
|
|||||||
* @param property object to read the actual property value
|
* @param property object to read the actual property value
|
||||||
*/
|
*/
|
||||||
data class OldParam(var resultsIndex: Int, val property: PropertySerializer) {
|
data class OldParam(var resultsIndex: Int, val property: PropertySerializer) {
|
||||||
fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput, new: Array<Any?>) =
|
fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput,
|
||||||
property.readProperty(obj, schemas, input).apply {
|
new: Array<Any?>
|
||||||
|
) = property.readProperty(obj, schemas, input).apply {
|
||||||
if(resultsIndex >= 0) {
|
if(resultsIndex >= 0) {
|
||||||
new[resultsIndex] = this
|
new[resultsIndex] = this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
override fun toString(): String {
|
||||||
|
return "resultsIndex = $resultsIndex property = ${property.name}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
val logger = contextLogger()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unlike the generic deserialization case where we need to locate the primary constructor
|
* Unlike the generic deserialization 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
|
* for the object (or our best guess) in the case of an object whose structure has changed
|
||||||
@ -63,22 +69,37 @@ abstract class EvolutionSerializer(
|
|||||||
|
|
||||||
if (!isConcrete(clazz)) return null
|
if (!isConcrete(clazz)) return null
|
||||||
|
|
||||||
val oldArgumentSet = oldArgs.map { Pair(it.key as String?, it.value.property.resolvedType) }
|
val oldArgumentSet = oldArgs.map { Pair(it.key as String?, it.value.property.resolvedType.asClass()) }
|
||||||
|
|
||||||
var maxConstructorVersion = Integer.MIN_VALUE
|
var maxConstructorVersion = Integer.MIN_VALUE
|
||||||
var constructor: KFunction<Any>? = null
|
var constructor: KFunction<Any>? = null
|
||||||
|
|
||||||
clazz.kotlin.constructors.forEach {
|
clazz.kotlin.constructors.forEach {
|
||||||
val version = it.findAnnotation<DeprecatedConstructorForDeserialization>()?.version ?: Integer.MIN_VALUE
|
val version = it.findAnnotation<DeprecatedConstructorForDeserialization>()?.version ?: Integer.MIN_VALUE
|
||||||
if (oldArgumentSet.containsAll(it.parameters.map { v -> Pair(v.name, v.type.javaType) }) &&
|
|
||||||
version > maxConstructorVersion) {
|
if (version > maxConstructorVersion &&
|
||||||
|
oldArgumentSet.containsAll(it.parameters.map { v -> Pair(v.name, v.type.javaType.asClass()) })
|
||||||
|
) {
|
||||||
constructor = it
|
constructor = it
|
||||||
maxConstructorVersion = version
|
maxConstructorVersion = version
|
||||||
|
|
||||||
|
with(logger) {
|
||||||
|
info("Select annotated constructor version=$version nparams=${it.parameters.size}")
|
||||||
|
debug{" params=${it.parameters}"}
|
||||||
|
}
|
||||||
|
} else if (version != Integer.MIN_VALUE){
|
||||||
|
with(logger) {
|
||||||
|
info("Ignore annotated constructor version=$version nparams=${it.parameters.size}")
|
||||||
|
debug{" params=${it.parameters}"}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we didn't get an exact match revert to existing behaviour, if the new parameters
|
// 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
|
// are not mandatory (i.e. nullable) things are fine
|
||||||
return constructor ?: constructorForDeserialization(type)
|
return constructor ?: run {
|
||||||
|
logger.info("Failed to find annotated historic constructor")
|
||||||
|
constructorForDeserialization(type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun makeWithConstructor(
|
private fun makeWithConstructor(
|
||||||
|
@ -18,6 +18,7 @@ import java.io.NotSerializableException
|
|||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
import net.corda.nodeapi.internal.serialization.amqp.custom.InstantSerializer
|
||||||
|
|
||||||
// To regenerate any of the binary test files do the following
|
// To regenerate any of the binary test files do the following
|
||||||
//
|
//
|
||||||
@ -38,8 +39,8 @@ class EvolvabilityTests {
|
|||||||
val sf = testDefaultFactory()
|
val sf = testDefaultFactory()
|
||||||
val resource = "EvolvabilityTests.simpleOrderSwapSameType"
|
val resource = "EvolvabilityTests.simpleOrderSwapSameType"
|
||||||
|
|
||||||
val A = 1
|
val a = 1
|
||||||
val B = 2
|
val b = 2
|
||||||
|
|
||||||
// Original version of the class for the serialised version of this class
|
// Original version of the class for the serialised version of this class
|
||||||
// data class C (val a: Int, val b: Int)
|
// data class C (val a: Int, val b: Int)
|
||||||
@ -54,8 +55,8 @@ class EvolvabilityTests {
|
|||||||
val sc2 = f.readBytes()
|
val sc2 = f.readBytes()
|
||||||
val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes<C>(sc2))
|
val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes<C>(sc2))
|
||||||
|
|
||||||
assertEquals(A, deserializedC.a)
|
assertEquals(a, deserializedC.a)
|
||||||
assertEquals(B, deserializedC.b)
|
assertEquals(b, deserializedC.b)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -206,6 +207,86 @@ class EvolvabilityTests {
|
|||||||
assertEquals("hello", deserializedCC.b)
|
assertEquals("hello", deserializedCC.b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun addMandatoryFieldWithAltConstructorForceReorder() {
|
||||||
|
val sf = testDefaultFactory()
|
||||||
|
val z = 30
|
||||||
|
val y = 20
|
||||||
|
val resource = "EvolvabilityTests.addMandatoryFieldWithAltConstructorForceReorder"
|
||||||
|
|
||||||
|
// Original version of the class as it was serialised
|
||||||
|
// data class CC(val z: Int, val y: Int)
|
||||||
|
// File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(CC(z, y)).bytes)
|
||||||
|
|
||||||
|
@Suppress("UNUSED")
|
||||||
|
data class CC(val z: Int, val y: Int, val a: String) {
|
||||||
|
@DeprecatedConstructorForDeserialization(1)
|
||||||
|
constructor (z: Int, y: Int) : this(z, y, "10")
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = EvolvabilityTests::class.java.getResource(resource)
|
||||||
|
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(url.readBytes()))
|
||||||
|
|
||||||
|
assertEquals("10", deserializedCC.a)
|
||||||
|
assertEquals(y, deserializedCC.y)
|
||||||
|
assertEquals(z, deserializedCC.z)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun moreComplexNonNullWithReorder() {
|
||||||
|
val resource = "${javaClass.simpleName}.${testName()}"
|
||||||
|
|
||||||
|
data class NetworkParametersExample(
|
||||||
|
val minimumPlatformVersion: Int,
|
||||||
|
val notaries: List<String>,
|
||||||
|
val maxMessageSize: Int,
|
||||||
|
val maxTransactionSize: Int,
|
||||||
|
val modifiedTime: Instant,
|
||||||
|
val epoch: Int,
|
||||||
|
val whitelistedContractImplementations: Map<String, List<Int>>,
|
||||||
|
/* to regenerate test class, comment out this element */
|
||||||
|
val eventHorizon: Int
|
||||||
|
) {
|
||||||
|
// when regenerating test class this won't be required
|
||||||
|
@DeprecatedConstructorForDeserialization(1)
|
||||||
|
@Suppress("UNUSED")
|
||||||
|
constructor (
|
||||||
|
minimumPlatformVersion: Int,
|
||||||
|
notaries: List<String>,
|
||||||
|
maxMessageSize: Int,
|
||||||
|
maxTransactionSize: Int,
|
||||||
|
modifiedTime: Instant,
|
||||||
|
epoch: Int,
|
||||||
|
whitelistedContractImplementations: Map<String, List<Int>>
|
||||||
|
) : this(minimumPlatformVersion,
|
||||||
|
notaries,
|
||||||
|
maxMessageSize,
|
||||||
|
maxTransactionSize,
|
||||||
|
modifiedTime,
|
||||||
|
epoch,
|
||||||
|
whitelistedContractImplementations,
|
||||||
|
Int.MAX_VALUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
val factory = testDefaultFactory().apply {
|
||||||
|
register(InstantSerializer(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncomment to regenerate test case
|
||||||
|
// File(URI("$localPath/$resource")).writeBytes(SerializationOutput(factory).serialize(
|
||||||
|
// NetworkParametersExample(
|
||||||
|
// 10,
|
||||||
|
// listOf("Notary1", "Notary2"),
|
||||||
|
// 100,
|
||||||
|
// 10,
|
||||||
|
// Instant.now(),
|
||||||
|
// 9,
|
||||||
|
// mapOf("A" to listOf(1, 2, 3), "B" to listOf (4, 5, 6)))).bytes)
|
||||||
|
|
||||||
|
val url = EvolvabilityTests::class.java.getResource(resource)
|
||||||
|
DeserializationInput(factory).deserialize(SerializedBytes<NetworkParametersExample>(url.readBytes()))
|
||||||
|
}
|
||||||
|
|
||||||
@Test(expected = NotSerializableException::class)
|
@Test(expected = NotSerializableException::class)
|
||||||
@Suppress("UNUSED")
|
@Suppress("UNUSED")
|
||||||
fun addMandatoryFieldWithAltConstructorUnAnnotated() {
|
fun addMandatoryFieldWithAltConstructorUnAnnotated() {
|
||||||
@ -487,7 +568,7 @@ class EvolvabilityTests {
|
|||||||
//
|
//
|
||||||
@Test
|
@Test
|
||||||
@Ignore("Test fails after moving NetworkParameters and NotaryInfo into core from node-api")
|
@Ignore("Test fails after moving NetworkParameters and NotaryInfo into core from node-api")
|
||||||
fun readBrokenNetworkParameters(){
|
fun readBrokenNetworkParameters() {
|
||||||
val sf = testDefaultFactory()
|
val sf = testDefaultFactory()
|
||||||
sf.register(net.corda.nodeapi.internal.serialization.amqp.custom.InstantSerializer(sf))
|
sf.register(net.corda.nodeapi.internal.serialization.amqp.custom.InstantSerializer(sf))
|
||||||
sf.register(net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer)
|
sf.register(net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer)
|
||||||
@ -524,7 +605,7 @@ class EvolvabilityTests {
|
|||||||
val resource = "networkParams.<corda version>.<commit sha>"
|
val resource = "networkParams.<corda version>.<commit sha>"
|
||||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||||
val networkParameters = NetworkParameters(
|
val networkParameters = NetworkParameters(
|
||||||
3, listOf(NotaryInfo(DUMMY_NOTARY, false)),1000, 1000, Instant.EPOCH, 1 , emptyMap())
|
3, listOf(NotaryInfo(DUMMY_NOTARY, false)), 1000, 1000, Instant.EPOCH, 1, emptyMap())
|
||||||
|
|
||||||
val sf = testDefaultFactory()
|
val sf = testDefaultFactory()
|
||||||
sf.register(net.corda.nodeapi.internal.serialization.amqp.custom.InstantSerializer(sf))
|
sf.register(net.corda.nodeapi.internal.serialization.amqp.custom.InstantSerializer(sf))
|
||||||
|
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user