CORDA-3745: Modify DJVM serializers to support Enum Evolution. (#6189)

This commit is contained in:
Chris Rankin 2020-04-30 14:59:10 +01:00 committed by GitHub
parent 2ce76e407d
commit 107819f5b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 240 additions and 57 deletions

View File

@ -4,6 +4,10 @@ import net.corda.core.internal.BasicVerifier
import net.corda.core.internal.Verifier
import net.corda.core.serialization.ConstructorForDeserialization
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.CordaSerializationTransformEnumDefault
import net.corda.core.serialization.CordaSerializationTransformEnumDefaults
import net.corda.core.serialization.CordaSerializationTransformRename
import net.corda.core.serialization.CordaSerializationTransformRenames
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.LedgerTransaction
@ -38,6 +42,10 @@ class DeterministicVerifierFactoryService(
whitelist = Whitelist.MINIMAL,
visibleAnnotations = setOf(
CordaSerializable::class.java,
CordaSerializationTransformEnumDefault::class.java,
CordaSerializationTransformEnumDefaults::class.java,
CordaSerializationTransformRename::class.java,
CordaSerializationTransformRenames::class.java,
ConstructorForDeserialization::class.java,
DeprecatedConstructorForDeserialization::class.java
),

View File

@ -98,7 +98,8 @@ class SandboxSerializerFactoryFactory(
localSerializerFactory = localSerializerFactory,
classLoader = classLoader,
mustPreserveDataWhenEvolving = context.preventDataLoss,
primitiveTypes = primitiveTypes
primitiveTypes = primitiveTypes,
baseTypes = localTypes
)
val remoteSerializerFactory = DefaultRemoteSerializerFactory(

View File

@ -61,8 +61,9 @@ fun createSandboxSerializationEnv(
@Suppress("unchecked_cast")
val isEnumPredicate = predicateFactory.apply(CheckEnum::class.java) as Predicate<Class<*>>
@Suppress("unchecked_cast")
val enumConstants = taskFactory.apply(DescribeEnum::class.java)
.andThen(taskFactory.apply(GetEnumNames::class.java))
val enumConstants = taskFactory.apply(DescribeEnum::class.java) as Function<Class<*>, Array<out Any>>
@Suppress("unchecked_cast")
val enumConstantNames = enumConstants.andThen(taskFactory.apply(GetEnumNames::class.java))
.andThen { (it as Array<out Any>).map(Any::toString) } as Function<Class<*>, List<String>>
val sandboxLocalTypes = BaseLocalTypes(
@ -72,7 +73,8 @@ fun createSandboxSerializationEnv(
mapClass = classLoader.toSandboxClass(Map::class.java),
stringClass = classLoader.toSandboxClass(String::class.java),
isEnum = isEnumPredicate,
enumConstants = enumConstants
enumConstants = enumConstants,
enumConstantNames = enumConstantNames
)
val schemeBuilder = SandboxSerializationSchemeBuilder(
classLoader = classLoader,

View File

@ -0,0 +1,155 @@
package net.corda.serialization.djvm
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.CordaSerializationTransformEnumDefault
import net.corda.core.serialization.CordaSerializationTransformEnumDefaults
import net.corda.core.serialization.CordaSerializationTransformRename
import net.corda.core.serialization.CordaSerializationTransformRenames
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.internal._contextSerializationEnv
import net.corda.core.serialization.serialize
import net.corda.serialization.djvm.EvolvedEnum.ONE
import net.corda.serialization.djvm.EvolvedEnum.TWO
import net.corda.serialization.djvm.EvolvedEnum.THREE
import net.corda.serialization.djvm.EvolvedEnum.FOUR
import net.corda.serialization.djvm.OriginalEnum.One
import net.corda.serialization.djvm.OriginalEnum.Two
import net.corda.serialization.djvm.SandboxType.KOTLIN
import net.corda.serialization.internal.amqp.CompositeType
import net.corda.serialization.internal.amqp.DeserializationInput
import net.corda.serialization.internal.amqp.RestrictedType
import net.corda.serialization.internal.amqp.Transform
import net.corda.serialization.internal.amqp.TransformTypes
import net.corda.serialization.internal.amqp.TypeNotation
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.fail
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.ArgumentsProvider
import org.junit.jupiter.params.provider.ArgumentsSource
import java.util.EnumMap
import java.util.function.Function
import java.util.stream.Stream
@ExtendWith(LocalSerialization::class)
class DeserializeEnumWithEvolutionTest : TestBase(KOTLIN) {
class EvolutionArgumentProvider : ArgumentsProvider {
override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> {
return Stream.of(
Arguments.of(ONE, One),
Arguments.of(TWO, Two),
Arguments.of(THREE, One),
Arguments.of(FOUR, Two)
)
}
}
private fun String.devolve() = replace("Evolved", "Original")
private fun devolveType(type: TypeNotation): TypeNotation {
return when (type) {
is CompositeType -> type.copy(
name = type.name.devolve(),
fields = type.fields.map { it.copy(type = it.type.devolve()) }
)
is RestrictedType -> type.copy(name = type.name.devolve())
else -> type
}
}
private fun SerializedBytes<*>.devolve(context: SerializationContext): SerializedBytes<Any> {
val envelope = DeserializationInput.getEnvelope(this, context.encodingWhitelist).apply {
val schemaTypes = schema.types.map(::devolveType)
with(schema.types as MutableList<TypeNotation>) {
clear()
addAll(schemaTypes)
}
val transforms = transformsSchema.types.asSequence().associateTo(LinkedHashMap()) {
it.key.devolve() to it.value
}
with(transformsSchema.types as MutableMap<String, EnumMap<TransformTypes, MutableList<Transform>>>) {
clear()
putAll(transforms)
}
}
return SerializedBytes(envelope.write())
}
@ParameterizedTest
@ArgumentsSource(EvolutionArgumentProvider::class)
fun `test deserialising evolved enum`(value: EvolvedEnum, expected: OriginalEnum) {
val context = (_contextSerializationEnv.get() ?: fail("No serialization environment!")).p2pContext
val evolvedData = value.serialize()
val originalData = evolvedData.devolve(context)
sandbox {
_contextSerializationEnv.set(createSandboxSerializationEnv(classLoader))
val sandboxOriginal = originalData.deserializeFor(classLoader)
assertEquals("sandbox." + OriginalEnum::class.java.name, sandboxOriginal::class.java.name)
assertEquals(expected.toString(), sandboxOriginal.toString())
}
}
@ParameterizedTest
@ArgumentsSource(EvolutionArgumentProvider::class)
fun `test deserialising data with evolved enum`(value: EvolvedEnum, expected: OriginalEnum) {
val context = (_contextSerializationEnv.get() ?: fail("No serialization environment!")).p2pContext
val evolvedData = EvolvedData(value).serialize()
val originalData = evolvedData.devolve(context)
sandbox {
_contextSerializationEnv.set(createSandboxSerializationEnv(classLoader))
val sandboxOriginal = originalData.deserializeFor(classLoader)
val taskFactory = classLoader.createRawTaskFactory()
val result = taskFactory.compose(classLoader.createSandboxFunction())
.apply(ShowOriginalData::class.java)
.apply(sandboxOriginal) ?: fail("Result cannot be null")
assertThat(result.toString())
.isEqualTo(ShowOriginalData().apply(OriginalData(expected)))
}
}
class ShowOriginalData : Function<OriginalData, String> {
override fun apply(input: OriginalData): String {
return with(input) {
"Name='${value.name}', Ordinal='${value.ordinal}'"
}
}
}
}
@CordaSerializable
enum class OriginalEnum {
One,
Two
}
@CordaSerializable
data class OriginalData(val value: OriginalEnum)
@CordaSerializable
@CordaSerializationTransformRenames(
CordaSerializationTransformRename(from = "One", to = "ONE"),
CordaSerializationTransformRename(from = "Two", to = "TWO")
)
@CordaSerializationTransformEnumDefaults(
CordaSerializationTransformEnumDefault(new = "THREE", old = "One"),
CordaSerializationTransformEnumDefault(new = "FOUR", old = "Two")
)
enum class EvolvedEnum {
ONE,
TWO,
THREE,
FOUR
}
@CordaSerializable
data class EvolvedData(val value: EvolvedEnum)

View File

@ -4,22 +4,14 @@ import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.internal._contextSerializationEnv
import net.corda.core.serialization.serialize
import net.corda.serialization.djvm.SandboxType.KOTLIN
import net.corda.serialization.internal.SectionId
import net.corda.serialization.internal.amqp.CompositeType
import net.corda.serialization.internal.amqp.DeserializationInput
import net.corda.serialization.internal.amqp.Envelope
import net.corda.serialization.internal.amqp.TypeNotation
import net.corda.serialization.internal.amqp.alsoAsByteBuffer
import net.corda.serialization.internal.amqp.amqpMagic
import net.corda.serialization.internal.amqp.withDescribed
import net.corda.serialization.internal.amqp.withList
import org.apache.qpid.proton.codec.Data
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.fail
import java.io.ByteArrayOutputStream
import java.util.function.Function
@ExtendWith(LocalSerialization::class)
@ -37,12 +29,8 @@ class SafeDeserialisationTest : TestBase(KOTLIN) {
val innocentData = innocent.serialize()
val envelope = DeserializationInput.getEnvelope(innocentData, context.encodingWhitelist).apply {
val innocentType = schema.types[0] as CompositeType
(schema.types as MutableList<TypeNotation>)[0] = CompositeType(
name = innocentType.name.replace("Innocent", "VeryEvil"),
label = innocentType.label,
provides = innocentType.provides,
descriptor = innocentType.descriptor,
fields = innocentType.fields
(schema.types as MutableList<TypeNotation>)[0] = innocentType.copy(
name = innocentType.name.replace("Innocent", "VeryEvil")
)
}
val evilData = SerializedBytes<Any>(envelope.write())
@ -68,23 +56,6 @@ class SafeDeserialisationTest : TestBase(KOTLIN) {
}
}
private fun Envelope.write(): ByteArray {
val data = Data.Factory.create()
data.withDescribed(Envelope.DESCRIPTOR_OBJECT) {
withList {
putObject(obj)
putObject(schema)
putObject(transformsSchema)
}
}
return ByteArrayOutputStream().use {
amqpMagic.writeTo(it)
SectionId.DATA_AND_STOP.writeTo(it)
it.alsoAsByteBuffer(data.encodedSize().toInt(), data::encode)
it.toByteArray()
}
}
class ShowInnocentData : Function<InnocentData, String> {
override fun apply(data: InnocentData): String {
return "${data::class.java.name}: ${data.message}, ${data.number}"

View File

@ -2,6 +2,10 @@ package net.corda.serialization.djvm
import net.corda.core.serialization.ConstructorForDeserialization
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.CordaSerializationTransformEnumDefault
import net.corda.core.serialization.CordaSerializationTransformEnumDefaults
import net.corda.core.serialization.CordaSerializationTransformRename
import net.corda.core.serialization.CordaSerializationTransformRenames
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import net.corda.djvm.SandboxConfiguration
import net.corda.djvm.SandboxRuntimeContext
@ -51,6 +55,10 @@ abstract class TestBase(type: SandboxType) {
whitelist = MINIMAL,
visibleAnnotations = setOf(
CordaSerializable::class.java,
CordaSerializationTransformEnumDefault::class.java,
CordaSerializationTransformEnumDefaults::class.java,
CordaSerializationTransformRename::class.java,
CordaSerializationTransformRenames::class.java,
ConstructorForDeserialization::class.java,
DeprecatedConstructorForDeserialization::class.java
),

View File

@ -0,0 +1,28 @@
@file:JvmName("TestHelpers")
package net.corda.serialization.djvm
import net.corda.serialization.internal.SectionId
import net.corda.serialization.internal.amqp.Envelope
import net.corda.serialization.internal.amqp.alsoAsByteBuffer
import net.corda.serialization.internal.amqp.amqpMagic
import net.corda.serialization.internal.amqp.withDescribed
import net.corda.serialization.internal.amqp.withList
import org.apache.qpid.proton.codec.Data
import java.io.ByteArrayOutputStream
fun Envelope.write(): ByteArray {
val data = Data.Factory.create()
data.withDescribed(Envelope.DESCRIPTOR_OBJECT) {
withList {
putObject(obj)
putObject(schema)
putObject(transformsSchema)
}
}
return ByteArrayOutputStream().use {
amqpMagic.writeTo(it)
SectionId.DATA_AND_STOP.writeTo(it)
it.alsoAsByteBuffer(data.encodedSize().toInt(), data::encode)
it.toByteArray()
}
}

View File

@ -1,6 +1,7 @@
package net.corda.serialization.internal.amqp
import net.corda.core.serialization.SerializationContext
import net.corda.serialization.internal.model.BaseLocalTypes
import org.apache.qpid.proton.codec.Data
import java.lang.UnsupportedOperationException
import java.lang.reflect.Type
@ -34,6 +35,7 @@ import java.lang.reflect.Type
class EnumEvolutionSerializer(
override val type: Type,
factory: LocalSerializerFactory,
private val baseLocalTypes: BaseLocalTypes,
private val conversions: Map<String, String>,
private val ordinals: Map<String, Int>) : AMQPSerializer<Any> {
override val typeDescriptor = factory.createDescriptor(type)
@ -46,7 +48,7 @@ class EnumEvolutionSerializer(
val converted = conversions[enumName] ?: throw AMQPNotSerializableException(type, "No rule to evolve enum constant $type::$enumName")
val ordinal = ordinals[converted] ?: throw AMQPNotSerializableException(type, "Ordinal not found for enum value $type::$converted")
return type.asClass().enumConstants[ordinal]
return baseLocalTypes.enumConstants.apply(type.asClass())[ordinal]
}
override fun writeClassInfo(output: SerializationOutput) {

View File

@ -40,7 +40,8 @@ class DefaultEvolutionSerializerFactory(
private val localSerializerFactory: LocalSerializerFactory,
private val classLoader: ClassLoader,
private val mustPreserveDataWhenEvolving: Boolean,
override val primitiveTypes: Map<Class<*>, Class<*>>
override val primitiveTypes: Map<Class<*>, Class<*>>,
private val baseTypes: BaseLocalTypes
): EvolutionSerializerFactory {
// Invert the "primitive -> boxed primitive" mapping.
private val primitiveBoxedTypes: Map<Class<*>, Class<*>>
@ -172,7 +173,7 @@ class DefaultEvolutionSerializerFactory(
if (constantsAreReordered(localOrdinals, convertedOrdinals)) throw EvolutionSerializationException(this,
"Constants have been reordered, additions must be appended to the end")
return EnumEvolutionSerializer(localTypeInformation.observedType, localSerializerFactory, conversions, localOrdinals)
return EnumEvolutionSerializer(localTypeInformation.observedType, localSerializerFactory, baseTypes, conversions, localOrdinals)
}
private fun constantsAreReordered(localOrdinals: Map<String, Int>, convertedOrdinals: Map<Int, String>): Boolean =

View File

@ -97,10 +97,8 @@ object SerializerFactoryBuilder {
mustPreserveDataWhenEvolving: Boolean): SerializerFactory {
val customSerializerRegistry = CachingCustomSerializerRegistry(descriptorBasedSerializerRegistry)
val localTypeModel = ConfigurableLocalTypeModel(
WhitelistBasedTypeModelConfiguration(
whitelist,
customSerializerRegistry))
val typeModelConfiguration = WhitelistBasedTypeModelConfiguration(whitelist, customSerializerRegistry)
val localTypeModel = ConfigurableLocalTypeModel(typeModelConfiguration)
val fingerPrinter = overrideFingerPrinter ?:
TypeModellingFingerPrinter(customSerializerRegistry)
@ -124,7 +122,8 @@ object SerializerFactoryBuilder {
localSerializerFactory,
classCarpenter.classloader,
mustPreserveDataWhenEvolving,
javaPrimitiveTypes
javaPrimitiveTypes,
typeModelConfiguration.baseTypes
) else NoEvolutionSerializerFactory
val remoteSerializerFactory = DefaultRemoteSerializerFactory(

View File

@ -46,7 +46,7 @@ abstract class Transform : DescribedType {
* descendants of this class
*/
override fun newInstance(obj: Any?): Transform {
val described = Transform.checkDescribed(obj) as List<*>
val described = checkDescribed(obj) as List<*>
return when (described[0]) {
EnumDefaultSchemaTransform.typeName -> EnumDefaultSchemaTransform.newInstance(described)
RenameSchemaTransform.typeName -> RenameSchemaTransform.newInstance(described)
@ -195,18 +195,24 @@ object TransformsAnnotationProcessor {
* Obtain all of the transforms applied for the given [Class].
*/
fun getTransformsSchema(type: Class<*>): TransformsMap {
val result = TransformsMap(TransformTypes::class.java)
// We only have transforms for enums at present.
if (!type.isEnum) return result
return when {
// This only detects Enum classes that are outside the DJVM sandbox.
type.isEnum -> getEnumTransformsSchema(type)
// We only have transforms for enums at present.
else -> TransformsMap(TransformTypes::class.java)
}
}
fun getEnumTransformsSchema(type: Class<*>): TransformsMap {
val result = TransformsMap(TransformTypes::class.java)
supportedTransforms.forEach { supportedTransform ->
val annotationContainer = type.getAnnotation(supportedTransform.type) ?: return@forEach
result.processAnnotations(
type,
supportedTransform.enum,
supportedTransform.getAnnotations(annotationContainer))
type,
supportedTransform.enum,
supportedTransform.getAnnotations(annotationContainer))
}
return result
}

View File

@ -61,7 +61,8 @@ private val DEFAULT_BASE_TYPES = BaseLocalTypes(
mapClass = Map::class.java,
stringClass = String::class.java,
isEnum = Predicate { clazz -> clazz.isEnum },
enumConstants = Function { clazz ->
enumConstants = Function { clazz -> clazz.enumConstants },
enumConstantNames = Function { clazz ->
(clazz as Class<out Enum<*>>).enumConstants.map(Enum<*>::name)
}
)

View File

@ -115,13 +115,13 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
baseTypes.mapClass.isAssignableFrom(type) -> AMap(type, typeIdentifier, Unknown, Unknown)
type === baseTypes.stringClass -> Atomic(type, typeIdentifier)
type.kotlin.javaPrimitiveType != null -> Atomic(type, typeIdentifier)
baseTypes.isEnum.test(type) -> baseTypes.enumConstants.apply(type).let { enumConstants ->
baseTypes.isEnum.test(type) -> baseTypes.enumConstantNames.apply(type).let { enumConstantNames ->
AnEnum(
type,
typeIdentifier,
enumConstants,
enumConstantNames,
buildInterfaceInformation(type),
getEnumTransforms(type, enumConstants)
getEnumTransforms(type, enumConstantNames)
)
}
type.kotlinObjectInstance != null -> Singleton(
@ -145,7 +145,7 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
private fun getEnumTransforms(type: Class<*>, enumConstants: List<String>): EnumTransforms {
try {
val constants = enumConstants.asSequence().mapIndexed { index, constant -> constant to index }.toMap()
return EnumTransforms.build(TransformsAnnotationProcessor.getTransformsSchema(type), constants)
return EnumTransforms.build(TransformsAnnotationProcessor.getEnumTransformsSchema(type), constants)
} catch (e: InvalidEnumTransformsException) {
throw NotSerializableDetailedException(type.name, e.message!!)
}

View File

@ -136,5 +136,6 @@ class BaseLocalTypes(
val mapClass: Class<*>,
val stringClass: Class<*>,
val isEnum: Predicate<Class<*>>,
val enumConstants: Function<Class<*>, List<String>>
val enumConstants: Function<Class<*>, Array<out Any>>,
val enumConstantNames: Function<Class<*>, List<String>>
)