mirror of
https://github.com/corda/corda.git
synced 2024-12-19 21:17:58 +00:00
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
This commit is contained in:
parent
349d9a5ffe
commit
ddf45f4e07
@ -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<out Any>)
|
||||
fun registerExternal(customSerializer: CorDappCustomSerializer)
|
||||
|
||||
fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer<Any>?
|
||||
}
|
||||
|
||||
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<CustomSerializerIdentifier, AMQPSerializer<Any>> = DefaultCacheProvider.createCache()
|
||||
private var customSerializers: List<SerializerFor> = 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<out Any>) {
|
||||
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<Any>? {
|
||||
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<Any>? {
|
||||
// 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<Any>
|
||||
} else {
|
||||
// Make a subclass serializer for the subclass and return that...
|
||||
CustomSerializer.SubClass(clazz, uncheckedCast(customSerializer))
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
@ -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<Any>?
|
||||
operator fun set(descriptor: String, serializer: AMQPSerializer<Any>)
|
||||
fun getOrBuild(descriptor: String, builder: () -> AMQPSerializer<Any>): AMQPSerializer<Any>
|
||||
}
|
||||
|
||||
class DefaultDescriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry {
|
||||
|
||||
private val registry: MutableMap<String, AMQPSerializer<Any>> = DefaultCacheProvider.createCache()
|
||||
|
||||
override fun get(descriptor: String): AMQPSerializer<Any>? = registry[descriptor]
|
||||
|
||||
override fun set(descriptor: String, serializer: AMQPSerializer<Any>) {
|
||||
registry.putIfAbsent(descriptor, serializer)
|
||||
}
|
||||
|
||||
override fun getOrBuild(descriptor: String, builder: () -> AMQPSerializer<Any>) =
|
||||
get(descriptor) ?: builder().also { newSerializer -> this[descriptor] = newSerializer }
|
||||
}
|
@ -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
|
||||
)
|
@ -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")
|
||||
}
|
@ -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<? extends String>` becomes `List<String>`.
|
||||
*
|
||||
* 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<String>,
|
||||
val interfaces: List<LocalTypeInformation>): 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<PropertyName, LocalPropertyInformation>,
|
||||
val interfaces: List<LocalTypeInformation>,
|
||||
val typeParameters: List<LocalTypeInformation>) : 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<PropertyName, LocalPropertyInformation>,
|
||||
val superclass: LocalTypeInformation,
|
||||
val interfaces: List<LocalTypeInformation>,
|
||||
val typeParameters: List<LocalTypeInformation>) : 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>) : 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<EvolverConstructorInformation>,
|
||||
val properties: Map<PropertyName, LocalPropertyInformation>,
|
||||
val superclass: LocalTypeInformation,
|
||||
val interfaces: List<LocalTypeInformation>,
|
||||
val typeParameters: List<LocalTypeInformation>) : 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<PropertyName, LocalPropertyInformation>,
|
||||
val superclass: LocalTypeInformation,
|
||||
val interfaces: List<LocalTypeInformation>,
|
||||
val typeParameters: List<LocalTypeInformation>) : 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<Any>,
|
||||
val parameters: List<LocalConstructorParameterInformation>) {
|
||||
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<String, LocalPropertyInformation>)
|
||||
|
||||
/**
|
||||
* 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<LocalTypeInformation>, 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<String, LocalPropertyInformation>) =
|
||||
properties.entries.asSequence().sortedBy { it.key }.joinToString("\n", "\n", "") {
|
||||
it.prettyPrint()
|
||||
}
|
||||
|
||||
private fun Map.Entry<String, LocalPropertyInformation>.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()
|
||||
}
|
||||
|
@ -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<TypeIdentifier> = 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>): LocalTypeInformation.Abstract =
|
||||
LocalTypeInformation.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)
|
||||
|
||||
private inline fun <T> 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>): 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<PropertyName, LocalPropertyInformation>): 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<Type> get() = exploreType(this)
|
||||
|
||||
private fun exploreType(type: Type, interfaces: MutableSet<Type> = LinkedHashSet()): MutableSet<Type> {
|
||||
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<PropertyName, LocalPropertyInformation> =
|
||||
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<PropertyName, LocalPropertyInformation> =
|
||||
(calculatedProperties(rawType) + nonCalculatedProperties(rawType, constructorInformation))
|
||||
.sortedBy { (name, _) -> name }
|
||||
.toMap(LinkedHashMap())
|
||||
|
||||
private fun nonCalculatedProperties(rawType: Class<*>, constructorInformation: LocalConstructorInformation): Sequence<Pair<String, LocalPropertyInformation>> =
|
||||
if (constructorInformation.hasParameters) getConstructorPairedProperties(constructorInformation, rawType)
|
||||
else getterSetterProperties(rawType)
|
||||
|
||||
private fun getConstructorPairedProperties(constructorInformation: LocalConstructorInformation, rawType: Class<*>): Sequence<Pair<String, LocalPropertyInformation>> {
|
||||
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<String, Int>,
|
||||
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<Pair<String, LocalPropertyInformation>> =
|
||||
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<Pair<String, LocalPropertyInformation>> =
|
||||
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<LocalTypeInformation> =
|
||||
type.actualTypeArguments.map {
|
||||
resolveAndBuild(it)
|
||||
}
|
||||
|
||||
private fun buildConstructorInformation(type: Type, observedConstructor: KFunction<Any>): 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<Any>? {
|
||||
val clazz = type.asClass()
|
||||
if (!clazz.isConcreteClass || clazz.isSynthetic) return null
|
||||
|
||||
val kotlinCtors = clazz.kotlin.constructors
|
||||
|
||||
val annotatedCtors = kotlinCtors.filter { it.findAnnotation<ConstructorForDeserialization>() != 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<KFunction<Any>> {
|
||||
val clazz = type.asClass()
|
||||
if (!clazz.isConcreteClass || clazz.isSynthetic) return emptyList()
|
||||
|
||||
return clazz.kotlin.constructors.asSequence()
|
||||
.mapNotNull {
|
||||
val version = it.findAnnotation<DeprecatedConstructorForDeserialization>()?.version
|
||||
if (version == null) null else version to it
|
||||
}
|
||||
.sortedBy { (version, ctor) -> version }
|
||||
.map { (version, ctor) -> ctor.apply { isAccessible = true} }
|
||||
.toList()
|
||||
}
|
@ -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<TypeIdentifier, LocalTypeInformation>()
|
||||
|
||||
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
|
||||
}
|
@ -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].
|
||||
*/
|
||||
|
@ -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<K, V> {
|
||||
val list: List<V>
|
||||
val map: Map<K, V>
|
||||
val array: Array<List<V>>
|
||||
}
|
||||
|
||||
open class StringKeyedCollectionHolder<T>(override val list: List<T>, override val map: Map<String, T>, override val array: Array<List<T>>) : CollectionHolder<String, T>
|
||||
|
||||
class StringCollectionHolder(list: List<String>, map: Map<String, String>, array: Array<List<String>>) : StringKeyedCollectionHolder<String>(list, map, array)
|
||||
|
||||
@Suppress("unused")
|
||||
class Nested(
|
||||
val collectionHolder: StringKeyedCollectionHolder<out Int>?,
|
||||
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<out Int>?, intArray: IntArray)
|
||||
|
||||
@Test
|
||||
fun `Primitives and collections`() {
|
||||
assertInformation<CollectionHolder<UUID, LocalDateTime>>("CollectionHolder<UUID, LocalDateTime>")
|
||||
|
||||
assertInformation<StringKeyedCollectionHolder<Int>>("""
|
||||
StringKeyedCollectionHolder<Integer>(list: List<Integer>, map: Map<String, Integer>, array: List<Integer>[]): CollectionHolder<String, Integer>
|
||||
array: List<Integer>[]
|
||||
list: List<Integer>
|
||||
map: Map<String, Integer>
|
||||
""")
|
||||
|
||||
assertInformation<StringCollectionHolder>("""
|
||||
StringCollectionHolder(list: List<String>, map: Map<String, String>, array: List<String>[]): StringKeyedCollectionHolder<String>, CollectionHolder<String, String>
|
||||
array: List<String>[]
|
||||
list: List<String>
|
||||
map: Map<String, String>
|
||||
""")
|
||||
|
||||
assertInformation<Nested>("""
|
||||
Nested(collectionHolder: StringKeyedCollectionHolder<Integer>?, intArray: int[], optionalParam: Short?)
|
||||
collectionHolder (optional): StringKeyedCollectionHolder<Integer>(list: List<Integer>, map: Map<String, Integer>, array: List<Integer>[]): CollectionHolder<String, Integer>
|
||||
array: List<Integer>[]
|
||||
list: List<Integer>
|
||||
map: Map<String, Integer>
|
||||
intArray: int[]
|
||||
""")
|
||||
|
||||
assertInformation<NonComposableNested>("NonComposableNested")
|
||||
}
|
||||
|
||||
interface SuperSuper<A, B> {
|
||||
val a: A
|
||||
val b: B
|
||||
}
|
||||
|
||||
interface Super<C> : SuperSuper<C, Double> {
|
||||
val c: List<C>
|
||||
}
|
||||
|
||||
abstract class Abstract<T>(override val a: Array<T>, override val b: Double) : Super<Array<T>>
|
||||
|
||||
class Concrete(a: Array<Int>, b: Double, override val c: List<Array<Int>>, val d: Int) : Abstract<Int>(a, b)
|
||||
|
||||
@Test
|
||||
fun `interfaces and superclasses`() {
|
||||
assertInformation<SuperSuper<Int, Int>>("SuperSuper<Integer, Integer>")
|
||||
assertInformation<Super<UUID>>("Super<UUID>: SuperSuper<UUID, Double>")
|
||||
assertInformation<Abstract<LocalDateTime>>("""
|
||||
Abstract<LocalDateTime>: Super<LocalDateTime[]>, SuperSuper<LocalDateTime[], Double>
|
||||
a: LocalDateTime[]
|
||||
b: Double
|
||||
""")
|
||||
assertInformation<Concrete>("""
|
||||
Concrete(a: Integer[], b: double, c: List<Integer[]>, d: int): Abstract<Integer>, Super<Integer[]>, SuperSuper<Integer[], Double>
|
||||
a: Integer[]
|
||||
b: Double
|
||||
c: List<Integer[]>
|
||||
d: int
|
||||
""")
|
||||
}
|
||||
|
||||
interface OldStylePojo<A> {
|
||||
var a: A?
|
||||
var b: String
|
||||
@get:SerializableCalculatedProperty
|
||||
val c: String
|
||||
}
|
||||
|
||||
class OldStylePojoImpl : OldStylePojo<IntArray> {
|
||||
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>("""
|
||||
OldStylePojoImpl(): OldStylePojo<int[]>
|
||||
a (optional): int[]
|
||||
b: String
|
||||
c (calculated): String
|
||||
""")
|
||||
}
|
||||
|
||||
class AliasingOldStylePojoImpl(override var a: String?, override var b: String, override val c: String): OldStylePojo<String>
|
||||
|
||||
@Test
|
||||
fun `calculated properties aliased by fields in implementing classes`() {
|
||||
assertInformation<AliasingOldStylePojoImpl>("""
|
||||
AliasingOldStylePojoImpl(a: String?, b: String, c: String): OldStylePojo<String>
|
||||
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<out Any>) {}
|
||||
|
||||
override fun registerExternal(customSerializer: CorDappCustomSerializer) {}
|
||||
|
||||
override fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer<Any>? = null
|
||||
}
|
||||
val modelWithoutOpacity = ConfigurableLocalTypeModel(WhitelistBasedTypeModelConfiguration(AllWhitelist, serializerRegistry) )
|
||||
assertTrue(modelWithoutOpacity.inspect(typeOf<Exception>()) is LocalTypeInformation.NonComposable)
|
||||
assertTrue(modelWithoutOpacity.inspect(typeOf<TransitivelyNonComposable>()) is LocalTypeInformation.NonComposable)
|
||||
}
|
||||
|
||||
private inline fun <reified T> assertInformation(expected: String) {
|
||||
assertEquals(expected.trimIndent(), model.inspect(typeOf<T>()).prettyPrint())
|
||||
}
|
||||
|
||||
/**
|
||||
* Handy for seeing what the inspector/pretty printer actually outputs for a type
|
||||
*/
|
||||
@Suppress("unused")
|
||||
private inline fun <reified T> printInformation() {
|
||||
println(model.inspect(typeOf<T>()).prettyPrint())
|
||||
}
|
||||
|
||||
private inline fun <reified T> typeOf(): Type = object : TypeToken<T>() {}.type
|
||||
}
|
Loading…
Reference in New Issue
Block a user