diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectBuilder.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectBuilder.kt index 75f1242a61..373f96ca8f 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectBuilder.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectBuilder.kt @@ -79,7 +79,7 @@ interface ObjectBuilder { * Create an [ObjectBuilderProvider] for the given [LocalTypeInformation.Composable]. */ fun makeProvider(typeInformation: LocalTypeInformation.Composable): ObjectBuilderProvider = - makeProvider(typeInformation.typeIdentifier, typeInformation.constructor, typeInformation.properties) + makeProvider(typeInformation.typeIdentifier, typeInformation.constructor, typeInformation.properties, false) /** * Create an [ObjectBuilderProvider] for the given type, constructor and set of properties. @@ -90,15 +90,17 @@ interface ObjectBuilder { fun makeProvider( typeIdentifier: TypeIdentifier, constructor: LocalConstructorInformation, - properties: Map + properties: Map, + includeAllConstructorParameters: Boolean ): ObjectBuilderProvider = - if (constructor.hasParameters) makeConstructorBasedProvider(properties, typeIdentifier, constructor) + if (constructor.hasParameters) makeConstructorBasedProvider(properties, typeIdentifier, constructor, includeAllConstructorParameters) else makeSetterBasedProvider(properties, typeIdentifier, constructor) private fun makeConstructorBasedProvider( properties: Map, typeIdentifier: TypeIdentifier, - constructor: LocalConstructorInformation + constructor: LocalConstructorInformation, + includeAllConstructorParameters: Boolean ): ObjectBuilderProvider { requireForSer(properties.values.all { when (it) { @@ -119,6 +121,15 @@ interface ObjectBuilder { "but property $name is not constructor-paired" ) } + }.toMutableMap() + + if (includeAllConstructorParameters) { + // Add constructor parameters not in the list of properties + // so we can use them in object evolution + for ((parameterIndex, parameter) in constructor.parameters.withIndex()) { + // Only use the parameters not already matched to properties + constructorIndices.putIfAbsent(parameter.name, parameterIndex) + } } val propertySlots = constructorIndices.keys.mapIndexed { slot, name -> name to slot }.toMap() @@ -254,7 +265,7 @@ class EvolutionObjectBuilder( remoteTypeInformation: RemoteTypeInformation.Composable, mustPreserveData: Boolean ): () -> ObjectBuilder { - val localBuilderProvider = ObjectBuilder.makeProvider(typeIdentifier, constructor, localProperties) + val localBuilderProvider = ObjectBuilder.makeProvider(typeIdentifier, constructor, localProperties, true) val remotePropertyNames = remoteTypeInformation.properties.keys.sorted() val reroutedIndices = remotePropertyNames.map { propertyName -> diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolutionObjectBuilderRenamedPropertyTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolutionObjectBuilderRenamedPropertyTests.kt new file mode 100644 index 0000000000..0e7f795839 --- /dev/null +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolutionObjectBuilderRenamedPropertyTests.kt @@ -0,0 +1,90 @@ +package net.corda.serialization.internal.amqp + +import net.corda.core.contracts.BelongsToContract +import net.corda.core.contracts.Contract +import net.corda.core.contracts.ContractState +import net.corda.core.identity.AbstractParty +import net.corda.core.serialization.DeprecatedConstructorForDeserialization +import net.corda.core.serialization.SerializedBytes +import net.corda.core.transactions.LedgerTransaction +import net.corda.serialization.internal.amqp.testutils.deserialize +import net.corda.serialization.internal.amqp.testutils.serialize +import net.corda.serialization.internal.amqp.testutils.testDefaultFactory +import net.corda.serialization.internal.amqp.testutils.writeTestResource +import org.assertj.core.api.Assertions +import org.junit.Test + +class EvolutionObjectBuilderRenamedPropertyTests +{ + private val cordappVersionTestValue = 38854445 + private val dataTestValue = "d7af8af0-c10e-45bc-a5f7-92de432be0ef" + private val xTestValue = 7568055 + private val yTestValue = 4113687 + + class TemplateContract : Contract { + override fun verify(tx: LedgerTransaction) { } + } + + /** + * Step 1 + * + * This is the original class definition in object evolution. + */ +// @BelongsToContract(TemplateContract::class) +// data class TemplateState(val cordappVersion: Int, val data: String, val x : Int?, override val participants: List = listOf()) : ContractState + + /** + * Step 2 + * + * This is an intermediate class definition in object evolution. + * The y property has been added and a constructor copies the value of x into y. x is now set to null by the constructor. + */ +// @BelongsToContract(TemplateContract::class) +// data class TemplateState(val cordappVersion: Int, val data: String, val x : Int?, val y : String?, override val participants: List = listOf()) : ContractState { +// @DeprecatedConstructorForDeserialization(1) +// constructor(cordappVersion: Int, data : String, x : Int?, participants: List) +// : this(cordappVersion, data, null, x?.toString(), participants) +// } + + /** + * Step 3 + * + * This is the final class definition in object evolution. + * The x property has been removed but the constructor that copies values of x into y still exists. We expect previous versions of this + * object to pass the value of x to the constructor when deserialized. + */ + @BelongsToContract(TemplateContract::class) + data class TemplateState(val cordappVersion: Int, val data: String, val y : String?, override val participants: List = listOf()) : ContractState { + @DeprecatedConstructorForDeserialization(1) + constructor(cordappVersion: Int, data : String, x : Int?, participants: List) : this(cordappVersion, data, x?.toString(), participants) + } + + @Test(timeout=300_000) + fun `Step 1 to Step 3`() { + + // The next two commented lines are how the serialized data is generated. To regenerate the data, uncomment these along + // with the correct version of the class and rerun the test. This will generate a new file in the project resources. + +// val step1 = TemplateState(cordappVersionTestValue, dataTestValue, xTestValue) +// saveSerializedObject(step1) + + // serialization/src/test/resources/net/corda/serialization/internal/amqp/EvolutionObjectBuilderRenamedPropertyTests.Step1 + val bytes = this::class.java.getResource("EvolutionObjectBuilderRenamedPropertyTests.Step1").readBytes() + + val serializerFactory: SerializerFactory = testDefaultFactory() + val deserializedObject = DeserializationInput(serializerFactory) + .deserialize(SerializedBytes(bytes)) + + Assertions.assertThat(deserializedObject.cordappVersion).isEqualTo(cordappVersionTestValue) + Assertions.assertThat(deserializedObject.data).isEqualTo(dataTestValue) +// Assertions.assertThat(deserializedObject.x).isEqualTo(xTestValue) + Assertions.assertThat(deserializedObject.y).isEqualTo(xTestValue.toString()) + Assertions.assertThat(deserializedObject).isInstanceOf(TemplateState::class.java) + } + + /** + * Write serialized object to resources folder + */ + @Suppress("unused") + fun saveSerializedObject(obj : T) = writeTestResource(SerializationOutput(testDefaultFactory()).serialize(obj)) +} \ No newline at end of file diff --git a/serialization/src/test/resources/net/corda/serialization/internal/amqp/EvolutionObjectBuilderRenamedPropertyTests.Step1 b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EvolutionObjectBuilderRenamedPropertyTests.Step1 new file mode 100644 index 0000000000..c2a94d7f11 Binary files /dev/null and b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EvolutionObjectBuilderRenamedPropertyTests.Step1 differ