mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
CORDA-3716: Fix SandboxEnumSerializer to handle enums that override toString(). (#6159)
* CORDA-3716: Fix SandboxEnumSerializer to handle enums that override toString(). * Remove more uses of Enum.toString() from the Corda serializer. * Add test coverage for this case to standard enum serializer. * Increase maxWaitTimeout in IRSDemoTest to 150 seconds.
This commit is contained in:
parent
45b43f116d
commit
6f437b5b09
@ -48,7 +48,7 @@ class IRSDemoTest {
|
||||
private val rpcUsers = listOf(User("user", "password", setOf("ALL")))
|
||||
private val currentDate: LocalDate = LocalDate.now()
|
||||
private val futureDate: LocalDate = currentDate.plusMonths(6)
|
||||
private val maxWaitTime: Duration = 60.seconds
|
||||
private val maxWaitTime: Duration = 150.seconds
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `runs IRS demo`() {
|
||||
|
@ -0,0 +1,9 @@
|
||||
package net.corda.serialization.djvm.deserializers
|
||||
|
||||
import java.util.function.Function
|
||||
|
||||
class GetEnumNames : Function<Array<Enum<*>>, Array<String>> {
|
||||
override fun apply(enumValues: Array<Enum<*>>): Array<String> {
|
||||
return enumValues.map(Enum<*>::name).toTypedArray()
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ import net.corda.djvm.rewiring.createSandboxPredicate
|
||||
import net.corda.djvm.rewiring.SandboxClassLoader
|
||||
import net.corda.serialization.djvm.deserializers.CheckEnum
|
||||
import net.corda.serialization.djvm.deserializers.DescribeEnum
|
||||
import net.corda.serialization.djvm.deserializers.GetEnumNames
|
||||
import net.corda.serialization.djvm.serializers.PrimitiveSerializer
|
||||
import net.corda.serialization.internal.GlobalTransientClassWhiteList
|
||||
import net.corda.serialization.internal.SerializationContextImpl
|
||||
@ -60,7 +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) as Function<Class<*>, Array<out Any>>
|
||||
val enumConstants = taskFactory.apply(DescribeEnum::class.java)
|
||||
.andThen(taskFactory.apply(GetEnumNames::class.java))
|
||||
.andThen { (it as Array<out Any>).map(Any::toString) } as Function<Class<*>, List<String>>
|
||||
|
||||
val sandboxLocalTypes = BaseLocalTypes(
|
||||
collectionClass = classLoader.toSandboxClass(Collection::class.java),
|
||||
|
@ -4,6 +4,7 @@ import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.djvm.rewiring.SandboxClassLoader
|
||||
import net.corda.serialization.djvm.deserializers.CheckEnum
|
||||
import net.corda.serialization.djvm.deserializers.DescribeEnum
|
||||
import net.corda.serialization.djvm.deserializers.GetEnumNames
|
||||
import net.corda.serialization.djvm.toSandboxAnyClass
|
||||
import net.corda.serialization.internal.amqp.AMQPNotSerializableException
|
||||
import net.corda.serialization.internal.amqp.AMQPSerializer
|
||||
@ -32,6 +33,10 @@ class SandboxEnumSerializer(
|
||||
private val describeEnum: Function<Class<*>, Array<Any>>
|
||||
= taskFactory.apply(DescribeEnum::class.java) as Function<Class<*>, Array<Any>>
|
||||
@Suppress("unchecked_cast")
|
||||
private val getEnumNames: Function<Array<Any>, List<String>>
|
||||
= (taskFactory.apply(GetEnumNames::class.java) as Function<Array<Any>, Array<Any>>)
|
||||
.andThen { it.map(Any::toString) }
|
||||
@Suppress("unchecked_cast")
|
||||
private val isEnum: Predicate<Class<*>>
|
||||
= predicateFactory.apply(CheckEnum::class.java) as Predicate<Class<*>>
|
||||
|
||||
@ -46,7 +51,8 @@ class SandboxEnumSerializer(
|
||||
return null
|
||||
}
|
||||
val members = describeEnum.apply(declaredType)
|
||||
return ConcreteEnumSerializer(declaredType, members, localFactory)
|
||||
val memberNames = getEnumNames.apply(members)
|
||||
return ConcreteEnumSerializer(declaredType, members, memberNames, localFactory)
|
||||
}
|
||||
|
||||
override fun readObject(
|
||||
@ -65,6 +71,7 @@ class SandboxEnumSerializer(
|
||||
private class ConcreteEnumSerializer(
|
||||
declaredType: Class<*>,
|
||||
private val members: Array<Any>,
|
||||
private val memberNames: List<String>,
|
||||
factory: LocalSerializerFactory
|
||||
) : AMQPSerializer<Any> {
|
||||
override val type: Class<*> = declaredType
|
||||
@ -78,7 +85,7 @@ private class ConcreteEnumSerializer(
|
||||
LocalTypeInformation.AnEnum(
|
||||
declaredType,
|
||||
TypeIdentifier.forGenericType(declaredType),
|
||||
members.map(Any::toString),
|
||||
memberNames,
|
||||
emptyList(),
|
||||
EnumTransforms.empty
|
||||
)
|
||||
@ -92,7 +99,7 @@ private class ConcreteEnumSerializer(
|
||||
val enumOrd = obj[1] as Int
|
||||
val fromOrd = members[enumOrd]
|
||||
|
||||
if (enumName != fromOrd.toString()) {
|
||||
if (enumName != memberNames[enumOrd]) {
|
||||
throw AMQPNotSerializableException(
|
||||
type,
|
||||
"Deserializing obj as enum $type with value $enumName.$enumOrd but ordinality has changed"
|
||||
|
@ -0,0 +1,60 @@
|
||||
package net.corda.serialization.djvm
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.internal._contextSerializationEnv
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.serialization.djvm.SandboxType.KOTLIN
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.junit.jupiter.api.fail
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.EnumSource
|
||||
import java.util.function.Function
|
||||
|
||||
@ExtendWith(LocalSerialization::class)
|
||||
class DeserializeCustomisedEnumTest : TestBase(KOTLIN) {
|
||||
@ParameterizedTest
|
||||
@EnumSource(UserRole::class)
|
||||
fun `test deserialize enum with custom toString`(role: UserRole) {
|
||||
val userEnumData = UserEnumData(role)
|
||||
val data = userEnumData.serialize()
|
||||
|
||||
sandbox {
|
||||
_contextSerializationEnv.set(createSandboxSerializationEnv(classLoader))
|
||||
|
||||
val sandboxData = data.deserializeFor(classLoader)
|
||||
|
||||
val taskFactory = classLoader.createRawTaskFactory()
|
||||
val showUserEnumData = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowUserEnumData::class.java)
|
||||
val result = showUserEnumData.apply(sandboxData) ?: fail("Result cannot be null")
|
||||
|
||||
assertEquals(ShowUserEnumData().apply(userEnumData), result.toString())
|
||||
assertEquals("UserRole: name='${role.roleName}', ordinal='${role.ordinal}'", result.toString())
|
||||
assertEquals(SANDBOX_STRING, result::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
class ShowUserEnumData : Function<UserEnumData, String> {
|
||||
override fun apply(input: UserEnumData): String {
|
||||
return with(input) {
|
||||
"UserRole: name='${role.roleName}', ordinal='${role.ordinal}'"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Role {
|
||||
val roleName: String
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@CordaSerializable
|
||||
enum class UserRole(override val roleName: String) : Role {
|
||||
CONTROLLER(roleName = "Controller"),
|
||||
WORKER(roleName = "Worker");
|
||||
|
||||
override fun toString() = roleName
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
data class UserEnumData(val role: UserRole)
|
@ -38,13 +38,14 @@ class LocalTypeModelTest : TestBase(KOTLIN) {
|
||||
return classLoader.toSandboxClass(T::class.java)
|
||||
}
|
||||
|
||||
private inline fun <reified LOCAL: LocalTypeInformation> assertLocalType(type: Class<*>) {
|
||||
assertLocalType(LOCAL::class.java, type)
|
||||
private inline fun <reified LOCAL: LocalTypeInformation> assertLocalType(type: Class<*>): LOCAL {
|
||||
return assertLocalType(LOCAL::class.java, type) as LOCAL
|
||||
}
|
||||
|
||||
private fun <LOCAL: LocalTypeInformation> assertLocalType(localType: Class<LOCAL>, type: Class<*>) {
|
||||
private fun <LOCAL: LocalTypeInformation> assertLocalType(localType: Class<LOCAL>, type: Class<*>): LocalTypeInformation {
|
||||
val typeData = serializerFactory.getTypeInformation(type)
|
||||
assertThat(typeData).isInstanceOf(localType)
|
||||
return typeData
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -174,6 +175,14 @@ class LocalTypeModelTest : TestBase(KOTLIN) {
|
||||
assertLocalType<AnEnum>(sandbox<ExampleEnum>(classLoader))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCustomEnum() = sandbox {
|
||||
_contextSerializationEnv.set(createSandboxSerializationEnv(classLoader))
|
||||
val anEnum = assertLocalType<AnEnum>(sandbox<CustomEnum>(classLoader))
|
||||
assertThat(anEnum.members)
|
||||
.containsExactlyElementsOf(CustomEnum::class.java.enumConstants.map(CustomEnum::name))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEnumSet() = sandbox {
|
||||
_contextSerializationEnv.set(createSandboxSerializationEnv(classLoader))
|
||||
@ -188,4 +197,14 @@ class LocalTypeModelTest : TestBase(KOTLIN) {
|
||||
_contextSerializationEnv.set(createSandboxSerializationEnv(classLoader))
|
||||
assertLocalType<AMap>(sandbox<Map<*,*>>(classLoader))
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
enum class CustomEnum {
|
||||
ONE,
|
||||
TWO;
|
||||
|
||||
override fun toString(): String {
|
||||
return "[${name.toLowerCase()}]"
|
||||
}
|
||||
}
|
||||
}
|
@ -14,11 +14,12 @@ class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: Local
|
||||
override val typeDescriptor = factory.createDescriptor(type)
|
||||
|
||||
init {
|
||||
@Suppress("unchecked_cast")
|
||||
typeNotation = RestrictedType(
|
||||
AMQPTypeIdentifiers.nameForType(declaredType),
|
||||
null, emptyList(), "list", Descriptor(typeDescriptor),
|
||||
declaredClass.enumConstants.zip(IntRange(0, declaredClass.enumConstants.size)).map {
|
||||
Choice(it.first.toString(), it.second.toString())
|
||||
(declaredClass as Class<out Enum<*>>).enumConstants.zip(IntRange(0, declaredClass.enumConstants.size)).map {
|
||||
Choice(it.first.name, it.second.toString())
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -53,6 +53,7 @@ private val opaqueTypes = setOf(
|
||||
Symbol::class.java
|
||||
)
|
||||
|
||||
@Suppress("unchecked_cast")
|
||||
private val DEFAULT_BASE_TYPES = BaseLocalTypes(
|
||||
collectionClass = Collection::class.java,
|
||||
enumSetClass = EnumSet::class.java,
|
||||
@ -60,5 +61,7 @@ private val DEFAULT_BASE_TYPES = BaseLocalTypes(
|
||||
mapClass = Map::class.java,
|
||||
stringClass = String::class.java,
|
||||
isEnum = Predicate { clazz -> clazz.isEnum },
|
||||
enumConstants = Function { clazz -> clazz.enumConstants }
|
||||
enumConstants = Function { clazz ->
|
||||
(clazz as Class<out Enum<*>>).enumConstants.map(Enum<*>::name)
|
||||
}
|
||||
)
|
@ -119,7 +119,7 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
|
||||
AnEnum(
|
||||
type,
|
||||
typeIdentifier,
|
||||
enumConstants.map(Any::toString),
|
||||
enumConstants,
|
||||
buildInterfaceInformation(type),
|
||||
getEnumTransforms(type, enumConstants)
|
||||
)
|
||||
@ -142,9 +142,9 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEnumTransforms(type: Class<*>, enumConstants: Array<out Any>): EnumTransforms {
|
||||
private fun getEnumTransforms(type: Class<*>, enumConstants: List<String>): EnumTransforms {
|
||||
try {
|
||||
val constants = enumConstants.asSequence().mapIndexed { index, constant -> constant.toString() to index }.toMap()
|
||||
val constants = enumConstants.asSequence().mapIndexed { index, constant -> constant to index }.toMap()
|
||||
return EnumTransforms.build(TransformsAnnotationProcessor.getTransformsSchema(type), constants)
|
||||
} catch (e: InvalidEnumTransformsException) {
|
||||
throw NotSerializableDetailedException(type.name, e.message!!)
|
||||
|
@ -136,5 +136,5 @@ class BaseLocalTypes(
|
||||
val mapClass: Class<*>,
|
||||
val stringClass: Class<*>,
|
||||
val isEnum: Predicate<Class<*>>,
|
||||
val enumConstants: Function<Class<*>, Array<out Any>>
|
||||
val enumConstants: Function<Class<*>, List<String>>
|
||||
)
|
||||
|
@ -3,6 +3,8 @@ package net.corda.serialization.internal.amqp
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.serialization.internal.EmptyWhitelist
|
||||
import net.corda.serialization.internal.amqp.testutils.TestSerializationOutput
|
||||
import net.corda.serialization.internal.amqp.testutils.deserialize
|
||||
import net.corda.serialization.internal.amqp.testutils.deserializeAndReturnEnvelope
|
||||
@ -11,6 +13,7 @@ import net.corda.serialization.internal.amqp.testutils.testDefaultFactoryNoEvolu
|
||||
import net.corda.serialization.internal.amqp.testutils.testName
|
||||
import net.corda.serialization.internal.carpenter.ClassCarpenterImpl
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.Assert.assertNotSame
|
||||
import org.junit.Test
|
||||
import java.io.NotSerializableException
|
||||
import java.time.DayOfWeek
|
||||
@ -279,4 +282,32 @@ class EnumTests {
|
||||
DeserializationInput(factory2).deserialize(bytes)
|
||||
}.isInstanceOf(NotSerializableException::class.java)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun deserializeCustomisedEnum() {
|
||||
val input = CustomEnumWrapper(CustomEnum.ONE)
|
||||
val factory1 = SerializerFactoryBuilder.build(EmptyWhitelist, ClassLoader.getSystemClassLoader())
|
||||
val serialized = TestSerializationOutput(VERBOSE, factory1).serialize(input)
|
||||
|
||||
val factory2 = SerializerFactoryBuilder.build(EmptyWhitelist, ClassLoader.getSystemClassLoader())
|
||||
val output = DeserializationInput(factory2).deserialize(serialized)
|
||||
|
||||
assertEquals(input, output)
|
||||
assertNotSame("Deserialized object should be brand new.", input, output)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@CordaSerializable
|
||||
enum class CustomEnum {
|
||||
ONE,
|
||||
TWO,
|
||||
THREE;
|
||||
|
||||
override fun toString(): String {
|
||||
return "[${name.toLowerCase()}]"
|
||||
}
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
data class CustomEnumWrapper(val data: CustomEnum)
|
||||
}
|
@ -4,9 +4,11 @@ import com.google.common.reflect.TypeToken
|
||||
import net.corda.core.serialization.SerializableCalculatedProperty
|
||||
import net.corda.serialization.internal.AllWhitelist
|
||||
import net.corda.serialization.internal.amqp.*
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.fail
|
||||
import java.lang.reflect.Type
|
||||
import java.util.*
|
||||
|
||||
@ -206,6 +208,26 @@ class LocalTypeModelTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
enum class CustomEnum {
|
||||
ONE,
|
||||
TWO;
|
||||
|
||||
override fun toString(): String {
|
||||
return "[${name.toLowerCase()}]"
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `test type information for customised enum`() {
|
||||
modelWithoutOpacity.inspect(typeOf<CustomEnum>()).let { typeInformation ->
|
||||
val anEnum = typeInformation as? LocalTypeInformation.AnEnum ?: fail("Not AnEnum!")
|
||||
assertThat(anEnum.members).containsExactlyElementsOf(
|
||||
CustomEnum::class.java.enumConstants.map(CustomEnum::name)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T> assertInformation(expected: String) {
|
||||
assertEquals(expected.trimIndent(), model.inspect(typeOf<T>()).prettyPrint())
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user