From ddf45f4e079d7b71f03e93ed149b525ff77b8fd7 Mon Sep 17 00:00:00 2001 From: Dominic Fox <40790090+distributedleetravis@users.noreply.github.com> Date: Mon, 19 Nov 2018 16:07:01 +0000 Subject: [PATCH] CORDA-2099: define local type model (#4118) * CORDA-2099 define LocalTypeInformation and related data types and functions * Enums don't have superclasses * Separate ACollection from AMap * Remove spurious import * Small fixes, slightly improved testing * Log warnings if types that we expect to be able to serialise are non-composable * Rename lookup -> findOrBuild * Pull changes from working branch * Missing files needed for unit test * Pull in whitelist-based type model configuration * Move opaque type list across * Remote duplicate declaration * Restore fixes from other PR --- .../internal/amqp/CustomSerializerRegistry.kt | 94 ++++ .../amqp/DescriptorBasedSerializerRegistry.kt | 29 ++ .../WhitelistBasedTypeModelConfiguration.kt | 45 ++ .../model/LocalPropertyInformation.kt | 71 +++ .../internal/model/LocalTypeInformation.kt | 374 ++++++++++++++++ .../model/LocalTypeInformationBuilder.kt | 410 ++++++++++++++++++ .../internal/model/LocalTypeModel.kt | 91 ++++ .../internal/model/RemoteTypeCarpenter.kt | 2 - .../internal/model/LocalTypeModelTests.kt | 166 +++++++ 9 files changed, 1280 insertions(+), 2 deletions(-) create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializerRegistry.kt create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DescriptorBasedSerializerRegistry.kt create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/amqp/WhitelistBasedTypeModelConfiguration.kt create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalPropertyInformation.kt create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformation.kt create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformationBuilder.kt create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeModel.kt create mode 100644 serialization/src/test/kotlin/net/corda/serialization/internal/model/LocalTypeModelTests.kt diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializerRegistry.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializerRegistry.kt new file mode 100644 index 0000000000..61478dc43c --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializerRegistry.kt @@ -0,0 +1,94 @@ +package net.corda.serialization.internal.amqp + +import net.corda.core.internal.uncheckedCast +import net.corda.core.utilities.contextLogger +import net.corda.serialization.internal.model.DefaultCacheProvider +import net.corda.serialization.internal.model.TypeIdentifier +import java.lang.reflect.Type + +interface CustomSerializerRegistry { + /** + * Register a custom serializer for any type that cannot be serialized or deserialized by the default serializer + * that expects to find getters and a constructor with a parameter for each property. + */ + fun register(customSerializer: CustomSerializer) + fun registerExternal(customSerializer: CorDappCustomSerializer) + + fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer? +} + +class CachingCustomSerializerRegistry( + private val descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry) + : CustomSerializerRegistry { + + companion object { + val logger = contextLogger() + } + + private data class CustomSerializerIdentifier(val actualTypeIdentifier: TypeIdentifier, val declaredTypeIdentifier: TypeIdentifier) + + private val customSerializersCache: MutableMap> = DefaultCacheProvider.createCache() + private var customSerializers: List = emptyList() + + /** + * Register a custom serializer for any type that cannot be serialized or deserialized by the default serializer + * that expects to find getters and a constructor with a parameter for each property. + */ + override fun register(customSerializer: CustomSerializer) { + logger.trace("action=\"Registering custom serializer\", class=\"${customSerializer.type}\"") + + descriptorBasedSerializerRegistry.getOrBuild(customSerializer.typeDescriptor.toString()) { + customSerializers += customSerializer + for (additional in customSerializer.additionalSerializers) { + register(additional) + } + customSerializer + } + } + + override fun registerExternal(customSerializer: CorDappCustomSerializer) { + logger.trace("action=\"Registering external serializer\", class=\"${customSerializer.type}\"") + + descriptorBasedSerializerRegistry.getOrBuild(customSerializer.typeDescriptor.toString()) { + customSerializers += customSerializer + customSerializer + } + } + + override fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer? { + val typeIdentifier = CustomSerializerIdentifier( + TypeIdentifier.forClass(clazz), + TypeIdentifier.forGenericType(declaredType)) + + return customSerializersCache[typeIdentifier] + ?: doFindCustomSerializer(clazz, declaredType)?.also { serializer -> + customSerializersCache.putIfAbsent(typeIdentifier, serializer) + } + } + + private fun doFindCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer? { + // e.g. Imagine if we provided a Map serializer this way, then it won't work if the declared type is + // AbstractMap, only Map. Otherwise it needs to inject additional schema for a RestrictedType source of the + // super type. Could be done, but do we need it? + for (customSerializer in customSerializers) { + if (customSerializer.isSerializerFor(clazz)) { + val declaredSuperClass = declaredType.asClass().superclass + + return if (declaredSuperClass == null + || !customSerializer.isSerializerFor(declaredSuperClass) + || !customSerializer.revealSubclassesInSchema + ) { + logger.debug("action=\"Using custom serializer\", class=${clazz.typeName}, " + + "declaredType=${declaredType.typeName}") + + @Suppress("UNCHECKED_CAST") + customSerializer as? AMQPSerializer + } else { + // Make a subclass serializer for the subclass and return that... + CustomSerializer.SubClass(clazz, uncheckedCast(customSerializer)) + } + } + } + return null + } +} \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DescriptorBasedSerializerRegistry.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DescriptorBasedSerializerRegistry.kt new file mode 100644 index 0000000000..2a2d17127d --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DescriptorBasedSerializerRegistry.kt @@ -0,0 +1,29 @@ +package net.corda.serialization.internal.amqp + +import net.corda.serialization.internal.model.DefaultCacheProvider + +/** + * The quickest way to find a serializer, if one has already been generated, is to look it up by type descriptor. + * + * This registry gets shared around between various participants that might want to use it as a lookup, or register + * serialisers that they have created with it. + */ +interface DescriptorBasedSerializerRegistry { + operator fun get(descriptor: String): AMQPSerializer? + operator fun set(descriptor: String, serializer: AMQPSerializer) + fun getOrBuild(descriptor: String, builder: () -> AMQPSerializer): AMQPSerializer +} + +class DefaultDescriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry { + + private val registry: MutableMap> = DefaultCacheProvider.createCache() + + override fun get(descriptor: String): AMQPSerializer? = registry[descriptor] + + override fun set(descriptor: String, serializer: AMQPSerializer) { + registry.putIfAbsent(descriptor, serializer) + } + + override fun getOrBuild(descriptor: String, builder: () -> AMQPSerializer) = + get(descriptor) ?: builder().also { newSerializer -> this[descriptor] = newSerializer } +} \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/WhitelistBasedTypeModelConfiguration.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/WhitelistBasedTypeModelConfiguration.kt new file mode 100644 index 0000000000..dfb7651b54 --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/WhitelistBasedTypeModelConfiguration.kt @@ -0,0 +1,45 @@ +package net.corda.serialization.internal.amqp + +import com.google.common.primitives.Primitives +import net.corda.core.serialization.ClassWhitelist +import net.corda.serialization.internal.model.LocalTypeModelConfiguration +import org.apache.qpid.proton.amqp.* +import java.lang.reflect.Type +import java.util.* + +/** + * [LocalTypeModelConfiguration] based on a [ClassWhitelist] + */ +class WhitelistBasedTypeModelConfiguration( + private val whitelist: ClassWhitelist, + private val customSerializerRegistry: CustomSerializerRegistry) + : LocalTypeModelConfiguration { + 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 +} + +// Copied from SerializerFactory so that we can have equivalent behaviour, for now. +private val opaqueTypes = setOf( + Character::class.java, + Char::class.java, + Boolean::class.java, + Byte::class.java, + UnsignedByte::class.java, + Short::class.java, + UnsignedShort::class.java, + Int::class.java, + UnsignedInteger::class.java, + Long::class.java, + UnsignedLong::class.java, + Float::class.java, + Double::class.java, + Decimal32::class.java, + Decimal64::class.java, + Decimal128::class.java, + Date::class.java, + UUID::class.java, + ByteArray::class.java, + String::class.java, + Symbol::class.java +) \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalPropertyInformation.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalPropertyInformation.kt new file mode 100644 index 0000000000..69b223d32b --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalPropertyInformation.kt @@ -0,0 +1,71 @@ +package net.corda.serialization.internal.model + +import java.lang.reflect.Field +import java.lang.reflect.Method + +/** + * Represents the information we have about a property of a type. + */ +sealed class LocalPropertyInformation(val isCalculated: Boolean) { + + /** + * [LocalTypeInformation] for the type of the property. + */ + abstract val type: LocalTypeInformation + + /** + * True if the property is a primitive type or is flagged as non-nullable, false otherwise. + */ + abstract val isMandatory: Boolean + + /** + * A property of an interface, for which we have only a getter method. + * + * @param observedGetter The method which can be used to obtain the value of this property from an instance of its owning type. + */ + data class ReadOnlyProperty(val observedGetter: Method, override val type: LocalTypeInformation, override val isMandatory: Boolean) : LocalPropertyInformation(false) + + /** + * A property for which we have both a getter, and a matching slot in an array of constructor parameters. + * + * @param observedGetter The method which can be used to obtain the value of this property from an instance of its owning type. + * @param constructorSlot The [ConstructorSlot] to which the property corresponds, used to populate an array of + * constructor arguments when creating instances of its owning type. + */ + data class ConstructorPairedProperty(val observedGetter: Method, val constructorSlot: ConstructorSlot, override val type: LocalTypeInformation, override val isMandatory: Boolean) : LocalPropertyInformation(false) + + /** + * A property for which we have no getter, but for which there is a backing field a matching slot in an array of + * constructor parameters. + * + * @param observedField The field which can be used to obtain the value of this property from an instance of its owning type. + * @param constructorSlot The [ConstructorSlot] to which the property corresponds, used to populate an array of + * constructor arguments when creating instances of its owning type. + */ + data class PrivateConstructorPairedProperty(val observedField: Field, val constructorSlot: ConstructorSlot, override val type: LocalTypeInformation, override val isMandatory: Boolean) : LocalPropertyInformation(false) + + /** + * A property for which we have both getter and setter methods (usually belonging to a POJO which is initialised + * with the default no-argument constructor and then configured via setters). + * + * @param observedGetter The method which can be used to obtain the value of this property from an instance of its owning type. + * @param observedSetter The method which can be used to set the value of this property on an instance of its owning type. + */ + data class GetterSetterProperty(val observedGetter: Method, val observedSetter: Method, override val type: LocalTypeInformation, override val isMandatory: Boolean) : LocalPropertyInformation(false) + + /** + * A property for which we have only a getter method, which is annotated with [SerializableCalculatedProperty]. + */ + data class CalculatedProperty(val observedGetter: Method, override val type: LocalTypeInformation, override val isMandatory: Boolean) : LocalPropertyInformation(true) +} + + +/** + * References a slot in an array of constructor parameters. + */ +data class ConstructorSlot(val parameterIndex: Int, val constructorInformation: LocalConstructorInformation) { + val parameterInformation get() = constructorInformation.parameters.getOrNull(parameterIndex) ?: + throw IllegalStateException("Constructor slot refers to parameter #$parameterIndex " + + "of constructor $constructorInformation, " + + "but constructor has only ${constructorInformation.parameters.size} parameters") +} \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformation.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformation.kt new file mode 100644 index 0000000000..206417138b --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformation.kt @@ -0,0 +1,374 @@ +package net.corda.serialization.internal.model + +import java.lang.reflect.* +import kotlin.reflect.KFunction +import java.util.* + +typealias PropertyName = String + +/** + * The [LocalTypeInformation] captured for a [Type] gathers together everything that can be ascertained about the type + * through runtime reflection, in the form of a directed acyclic graph (DAG) of types and relationships between types. + * + * Types can be related in the following ways: + * + * * Type A is the type of a _property_ of type B. + * * Type A is the type of an _interface_ of type B. + * * Type A is the type of the _superclass_ of type B. + * * Type A is the type of a _type parameter_ of type B. + * * Type A is an _array type_, of which type B is the _component type_. + * + * All of these relationships are represented by references and collections held by the objects representing the nodes + * themselves. + * + * A type is [Composable] if it is isomorphic to a dictionary of its property values, i.e. if we can obtain an instance + * of the type from a dictionary containing typed key/value pairs corresponding to its properties, and a dictionary from + * an instance of the type, and can round-trip (in both directions) between these representations without losing + * information. This is the basis for compositional serialization, i.e. building a serializer for a type out of the + * serializers we have for its property types. + * + * A type is [Atomic] if it cannot be decomposed or recomposed in this fashion (usually because it is the type of a + * scalar value of some sort, such as [Int]), and [Opaque] if we have chosen not to investigate its composability, + * typically because it is handled by a custom serializer. + * + * Abstract types are represented by [AnInterface] and [Abstract], the difference between them being that an [Abstract] + * type may have a superclass. + * + * If a concrete type does not have a unique deserialization constructor, it is represented by [NonComposable], meaning + * that we know how to take it apart but do not know how to put it back together again. + * + * An array of any type is represented by [ArrayOf]. Enums are represented by [AnEnum]. + * + * The type of [Any]/[java.lang.Object] is represented by [Top]. Unbounded wildcards, or wildcards whose upper bound is + * [Top], are represented by [Unknown]. Bounded wildcards are always resolved to their upper bounds, e.g. + * `List` becomes `List`. + * + * If we encounter a cycle while traversing the DAG, the type on which traversal detected the cycle is represented by + * [Cycle], and no further traversal is attempted from that type. Kotlin objects are represented by [Singleton]. + */ +sealed class LocalTypeInformation { + + companion object { + /** + * Using the provided [LocalTypeLookup] to record and locate already-visited nodes, traverse the DAG of related + * types beginning the with provided [Type] and construct a complete set of [LocalTypeInformation] for that type. + * + * @param type The [Type] to obtain [LocalTypeInformation] for. + * @param lookup The [LocalTypeLookup] to use to find previously-constructed [LocalTypeInformation]. + */ + fun forType(type: Type, lookup: LocalTypeLookup): LocalTypeInformation = + LocalTypeInformationBuilder(lookup).build(type, TypeIdentifier.forGenericType(type)) + } + + /** + * The actual type which was observed when constructing this type information. + */ + abstract val observedType: Type + + /** + * The [TypeIdentifier] for the type represented by this type information, used to cross-reference with + * [RemoteTypeInformation]. + */ + abstract val typeIdentifier: TypeIdentifier + + /** + * Obtain a multi-line, recursively-indented representation of this type information. + * + * @param simplifyClassNames By default, class names are printed as their "simple" class names, i.e. "String" instead + * of "java.lang.String". If this is set to `false`, then the full class name will be printed instead. + */ + fun prettyPrint(simplifyClassNames: Boolean = true): String = + LocalTypeInformationPrettyPrinter(simplifyClassNames).prettyPrint(this) + + /** + * The [LocalTypeInformation] corresponding to an unbounded wildcard ([TypeIdentifier.UnknownType]) + */ + object Unknown : LocalTypeInformation() { + override val observedType get() = TypeIdentifier.UnknownType.getLocalType() + override val typeIdentifier get() = TypeIdentifier.UnknownType + } + + /** + * The [LocalTypeInformation] corresponding to [java.lang.Object] / [Any] ([TypeIdentifier.TopType]) + */ + object Top : LocalTypeInformation() { + override val observedType get() = TypeIdentifier.TopType.getLocalType() + override val typeIdentifier get() = TypeIdentifier.TopType + } + + /** + * The [LocalTypeInformation] emitted if we hit a cycle while traversing the graph of related types. + */ + data class Cycle( + override val observedType: Type, + override val typeIdentifier: TypeIdentifier, + private val _follow: () -> LocalTypeInformation) : LocalTypeInformation() { + val follow: LocalTypeInformation get() = _follow() + + // Custom equals / hashcode because otherwise the "follow" lambda makes equality harder to reason about. + override fun equals(other: Any?): Boolean = + other is Cycle && + other.observedType == observedType && + other.typeIdentifier == typeIdentifier + + override fun hashCode(): Int = Objects.hash(observedType, typeIdentifier) + + override fun toString(): String = "Cycle($observedType, $typeIdentifier)" + } + + /** + * May in fact be a more complex class, but is treated as if atomic, i.e. we don't further expand its properties. + */ + data class Opaque(override val observedType: Class<*>, override val typeIdentifier: TypeIdentifier, + private val _expand: () -> LocalTypeInformation) : LocalTypeInformation() { + val expand: LocalTypeInformation get() = _expand() + + // Custom equals / hashcode because otherwise the "expand" lambda makes equality harder to reason about. + override fun equals(other: Any?): Boolean = + other is Cycle && + other.observedType == observedType && + other.typeIdentifier == typeIdentifier + + override fun hashCode(): Int = Objects.hash(observedType, typeIdentifier) + + override fun toString(): String = "Opaque($observedType, $typeIdentifier)" + } + + /** + * Represents a scalar type such as [Int]. + */ + data class Atomic(override val observedType: Class<*>, override val typeIdentifier: TypeIdentifier) : LocalTypeInformation() + + /** + * Represents an array of some other type. + * + * @param componentType The [LocalTypeInformation] for the component type of the array (e.g. [Int], if the type is [IntArray]) + */ + data class AnArray(override val observedType: Type, override val typeIdentifier: TypeIdentifier, val componentType: LocalTypeInformation) : LocalTypeInformation() + + /** + * Represents an `enum` + * + * @param members The string names of the members of the enum. + * @param superclass [LocalTypeInformation] for the superclass of the type (as enums can inherit from other types). + * @param interfaces [LocalTypeInformation] for each interface implemented by the type. + */ + data class AnEnum( + override val observedType: Class<*>, + override val typeIdentifier: TypeIdentifier, + val members: List, + val interfaces: List): LocalTypeInformation() + + /** + * Represents a type whose underlying class is an interface. + * + * @param properties [LocalPropertyInformation] for the read-only properties of the interface, i.e. its "getter" methods. + * @param interfaces [LocalTypeInformation] for the interfaces extended by this interface. + * @param typeParameters [LocalTypeInformation] for the resolved type parameters of the type. + */ + data class AnInterface( + override val observedType: Type, + override val typeIdentifier: TypeIdentifier, + val properties: Map, + val interfaces: List, + val typeParameters: List) : LocalTypeInformation() + + /** + * Represents a type whose underlying class is abstract. + * + * @param properties [LocalPropertyInformation] for the read-only properties of the interface, i.e. its "getter" methods. + * @param superclass [LocalTypeInformation] for the superclass of the underlying class of this type. + * @param interfaces [LocalTypeInformation] for the interfaces extended by this interface. + * @param typeParameters [LocalTypeInformation] for the resolved type parameters of the type. + */ + data class Abstract( + override val observedType: Type, + override val typeIdentifier: TypeIdentifier, + val properties: Map, + val superclass: LocalTypeInformation, + val interfaces: List, + val typeParameters: List) : LocalTypeInformation() + + /** + * Represents a type which has only a single instantiation, e.g. a Kotlin `object`. + * + * @param superclass [LocalTypeInformation] for the superclass of the underlying class of this type. + * @param interfaces [LocalTypeInformation] for the interfaces extended by this interface. + */ + data class Singleton(override val observedType: Type, override val typeIdentifier: TypeIdentifier, val superclass: LocalTypeInformation, val interfaces: List) : LocalTypeInformation() + + /** + * Represents a type whose instances can be reversibly decomposed into dictionaries of typed values. + * + * @param constructor [LocalConstructorInformation] for the constructor used when building instances of this type + * out of dictionaries of typed values. + * @param properties [LocalPropertyInformation] for the properties of the interface. + * @param superclass [LocalTypeInformation] for the superclass of the underlying class of this type. + * @param interfaces [LocalTypeInformation] for the interfaces extended by this interface. + * @param typeParameters [LocalTypeInformation] for the resolved type parameters of the type. + */ + data class Composable( + override val observedType: Type, + override val typeIdentifier: TypeIdentifier, + val constructor: LocalConstructorInformation, + val evolverConstructors: List, + val properties: Map, + val superclass: LocalTypeInformation, + val interfaces: List, + val typeParameters: List) : LocalTypeInformation() + + /** + * Represents a type whose instances may have observable properties (represented by "getter" methods), but for which + * we do not possess a method (such as a unique "deserialization constructor" satisfied by these properties) for + * creating a new instance from a dictionary of property values. + * + * @param constructor [LocalConstructorInformation] for the constructor of this type, if there is one. + * @param properties [LocalPropertyInformation] for the properties of the interface. + * @param superclass [LocalTypeInformation] for the superclass of the underlying class of this type. + * @param interfaces [LocalTypeInformation] for the interfaces extended by this interface. + * @param typeParameters [LocalTypeInformation] for the resolved type parameters of the type. + */ + data class NonComposable( + override val observedType: Type, + override val typeIdentifier: TypeIdentifier, + val constructor: LocalConstructorInformation?, + val properties: Map, + val superclass: LocalTypeInformation, + val interfaces: List, + val typeParameters: List) : LocalTypeInformation() + + /** + * Represents a type whose underlying class is a collection class such as [List] with a single type parameter. + * + * @param elementType [LocalTypeInformation] for the resolved type parameter of the type, i.e. the type of its + * elements. [Unknown] if the type is erased. + */ + data class ACollection(override val observedType: Type, override val typeIdentifier: TypeIdentifier, val elementType: LocalTypeInformation) : LocalTypeInformation() { + val isErased: Boolean get() = typeIdentifier is TypeIdentifier.Erased + + fun withElementType(parameter: LocalTypeInformation): ACollection = when(typeIdentifier) { + is TypeIdentifier.Erased -> { + val unerasedType = typeIdentifier.toParameterized(listOf(parameter.typeIdentifier)) + ACollection( + unerasedType.getLocalType(this::class.java.classLoader), + unerasedType, + parameter) + } + is TypeIdentifier.Parameterised -> { + val reparameterizedType = typeIdentifier.copy(parameters = listOf(parameter.typeIdentifier)) + ACollection( + reparameterizedType.getLocalType(this::class.java.classLoader), + reparameterizedType, + parameter + ) + } + else -> throw IllegalStateException("Cannot parameterise $this") + } + } + + /** + * Represents a type whose underlying class is a map class such as [Map] with two type parameters. + * + * @param keyType [LocalTypeInformation] for the first resolved type parameter of the type, i.e. the type of its + * keys. [Unknown] if the type is erased. + * @param valueType [LocalTypeInformation] for the second resolved type parameter of the type, i.e. the type of its + * values. [Unknown] if the type is erased. + */ + data class AMap(override val observedType: Type, override val typeIdentifier: TypeIdentifier, + val keyType: LocalTypeInformation, val valueType: LocalTypeInformation) : LocalTypeInformation() { + val isErased: Boolean get() = typeIdentifier is TypeIdentifier.Erased + + fun withParameters(keyType: LocalTypeInformation, valueType: LocalTypeInformation): AMap = when(typeIdentifier) { + is TypeIdentifier.Erased -> { + val unerasedType = typeIdentifier.toParameterized(listOf(keyType.typeIdentifier, valueType.typeIdentifier)) + AMap( + unerasedType.getLocalType(this::class.java.classLoader), + unerasedType, + keyType, valueType) + } + is TypeIdentifier.Parameterised -> { + val reparameterizedType = typeIdentifier.copy(parameters = listOf(keyType.typeIdentifier, valueType.typeIdentifier)) + AMap( + reparameterizedType.getLocalType(this::class.java.classLoader), + reparameterizedType, + keyType, valueType + ) + } + else -> throw IllegalStateException("Cannot parameterise $this") + } + } +} + +/** + * Represents information about a constructor. + */ +data class LocalConstructorInformation( + val observedMethod: KFunction, + val parameters: List) { + val hasParameters: Boolean get() = parameters.isNotEmpty() +} + +/** + * Represents information about a constructor that is specifically to be used for evolution, and is potentially matched + * with a different set of properties to the regular constructor. + */ +data class EvolverConstructorInformation( + val constructor: LocalConstructorInformation, + val properties: Map) + +/** + * Represents information about a constructor parameter + */ +data class LocalConstructorParameterInformation( + val name: String, + val type: LocalTypeInformation, + val isMandatory: Boolean) + +private data class LocalTypeInformationPrettyPrinter(private val simplifyClassNames: Boolean, private val indent: Int = 0) { + + fun prettyPrint(typeInformation: LocalTypeInformation): String = + with(typeInformation) { + when (this) { + is LocalTypeInformation.Abstract -> + typeIdentifier.prettyPrint() + + printInheritsFrom(interfaces, superclass) + + indentAnd { printProperties(properties) } + is LocalTypeInformation.AnInterface -> + typeIdentifier.prettyPrint() + printInheritsFrom(interfaces) + is LocalTypeInformation.Composable -> typeIdentifier.prettyPrint() + + printConstructor(constructor) + + printInheritsFrom(interfaces, superclass) + + indentAnd { printProperties(properties) } + else -> typeIdentifier.prettyPrint() + } + } + + private fun printConstructor(constructor: LocalConstructorInformation) = + constructor.parameters.joinToString(", ", "(", ")") { + it.name + + ": " + it.type.typeIdentifier.prettyPrint(simplifyClassNames) + + (if (!it.isMandatory) "?" else "") + } + + private fun printInheritsFrom(interfaces: List, superclass: LocalTypeInformation? = null): String { + val parents = if (superclass == null || superclass == LocalTypeInformation.Top) interfaces.asSequence() + else sequenceOf(superclass) + interfaces.asSequence() + return if (!parents.iterator().hasNext()) "" + else parents.joinToString(", ", ": ", "") { it.typeIdentifier.prettyPrint(simplifyClassNames) } + } + + private fun printProperties(properties: Map) = + properties.entries.asSequence().sortedBy { it.key }.joinToString("\n", "\n", "") { + it.prettyPrint() + } + + private fun Map.Entry.prettyPrint(): String = + " ".repeat(indent) + key + + (if(!value.isMandatory) " (optional)" else "") + + (if (value.isCalculated) " (calculated)" else "") + + ": " + value.type.prettyPrint(simplifyClassNames) + + private inline fun indentAnd(block: LocalTypeInformationPrettyPrinter.() -> String) = + copy(indent = indent + 1).block() +} + diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformationBuilder.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformationBuilder.kt new file mode 100644 index 0000000000..88df121a43 --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeInformationBuilder.kt @@ -0,0 +1,410 @@ +package net.corda.serialization.internal.model + +import net.corda.core.internal.isAbstractClass +import net.corda.core.internal.isConcreteClass +import net.corda.core.internal.kotlinObjectInstance +import net.corda.core.serialization.ConstructorForDeserialization +import net.corda.core.serialization.DeprecatedConstructorForDeserialization +import net.corda.core.utilities.contextLogger +import net.corda.serialization.internal.amqp.* +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 +import kotlin.reflect.full.memberProperties +import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.jvm.internal.KotlinReflectionInternalError +import kotlin.reflect.jvm.isAccessible +import kotlin.reflect.jvm.javaConstructor +import kotlin.reflect.jvm.javaGetter +import kotlin.reflect.jvm.javaType + +/** + * Provides the logic for building instances of [LocalTypeInformation] by reflecting over local [Type]s. + * + * @param lookup The [LocalTypeLookup] to use to locate and register constructed [LocalTypeInformation]. + * @param resolutionContext The [Type] to use when attempting to resolve type variables. + * @param visited The [Set] of [TypeIdentifier]s already visited while building information for a given [Type]. Note that + * this is not a [MutableSet], as we want to be able to backtrack while traversing through the graph of related types, and + * will find it useful to revert to earlier states of knowledge about which types have been visited on a given branch. + */ +internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup, val resolutionContext: Type? = null, val visited: Set = emptySet()) { + + companion object { + private val logger = contextLogger() + } + + /** + * Recursively build [LocalTypeInformation] for the given [Type] and [TypeIdentifier] + */ + fun build(type: Type, typeIdentifier: TypeIdentifier): LocalTypeInformation = + if (typeIdentifier in visited) LocalTypeInformation.Cycle(type, typeIdentifier) { + LocalTypeInformationBuilder(lookup, resolutionContext).build(type, typeIdentifier) + } + else lookup.findOrBuild(type, typeIdentifier) { isOpaque -> + copy(visited = visited + typeIdentifier).buildIfNotFound(type, typeIdentifier, isOpaque) + } + + private fun resolveAndBuild(type: Type): LocalTypeInformation { + val resolved = type.resolveAgainstContext() + return build(resolved, TypeIdentifier.forGenericType(resolved, resolutionContext + ?: type)) + } + + private fun Type.resolveAgainstContext(): Type = + if (resolutionContext == null) this else resolveAgainst(resolutionContext) + + 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.Unparameterised, + is TypeIdentifier.Erased -> buildForClass(rawType, typeIdentifier, isOpaque) + is TypeIdentifier.ArrayOf -> { + LocalTypeInformation.AnArray( + type, + typeIdentifier, + resolveAndBuild(type.componentType())) + } + is TypeIdentifier.Parameterised -> buildForParameterised(rawType, type as ParameterizedType, typeIdentifier, isOpaque) + } + } + + 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.kotlin.javaPrimitiveType != null -> LocalTypeInformation.Atomic(type.kotlin.javaPrimitiveType!!, typeIdentifier) + type.isEnum -> LocalTypeInformation.AnEnum( + type, + typeIdentifier, + type.enumConstants.map { it.toString() }, + buildInterfaceInformation(type)) + type.kotlinObjectInstance != null -> LocalTypeInformation.Singleton( + type, + typeIdentifier, + buildSuperclassInformation(type), + buildInterfaceInformation(type)) + type.isInterface -> buildInterface(type, typeIdentifier, emptyList()) + type.isAbstractClass -> buildAbstract(type, typeIdentifier, emptyList()) + else -> when { + isOpaque -> LocalTypeInformation.Opaque(type, typeIdentifier) { + buildNonAtomic(type, type, typeIdentifier, emptyList()) + } + else -> buildNonAtomic(type, type, typeIdentifier, emptyList()) + } + } + } + + private fun buildForParameterised( + rawType: Class<*>, + type: ParameterizedType, + 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) -> { + val (keyType, valueType) = buildTypeParameterInformation(type) + LocalTypeInformation.AMap(type, typeIdentifier, keyType, valueType) + } + rawType.isInterface -> buildInterface(type, typeIdentifier, buildTypeParameterInformation(type)) + rawType.isAbstractClass -> buildAbstract(type, typeIdentifier, buildTypeParameterInformation(type)) + else -> when { + isOpaque -> LocalTypeInformation.Opaque(rawType, typeIdentifier) { + buildNonAtomic(rawType, type, typeIdentifier, buildTypeParameterInformation(type)) + } + else -> buildNonAtomic(rawType, type, typeIdentifier, buildTypeParameterInformation(type)) + } + } + } + + private fun buildAbstract(type: Type, typeIdentifier: TypeIdentifier, + typeParameters: List): LocalTypeInformation.Abstract = + LocalTypeInformation.Abstract( + type, + typeIdentifier, + buildReadOnlyProperties(type.asClass()), + buildSuperclassInformation(type), + buildInterfaceInformation(type), + typeParameters) + + private fun buildInterface(type: Type, typeIdentifier: TypeIdentifier, + typeParameters: List): LocalTypeInformation.AnInterface = + LocalTypeInformation.AnInterface( + type, + typeIdentifier, + buildReadOnlyProperties(type.asClass()), + buildInterfaceInformation(type), + typeParameters) + + private inline fun withContext(newContext: Type, block: LocalTypeInformationBuilder.() -> T): T = + copy(resolutionContext = newContext).run(block) + + /** + * Build a non-atomic type, which is either [Composable] or [NonComposable]. + * + * Composability is a transitive property: a type is [Composable] iff it has a unique deserialization constructor _and_ + * all of its property types are also [Composable]. If not, the type is [NonComposable], meaning we cannot deserialize + * it without a custom serializer (in which case it should normally have been flagged as [Opaque]). + * + * Rather than throwing an exception if a type is [NonComposable], we capture its type information so that it can + * still be used to _serialize_ values, or as the basis for deciding on an evolution strategy. + */ + private fun buildNonAtomic(rawType: Class<*>, type: Type, typeIdentifier: TypeIdentifier, typeParameterInformation: List): LocalTypeInformation { + val superclassInformation = buildSuperclassInformation(type) + val interfaceInformation = buildInterfaceInformation(type) + val observedConstructor = constructorForDeserialization(type) + + if (observedConstructor == null) { + logger.warn("No unique deserialisation constructor found for class $rawType, type is marked as non-composable") + return LocalTypeInformation.NonComposable(type, typeIdentifier, null, buildReadOnlyProperties(rawType), + superclassInformation, interfaceInformation, typeParameterInformation) + } + + val constructorInformation = buildConstructorInformation(type, observedConstructor) + val properties = buildObjectProperties(rawType, constructorInformation) + + val hasNonComposableProperties = properties.values.any { it.type is LocalTypeInformation.NonComposable } + + if (!propertiesSatisfyConstructor(constructorInformation, properties) || hasNonComposableProperties) { + if (hasNonComposableProperties) { + logger.warn("Type ${type.typeName} has non-composable properties and has been marked as non-composable") + } else { + logger.warn("Properties of type ${type.typeName} do not satisfy its constructor, type has been marked as non-composable") + } + return LocalTypeInformation.NonComposable(type, typeIdentifier, constructorInformation, properties, superclassInformation, + interfaceInformation, typeParameterInformation) + } + + val evolverConstructors = evolverConstructors(type).map { ctor -> + val constructorInformation = buildConstructorInformation(type, ctor) + val evolverProperties = buildObjectProperties(rawType, constructorInformation) + EvolverConstructorInformation(constructorInformation, evolverProperties) + } + + return LocalTypeInformation.Composable(type, typeIdentifier, constructorInformation, evolverConstructors, properties, + superclassInformation, interfaceInformation, typeParameterInformation) + } + + // Can we supply all of the mandatory constructor parameters using values addressed by readable properties? + private fun propertiesSatisfyConstructor(constructorInformation: LocalConstructorInformation, properties: Map): Boolean { + if (!constructorInformation.hasParameters) return true + + val indicesAddressedByProperties = properties.values.asSequence().mapNotNull { + when (it) { + is LocalPropertyInformation.ConstructorPairedProperty -> it.constructorSlot.parameterIndex + is LocalPropertyInformation.PrivateConstructorPairedProperty -> it.constructorSlot.parameterIndex + else -> null + } + }.toSet() + + return (0 until constructorInformation.parameters.size).none { index -> + constructorInformation.parameters[index].isMandatory && index !in indicesAddressedByProperties + } + } + + private fun buildSuperclassInformation(type: Type): LocalTypeInformation = + resolveAndBuild(type.asClass().genericSuperclass) + + private fun buildInterfaceInformation(type: Type) = + type.allInterfaces.asSequence().mapNotNull { + if (it == type) return@mapNotNull null + resolveAndBuild(it) + }.toList() + + private val Type.allInterfaces: Set get() = exploreType(this) + + private fun exploreType(type: Type, interfaces: MutableSet = LinkedHashSet()): MutableSet { + val clazz = type.asClass() + + if (clazz.isInterface) { + // Ignore classes we've already seen, and stop exploring once we reach an excluded type. + if (clazz in interfaces || lookup.isExcluded(clazz)) return interfaces + else interfaces += type + } + + clazz.genericInterfaces.forEach { exploreType(it.resolveAgainstContext(), interfaces) } + if (clazz.genericSuperclass != null) exploreType(clazz.genericSuperclass.resolveAgainstContext(), interfaces) + + return interfaces + } + + private fun buildReadOnlyProperties(rawType: Class<*>): Map = + rawType.propertyDescriptors().asSequence().mapNotNull { (name, descriptor) -> + if (descriptor.field == null || descriptor.getter == null) null + else { + val paramType = (descriptor.getter.genericReturnType).resolveAgainstContext() + val paramTypeInformation = build(paramType, TypeIdentifier.forGenericType(paramType, resolutionContext + ?: rawType)) + val isMandatory = paramType.asClass().isPrimitive || !descriptor.getter.returnsNullable() + name to LocalPropertyInformation.ReadOnlyProperty(descriptor.getter, paramTypeInformation, isMandatory) + } + }.sortedBy { (name, _) -> name }.toMap(LinkedHashMap()) + + private fun buildObjectProperties(rawType: Class<*>, constructorInformation: LocalConstructorInformation): Map = + (calculatedProperties(rawType) + nonCalculatedProperties(rawType, constructorInformation)) + .sortedBy { (name, _) -> name } + .toMap(LinkedHashMap()) + + private fun nonCalculatedProperties(rawType: Class<*>, constructorInformation: LocalConstructorInformation): Sequence> = + if (constructorInformation.hasParameters) getConstructorPairedProperties(constructorInformation, rawType) + else getterSetterProperties(rawType) + + private fun getConstructorPairedProperties(constructorInformation: LocalConstructorInformation, rawType: Class<*>): Sequence> { + val constructorParameterIndices = constructorInformation.parameters.asSequence().mapIndexed { index, parameter -> + parameter.name to index + }.toMap() + + return rawType.propertyDescriptors().asSequence().mapNotNull { (name, descriptor) -> + val property = makeConstructorPairedProperty(constructorParameterIndices, name, descriptor, constructorInformation) + if (property == null) null else name to property + } + } + + private fun makeConstructorPairedProperty(constructorParameterIndices: Map, + name: String, + descriptor: PropertyDescriptor, + constructorInformation: LocalConstructorInformation): LocalPropertyInformation? { + val constructorIndex = constructorParameterIndices[name] ?: + // In some very rare cases we have a constructor parameter matched by a getter with no backing field, + // and cannot infer whether the property name should be capitalised or not. + constructorParameterIndices[name.decapitalize()] ?: return null + + if (descriptor.getter == null) { + if (descriptor.field == null) return null + val paramType = descriptor.field.genericType + val paramTypeInformation = resolveAndBuild(paramType) + + return LocalPropertyInformation.PrivateConstructorPairedProperty( + descriptor.field, + ConstructorSlot(constructorIndex, constructorInformation), + paramTypeInformation, + constructorInformation.parameters[constructorIndex].isMandatory) + } + + val paramType = descriptor.getter.genericReturnType + val paramTypeInformation = resolveAndBuild(paramType) + + return LocalPropertyInformation.ConstructorPairedProperty( + descriptor.getter, + ConstructorSlot(constructorIndex, constructorInformation), + paramTypeInformation, + descriptor.getter.returnType.isPrimitive || + !descriptor.getter.returnsNullable()) + } + + private fun getterSetterProperties(rawType: Class<*>): Sequence> = + rawType.propertyDescriptors().asSequence().mapNotNull { (name, descriptor) -> + if (descriptor.getter == null || descriptor.setter == null || descriptor.field == null) null + else { + val paramType = descriptor.getter.genericReturnType + val paramTypeInformation = resolveAndBuild(paramType) + val isMandatory = paramType.asClass().isPrimitive || !descriptor.getter.returnsNullable() + + name to LocalPropertyInformation.GetterSetterProperty( + descriptor.getter, + descriptor.setter, + paramTypeInformation, + isMandatory) + } + } + + private fun calculatedProperties(rawType: Class<*>): Sequence> = + rawType.calculatedPropertyDescriptors().asSequence().map { (name, v) -> + val paramType = v.getter!!.genericReturnType + val paramTypeInformation = resolveAndBuild(paramType) + val isMandatory = paramType.asClass().isPrimitive || !v.getter.returnsNullable() + + name to LocalPropertyInformation.CalculatedProperty(v.getter, paramTypeInformation, isMandatory) + } + + private fun buildTypeParameterInformation(type: ParameterizedType): List = + type.actualTypeArguments.map { + resolveAndBuild(it) + } + + private fun buildConstructorInformation(type: Type, observedConstructor: KFunction): LocalConstructorInformation { + if (observedConstructor.javaConstructor?.parameters?.getOrNull(0)?.name == "this$0") + throw NotSerializableException("Type '${type.typeName} has synthetic fields and is likely a nested inner class.") + + return LocalConstructorInformation(observedConstructor, observedConstructor.parameters.map { + val parameterType = it.type.javaType + LocalConstructorParameterInformation( + it.name ?: throw IllegalStateException("Unnamed parameter in constructor $observedConstructor"), + resolveAndBuild(parameterType), + parameterType.asClass().isPrimitive || !it.type.isMarkedNullable) + }) + } +} + +private fun Method.returnsNullable(): Boolean = try { + val returnTypeString = this.declaringClass.kotlin.memberProperties.firstOrNull { + it.javaGetter == this + }?.returnType?.toString() ?: "?" + + returnTypeString.endsWith('?') || returnTypeString.endsWith('!') +} catch (e: KotlinReflectionInternalError) { + // This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue + // is: https://youtrack.jetbrains.com/issue/KT-13077 + // TODO: Revisit this when Kotlin issue is fixed. + + true +} + +/** + * Code for finding the unique constructor we will use for deserialization. + * + * If any constructor is uniquely annotated with [@ConstructorForDeserialization], then that constructor is chosen. + * An error is reported if more than one constructor is annotated. + * + * Otherwise, if there is a Kotlin primary constructor, it selects that, and if not it selects either the unique + * constructor or, if there are two and one is the default no-argument constructor, the non-default constructor. + */ +private fun constructorForDeserialization(type: Type): KFunction? { + val clazz = type.asClass() + if (!clazz.isConcreteClass || clazz.isSynthetic) return null + + val kotlinCtors = clazz.kotlin.constructors + + val annotatedCtors = kotlinCtors.filter { it.findAnnotation() != null } + if (annotatedCtors.size > 1) return null + if (annotatedCtors.size == 1) return annotatedCtors.first().apply { isAccessible = true } + + val defaultCtor = kotlinCtors.firstOrNull { it.parameters.isEmpty() } + val nonDefaultCtors = kotlinCtors.filter { it != defaultCtor } + + val preferredCandidate = clazz.kotlin.primaryConstructor ?: + when(nonDefaultCtors.size) { + 1 -> nonDefaultCtors.first() + 0 -> defaultCtor + else -> null + } ?: return null + + return try { + preferredCandidate.apply { isAccessible = true } + } catch (e: SecurityException) { + null + } +} + +private fun evolverConstructors(type: Type): List> { + val clazz = type.asClass() + if (!clazz.isConcreteClass || clazz.isSynthetic) return emptyList() + + return clazz.kotlin.constructors.asSequence() + .mapNotNull { + val version = it.findAnnotation()?.version + if (version == null) null else version to it + } + .sortedBy { (version, ctor) -> version } + .map { (version, ctor) -> ctor.apply { isAccessible = true} } + .toList() +} \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeModel.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeModel.kt new file mode 100644 index 0000000000..9acdacb71e --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeModel.kt @@ -0,0 +1,91 @@ +package net.corda.serialization.internal.model + +import net.corda.core.serialization.ClassWhitelist +import net.corda.serialization.internal.amqp.* +import java.lang.reflect.* + +/** + * Provides a means for looking up [LocalTypeInformation] by [Type] and [TypeIdentifier], falling back to building it + * if the lookup can't supply it. + * + * The purpose of this class is to make a registry of [LocalTypeInformation] usable by a [LocalTypeInformationBuilder] that + * recursively builds [LocalTypeInformation] for all of the types visible by traversing the DAG of related types of a given + * [Type]. + */ +interface LocalTypeLookup { + + /** + * Either return the [LocalTypeInformation] held in the registry for the given [Type] and [TypeIdentifier] or, if + * no such information is registered, call the supplied builder to construct the type information, add it to the + * registry and then return it. + */ + fun findOrBuild(type: Type, typeIdentifier: TypeIdentifier, builder: (Boolean) -> LocalTypeInformation): LocalTypeInformation + + /** + * Indicates whether a type should be excluded from lists of interfaces associated with inspected types, i.e. + * because it is not whitelisted. + */ + fun isExcluded(type: Type): Boolean +} + +/** + * A [LocalTypeModel] maintains a registry of [LocalTypeInformation] for all [Type]s which have been observed within a + * given classloader context. + */ +interface LocalTypeModel { + /** + * Look for a [Type] in the registry, and return its associated [LocalTypeInformation] if found. If the [Type] is + * not in the registry, build [LocalTypeInformation] for that type, using this [LocalTypeModel] as the [LocalTypeLookup] + * for recursively resolving dependencies, place it in the registry, and return it. + * + * @param type The [Type] to get [LocalTypeInformation] for. + */ + fun inspect(type: Type): LocalTypeInformation + + /** + * Get [LocalTypeInformation] directly from the registry by [TypeIdentifier], returning null if no type information + * is registered for that identifier. + */ + operator fun get(typeIdentifier: TypeIdentifier): LocalTypeInformation? +} + +/** + * A [LocalTypeLookup] that is configurable with [LocalTypeModelConfiguration], which controls which types are seen as "opaque" + * and which are "excluded" (see docs for [LocalTypeModelConfiguration] for explanation of these terms. + * + * @param typeModelConfiguration Configuration controlling the behaviour of the [LocalTypeModel]'s type inspection. + */ +class ConfigurableLocalTypeModel(private val typeModelConfiguration: LocalTypeModelConfiguration): LocalTypeModel, LocalTypeLookup { + + private val typeInformationCache = DefaultCacheProvider.createCache() + + override fun isExcluded(type: Type): Boolean = typeModelConfiguration.isExcluded(type) + + override fun inspect(type: Type): LocalTypeInformation = LocalTypeInformation.forType(type, this) + + override fun findOrBuild(type: Type, typeIdentifier: TypeIdentifier, builder: (Boolean) -> LocalTypeInformation): LocalTypeInformation = + this[typeIdentifier] ?: builder(typeModelConfiguration.isOpaque(type)).apply { + typeInformationCache.putIfAbsent(typeIdentifier, this) + } + + override operator fun get(typeIdentifier: TypeIdentifier): LocalTypeInformation? = typeInformationCache[typeIdentifier] +} + +/** + * Configuration which controls how a [LocalTypeModel] inspects classes to build [LocalTypeInformation]. + */ +interface LocalTypeModelConfiguration { + /** + * [Type]s which are flagged as "opaque" are converted into instances of [LocalTypeInformation.Opaque] without + * further inspection - the type model doesn't attempt to inspect their superclass/interface hierarchy, locate + * constructors or enumerate their properties. Usually this will be because the type is handled by a custom + * serializer, so we don't need detailed information about it to help us build one. + */ + fun isOpaque(type: Type): Boolean + + /** + * [Type]s which are excluded are silently omitted from the superclass/interface hierarchy of other types' + * [LocalTypeInformation], usually because they are not included in a whitelist. + */ + fun isExcluded(type: Type): Boolean +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/RemoteTypeCarpenter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/RemoteTypeCarpenter.kt index f9e1341a23..75e4b9363d 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/RemoteTypeCarpenter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/RemoteTypeCarpenter.kt @@ -5,8 +5,6 @@ import net.corda.serialization.internal.carpenter.* import java.io.NotSerializableException import java.lang.reflect.Type -typealias PropertyName = String - /** * Constructs [Type]s using [RemoteTypeInformation]. */ diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/model/LocalTypeModelTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/model/LocalTypeModelTests.kt new file mode 100644 index 0000000000..d14646337f --- /dev/null +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/model/LocalTypeModelTests.kt @@ -0,0 +1,166 @@ +package net.corda.serialization.internal.model + +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.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import java.lang.reflect.Type +import java.time.LocalDateTime +import java.util.* + +class LocalTypeModelTests { + + private val descriptorBasedSerializerRegistry = DefaultDescriptorBasedSerializerRegistry() + private val customSerializerRegistry: CustomSerializerRegistry = CachingCustomSerializerRegistry(descriptorBasedSerializerRegistry) + private val model = ConfigurableLocalTypeModel(WhitelistBasedTypeModelConfiguration(AllWhitelist, customSerializerRegistry)) + + interface CollectionHolder { + val list: List + val map: Map + val array: Array> + } + + open class StringKeyedCollectionHolder(override val list: List, override val map: Map, override val array: Array>) : CollectionHolder + + class StringCollectionHolder(list: List, map: Map, array: Array>) : StringKeyedCollectionHolder(list, map, array) + + @Suppress("unused") + class Nested( + val collectionHolder: StringKeyedCollectionHolder?, + private val intArray: IntArray, + optionalParam: Short?) + + // This can't be treated as a composable type, because the [intArray] parameter is mandatory but we have no readable + // field or property to populate it from. + @Suppress("unused") + class NonComposableNested(val collectionHolder: StringKeyedCollectionHolder?, intArray: IntArray) + + @Test + fun `Primitives and collections`() { + assertInformation>("CollectionHolder") + + assertInformation>(""" + StringKeyedCollectionHolder(list: List, map: Map, array: List[]): CollectionHolder + array: List[] + list: List + map: Map + """) + + assertInformation(""" + StringCollectionHolder(list: List, map: Map, array: List[]): StringKeyedCollectionHolder, CollectionHolder + array: List[] + list: List + map: Map + """) + + assertInformation(""" + Nested(collectionHolder: StringKeyedCollectionHolder?, intArray: int[], optionalParam: Short?) + collectionHolder (optional): StringKeyedCollectionHolder(list: List, map: Map, array: List[]): CollectionHolder + array: List[] + list: List + map: Map + intArray: int[] + """) + + assertInformation("NonComposableNested") + } + + interface SuperSuper { + val a: A + val b: B + } + + interface Super : SuperSuper { + val c: List + } + + abstract class Abstract(override val a: Array, override val b: Double) : Super> + + class Concrete(a: Array, b: Double, override val c: List>, val d: Int) : Abstract(a, b) + + @Test + fun `interfaces and superclasses`() { + assertInformation>("SuperSuper") + assertInformation>("Super: SuperSuper") + assertInformation>(""" + Abstract: Super, SuperSuper + a: LocalDateTime[] + b: Double + """) + assertInformation(""" + Concrete(a: Integer[], b: double, c: List, d: int): Abstract, Super, SuperSuper + a: Integer[] + b: Double + c: List + d: int + """) + } + + interface OldStylePojo { + var a: A? + var b: String + @get:SerializableCalculatedProperty + val c: String + } + + class OldStylePojoImpl : OldStylePojo { + override var a: IntArray? = null + override var b: String = "" + override val c: String = a.toString() + b + } + + @Test + fun `getter setter and calculated properties`() { + assertInformation(""" + OldStylePojoImpl(): OldStylePojo + a (optional): int[] + b: String + c (calculated): String + """) + } + + class AliasingOldStylePojoImpl(override var a: String?, override var b: String, override val c: String): OldStylePojo + + @Test + fun `calculated properties aliased by fields in implementing classes`() { + assertInformation(""" + AliasingOldStylePojoImpl(a: String?, b: String, c: String): OldStylePojo + a (optional): String + b: String + c: String + """) + } + + class TransitivelyNonComposable(val a: String, val b: Exception) + + @Test + fun `non-composable types`() { + val serializerRegistry = object: CustomSerializerRegistry { + override fun register(customSerializer: CustomSerializer) {} + + override fun registerExternal(customSerializer: CorDappCustomSerializer) {} + + override fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer? = null + } + val modelWithoutOpacity = ConfigurableLocalTypeModel(WhitelistBasedTypeModelConfiguration(AllWhitelist, serializerRegistry) ) + assertTrue(modelWithoutOpacity.inspect(typeOf()) is LocalTypeInformation.NonComposable) + assertTrue(modelWithoutOpacity.inspect(typeOf()) is LocalTypeInformation.NonComposable) + } + + private inline fun assertInformation(expected: String) { + assertEquals(expected.trimIndent(), model.inspect(typeOf()).prettyPrint()) + } + + /** + * Handy for seeing what the inspector/pretty printer actually outputs for a type + */ + @Suppress("unused") + private inline fun printInformation() { + println(model.inspect(typeOf()).prettyPrint()) + } + + private inline fun typeOf(): Type = object : TypeToken() {}.type +} \ No newline at end of file