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:
Dominic Fox 2018-11-19 16:07:01 +00:00 committed by GitHub
parent 349d9a5ffe
commit ddf45f4e07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1280 additions and 2 deletions

View File

@ -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
}
}

View File

@ -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 }
}

View File

@ -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
)

View File

@ -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")
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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].
*/

View File

@ -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
}