CORDA-3536: Load DJVM serialization types more precisely to avoid runtime warnings. (#5896)

* Load DJVM serialization types more precisely to avoid runtime warnings.
* Remove unnecessary string concatenation from debug and trace log messages.
This commit is contained in:
Chris Rankin 2020-01-28 09:26:13 +00:00 committed by Christian Sailer
parent a4cada4a2e
commit c8a21cb8d2
18 changed files with 288 additions and 107 deletions

View File

@ -30,7 +30,7 @@ snakeYamlVersion=1.19
caffeineVersion=2.7.0
metricsVersion=4.1.0
metricsNewRelicVersion=1.1.1
djvmVersion=1.0-RC06
djvmVersion=1.0-RC07
deterministicRtVersion=1.0-RC02
openSourceBranch=https://github.com/corda/corda/blob/release/os/4.4
openSourceSamplesBranch=https://github.com/corda/samples/blob/release-V4

View File

@ -0,0 +1,9 @@
package net.corda.serialization.djvm.deserializers
import java.util.function.Predicate
class CheckEnum : Predicate<Class<*>> {
override fun test(clazz: Class<*>): Boolean {
return clazz.isEnum
}
}

View File

@ -52,11 +52,14 @@ import java.math.BigInteger
import java.util.Date
import java.util.UUID
import java.util.function.Function
import java.util.function.Predicate
class SandboxSerializationSchemeBuilder(
private val classLoader: SandboxClassLoader,
private val sandboxBasicInput: Function<in Any?, out Any?>,
private val rawTaskFactory: Function<in Any, out Function<in Any?, out Any?>>,
private val taskFactory: Function<Class<out Function<*, *>>, out Function<in Any?, out Any?>>,
private val predicateFactory: Function<Class<out Predicate<*>>, out Predicate<in Any?>>,
private val customSerializerClassNames: Set<String>,
private val serializationWhitelistNames: Set<String>,
private val serializerFactoryFactory: SerializerFactoryFactory
@ -66,7 +69,6 @@ class SandboxSerializationSchemeBuilder(
}
private fun getSerializerFactory(context: SerializationContext): SerializerFactory {
val taskFactory = rawTaskFactory.compose(classLoader.createSandboxFunction())
return serializerFactoryFactory.make(context).apply {
register(SandboxBitSetSerializer(classLoader, taskFactory, this))
register(SandboxCertPathSerializer(classLoader, taskFactory, this))
@ -100,7 +102,7 @@ class SandboxSerializationSchemeBuilder(
register(SandboxCharacterSerializer(classLoader, sandboxBasicInput))
register(SandboxCollectionSerializer(classLoader, taskFactory, this))
register(SandboxMapSerializer(classLoader, taskFactory, this))
register(SandboxEnumSerializer(classLoader, taskFactory, this))
register(SandboxEnumSerializer(classLoader, taskFactory, predicateFactory, this))
register(SandboxPublicKeySerializer(classLoader, taskFactory))
register(SandboxToStringSerializer(BigDecimal::class.java, classLoader, rawTaskFactory, sandboxBasicInput))
register(SandboxToStringSerializer(BigInteger::class.java, classLoader, rawTaskFactory, sandboxBasicInput))

View File

@ -14,9 +14,11 @@ import net.corda.serialization.internal.amqp.SerializerFactory
import net.corda.serialization.internal.amqp.SerializerFactoryFactory
import net.corda.serialization.internal.amqp.WhitelistBasedTypeModelConfiguration
import net.corda.serialization.internal.amqp.createClassCarpenter
import net.corda.serialization.internal.model.BaseLocalTypes
import net.corda.serialization.internal.model.ClassCarpentingTypeLoader
import net.corda.serialization.internal.model.ConfigurableLocalTypeModel
import net.corda.serialization.internal.model.SchemaBuildingRemoteTypeCarpenter
import net.corda.serialization.internal.amqp.SerializerFactoryBuilder
import net.corda.serialization.internal.model.TypeLoader
import net.corda.serialization.internal.model.TypeModellingFingerPrinter
import java.lang.Boolean
@ -36,7 +38,8 @@ import java.util.function.Predicate
* This has all been lovingly copied from [SerializerFactoryBuilder].
*/
class SandboxSerializerFactoryFactory(
private val primitiveSerializerFactory: Function<Class<*>, AMQPSerializer<Any>>
private val primitiveSerializerFactory: Function<Class<*>, AMQPSerializer<Any>>,
private val localTypes: BaseLocalTypes
) : SerializerFactoryFactory {
override fun make(context: SerializationContext): SerializerFactory {
@ -65,7 +68,11 @@ class SandboxSerializerFactoryFactory(
)
val localTypeModel = ConfigurableLocalTypeModel(
WhitelistBasedTypeModelConfiguration(context.whitelist, customSerializerRegistry)
WhitelistBasedTypeModelConfiguration(
whitelist = context.whitelist,
customSerializerRegistry = customSerializerRegistry,
baseTypes = localTypes
)
)
val fingerPrinter = TypeModellingFingerPrinter(customSerializerRegistry)

View File

@ -7,14 +7,21 @@ import net.corda.core.serialization.SerializationFactory
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.internal.SerializationEnvironment
import net.corda.core.utilities.ByteSequence
import net.corda.djvm.rewiring.createRawPredicateFactory
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.serializers.PrimitiveSerializer
import net.corda.serialization.internal.GlobalTransientClassWhiteList
import net.corda.serialization.internal.SerializationContextImpl
import net.corda.serialization.internal.SerializationFactoryImpl
import net.corda.serialization.internal.amqp.AMQPSerializer
import net.corda.serialization.internal.amqp.amqpMagic
import net.corda.serialization.internal.model.BaseLocalTypes
import java.util.EnumSet
import java.util.function.Function
import java.util.function.Predicate
@Suppress("NOTHING_TO_INLINE")
inline fun SandboxClassLoader.toSandboxAnyClass(clazz: Class<*>): Class<Any> {
@ -42,20 +49,40 @@ fun createSandboxSerializationEnv(
encoding = null
)
val rawTaskFactory = classLoader.createRawTaskFactory()
val sandboxBasicInput = classLoader.createBasicInput()
val rawTaskFactory = classLoader.createRawTaskFactory()
val taskFactory = rawTaskFactory.compose(classLoader.createSandboxFunction())
val predicateFactory = classLoader.createRawPredicateFactory().compose(classLoader.createSandboxPredicate())
val primitiveSerializerFactory: Function<Class<*>, AMQPSerializer<Any>> = Function { clazz ->
PrimitiveSerializer(clazz, sandboxBasicInput)
}
@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 sandboxLocalTypes = BaseLocalTypes(
collectionClass = classLoader.toSandboxClass(Collection::class.java),
enumSetClass = classLoader.toSandboxClass(EnumSet::class.java),
exceptionClass = classLoader.toSandboxClass(Exception::class.java),
mapClass = classLoader.toSandboxClass(Map::class.java),
stringClass = classLoader.toSandboxClass(String::class.java),
isEnum = isEnumPredicate,
enumConstants = enumConstants
)
val schemeBuilder = SandboxSerializationSchemeBuilder(
classLoader = classLoader,
sandboxBasicInput = sandboxBasicInput,
rawTaskFactory = rawTaskFactory,
taskFactory = taskFactory,
predicateFactory = predicateFactory,
customSerializerClassNames = customSerializerClassNames,
serializationWhitelistNames = serializationWhitelistNames,
serializerFactoryFactory = SandboxSerializerFactoryFactory(primitiveSerializerFactory)
serializerFactoryFactory = SandboxSerializerFactoryFactory(
primitiveSerializerFactory = primitiveSerializerFactory,
localTypes = sandboxLocalTypes
)
)
val factory = SerializationFactoryImpl(mutableMapOf()).apply {
registerScheme(schemeBuilder.buildFor(p2pContext))

View File

@ -35,9 +35,9 @@ class SandboxCollectionSerializer(
private val unsupportedTypes: Set<Class<Any>> = listOf(
EnumSet::class.java
).map {
).mapTo(LinkedHashSet()) {
classLoader.toSandboxAnyClass(it)
}.toSet()
}
// The order matters here - the first match should be the most specific one.
// Kotlin preserves the ordering for us by associating into a LinkedHashMap.
@ -95,9 +95,9 @@ private class ConcreteCollectionSerializer(
override val typeDescriptor: Symbol by lazy {
factory.createDescriptor(
LocalTypeInformation.ACollection(
observedType = declaredType.rawType,
observedType = declaredType,
typeIdentifier = TypeIdentifier.forGenericType(declaredType),
elementType =factory.getTypeInformation(declaredType.actualTypeArguments[0])
elementType = factory.getTypeInformation(declaredType.actualTypeArguments[0])
)
)
}
@ -109,7 +109,7 @@ private class ConcreteCollectionSerializer(
context: SerializationContext
): Any {
val inboundType = type.actualTypeArguments[0]
return ifThrowsAppend({ type.typeName }) {
return ifThrowsAppend(type::getTypeName) {
val args = (obj as List<*>).map {
input.readObjectOrNull(redescribe(it, inboundType), schemas, inboundType, context)
}.toTypedArray()

View File

@ -2,6 +2,7 @@ package net.corda.serialization.djvm.serializers
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.toSandboxAnyClass
import net.corda.serialization.internal.amqp.AMQPNotSerializableException
@ -19,23 +20,32 @@ import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type
import java.util.function.Function
import java.util.function.Predicate
class SandboxEnumSerializer(
classLoader: SandboxClassLoader,
taskFactory: Function<Class<out Function<*, *>>, out Function<in Any?, out Any?>>,
predicateFactory: Function<Class<out Predicate<*>>, out Predicate<in Any?>>,
private val localFactory: LocalSerializerFactory
) : CustomSerializer.Implements<Any>(clazz = classLoader.toSandboxAnyClass(Enum::class.java)) {
@Suppress("unchecked_cast")
private val describer: Function<Class<*>, Array<Any>>
private val describeEnum: Function<Class<*>, Array<Any>>
= taskFactory.apply(DescribeEnum::class.java) as Function<Class<*>, Array<Any>>
@Suppress("unchecked_cast")
private val isEnum: Predicate<Class<*>>
= predicateFactory.apply(CheckEnum::class.java) as Predicate<Class<*>>
override val schemaForDocumentation: Schema = Schema(emptyList())
override fun isSerializerFor(clazz: Class<*>): Boolean {
return super.isSerializerFor(clazz) && isEnum.test(clazz)
}
override fun specialiseFor(declaredType: Type): AMQPSerializer<Any>? {
if (declaredType !is Class<*>) {
return null
}
val members = describer.apply(declaredType)
val members = describeEnum.apply(declaredType)
return ConcreteEnumSerializer(declaredType, members, localFactory)
}
@ -68,7 +78,7 @@ private class ConcreteEnumSerializer(
LocalTypeInformation.AnEnum(
declaredType,
TypeIdentifier.forGenericType(declaredType),
members.map { it.toString() },
members.map(Any::toString),
emptyList(),
EnumTransforms.empty
)

View File

@ -85,9 +85,9 @@ private class ConcreteMapSerializer(
override val typeDescriptor: Symbol by lazy {
factory.createDescriptor(
LocalTypeInformation.AMap(
observedType = declaredType.rawType,
observedType = declaredType,
typeIdentifier = TypeIdentifier.forGenericType(declaredType),
keyType =factory.getTypeInformation(declaredType.actualTypeArguments[0]),
keyType = factory.getTypeInformation(declaredType.actualTypeArguments[0]),
valueType = factory.getTypeInformation(declaredType.actualTypeArguments[1])
)
)
@ -101,7 +101,7 @@ private class ConcreteMapSerializer(
): Any {
val inboundKeyType = type.actualTypeArguments[0]
val inboundValueType = type.actualTypeArguments[1]
return ifThrowsAppend({ type.typeName }) {
return ifThrowsAppend(type::getTypeName) {
val entries = (obj as Map<*, *>).map {
arrayOf(
input.readObjectOrNull(redescribe(it.key, inboundKeyType), schemas, inboundKeyType, context),

View File

@ -7,6 +7,10 @@ import net.corda.serialization.djvm.SandboxType.KOTLIN
import net.corda.serialization.internal.SerializationFactoryImpl
import net.corda.serialization.internal.amqp.SerializerFactory
import net.corda.serialization.internal.model.LocalTypeInformation
import net.corda.serialization.internal.model.LocalTypeInformation.ACollection
import net.corda.serialization.internal.model.LocalTypeInformation.AnEnum
import net.corda.serialization.internal.model.LocalTypeInformation.AMap
import net.corda.serialization.internal.model.LocalTypeInformation.Abstract
import net.corda.serialization.internal.model.LocalTypeInformation.Atomic
import net.corda.serialization.internal.model.LocalTypeInformation.Opaque
import org.apache.qpid.proton.amqp.Decimal128
@ -19,11 +23,10 @@ import org.apache.qpid.proton.amqp.UnsignedLong
import org.apache.qpid.proton.amqp.UnsignedShort
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import java.util.Date
import java.util.EnumSet
import java.util.UUID
@ExtendWith(LocalSerialization::class)
class LocalTypeModelTest : TestBase(KOTLIN) {
private val serializerFactory: SerializerFactory get() {
val factory = SerializationFactory.defaultFactory as SerializationFactoryImpl
@ -47,7 +50,7 @@ class LocalTypeModelTest : TestBase(KOTLIN) {
@Test
fun testString() = sandbox {
_contextSerializationEnv.set(createSandboxSerializationEnv(classLoader))
assertLocalType<Opaque>(sandbox<String>(classLoader))
assertLocalType<Atomic>(sandbox<String>(classLoader))
}
@Test
@ -158,4 +161,31 @@ class LocalTypeModelTest : TestBase(KOTLIN) {
_contextSerializationEnv.set(createSandboxSerializationEnv(classLoader))
assertLocalType<Opaque>(sandbox<Date>(classLoader))
}
@Test
fun testCollection() = sandbox {
_contextSerializationEnv.set(createSandboxSerializationEnv(classLoader))
assertLocalType<ACollection>(sandbox<Collection<*>>(classLoader))
}
@Test
fun testEnum() = sandbox {
_contextSerializationEnv.set(createSandboxSerializationEnv(classLoader))
assertLocalType<AnEnum>(sandbox<ExampleEnum>(classLoader))
}
@Test
fun testEnumSet() = sandbox {
_contextSerializationEnv.set(createSandboxSerializationEnv(classLoader))
assertLocalType<Abstract>(sandbox<EnumSet<*>>(classLoader))
val exampleEnumSet = EnumSet.noneOf(ExampleEnum::class.java)
assertLocalType<Opaque>(classLoader.toSandboxClass(exampleEnumSet::class.java))
}
@Test
fun testMap() = sandbox {
_contextSerializationEnv.set(createSandboxSerializationEnv(classLoader))
assertLocalType<AMap>(sandbox<Map<*,*>>(classLoader))
}
}

View File

@ -71,27 +71,43 @@ abstract class TestBase(type: SandboxType) {
SandboxType.JAVA -> TESTING_LIBRARIES.filter { isDirectory(it) }
}
fun sandbox(action: SandboxRuntimeContext.() -> Unit) {
return sandbox(WARNING, emptySet(), emptySet(), action)
inline fun sandbox(crossinline action: SandboxRuntimeContext.() -> Unit) {
sandbox(Consumer { ctx -> action(ctx) })
}
fun sandbox(visibleAnnotations: Set<Class<out Annotation>>, action: SandboxRuntimeContext.() -> Unit) {
return sandbox(WARNING, visibleAnnotations, emptySet(), action)
fun sandbox(action: Consumer<SandboxRuntimeContext>) {
sandbox(WARNING, emptySet(), emptySet(), action)
}
inline fun sandbox(visibleAnnotations: Set<Class<out Annotation>>, crossinline action: SandboxRuntimeContext.() -> Unit) {
sandbox(visibleAnnotations, Consumer { ctx -> action(ctx) })
}
fun sandbox(visibleAnnotations: Set<Class<out Annotation>>, action: Consumer<SandboxRuntimeContext>) {
sandbox(WARNING, visibleAnnotations, emptySet(), action)
}
inline fun sandbox(
visibleAnnotations: Set<Class<out Annotation>>,
sandboxOnlyAnnotations: Set<String>,
crossinline action: SandboxRuntimeContext.() -> Unit
) {
sandbox(visibleAnnotations, sandboxOnlyAnnotations, Consumer { ctx -> action(ctx) })
}
fun sandbox(
visibleAnnotations: Set<Class<out Annotation>>,
sandboxOnlyAnnotations: Set<String>,
action: SandboxRuntimeContext.() -> Unit
action: Consumer<SandboxRuntimeContext>
) {
return sandbox(WARNING, visibleAnnotations, sandboxOnlyAnnotations, action)
sandbox(WARNING, visibleAnnotations, sandboxOnlyAnnotations, action)
}
fun sandbox(
minimumSeverityLevel: Severity,
visibleAnnotations: Set<Class<out Annotation>>,
sandboxOnlyAnnotations: Set<String>,
action: SandboxRuntimeContext.() -> Unit
action: Consumer<SandboxRuntimeContext>
) {
var thrownException: Throwable? = null
thread(start = false) {
@ -100,9 +116,7 @@ abstract class TestBase(type: SandboxType) {
it.setMinimumSeverityLevel(minimumSeverityLevel)
it.setSandboxOnlyAnnotations(sandboxOnlyAnnotations)
it.setVisibleAnnotations(visibleAnnotations)
})).use(Consumer { ctx ->
ctx.action()
})
})).use(action)
}
}.apply {
uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { _, ex ->

View File

@ -4,6 +4,8 @@ import net.corda.core.CordaThrowable
import net.corda.core.internal.uncheckedCast
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.core.utilities.trace
import net.corda.serialization.internal.model.DefaultCacheProvider
import net.corda.serialization.internal.model.TypeIdentifier
import java.lang.reflect.Type
@ -93,10 +95,10 @@ class CachingCustomSerializerRegistry(
* that expects to find getters and a constructor with a parameter for each property.
*/
override fun register(customSerializer: CustomSerializer<out Any>) {
logger.trace("action=\"Registering custom serializer\", class=\"${customSerializer.type}\"")
logger.trace { "action=\"Registering custom serializer\", class=\"${customSerializer.type}\"" }
if (customSerializersCache.isNotEmpty()) {
logger.warn("Attempting to register custom serializer $customSerializer.type} in an active cache." +
logger.warn("Attempting to register custom serializer ${customSerializer.type} in an active cache." +
"All serializers should be registered before the cache comes into use.")
}
@ -119,7 +121,7 @@ class CachingCustomSerializerRegistry(
}
override fun registerExternal(customSerializer: CorDappCustomSerializer) {
logger.trace("action=\"Registering external serializer\", class=\"${customSerializer.type}\"")
logger.trace { "action=\"Registering external serializer\", class=\"${customSerializer.type}\"" }
if (customSerializersCache.isNotEmpty()) {
logger.warn("Attempting to register custom serializer ${customSerializer.type} in an active cache." +
@ -153,8 +155,7 @@ class CachingCustomSerializerRegistry(
(declaredSuperClass == null
|| !customSerializer.isSerializerFor(declaredSuperClass)
|| !customSerializer.revealSubclassesInSchema) -> {
logger.debug("action=\"Using custom serializer\", class=${clazz.typeName}, " +
"declaredType=${declaredType.typeName}")
logger.debug { "action=\"Using custom serializer\", class=${clazz.typeName}, declaredType=${declaredType.typeName}" }
@Suppress("UNCHECKED_CAST")
customSerializer as? AMQPSerializer<Any>

View File

@ -7,6 +7,7 @@ import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializedBytes
import net.corda.core.utilities.ByteSequence
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.trace
import net.corda.serialization.internal.*
import net.corda.serialization.internal.model.TypeIdentifier
import org.apache.qpid.proton.amqp.Binary
@ -119,7 +120,7 @@ class DeserializationInput constructor(
des {
val envelope = getEnvelope(bytes, context.encodingWhitelist)
logger.trace("deserialize blob scheme=\"${envelope.schema}\"")
logger.trace { "deserialize blob scheme=\"${envelope.schema}\"" }
doReadObject(envelope, clazz, context)
}

View File

@ -3,8 +3,10 @@ package net.corda.serialization.internal.amqp
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.internal.MissingSerializerException
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.trace
import net.corda.serialization.internal.model.*
import java.io.NotSerializableException
import java.util.Collections.singletonList
/**
* A factory that knows how to create serializers to deserialize values sent to us by remote parties.
@ -65,7 +67,7 @@ class DefaultRemoteSerializerFactory(
): AMQPSerializer<Any> =
// If we have seen this descriptor before, we assume we have seen everything in this schema before.
descriptorBasedSerializerRegistry.getOrBuild(typeDescriptor) {
logger.trace("get Serializer descriptor=$typeDescriptor")
logger.trace { "get Serializer descriptor=$typeDescriptor" }
// Interpret all of the types in the schema into RemoteTypeInformation, and reflect that into LocalTypeInformation.
val remoteTypeInformationMap = remoteTypeModel.interpret(schema)
@ -75,7 +77,7 @@ class DefaultRemoteSerializerFactory(
// This will save us having to re-interpret the entire schema on re-entry when deserialising individual property values.
val serializers = reflected.mapValues { (descriptor, remoteLocalPair) ->
descriptorBasedSerializerRegistry.getOrBuild(descriptor) {
getUncached(remoteLocalPair.remoteTypeInformation, remoteLocalPair.localTypeInformation)
getUncached(remoteLocalPair.remoteTypeInformation, remoteLocalPair.localTypeInformation, context)
}
}
@ -88,7 +90,8 @@ class DefaultRemoteSerializerFactory(
private fun getUncached(
remoteTypeInformation: RemoteTypeInformation,
localTypeInformation: LocalTypeInformation
localTypeInformation: LocalTypeInformation,
context: SerializationContext
): AMQPSerializer<Any> {
val remoteDescriptor = remoteTypeInformation.typeDescriptor
@ -109,6 +112,13 @@ class DefaultRemoteSerializerFactory(
evolutionSerializerFactory.getEvolutionSerializer(remoteTypeInformation, localTypeInformation)
?: localSerializer
// The type descriptors are never going to match when we deserialise into
// the DJVM's sandbox, but we don't want the node logs to fill up with
// Big 'n Scary warnings either. Assume that the local serializer is fine
// provided the local type is the same one we expect when loading the
// remote class.
remoteTypeInformation.isCompatibleWith(localTypeInformation, context) -> localSerializer
// Descriptors don't match, and something is probably broken, but we let the framework do what it can with the local
// serialiser (BlobInspectorTest uniquely breaks if we throw an exception here, and passes if we just warn and continue).
else -> {
@ -134,7 +144,7 @@ ${localTypeInformation.prettyPrint(false)}
}
return remoteInformation.mapValues { (_, remoteInformation) ->
RemoteAndLocalTypeInformation(remoteInformation, localInformationByIdentifier[remoteInformation.typeIdentifier]!!)
RemoteAndLocalTypeInformation(remoteInformation, localInformationByIdentifier.getValue(remoteInformation.typeIdentifier))
}
}
@ -145,7 +155,16 @@ ${localTypeInformation.prettyPrint(false)}
}
private fun RemoteTypeInformation.isDeserialisableWithoutEvolutionTo(localTypeInformation: LocalTypeInformation) =
this is RemoteTypeInformation.Parameterised &&
this is RemoteTypeInformation.Parameterised &&
(localTypeInformation is LocalTypeInformation.ACollection ||
localTypeInformation is LocalTypeInformation.AMap)
private fun RemoteTypeInformation.isCompatibleWith(
localTypeInformation: LocalTypeInformation,
context: SerializationContext
): Boolean {
val localTypes = typeLoader.load(singletonList(this), context)
return localTypes.size == 1
&& localTypeInformation.observedType == localTypes.values.first()
}
}

View File

@ -3,9 +3,6 @@ package net.corda.serialization.internal.amqp
import net.corda.core.KeepForDJVM
import net.corda.core.serialization.CordaSerializationTransformEnumDefault
import net.corda.core.serialization.CordaSerializationTransformRename
import net.corda.serialization.internal.NotSerializableDetailedException
import net.corda.serialization.internal.model.EnumTransforms
import net.corda.serialization.internal.model.InvalidEnumTransformsException
import net.corda.serialization.internal.model.LocalTypeInformation
import org.apache.qpid.proton.amqp.DescribedType
import org.apache.qpid.proton.codec.DescribedTypeConstructor

View File

@ -2,18 +2,27 @@ package net.corda.serialization.internal.amqp
import com.google.common.primitives.Primitives
import net.corda.core.serialization.ClassWhitelist
import net.corda.serialization.internal.model.BaseLocalTypes
import net.corda.serialization.internal.model.LocalTypeModelConfiguration
import org.apache.qpid.proton.amqp.*
import java.lang.reflect.Type
import java.util.*
import java.util.Date
import java.util.EnumSet
import java.util.UUID
import java.util.function.Function
import java.util.function.Predicate
/**
* [LocalTypeModelConfiguration] based on a [ClassWhitelist]
*/
class WhitelistBasedTypeModelConfiguration(
private val whitelist: ClassWhitelist,
private val customSerializerRegistry: CustomSerializerRegistry)
: LocalTypeModelConfiguration {
private val customSerializerRegistry: CustomSerializerRegistry,
override val baseTypes: BaseLocalTypes
) : LocalTypeModelConfiguration {
constructor(whitelist: ClassWhitelist, customSerializerRegistry: CustomSerializerRegistry)
: this(whitelist, customSerializerRegistry, DEFAULT_BASE_TYPES)
override fun isExcluded(type: Type): Boolean = whitelist.isNotWhitelisted(type.asClass())
override fun isOpaque(type: Type): Boolean = Primitives.unwrap(type.asClass()) in opaqueTypes ||
customSerializerRegistry.findCustomSerializer(type.asClass(), type) != null
@ -42,4 +51,14 @@ private val opaqueTypes = setOf(
ByteArray::class.java,
String::class.java,
Symbol::class.java
)
private val DEFAULT_BASE_TYPES = BaseLocalTypes(
collectionClass = Collection::class.java,
enumSetClass = EnumSet::class.java,
exceptionClass = Exception::class.java,
mapClass = Map::class.java,
stringClass = String::class.java,
isEnum = Predicate { clazz -> clazz.isEnum },
enumConstants = Function { clazz -> clazz.enumConstants }
)

View File

@ -7,11 +7,24 @@ import net.corda.core.serialization.ConstructorForDeserialization
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import net.corda.serialization.internal.NotSerializableDetailedException
import net.corda.serialization.internal.amqp.*
import net.corda.serialization.internal.model.LocalTypeInformation.Abstract
import net.corda.serialization.internal.model.LocalTypeInformation.AnArray
import net.corda.serialization.internal.model.LocalTypeInformation.AnEnum
import net.corda.serialization.internal.model.LocalTypeInformation.AnInterface
import net.corda.serialization.internal.model.LocalTypeInformation.Atomic
import net.corda.serialization.internal.model.LocalTypeInformation.ACollection
import net.corda.serialization.internal.model.LocalTypeInformation.AMap
import net.corda.serialization.internal.model.LocalTypeInformation.Composable
import net.corda.serialization.internal.model.LocalTypeInformation.Cycle
import net.corda.serialization.internal.model.LocalTypeInformation.NonComposable
import net.corda.serialization.internal.model.LocalTypeInformation.Opaque
import net.corda.serialization.internal.model.LocalTypeInformation.Singleton
import net.corda.serialization.internal.model.LocalTypeInformation.Top
import net.corda.serialization.internal.model.LocalTypeInformation.Unknown
import java.io.NotSerializableException
import java.lang.reflect.Method
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import java.util.*
import kotlin.collections.LinkedHashMap
import kotlin.reflect.KFunction
import kotlin.reflect.full.findAnnotation
@ -35,8 +48,10 @@ import kotlin.reflect.jvm.javaType
internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
var resolutionContext: Type? = null,
var visited: Set<TypeIdentifier> = emptySet(),
val cycles: MutableList<LocalTypeInformation.Cycle> = mutableListOf(),
val cycles: MutableList<Cycle> = mutableListOf(),
var validateProperties: Boolean = true) {
private val baseTypes = lookup.baseTypes
/**
* If we are examining the type of a read-only property, or a type flagged as [Opaque], then we do not need to warn
* if the [LocalTypeInformation] for that type (or any of its related types) is [LocalTypeInformation.NonComposable].
@ -55,7 +70,7 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
* Recursively build [LocalTypeInformation] for the given [Type] and [TypeIdentifier]
*/
fun build(type: Type, typeIdentifier: TypeIdentifier): LocalTypeInformation =
if (typeIdentifier in visited) LocalTypeInformation.Cycle(type, typeIdentifier).apply { cycles.add(this) }
if (typeIdentifier in visited) Cycle(type, typeIdentifier).apply { cycles.add(this) }
else lookup.findOrBuild(type, typeIdentifier) { isOpaque ->
val previous = visited
try {
@ -78,15 +93,16 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
private fun buildIfNotFound(type: Type, typeIdentifier: TypeIdentifier, isOpaque: Boolean): LocalTypeInformation {
val rawType = type.asClass()
return when (typeIdentifier) {
is TypeIdentifier.TopType -> LocalTypeInformation.Top
is TypeIdentifier.UnknownType -> LocalTypeInformation.Unknown
is TypeIdentifier.TopType -> Top
is TypeIdentifier.UnknownType -> Unknown
is TypeIdentifier.Unparameterised,
is TypeIdentifier.Erased -> buildForClass(rawType, typeIdentifier, isOpaque)
is TypeIdentifier.ArrayOf -> {
LocalTypeInformation.AnArray(
type,
typeIdentifier,
resolveAndBuild(type.componentType()))
AnArray(
type,
typeIdentifier,
resolveAndBuild(type.componentType())
)
}
is TypeIdentifier.Parameterised -> buildForParameterised(rawType, type as ParameterizedType, typeIdentifier, isOpaque)
}
@ -94,38 +110,41 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
private fun buildForClass(type: Class<*>, typeIdentifier: TypeIdentifier, isOpaque: Boolean): LocalTypeInformation = withContext(type) {
when {
Collection::class.java.isAssignableFrom(type) &&
!EnumSet::class.java.isAssignableFrom(type) -> LocalTypeInformation.ACollection(type, typeIdentifier, LocalTypeInformation.Unknown)
Map::class.java.isAssignableFrom(type) -> LocalTypeInformation.AMap(type, typeIdentifier, LocalTypeInformation.Unknown, LocalTypeInformation.Unknown)
type == String::class.java -> LocalTypeInformation.Atomic(String::class.java, typeIdentifier)
type.kotlin.javaPrimitiveType != null ->LocalTypeInformation.Atomic(type, typeIdentifier)
type.isEnum -> LocalTypeInformation.AnEnum(
baseTypes.collectionClass.isAssignableFrom(type) &&
!baseTypes.enumSetClass.isAssignableFrom(type) -> ACollection(type, typeIdentifier, Unknown)
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 ->
AnEnum(
type,
typeIdentifier,
type.enumConstants.map { it.toString() },
enumConstants.map(Any::toString),
buildInterfaceInformation(type),
getEnumTransforms(type))
type.kotlinObjectInstance != null -> LocalTypeInformation.Singleton(
getEnumTransforms(type, enumConstants)
)
}
type.kotlinObjectInstance != null -> Singleton(
type,
typeIdentifier,
buildSuperclassInformation(type),
buildInterfaceInformation(type))
type.isInterface -> buildInterface(type, typeIdentifier, emptyList())
type.isAbstractClass -> buildAbstract(type, typeIdentifier, emptyList())
isOpaque -> LocalTypeInformation.Opaque(
isOpaque -> Opaque(
type,
typeIdentifier,
suppressValidation { buildNonAtomic(type, type, typeIdentifier, emptyList()) })
Exception::class.java.isAssignableFrom(type.asClass()) -> suppressValidation {
baseTypes.exceptionClass.isAssignableFrom(type.asClass()) -> suppressValidation {
buildNonAtomic(type, type, typeIdentifier, emptyList())
}
else -> buildNonAtomic(type, type, typeIdentifier, emptyList())
}
}
private fun getEnumTransforms(type: Class<*>): EnumTransforms {
private fun getEnumTransforms(type: Class<*>, enumConstants: Array<out Any>): EnumTransforms {
try {
val constants = type.enumConstants.asSequence().mapIndexed { index, constant -> constant.toString() to index }.toMap()
val constants = enumConstants.asSequence().mapIndexed { index, constant -> constant.toString() to index }.toMap()
return EnumTransforms.build(TransformsAnnotationProcessor.getTransformsSchema(type), constants)
} catch (e: InvalidEnumTransformsException) {
throw NotSerializableDetailedException(type.name, e.message!!)
@ -138,16 +157,16 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
typeIdentifier: TypeIdentifier.Parameterised,
isOpaque: Boolean): LocalTypeInformation = withContext(type) {
when {
Collection::class.java.isAssignableFrom(rawType) &&
!EnumSet::class.java.isAssignableFrom(rawType) ->
LocalTypeInformation.ACollection(type, typeIdentifier, buildTypeParameterInformation(type)[0])
Map::class.java.isAssignableFrom(rawType) -> {
baseTypes.collectionClass.isAssignableFrom(rawType) &&
!baseTypes.enumSetClass.isAssignableFrom(rawType) ->
ACollection(type, typeIdentifier, buildTypeParameterInformation(type)[0])
baseTypes.mapClass.isAssignableFrom(rawType) -> {
val (keyType, valueType) = buildTypeParameterInformation(type)
LocalTypeInformation.AMap(type, typeIdentifier, keyType, valueType)
AMap(type, typeIdentifier, keyType, valueType)
}
rawType.isInterface -> buildInterface(type, typeIdentifier, buildTypeParameterInformation(type))
rawType.isAbstractClass -> buildAbstract(type, typeIdentifier, buildTypeParameterInformation(type))
isOpaque -> LocalTypeInformation.Opaque(rawType,
isOpaque -> Opaque(rawType,
typeIdentifier,
suppressValidation { buildNonAtomic(rawType, type, typeIdentifier, buildTypeParameterInformation(type)) })
else -> buildNonAtomic(rawType, type, typeIdentifier, buildTypeParameterInformation(type))
@ -155,23 +174,25 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
}
private fun buildAbstract(type: Type, typeIdentifier: TypeIdentifier,
typeParameters: List<LocalTypeInformation>): LocalTypeInformation.Abstract =
LocalTypeInformation.Abstract(
type,
typeIdentifier,
buildReadOnlyProperties(type.asClass()),
buildSuperclassInformation(type),
buildInterfaceInformation(type),
typeParameters)
typeParameters: List<LocalTypeInformation>): Abstract =
Abstract(
type,
typeIdentifier,
buildReadOnlyProperties(type.asClass()),
buildSuperclassInformation(type),
buildInterfaceInformation(type),
typeParameters
)
private fun buildInterface(type: Type, typeIdentifier: TypeIdentifier,
typeParameters: List<LocalTypeInformation>): LocalTypeInformation.AnInterface =
LocalTypeInformation.AnInterface(
type,
typeIdentifier,
buildReadOnlyProperties(type.asClass()),
buildInterfaceInformation(type),
typeParameters)
typeParameters: List<LocalTypeInformation>): AnInterface =
AnInterface(
type,
typeIdentifier,
buildReadOnlyProperties(type.asClass()),
buildInterfaceInformation(type),
typeParameters
)
private inline fun <T> withContext(newContext: Type, block: LocalTypeInformationBuilder.() -> T): T {
val previous = resolutionContext
@ -196,11 +217,11 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
private fun buildNonAtomic(rawType: Class<*>, type: Type, typeIdentifier: TypeIdentifier, typeParameterInformation: List<LocalTypeInformation>): LocalTypeInformation {
val superclassInformation = buildSuperclassInformation(type)
val interfaceInformation = buildInterfaceInformation(type)
val observedConstructor = constructorForDeserialization(type) ?: return LocalTypeInformation.NonComposable(
val observedConstructor = constructorForDeserialization(type) ?: return NonComposable(
observedType = type,
typeIdentifier = typeIdentifier,
constructor = null,
properties = if (rawType == Class::class.java) {
properties = if (rawType === Class::class.java) {
// Do NOT drill down into the internals of java.lang.Class.
emptyMap()
} else {
@ -220,7 +241,7 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
if (!propertiesSatisfyConstructor(constructorInformation, properties)) {
val missingConstructorProperties = missingMandatoryConstructorProperties(constructorInformation, properties)
val missingParameters = missingConstructorProperties.map(LocalConstructorParameterInformation::name)
return LocalTypeInformation.NonComposable(
return NonComposable(
observedType = type,
typeIdentifier = typeIdentifier,
constructor = constructorInformation,
@ -229,16 +250,16 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
interfaces = interfaceInformation,
typeParameters = typeParameterInformation,
nonComposableSubtypes = missingConstructorProperties
.filterIsInstanceTo(LinkedHashSet(), LocalTypeInformation.NonComposable::class.java),
.filterIsInstanceTo(LinkedHashSet(), NonComposable::class.java),
reason = "Mandatory constructor parameters $missingParameters are missing from the readable properties ${properties.keys}",
remedy = "Either provide getters or readable fields for $missingParameters, or provide a custom serializer for this type"
)
}
val nonComposableProperties = properties.filterValues { it.type is LocalTypeInformation.NonComposable }
val nonComposableProperties = properties.filterValues { it.type is NonComposable }
if (nonComposableProperties.isNotEmpty()) {
return LocalTypeInformation.NonComposable(
return NonComposable(
observedType = type,
typeIdentifier = typeIdentifier,
constructor = constructorInformation,
@ -247,7 +268,7 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
interfaces = interfaceInformation,
typeParameters = typeParameterInformation,
nonComposableSubtypes = nonComposableProperties.values.mapTo(LinkedHashSet()) {
it.type as LocalTypeInformation.NonComposable
it.type as NonComposable
},
reason = nonComposablePropertiesErrorReason(nonComposableProperties),
remedy = "Either ensure that the properties ${nonComposableProperties.keys} are serializable, or provide a custom serializer for this type"
@ -260,7 +281,7 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
EvolutionConstructorInformation(evolutionConstructorInformation, evolutionProperties)
}
return LocalTypeInformation.Composable(type, typeIdentifier, constructorInformation, evolutionConstructors, properties,
return Composable(type, typeIdentifier, constructorInformation, evolutionConstructors, properties,
superclassInformation, interfaceInformation, typeParameterInformation)
}
@ -268,13 +289,13 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
private fun propertiesSatisfyConstructor(constructorInformation: LocalConstructorInformation, properties: Map<PropertyName, LocalPropertyInformation>): Boolean {
if (!constructorInformation.hasParameters) return true
val indicesAddressedByProperties = properties.values.asSequence().mapNotNull {
val indicesAddressedByProperties = properties.values.asSequence().mapNotNullTo(LinkedHashSet()) {
when (it) {
is LocalPropertyInformation.ConstructorPairedProperty -> it.constructorSlot.parameterIndex
is LocalPropertyInformation.PrivateConstructorPairedProperty -> it.constructorSlot.parameterIndex
else -> null
}
}.toSet()
}
return (constructorInformation.parameters.indices).none { index ->
constructorInformation.parameters[index].isMandatory && index !in indicesAddressedByProperties
@ -287,13 +308,13 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
): List<LocalConstructorParameterInformation> {
if (!constructorInformation.hasParameters) return emptyList()
val indicesAddressedByProperties = properties.values.asSequence().mapNotNull {
val indicesAddressedByProperties = properties.values.asSequence().mapNotNullTo(LinkedHashSet()) {
when (it) {
is LocalPropertyInformation.ConstructorPairedProperty -> it.constructorSlot.parameterIndex
is LocalPropertyInformation.PrivateConstructorPairedProperty -> it.constructorSlot.parameterIndex
else -> null
}
}.toSet()
}
return (constructorInformation.parameters.indices).mapNotNull { index ->
val parameter = constructorInformation.parameters[index]
@ -306,7 +327,7 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
private fun nonComposablePropertiesErrorReason(nonComposableProperties: Map<PropertyName, LocalPropertyInformation>): String {
val reasons = nonComposableProperties.entries.joinToString("\n") { (key, value) ->
"$key [${value.type.observedType}]: ${(value.type as LocalTypeInformation.NonComposable).reason}"
"$key [${value.type.observedType}]: ${(value.type as NonComposable).reason}"
.replace("\n", "\n ")
}
return "Has properties ${nonComposableProperties.keys} of types that are not serializable:\n" + reasons
@ -374,7 +395,7 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
}
val property = makeConstructorPairedProperty(
constructorParameterIndices[normalisedName]!!,
constructorParameterIndices.getValue(normalisedName),
descriptor,
constructorInformation)
if (property == null) null else normalisedName to property

View File

@ -1,6 +1,8 @@
package net.corda.serialization.internal.model
import java.lang.reflect.*
import java.util.function.Function
import java.util.function.Predicate
/**
* Provides a means for looking up [LocalTypeInformation] by [Type] and [TypeIdentifier], falling back to building it
@ -24,6 +26,12 @@ interface LocalTypeLookup {
* because it is not whitelisted.
*/
fun isExcluded(type: Type): Boolean
/**
* These classes are used by [LocalTypeInformationBuilder] to
* build the correct [LocalTypeInformation] subclasses.
*/
val baseTypes: BaseLocalTypes
}
/**
@ -72,6 +80,8 @@ class ConfigurableLocalTypeModel(private val typeModelConfiguration: LocalTypeMo
override fun isExcluded(type: Type): Boolean = typeModelConfiguration.isExcluded(type)
override val baseTypes = typeModelConfiguration.baseTypes
/**
* Merge the local cache back into the global cache, once we've finished traversal (and patched all cycles).
*/
@ -111,4 +121,20 @@ interface LocalTypeModelConfiguration {
* [LocalTypeInformation], usually because they are not included in a whitelist.
*/
fun isExcluded(type: Type): Boolean
/**
* These classes are used by [LocalTypeInformationBuilder] to
* build the correct [LocalTypeInformation] subclasses.
*/
val baseTypes: BaseLocalTypes
}
class BaseLocalTypes(
val collectionClass: Class<*>,
val enumSetClass: Class<*>,
val exceptionClass: Class<*>,
val mapClass: Class<*>,
val stringClass: Class<*>,
val isEnum: Predicate<Class<*>>,
val enumConstants: Function<Class<*>, Array<out Any>>
)

View File

@ -2,8 +2,6 @@ package net.corda.serialization.internal.model
import net.corda.core.serialization.SerializationContext
import net.corda.serialization.internal.carpenter.*
import java.io.NotSerializableException
import java.lang.ClassCastException
import java.lang.reflect.Type
/**