CORDA-2099 remote type model (#4179)

* CORDA-2099 create remote type model

* Comments

* Test the class-carpenting type loader

* Comment on cache usage

* Pull changes from main serialisation branch

* Add missing file

* Minor tweaks
This commit is contained in:
Dominic Fox 2018-11-19 11:03:32 +00:00 committed by GitHub
parent 8ea6f1c7c5
commit f66944cac5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1449 additions and 20 deletions

View File

@ -0,0 +1,209 @@
package net.corda.serialization.internal.amqp
import net.corda.serialization.internal.model.*
import java.io.NotSerializableException
import java.util.*
/**
* Interprets AMQP [Schema] information to obtain [RemoteTypeInformation], caching by [TypeDescriptor].
*/
class AMQPRemoteTypeModel {
private val cache: MutableMap<TypeDescriptor, RemoteTypeInformation> = DefaultCacheProvider.createCache()
/**
* Interpret a [Schema] to obtain a [Map] of all of the [RemoteTypeInformation] contained therein, indexed by
* [TypeDescriptor].
*
* A [Schema] contains a set of [TypeNotation]s, which we recursively convert into [RemoteTypeInformation],
* associating each new piece of [RemoteTypeInformation] with the [TypeDescriptor] attached to it in the schema.
*
* We start by building a [Map] of [TypeNotation] by [TypeIdentifier], using [AMQPTypeIdentifierParser] to convert
* AMQP type names into [TypeIdentifier]s. This is used as a lookup for resolving notations that are referred to by
* type name from other notations, e.g. the types of properties.
*
* We also build a [Map] of [TypeNotation] by [TypeDescriptor], which we then convert into [RemoteTypeInformation]
* while merging with the cache.
*/
fun interpret(serializationSchemas: SerializationSchemas): Map<TypeDescriptor, RemoteTypeInformation> {
val (schema, transforms) = serializationSchemas
val notationLookup = schema.types.associateBy { it.name.typeIdentifier }
val byTypeDescriptor = schema.types.associateBy { it.typeDescriptor }
val enumTransformsLookup = transforms.types.asSequence().map { (name, transformSet) ->
name.typeIdentifier to interpretTransformSet(transformSet)
}.toMap()
val interpretationState = InterpretationState(notationLookup, enumTransformsLookup, cache, emptySet())
return byTypeDescriptor.mapValues { (typeDescriptor, typeNotation) ->
cache.getOrPut(typeDescriptor) { interpretationState.run { typeNotation.name.typeIdentifier.interpretIdentifier() } }
}
}
data class InterpretationState(val notationLookup: Map<TypeIdentifier, TypeNotation>,
val enumTransformsLookup: Map<TypeIdentifier, EnumTransforms>,
val cache: MutableMap<TypeDescriptor, RemoteTypeInformation>,
val seen: Set<TypeIdentifier>) {
private inline fun <T> forgetSeen(block: InterpretationState.() -> T): T =
withSeen(emptySet(), block)
private inline fun <T> withSeen(typeIdentifier: TypeIdentifier, block: InterpretationState.() -> T): T =
withSeen(seen + typeIdentifier, block)
private inline fun <T> withSeen(seen: Set<TypeIdentifier>, block: InterpretationState.() -> T): T =
copy(seen = seen).run(block)
/**
* Follow a [TypeIdentifier] to the [TypeNotation] associated with it in the lookup, and interpret that notation.
* If there is no such notation, interpret the [TypeIdentifier] directly into [RemoteTypeInformation].
*
* If we have visited this [TypeIdentifier] before while traversing the graph of related [TypeNotation]s, then we
* know we have hit a cycle and respond accordingly.
*/
fun TypeIdentifier.interpretIdentifier(): RemoteTypeInformation =
if (this in seen) RemoteTypeInformation.Cycle(this) { forgetSeen { interpretIdentifier() } }
else withSeen(this) {
val identifier = this@interpretIdentifier
notationLookup[identifier]?.interpretNotation(identifier) ?: interpretNoNotation()
}
/**
* Either fetch from the cache, or interpret, cache, and return, the [RemoteTypeInformation] corresponding to this
* [TypeNotation].
*/
private fun TypeNotation.interpretNotation(identifier: TypeIdentifier): RemoteTypeInformation =
cache.getOrPut(typeDescriptor) {
when (this) {
is CompositeType -> interpretComposite(identifier)
is RestrictedType -> interpretRestricted(identifier)
}
}
/**
* Interpret the properties, interfaces and type parameters in this [TypeNotation], and return suitable
* [RemoteTypeInformation].
*/
private fun CompositeType.interpretComposite(identifier: TypeIdentifier): RemoteTypeInformation {
val properties = fields.asSequence().map { it.interpret() }.toMap()
val typeParameters = identifier.interpretTypeParameters()
val interfaceIdentifiers = provides.map { name -> name.typeIdentifier }
val isInterface = identifier in interfaceIdentifiers
val interfaces = interfaceIdentifiers.mapNotNull { interfaceIdentifier ->
if (interfaceIdentifier == identifier) null
else interfaceIdentifier.interpretIdentifier()
}
return if (isInterface) RemoteTypeInformation.AnInterface(typeDescriptor, identifier, properties, interfaces, typeParameters)
else RemoteTypeInformation.Composable(typeDescriptor, identifier, properties, interfaces, typeParameters)
}
/**
* Type parameters are read off from the [TypeIdentifier] we translated the AMQP type name into.
*/
private fun TypeIdentifier.interpretTypeParameters(): List<RemoteTypeInformation> = when (this) {
is TypeIdentifier.Parameterised -> parameters.map { it.interpretIdentifier() }
else -> emptyList()
}
/**
* Interpret a [RestrictedType] into suitable [RemoteTypeInformation].
*/
private fun RestrictedType.interpretRestricted(identifier: TypeIdentifier): RemoteTypeInformation = when (identifier) {
is TypeIdentifier.Parameterised ->
RemoteTypeInformation.Parameterised(
typeDescriptor,
identifier,
identifier.interpretTypeParameters())
is TypeIdentifier.ArrayOf ->
RemoteTypeInformation.AnArray(
typeDescriptor,
identifier,
identifier.componentType.interpretIdentifier())
is TypeIdentifier.Unparameterised ->
if (choices.isEmpty()) {
RemoteTypeInformation.Unparameterised(
typeDescriptor,
identifier)
} else RemoteTypeInformation.AnEnum(
typeDescriptor,
identifier,
choices.map { it.name },
enumTransformsLookup[identifier] ?: EnumTransforms.empty)
else -> throw NotSerializableException("Cannot interpret restricted type $this")
}
/**
* Interpret a [Field] into a name/[RemotePropertyInformation] pair.
*/
private fun Field.interpret(): Pair<String, RemotePropertyInformation> {
val identifier = type.typeIdentifier
// A type of "*" is replaced with the value of the "requires" field
val fieldTypeIdentifier = if (identifier == TypeIdentifier.TopType && !requires.isEmpty()) {
requires[0].typeIdentifier
} else identifier
// We convert Java Object types to Java primitive types if the field is mandatory.
val fieldType = fieldTypeIdentifier.forcePrimitive(mandatory).interpretIdentifier()
return name to RemotePropertyInformation(
fieldType,
mandatory)
}
/**
* If there is no [TypeNotation] in the [Schema] matching a given [TypeIdentifier], we interpret the [TypeIdentifier]
* directly.
*/
private fun TypeIdentifier.interpretNoNotation(): RemoteTypeInformation =
when (this) {
is TypeIdentifier.TopType -> RemoteTypeInformation.Top
is TypeIdentifier.UnknownType -> RemoteTypeInformation.Unknown
is TypeIdentifier.ArrayOf ->
RemoteTypeInformation.AnArray(
name,
this,
componentType.interpretIdentifier())
is TypeIdentifier.Parameterised ->
RemoteTypeInformation.Parameterised(
name,
this,
parameters.map { it.interpretIdentifier() })
else -> RemoteTypeInformation.Unparameterised(name, this)
}
}
}
private fun interpretTransformSet(transformSet: EnumMap<TransformTypes, MutableList<Transform>>): EnumTransforms {
val defaultTransforms = transformSet[TransformTypes.EnumDefault]?.toList() ?: emptyList()
val defaults = defaultTransforms.associate { transform -> (transform as EnumDefaultSchemaTransform).new to transform.old }
val renameTransforms = transformSet[TransformTypes.Rename]?.toList() ?: emptyList()
val renames = renameTransforms.associate { transform -> (transform as RenameSchemaTransform).to to transform.from }
return EnumTransforms(defaults, renames)
}
private val TypeNotation.typeDescriptor: String get() = descriptor.name?.toString() ?:
throw NotSerializableException("Type notation has no type descriptor: $this")
private val String.typeIdentifier get(): TypeIdentifier = AMQPTypeIdentifierParser.parse(this)
/**
* Force e.g. [java.lang.Integer] to `int`, if it is the type of a mandatory field.
*/
private fun TypeIdentifier.forcePrimitive(mandatory: Boolean) =
if (mandatory) primitives[this] ?: this
else this
private val primitives = sequenceOf(
Boolean::class,
Byte::class,
Char::class,
Int::class,
Short::class,
Long::class,
Float::class,
Double::class).associate {
TypeIdentifier.forClass(it.javaObjectType) to TypeIdentifier.forClass(it.javaPrimitiveType!!)
}

View File

@ -0,0 +1,190 @@
package net.corda.serialization.internal.amqp
import com.google.common.primitives.Primitives
import net.corda.serialization.internal.model.TypeIdentifier
import org.apache.qpid.proton.amqp.*
import java.io.NotSerializableException
import java.lang.StringBuilder
import java.util.*
/**
* Thrown if the type string parser enters an illegal state.
*/
class IllegalTypeNameParserStateException(message: String): NotSerializableException(message)
/**
* Provides a state machine which knows how to parse AMQP type strings into [TypeIdentifier]s.
*/
object AMQPTypeIdentifierParser {
internal const val MAX_TYPE_PARAM_DEPTH = 32
private const val MAX_ARRAY_DEPTH = 32
/**
* Given a string representing a serialized AMQP type, construct a TypeIdentifier for that string.
*
* @param typeString The AMQP type string to parse
* @return A [TypeIdentifier] representing the type represented by the input string.
*/
fun parse(typeString: String): TypeIdentifier {
validate(typeString)
return typeString.fold<ParseState>(ParseState.ParsingRawType(null)) { state, c ->
state.accept(c)
}.getTypeIdentifier()
}
// Make sure our inputs aren't designed to blow things up.
private fun validate(typeString: String) {
var maxTypeParamDepth = 0
var typeParamdepth = 0
var maxArrayDepth = 0
var wasArray = false
var arrayDepth = 0
for (c in typeString) {
if (c.isWhitespace() || c.isJavaIdentifierPart() || c.isJavaIdentifierStart() ||
c == '.' || c == ',' || c == '?' || c == '*') continue
when(c) {
'<' -> maxTypeParamDepth = Math.max(++typeParamdepth, typeParamdepth)
'>' -> typeParamdepth--
'[' -> {
arrayDepth = if (wasArray) arrayDepth + 2 else 1
maxArrayDepth = Math.max(maxArrayDepth,arrayDepth)
}
']' -> arrayDepth--
else -> throw IllegalTypeNameParserStateException("Type name '$typeString' contains illegal character '$c'")
}
wasArray = c == ']'
}
if (maxTypeParamDepth >= MAX_TYPE_PARAM_DEPTH)
throw IllegalTypeNameParserStateException("Nested depth of type parameters exceeds maximum of $MAX_TYPE_PARAM_DEPTH")
if (maxArrayDepth >= MAX_ARRAY_DEPTH)
throw IllegalTypeNameParserStateException("Nested depth of arrays exceeds maximum of $MAX_ARRAY_DEPTH")
}
private sealed class ParseState {
abstract val parent: ParseState.ParsingParameterList?
abstract fun accept(c: Char): ParseState
abstract fun getTypeIdentifier(): TypeIdentifier
fun unexpected(c: Char): ParseState = throw IllegalTypeNameParserStateException("Unexpected character: '$c'")
fun notInParameterList(c: Char): ParseState =
throw IllegalTypeNameParserStateException("'$c' encountered, but not parsing type parameter list")
/**
* We are parsing a raw type name, either at the top level or as part of a list of type parameters.
*/
data class ParsingRawType(override val parent: ParseState.ParsingParameterList?, val buffer: StringBuilder = StringBuilder()) : ParseState() {
override fun accept(c: Char) = when (c) {
',' ->
if (parent == null) notInParameterList(c)
else ParsingRawType(parent.addParameter(getTypeIdentifier()))
'[' -> ParsingArray(getTypeIdentifier(), parent)
']' -> unexpected(c)
'<' -> ParsingRawType(ParsingParameterList(getTypeName(), parent))
'>' -> parent?.addParameter(getTypeIdentifier())?.accept(c) ?: notInParameterList(c)
else -> apply { buffer.append(c) }
}
private fun getTypeName(): String {
val typeName = buffer.toString().trim()
if (typeName.contains(' '))
throw IllegalTypeNameParserStateException("Illegal whitespace in type name $typeName")
return typeName
}
override fun getTypeIdentifier(): TypeIdentifier {
val typeName = getTypeName()
return when (typeName) {
"*" -> TypeIdentifier.TopType
"?" -> TypeIdentifier.UnknownType
in simplified -> simplified[typeName]!!
else -> TypeIdentifier.Unparameterised(typeName)
}
}
}
/**
* We are parsing a parameter list, and expect either to start a new parameter, add array-ness to the last
* parameter we have, or end the list.
*/
data class ParsingParameterList(val typeName: String, override val parent: ParsingParameterList?, val parameters: List<TypeIdentifier> = emptyList()) : ParseState() {
override fun accept(c: Char) = when (c) {
' ' -> this
',' -> ParsingRawType(this)
'[' ->
if (parameters.isEmpty()) unexpected(c)
else ParsingArray(
// Start adding array-ness to the last parameter we have.
parameters[parameters.lastIndex],
// Take a copy of this state, dropping the last parameter which will be added back on
// when array parsing completes.
copy(parameters = parameters.subList(0, parameters.lastIndex)))
'>' -> parent?.addParameter(getTypeIdentifier()) ?: Complete(getTypeIdentifier())
else -> unexpected(c)
}
fun addParameter(parameter: TypeIdentifier) = copy(parameters = parameters + parameter)
override fun getTypeIdentifier() = TypeIdentifier.Parameterised(typeName, null, parameters)
}
/**
* We are adding array-ness to some type identifier.
*/
data class ParsingArray(val componentType: TypeIdentifier, override val parent: ParseState.ParsingParameterList?) : ParseState() {
override fun accept(c: Char) = when (c) {
' ' -> this
'p' -> ParsingArray(forcePrimitive(componentType), parent)
']' -> parent?.addParameter(getTypeIdentifier()) ?: Complete(getTypeIdentifier())
else -> unexpected(c)
}
override fun getTypeIdentifier() = TypeIdentifier.ArrayOf(componentType)
private fun forcePrimitive(componentType: TypeIdentifier): TypeIdentifier =
TypeIdentifier.forClass(Primitives.unwrap(componentType.getLocalType().asClass()))
}
/**
* We have a complete type identifier, and all we can do to it is add array-ness.
*/
data class Complete(val identifier: TypeIdentifier) : ParseState() {
override val parent: ParseState.ParsingParameterList? get() = null
override fun accept(c: Char): ParseState = when (c) {
' ' -> this
'[' -> ParsingArray(identifier, null)
else -> unexpected(c)
}
override fun getTypeIdentifier() = identifier
}
}
private val simplified = mapOf(
"string" to String::class,
"boolean" to Boolean::class,
"byte" to Byte::class,
"char" to Char::class,
"int" to Int::class,
"short" to Short::class,
"long" to Long::class,
"double" to Double::class,
"float" to Float::class,
"ubyte" to UnsignedByte::class,
"uint" to UnsignedInteger::class,
"ushort" to UnsignedShort::class,
"ulong" to UnsignedLong::class,
"decimal32" to Decimal32::class,
"decimal64" to Decimal64::class,
"decimal128" to Decimal128::class,
"binary" to ByteArray::class,
"timestamp" to Date::class,
"uuid" to UUID::class,
"symbol" to Symbol::class).mapValues { (_, v) ->
TypeIdentifier.forClass(v.javaObjectType)
}
}

View File

@ -0,0 +1,63 @@
package net.corda.serialization.internal.amqp
import net.corda.serialization.internal.model.TypeIdentifier
import org.apache.qpid.proton.amqp.*
import java.lang.reflect.Type
import java.util.*
object AMQPTypeIdentifiers {
fun isPrimitive(type: Type): Boolean = isPrimitive(TypeIdentifier.forGenericType(type))
fun isPrimitive(typeIdentifier: TypeIdentifier) = typeIdentifier in primitiveTypeNamesByName
fun primitiveTypeName(type: Type): String? =
primitiveTypeNamesByName[TypeIdentifier.forGenericType(type)]
private val primitiveTypeNamesByName = sequenceOf(
Character::class to "char",
Char::class to "char",
Boolean::class to "boolean",
Byte::class to "byte",
UnsignedByte::class to "ubyte",
Short::class to "short",
UnsignedShort::class to "ushort",
Int::class to "int",
UnsignedInteger::class to "uint",
Long::class to "long",
UnsignedLong::class to "ulong",
Float::class to "float",
Double::class to "double",
Decimal32::class to "decimal32",
Decimal64::class to "decimal62",
Decimal128::class to "decimal128",
Date::class to "timestamp",
UUID::class to "uuid",
ByteArray::class to "binary",
String::class to "string",
Symbol::class to "symbol")
.flatMap { (klass, name) ->
val typeIdentifier = TypeIdentifier.forClass(klass.javaObjectType)
val primitiveTypeIdentifier = klass.javaPrimitiveType?.let { TypeIdentifier.forClass(it) }
if (primitiveTypeIdentifier == null) sequenceOf(typeIdentifier to name)
else sequenceOf(typeIdentifier to name, primitiveTypeIdentifier to name)
}.toMap()
fun nameForType(typeIdentifier: TypeIdentifier): String = when(typeIdentifier) {
is TypeIdentifier.Erased -> typeIdentifier.name
is TypeIdentifier.Unparameterised -> primitiveTypeNamesByName[typeIdentifier] ?: typeIdentifier.name
is TypeIdentifier.UnknownType,
is TypeIdentifier.TopType -> "?"
is TypeIdentifier.ArrayOf ->
if (typeIdentifier == primitiveByteArrayType) "binary"
else nameForType(typeIdentifier.componentType) +
if (typeIdentifier.componentType is TypeIdentifier.Unparameterised &&
typeIdentifier.componentType.isPrimitive) "[p]"
else "[]"
is TypeIdentifier.Parameterised -> typeIdentifier.name + typeIdentifier.parameters.joinToString(", ", "<", ">") {
nameForType(it)
}
}
private val primitiveByteArrayType = TypeIdentifier.ArrayOf(TypeIdentifier.forClass(Byte::class.javaPrimitiveType!!))
fun nameForType(type: Type): String = nameForType(TypeIdentifier.forGenericType(type))
}

View File

@ -0,0 +1,124 @@
package net.corda.serialization.internal.model
import java.io.NotSerializableException
import java.lang.reflect.Type
/**
* Once we have the complete graph of types requiring carpentry to hand, we can use it to sort those types in reverse-
* dependency order, i.e. beginning with those types that have no dependencies on other types, then the types that
* depended on those types, and so on. This means we can feed types directly to the [RemoteTypeCarpenter], and don't
* have to use the [CarpenterMetaSchema].
*
* @param typesRequiringCarpentry The set of [RemoteTypeInformation] for types that are not reachable by the current
* classloader.
*/
class CarpentryDependencyGraph private constructor(private val typesRequiringCarpentry: Set<RemoteTypeInformation>) {
companion object {
/**
* Sort the [typesRequiringCarpentry] into reverse-dependency order, then pass them to the provided
* [Type]-builder, collating the results into a [Map] of [Type] by [TypeIdentifier]
*/
fun buildInReverseDependencyOrder(
typesRequiringCarpentry: Set<RemoteTypeInformation>,
getOrBuild: (RemoteTypeInformation) -> Type): Map<TypeIdentifier, Type> =
CarpentryDependencyGraph(typesRequiringCarpentry).buildInOrder(getOrBuild)
}
/**
* A map of inbound edges by node.
*
* A [RemoteTypeInformation] map key is a type that requires other types to have been constructed before it can be
* constructed.
*
* Each [RemoteTypeInformation] in the corresponding [Set] map value is one of the types that the key-type depends on.
*
* No key ever maps to an empty set: types with no dependencies are not included in this map.
*/
private val dependencies = mutableMapOf<RemoteTypeInformation, MutableSet<RemoteTypeInformation>>()
/**
* If it is in [typesRequiringCarpentry], then add an edge from [dependee] to this type to the [dependencies] graph.
*/
private fun RemoteTypeInformation.dependsOn(dependee: RemoteTypeInformation) = dependsOn(listOf(dependee))
/**
* Add an edge from each of these [dependees] that are in [typesRequiringCarpentry] to this type to the
* [dependencies] graph.
*/
private fun RemoteTypeInformation.dependsOn(dependees: Collection<RemoteTypeInformation>) {
val dependeesInTypesRequiringCarpentry = dependees.filter { it in typesRequiringCarpentry }
if (dependeesInTypesRequiringCarpentry.isEmpty()) return // we don't want to put empty sets into the map.
dependencies.compute(this) { _, dependees ->
dependees?.apply { addAll(dependeesInTypesRequiringCarpentry) } ?:
dependeesInTypesRequiringCarpentry.toMutableSet()
}
}
/**
* Traverses each of the [typesRequiringCarpentry], building (or obtaining from a cache) the corresponding [Type]
* and populating them into a [Map] of [Type] by [TypeIdentifier].
*/
private fun buildInOrder(getOrBuild: (RemoteTypeInformation) -> Type): Map<TypeIdentifier, Type> {
typesRequiringCarpentry.forEach { it.recordDependencies() }
return topologicalSort(typesRequiringCarpentry).associate { information ->
information.typeIdentifier to getOrBuild(information)
}
}
/**
* Record appropriate dependencies for each type of [RemoteTypeInformation]
*/
private fun RemoteTypeInformation.recordDependencies() = when (this) {
is RemoteTypeInformation.Composable -> {
dependsOn(typeParameters)
dependsOn(interfaces)
dependsOn(properties.values.map { it.type })
}
is RemoteTypeInformation.AnInterface -> {
dependsOn(typeParameters)
dependsOn(interfaces)
dependsOn(properties.values.map { it.type })
}
is RemoteTypeInformation.AnArray -> dependsOn(componentType)
is RemoteTypeInformation.Parameterised -> dependsOn(typeParameters)
else -> {}
}
/**
* Separate out those [types] which have [noDependencies] from those which still have dependencies.
*
* Remove the types with no dependencies from the graph, identifying which types are left with no inbound dependees
* as a result, then return the types with no dependencies concatenated with the [topologicalSort] of the remaining
* types, minus the newly-independent types.
*/
private fun topologicalSort(
types: Set<RemoteTypeInformation>,
noDependencies: Set<RemoteTypeInformation> = types - dependencies.keys): Sequence<RemoteTypeInformation> {
// Types which still have dependencies.
val remaining = dependencies.keys.toSet()
// Remove the types which have no dependencies from the dependencies of the remaining types, and identify
// those types which have no dependencies left after we've done this.
val newlyIndependent = dependencies.asSequence().mapNotNull { (dependent, dependees) ->
dependees.removeAll(noDependencies)
if (dependees.isEmpty()) dependent else null
}.toSet()
// If there are still types with dependencies, and we have no dependencies we can remove, then we can't continue.
if (newlyIndependent.isEmpty() && dependencies.isNotEmpty()) {
throw NotSerializableException(
"Cannot build dependencies for " +
dependencies.keys.map { it.typeIdentifier.prettyPrint(false) })
}
// Remove the types which have no dependencies remaining, maintaining the invariant that no key maps to an
// empty set.
dependencies.keys.removeAll(newlyIndependent)
// Return the types that had no dependencies, then recurse to process the remainder.
return noDependencies.asSequence() +
if (dependencies.isEmpty()) newlyIndependent.asSequence() else topologicalSort(remaining, newlyIndependent)
}
}

View File

@ -0,0 +1,84 @@
package net.corda.serialization.internal.model
import net.corda.serialization.internal.amqp.asClass
import net.corda.serialization.internal.carpenter.*
import java.io.NotSerializableException
import java.lang.reflect.Type
typealias PropertyName = String
/**
* Constructs [Type]s using [RemoteTypeInformation].
*/
interface RemoteTypeCarpenter {
fun carpent(typeInformation: RemoteTypeInformation): Type
}
/**
* A [RemoteTypeCarpenter] that converts [RemoteTypeInformation] into [Schema] objects for the [ClassCarpenter] to use.
*/
class SchemaBuildingRemoteTypeCarpenter(private val carpenter: ClassCarpenter): RemoteTypeCarpenter {
private val classLoader: ClassLoader get() = carpenter.classloader
override fun carpent(typeInformation: RemoteTypeInformation): Type {
try {
when (typeInformation) {
is RemoteTypeInformation.AnInterface -> typeInformation.carpentInterface()
is RemoteTypeInformation.Composable -> typeInformation.carpentComposable()
is RemoteTypeInformation.AnEnum -> typeInformation.carpentEnum()
else -> {
} // Anything else, such as arrays, will be taken care of by the above
}
} catch (e: ClassCarpenterException) {
throw NotSerializableException("${typeInformation.typeIdentifier.name}: ${e.message}")
}
return typeInformation.typeIdentifier.getLocalType(classLoader)
}
private val RemoteTypeInformation.erasedLocalClass get() = typeIdentifier.getLocalType(classLoader).asClass()
private fun RemoteTypeInformation.AnInterface.carpentInterface() {
val fields = getFields(typeIdentifier.name, properties)
val schema = CarpenterSchemaFactory.newInstance(
name = typeIdentifier.name,
fields = fields,
interfaces = getInterfaces(typeIdentifier.name, interfaces),
isInterface = true)
carpenter.build(schema)
}
private fun RemoteTypeInformation.Composable.carpentComposable() {
val fields = getFields(typeIdentifier.name, properties)
val schema = CarpenterSchemaFactory.newInstance(
name = typeIdentifier.name,
fields = fields,
interfaces = getInterfaces(typeIdentifier.name, interfaces),
isInterface = false)
carpenter.build(schema)
}
private fun getFields(ownerName: String, properties: Map<PropertyName, RemotePropertyInformation>) =
properties.mapValues { (name, property) ->
try {
FieldFactory.newInstance(property.isMandatory, name, property.type.erasedLocalClass)
} catch (e: ClassNotFoundException) {
throw UncarpentableException(ownerName, name, property.type.typeIdentifier.name)
}
}
private fun getInterfaces(ownerName: String, interfaces: List<RemoteTypeInformation>): List<Class<*>> =
interfaces.map {
try {
it.erasedLocalClass
} catch (e: ClassNotFoundException) {
throw UncarpentableException(ownerName, "[interface]", it.typeIdentifier.name)
}
}
private fun RemoteTypeInformation.AnEnum.carpentEnum() {
carpenter.build(EnumSchema(name = typeIdentifier.name, fields = members.associate { it to EnumField() }))
}
}

View File

@ -0,0 +1,196 @@
package net.corda.serialization.internal.model
import net.corda.serialization.internal.amqp.Transform
import net.corda.serialization.internal.amqp.TransformTypes
import java.util.*
typealias TypeDescriptor = String
/**
* Represents a property of a remotely-defined type.
*
* @param type The type of the property.
* @param isMandatory Whether the property is mandatory (i.e. non-nullable).
*/
data class RemotePropertyInformation(val type: RemoteTypeInformation, val isMandatory: Boolean)
/**
* The [RemoteTypeInformation] extracted from a remote data source's description of its type schema captures the
* information contained in that schema in a form similar to that of [LocalTypeInformation], but stripped of any
* reference to local type information such as [Type]s, [Method]s, constructors and so on.
*
* It has two main uses:
*
* 1) Comparison with [LocalTypeInformation] to determine compatibility and whether type evolution should be attempted.
* 2) Providing a specification to a [ClassCarpenter] that will synthesize a [Type] at runtime.
*
* A [TypeLoader] knows how to load types described by [RemoteTypeInformation], using a [ClassCarpenter] to build
* synthetic types where needed, so that every piece of [RemoteTypeInformation] is matched to a corresponding local
* [Type] for which [LocalTypeInformation] can be generated. Once we have both [RemoteTypeInformation] and
* [LocalTypeInformation] in hand, we can make decisions about the compatibility between the remote and local type
* schemas.
*
* In the future, it may make sense to generate type schema information by reflecting [LocalTypeInformation] into
* [RemoteTypeInformation].
*
* Each piece of [RemoteTypeInformation] has both a [TypeIdentifier], which is not guaranteed to be globally uniquely
* identifying, and a [TypeDescriptor], which is.
*
* [TypeIdentifier]s are not globally uniquely identifying because
* multiple remote sources may define their own versions of the same type, with potentially different properties. However,
* they are unique to a given message-exchange session, and are used as unique references for types within the type
* schema associated with a given message.
*
* [TypeDescriptor]s are obtained by "fingerprinting" [LocalTypeInformation], and represent a hashed digest of all of
* the information locally available about a type. If a remote [TypeDescriptor] matches that of a local type, then we
* know that they are fully schema-compatible. However, it is possible for two types to diverge due to inconsistent
* erasure, so that they will have different [TypeDescriptor]s, and yet represent the "same" type for purposes of
* serialisation. In this case, we will determine compatibility based on comparison of the [RemoteTypeInformation]'s
* type graph with that of the [LocalTypeInformation] which reflects it.
*/
sealed class RemoteTypeInformation {
/**
* The globally-unique [TypeDescriptor] of the represented type.
*/
abstract val typeDescriptor: TypeDescriptor
/**
* The [TypeIdentifier] of the represented type.
*/
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 =
RemoteTypeInformationPrettyPrinter(simplifyClassNames).prettyPrint(this)
/**
* The [RemoteTypeInformation] corresponding to an unbounded wildcard ([TypeIdentifier.UnknownType])
*/
object Unknown : RemoteTypeInformation() {
override val typeDescriptor = "?"
override val typeIdentifier = TypeIdentifier.UnknownType
}
/**
* The [RemoteTypeInformation] corresponding to [java.lang.Object] / [Any] ([TypeIdentifier.TopType])
*/
object Top : RemoteTypeInformation() {
override val typeDescriptor = "*"
override val typeIdentifier = TypeIdentifier.TopType
}
/**
* The [RemoteTypeInformation] emitted if we hit a cycle while traversing the graph of related types.
*/
data class Cycle(override val typeIdentifier: TypeIdentifier, private val _follow: () -> RemoteTypeInformation) : RemoteTypeInformation() {
override val typeDescriptor = typeIdentifier.name
val follow: RemoteTypeInformation get() = _follow()
override fun equals(other: Any?): Boolean = other is Cycle && other.typeIdentifier == typeIdentifier
override fun hashCode(): Int = typeIdentifier.hashCode()
override fun toString(): String = "Cycle($typeIdentifier)"
}
/**
* Representation of a simple unparameterised type.
*/
data class Unparameterised(override val typeDescriptor: TypeDescriptor, override val typeIdentifier: TypeIdentifier) : RemoteTypeInformation()
/**
* Representation of a type with type parameters.
*
* @param typeParameters The type parameters of the type.
*/
data class Parameterised(override val typeDescriptor: TypeDescriptor, override val typeIdentifier: TypeIdentifier, val typeParameters: List<RemoteTypeInformation>) : RemoteTypeInformation()
/**
* Representation of an array of some other type.
*
* @param componentType The component type of the array.
*/
data class AnArray(override val typeDescriptor: TypeDescriptor, override val typeIdentifier: TypeIdentifier, val componentType: RemoteTypeInformation) : RemoteTypeInformation()
/**
* Representation of an Enum type.
*
* @param members The members of the enum.
*/
data class AnEnum(override val typeDescriptor: TypeDescriptor,
override val typeIdentifier: TypeIdentifier,
val members: List<String>,
val transforms: EnumTransforms) : RemoteTypeInformation()
/**
* Representation of an interface.
*
* @param properties The properties (i.e. "getter" methods) of the interface.
* @param interfaces The interfaces extended by the interface.
* @param typeParameters The type parameters of the interface.
*/
data class AnInterface(override val typeDescriptor: TypeDescriptor, override val typeIdentifier: TypeIdentifier, val properties: Map<String, RemotePropertyInformation>, val interfaces: List<RemoteTypeInformation>, val typeParameters: List<RemoteTypeInformation>) : RemoteTypeInformation()
/**
* Representation of a concrete POJO-like class.
*
* @param properties The properties of the class.
* @param interfaces The interfaces extended by the class.
* @param typeParameters The type parameters of the class.
*/
data class Composable(
override val typeDescriptor: TypeDescriptor,
override val typeIdentifier: TypeIdentifier,
val properties: Map<String, RemotePropertyInformation>,
val interfaces: List<RemoteTypeInformation>,
val typeParameters: List<RemoteTypeInformation>) : RemoteTypeInformation()
}
private data class RemoteTypeInformationPrettyPrinter(private val simplifyClassNames: Boolean = true, private val indent: Int = 0) {
fun prettyPrint(remoteTypeInformation: RemoteTypeInformation): String = with(remoteTypeInformation){
when (this) {
is RemoteTypeInformation.AnInterface -> typeIdentifier.prettyPrint(simplifyClassNames) +
printInterfaces(interfaces) +
indentAnd { printProperties(properties) }
is RemoteTypeInformation.Composable -> typeIdentifier.prettyPrint(simplifyClassNames) +
printInterfaces(interfaces) +
indentAnd { printProperties(properties) }
is RemoteTypeInformation.AnEnum -> typeIdentifier.prettyPrint(simplifyClassNames) +
members.joinToString("|", "(", ")")
else -> typeIdentifier.prettyPrint(simplifyClassNames)
}
}
private inline fun indentAnd(block: RemoteTypeInformationPrettyPrinter.() -> String) =
copy(indent = indent + 1).block()
private fun printInterfaces(interfaces: List<RemoteTypeInformation>) =
if (interfaces.isEmpty()) ""
else interfaces.joinToString(", ", ": ", "") {
it.typeIdentifier.prettyPrint(simplifyClassNames)
}
private fun printProperties(properties: Map<String, RemotePropertyInformation>) =
properties.entries.sortedBy { it.key }.joinToString("\n", "\n", "") {
it.prettyPrint()
}
private fun Map.Entry<String, RemotePropertyInformation>.prettyPrint(): String =
" ".repeat(indent) + key +
(if(!value.isMandatory) " (optional)" else "") +
": " + value.type.prettyPrint(simplifyClassNames)
}
data class EnumTransforms(val defaults: Map<String, String>, val renames: Map<String, String>) {
val size: Int get() = defaults.size + renames.size
companion object {
val empty = EnumTransforms(emptyMap(), emptyMap())
}
}

View File

@ -1,7 +1,16 @@
package net.corda.serialization.internal.model
import com.google.common.reflect.TypeToken
import net.corda.serialization.internal.amqp.asClass
import java.io.NotSerializableException
import java.lang.reflect.*
import java.util.*
/**
* Thrown if a [TypeIdentifier] is incompatible with the local [Type] to which it refers,
* i.e. if the number of type parameters does not match.
*/
class IncompatibleTypeIdentifierException(message: String) : NotSerializableException(message)
/**
* Used as a key for retrieving cached type information. We need slightly more information than the bare classname,
@ -21,18 +30,30 @@ sealed class TypeIdentifier {
*/
abstract val name: String
/**
* Obtain the local type matching this identifier
*
* @param classLoader The classloader to use to load the type.
* @throws ClassNotFoundException if the type or any of its parameters cannot be loaded.
* @throws IncompatibleTypeIdentifierException if the type identifier is incompatible with the locally-defined type
* to which it refers.
*/
abstract fun getLocalType(classLoader: ClassLoader = ClassLoader.getSystemClassLoader()): Type
open val erased: TypeIdentifier get() = this
/**
* Obtain a nicely-formatted representation of the identified type, for help with debugging.
*/
fun prettyPrint(simplifyClassNames: Boolean = true): String = when(this) {
is TypeIdentifier.Unknown -> "?"
is TypeIdentifier.Top -> "*"
is TypeIdentifier.UnknownType -> "?"
is TypeIdentifier.TopType -> "*"
is TypeIdentifier.Unparameterised -> name.simplifyClassNameIfRequired(simplifyClassNames)
is TypeIdentifier.Erased -> "${name.simplifyClassNameIfRequired(simplifyClassNames)} (erased)"
is TypeIdentifier.ArrayOf -> "${componentType.prettyPrint()}[]"
is TypeIdentifier.ArrayOf -> "${componentType.prettyPrint(simplifyClassNames)}[]"
is TypeIdentifier.Parameterised ->
name.simplifyClassNameIfRequired(simplifyClassNames) + parameters.joinToString(", ", "<", ">") {
it.prettyPrint()
it.prettyPrint(simplifyClassNames)
}
}
@ -46,10 +67,10 @@ sealed class TypeIdentifier {
* @param type The class to get a [TypeIdentifier] for.
*/
fun forClass(type: Class<*>): TypeIdentifier = when {
type.name == "java.lang.Object" -> Top
type.name == "java.lang.Object" -> TopType
type.isArray -> ArrayOf(forClass(type.componentType))
type.typeParameters.isEmpty() -> Unparameterised(type.name)
else -> Erased(type.name)
else -> Erased(type.name, type.typeParameters.size)
}
/**
@ -63,45 +84,92 @@ sealed class TypeIdentifier {
* class implementing a parameterised interface and specifying values for type variables which are referred to
* by methods defined in the interface.
*/
fun forGenericType(type: Type, resolutionContext: Type = type): TypeIdentifier = when(type) {
is ParameterizedType -> Parameterised((type.rawType as Class<*>).name, type.actualTypeArguments.map {
forGenericType(it.resolveAgainst(resolutionContext))
})
is Class<*> -> forClass(type)
is GenericArrayType -> ArrayOf(forGenericType(type.genericComponentType.resolveAgainst(resolutionContext)))
else -> Unknown
}
fun forGenericType(type: Type, resolutionContext: Type = type): TypeIdentifier =
when(type) {
is ParameterizedType -> Parameterised(
(type.rawType as Class<*>).name,
type.ownerType?.let { forGenericType(it) },
type.actualTypeArguments.map {
forGenericType(it.resolveAgainst(resolutionContext))
})
is Class<*> -> forClass(type)
is GenericArrayType -> ArrayOf(forGenericType(type.genericComponentType.resolveAgainst(resolutionContext)))
is WildcardType -> type.upperBound.let { if (it == type) UnknownType else forGenericType(it) }
else -> UnknownType
}
}
/**
* The [TypeIdentifier] of [Any] / [java.lang.Object].
*/
object Top : TypeIdentifier() {
object TopType : TypeIdentifier() {
override val name get() = "*"
override fun toString() = "Top"
override fun getLocalType(classLoader: ClassLoader): Type = Any::class.java
override fun toString() = "TopType"
}
private object UnboundedWildcardType : WildcardType {
override fun getLowerBounds(): Array<Type> = emptyArray()
override fun getUpperBounds(): Array<Type> = arrayOf(Any::class.java)
override fun toString() = "?"
}
/**
* The [TypeIdentifier] of an unbounded wildcard.
*/
object Unknown : TypeIdentifier() {
object UnknownType : TypeIdentifier() {
override val name get() = "?"
override fun toString() = "Unknown"
override fun getLocalType(classLoader: ClassLoader): Type = UnboundedWildcardType
override fun toString() = "UnknownType"
}
/**
* Identifies a class with no type parameters.
*/
data class Unparameterised(override val name: String) : TypeIdentifier() {
companion object {
private val primitives = listOf(
Byte::class,
Boolean:: class,
Char::class,
Int::class,
Short::class,
Long::class,
Float::class,
Double::class).associate {
it.javaPrimitiveType!!.name to it.javaPrimitiveType
}
}
override fun toString() = "Unparameterised($name)"
override fun getLocalType(classLoader: ClassLoader): Type = primitives[name] ?: classLoader.loadClass(name)
val isPrimitive get() = name in primitives
}
/**
* Identifies a parameterised class such as List<Int>, for which we cannot obtain the type parameters at runtime
* because they have been erased.
*/
data class Erased(override val name: String) : TypeIdentifier() {
data class Erased(override val name: String, val erasedParameterCount: Int) : TypeIdentifier() {
fun toParameterized(parameters: List<TypeIdentifier>): TypeIdentifier {
if (parameters.size != erasedParameterCount) throw IncompatibleTypeIdentifierException(
"Erased type $name takes $erasedParameterCount parameters, but ${parameters.size} supplied"
)
return Parameterised(name, null, parameters)
}
override fun toString() = "Erased($name)"
override fun getLocalType(classLoader: ClassLoader): Type = classLoader.loadClass(name)
}
private class ReconstitutedGenericArrayType(private val componentType: Type) : GenericArrayType {
override fun getGenericComponentType(): Type = componentType
override fun toString() = "$componentType[]"
override fun equals(other: Any?): Boolean =
other is GenericArrayType && componentType == other.genericComponentType
override fun hashCode(): Int = Objects.hashCode(componentType)
}
/**
@ -112,6 +180,30 @@ sealed class TypeIdentifier {
data class ArrayOf(val componentType: TypeIdentifier) : TypeIdentifier() {
override val name get() = componentType.name + "[]"
override fun toString() = "ArrayOf(${componentType.prettyPrint()})"
override fun getLocalType(classLoader: ClassLoader): Type {
val component = componentType.getLocalType(classLoader)
return when (componentType) {
is Parameterised -> ReconstitutedGenericArrayType(component)
else -> java.lang.reflect.Array.newInstance(component.asClass(), 0).javaClass
}
}
}
private class ReconstitutedParameterizedType(
private val _rawType: Type,
private val _ownerType: Type?,
private val _actualTypeArguments: Array<Type>) : ParameterizedType {
override fun getRawType(): Type = _rawType
override fun getOwnerType(): Type? = _ownerType
override fun getActualTypeArguments(): Array<Type> = _actualTypeArguments
override fun toString(): String = TypeIdentifier.forGenericType(this).prettyPrint(false)
override fun equals(other: Any?): Boolean =
other is ParameterizedType &&
other.rawType == rawType &&
other.ownerType == ownerType &&
Arrays.equals(other.actualTypeArguments, actualTypeArguments)
override fun hashCode(): Int =
Arrays.hashCode(actualTypeArguments) xor Objects.hashCode(ownerType) xor Objects.hashCode(rawType)
}
/**
@ -119,8 +211,25 @@ sealed class TypeIdentifier {
*
* @param parameters [TypeIdentifier]s for each of the resolved type parameter values of this type.
*/
data class Parameterised(override val name: String, val parameters: List<TypeIdentifier>) : TypeIdentifier() {
data class Parameterised(override val name: String, val owner: TypeIdentifier?, val parameters: List<TypeIdentifier>) : TypeIdentifier() {
/**
* Get the type-erased equivalent of this type.
*/
override val erased: TypeIdentifier get() = Erased(name, parameters.size)
override fun toString() = "Parameterised(${prettyPrint()})"
override fun getLocalType(classLoader: ClassLoader): Type {
val rawType = classLoader.loadClass(name)
if (rawType.typeParameters.size != parameters.size) {
throw IncompatibleTypeIdentifierException(
"Class $rawType expects ${rawType.typeParameters.size} type arguments, " +
"but type ${this.prettyPrint(false)} has ${parameters.size}")
}
return ReconstitutedParameterizedType(
rawType,
owner?.getLocalType(classLoader),
parameters.map { it.getLocalType(classLoader) }.toTypedArray())
}
}
}

View File

@ -0,0 +1,61 @@
package net.corda.serialization.internal.model
import net.corda.serialization.internal.carpenter.*
import java.io.NotSerializableException
import java.lang.ClassCastException
import java.lang.reflect.Type
/**
* A [TypeLoader] obtains local types whose [TypeIdentifier]s will reflect those of remote types.
*/
interface TypeLoader {
/**
* Obtains local types which will have the same [TypeIdentifier]s as the remote types.
*
* @param remoteTypeInformation The type information for the remote types.
*/
fun load(remoteTypeInformation: Collection<RemoteTypeInformation>): Map<TypeIdentifier, Type>
}
/**
* A [TypeLoader] that uses the [ClassCarpenter] to build a class matching the supplied [RemoteTypeInformation] if none
* is visible from the current classloader.
*/
class ClassCarpentingTypeLoader(private val carpenter: RemoteTypeCarpenter, private val classLoader: ClassLoader): TypeLoader {
val cache = DefaultCacheProvider.createCache<TypeIdentifier, Type>()
override fun load(remoteTypeInformation: Collection<RemoteTypeInformation>): Map<TypeIdentifier, Type> {
val remoteInformationByIdentifier = remoteTypeInformation.associateBy { it.typeIdentifier }
// Grab all the types we can from the cache, or the classloader.
val noCarpentryRequired = remoteInformationByIdentifier.asSequence().mapNotNull { (identifier, _) ->
try {
identifier to cache.computeIfAbsent(identifier) { identifier.getLocalType(classLoader) }
} catch (e: ClassNotFoundException) {
null
}
}.toMap()
// If we have everything we need, return immediately.
if (noCarpentryRequired.size == remoteTypeInformation.size) return noCarpentryRequired
// Identify the types which need carpenting up.
val requiringCarpentry = remoteInformationByIdentifier.asSequence().mapNotNull { (identifier, information) ->
if (identifier in noCarpentryRequired) null else information
}.toSet()
// Build the types requiring carpentry in reverse-dependency order.
// Something else might be trying to carpent these types at the same time as us, so we always consult
// (and populate) the cache.
val carpented = CarpentryDependencyGraph.buildInReverseDependencyOrder(requiringCarpentry) { typeToCarpent ->
cache.computeIfAbsent(typeToCarpent.typeIdentifier) {
carpenter.carpent(typeToCarpent)
}
}
// Return the complete map of types.
return noCarpentryRequired + carpented
}
}

View File

@ -0,0 +1,84 @@
package net.corda.serialization.internal.amqp
import net.corda.serialization.internal.amqp.testutils.serializeAndReturnSchema
import net.corda.serialization.internal.amqp.testutils.testDefaultFactory
import net.corda.serialization.internal.model.*
import net.corda.testing.core.SerializationEnvironmentRule
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import java.lang.IllegalArgumentException
import java.util.*
class AMQPRemoteTypeModelTests {
@Rule
@JvmField
val serializationEnvRule = SerializationEnvironmentRule()
private val factory = testDefaultFactory()
private val typeModel = AMQPRemoteTypeModel()
interface Interface<P, Q, R> {
val array: Array<out P>
val list: List<Q>
val map: Map<Q, R>
}
enum class Enum : Interface<String, IntArray, Int> {
FOO, BAR, BAZ;
override val array: Array<out String> get() = emptyArray()
override val list: List<IntArray> get() = emptyList()
override val map: Map<IntArray, Int> get() = emptyMap()
}
open class Superclass<K, V>(override val array: Array<out String>, override val list: List<K>, override val map: Map<K, V>)
: Interface<String, K, V>
class C<V>(array: Array<out String>, list: List<UUID>, map: Map<UUID, V>, val enum: Enum): Superclass<UUID, V>(array, list, map)
class SimpleClass(val a: Int, val b: Double, val c: Short?, val d: ByteArray, val e: ByteArray?)
@Test
fun `round-trip some types through AMQP serialisations`() {
arrayOf("").assertRemoteType("String[]")
listOf(1).assertRemoteType("List<?>")
arrayOf(listOf(1)).assertRemoteType("List[]")
Enum.BAZ.assertRemoteType("Enum(FOO|BAR|BAZ)")
mapOf("string" to 1).assertRemoteType("Map<?, ?>")
arrayOf(byteArrayOf(1, 2, 3)).assertRemoteType("byte[][]")
SimpleClass(1, 2.0, null, byteArrayOf(1, 2, 3), byteArrayOf(4, 5, 6))
.assertRemoteType("""
SimpleClass
a: int
b: double
c (optional): Short
d: byte[]
e (optional): byte[]
""")
C(arrayOf("a", "b"), listOf(UUID.randomUUID()), mapOf(UUID.randomUUID() to intArrayOf(1, 2, 3)), Enum.BAZ)
.assertRemoteType("""
C: Interface<String, UUID, ?>
array: String[]
enum: Enum(FOO|BAR|BAZ)
list: List<UUID>
map: Map<UUID, ?>
""")
}
private fun getRemoteType(obj: Any): RemoteTypeInformation {
val output = SerializationOutput(factory)
val schema = output.serializeAndReturnSchema(obj)
val values = typeModel.interpret(SerializationSchemas(schema.schema, schema.transformsSchema)).values
return values.find { it.typeIdentifier.getLocalType().asClass().isAssignableFrom(obj::class.java) } ?:
throw IllegalArgumentException(
"Can't find ${obj::class.java.name} in ${values.map { it.typeIdentifier.name}}")
}
private fun Any.assertRemoteType(prettyPrinted: String) {
assertEquals(prettyPrinted.trimIndent(), getRemoteType(this).prettyPrint())
}
}

View File

@ -0,0 +1,197 @@
package net.corda.serialization.internal.amqp
import com.google.common.reflect.TypeToken
import net.corda.serialization.internal.model.TypeIdentifier
import org.apache.qpid.proton.amqp.UnsignedShort
import org.junit.Test
import java.io.NotSerializableException
import java.lang.reflect.Type
import java.time.LocalDateTime
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class AMQPTypeIdentifierParserTests {
@Test
fun `primitives and arrays`() {
assertParseResult<Int>("int")
assertParseResult<IntArray>("int[p]")
assertParseResult<Array<Int>>("int[]")
assertParseResult<Array<IntArray>>("int[p][]")
assertParseResult<Array<Array<Int>>>("int[][]")
assertParseResult<ByteArray>("binary")
assertParseResult<Array<ByteArray>>("binary[]")
assertParseResult<Array<UnsignedShort>>("ushort[]")
assertParseResult<Array<Array<String>>>("string[][]")
assertParseResult<UUID>("uuid")
assertParseResult<Date>("timestamp")
// We set a limit to the depth of arrays-of-arrays-of-arrays...
assertFailsWith<IllegalTypeNameParserStateException> {
AMQPTypeIdentifierParser.parse("string" + "[]".repeat(33))
}
}
@Test
fun `unparameterised types`() {
assertParseResult<LocalDateTime>("java.time.LocalDateTime")
assertParseResult<Array<LocalDateTime>>("java.time.LocalDateTime[]")
assertParseResult<Array<Array<LocalDateTime>>>("java.time.LocalDateTime[][]")
}
interface WithParameter<T> {
val value: T
}
interface WithParameters<P, Q> {
val p: Array<out P>
val q: WithParameter<Array<Q>>
}
@Test
fun `parameterised types, nested, with arrays`() {
assertParsesTo<WithParameters<IntArray, WithParameter<Array<WithParameters<Array<Array<Date>>, UUID>>>>>(
"WithParameters<int[], WithParameter<WithParameters<Date[][], UUID>[]>>"
)
// We set a limit to the maximum depth of nested type parameters.
assertFailsWith<IllegalTypeNameParserStateException> {
AMQPTypeIdentifierParser.parse("WithParameter<".repeat(33) + ">".repeat(33))
}
}
@Test
fun `compatibility test`() {
assertParsesCompatibly<Int>()
assertParsesCompatibly<IntArray>()
assertParsesCompatibly<Array<Int>>()
assertParsesCompatibly<List<Int>>()
assertParsesTo<WithParameter<*>>("WithParameter<?>")
assertParsesCompatibly<WithParameter<Int>>()
assertParsesCompatibly<Array<out WithParameter<Int>>>()
assertParsesCompatibly<WithParameters<IntArray, WithParameter<Array<WithParameters<Array<Array<Date>>, UUID>>>>>()
}
// Old tests for DeserializedParameterizedType
@Test
fun `test nested`() {
verify(" java.util.Map < java.util.Map< java.lang.String, java.lang.Integer >, java.util.Map < java.lang.Long , java.lang.String > >")
}
@Test
fun `test simple`() {
verify("java.util.List<java.lang.String>")
}
@Test
fun `test multiple args`() {
verify("java.util.Map<java.lang.String,java.lang.Integer>")
}
@Test
fun `test trailing whitespace`() {
verify("java.util.Map<java.lang.String, java.lang.Integer> ")
}
@Test
fun `test list of commands`() {
verify("java.util.List<net.corda.core.contracts.Command<net.corda.core.contracts.Command<net.corda.core.contracts.CommandData>>>")
}
@Test(expected = NotSerializableException::class)
fun `test trailing text`() {
verify("java.util.Map<java.lang.String, java.lang.Integer>foo")
}
@Test(expected = NotSerializableException::class)
fun `test trailing comma`() {
verify("java.util.Map<java.lang.String, java.lang.Integer,>")
}
@Test(expected = NotSerializableException::class)
fun `test leading comma`() {
verify("java.util.Map<,java.lang.String, java.lang.Integer>")
}
@Test(expected = NotSerializableException::class)
fun `test middle comma`() {
verify("java.util.Map<,java.lang.String,, java.lang.Integer>")
}
@Test(expected = NotSerializableException::class)
fun `test trailing close`() {
verify("java.util.Map<java.lang.String, java.lang.Integer>>")
}
@Test(expected = NotSerializableException::class)
fun `test empty params`() {
verify("java.util.Map<>")
}
@Test(expected = NotSerializableException::class)
fun `test mid whitespace`() {
verify("java.u til.List<java.lang.String>")
}
@Test(expected = NotSerializableException::class)
fun `test mid whitespace2`() {
verify("java.util.List<java.l ng.String>")
}
@Test(expected = NotSerializableException::class)
fun `test wrong number of parameters`() {
verify("java.util.List<java.lang.String, java.lang.Integer>")
}
@Test
fun `test no parameters`() {
verify("java.lang.String")
}
@Test(expected = NotSerializableException::class)
fun `test parameters on non-generic type`() {
verify("java.lang.String<java.lang.Integer>")
}
@Test(expected = NotSerializableException::class)
fun `test excessive nesting`() {
var nested = "java.lang.Integer"
for (i in 1..AMQPTypeIdentifierParser.MAX_TYPE_PARAM_DEPTH) {
nested = "java.util.List<$nested>"
}
verify(nested)
}
private inline fun <reified T> assertParseResult(typeString: String) {
assertEquals(TypeIdentifier.forGenericType(typeOf<T>()), AMQPTypeIdentifierParser.parse(typeString))
}
private inline fun <reified T> typeOf() = object : TypeToken<T>() {}.type
private inline fun <reified T> assertParsesCompatibly() = assertParsesCompatibly(typeOf<T>())
private fun assertParsesCompatibly(type: Type) {
assertParsesTo(type, TypeIdentifier.forGenericType(type).prettyPrint())
}
private inline fun <reified T> assertParsesTo(expectedIdentifierPrettyPrint: String) {
assertParsesTo(typeOf<T>(), expectedIdentifierPrettyPrint)
}
private fun assertParsesTo(type: Type, expectedIdentifierPrettyPrint: String) {
val nameForType = AMQPTypeIdentifiers.nameForType(type)
val parsedIdentifier = AMQPTypeIdentifierParser.parse(nameForType)
assertEquals(expectedIdentifierPrettyPrint, parsedIdentifier.prettyPrint())
}
private fun normalise(string: String): String {
return string.replace(" ", "")
}
private fun verify(typeName: String) {
val type = AMQPTypeIdentifierParser.parse(typeName).getLocalType()
assertEquals(normalise(typeName), normalise(type.typeName))
}
}

View File

@ -0,0 +1,112 @@
package net.corda.serialization.internal.model
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.common.reflect.TypeToken
import net.corda.serialization.internal.AllWhitelist
import net.corda.serialization.internal.amqp.asClass
import net.corda.serialization.internal.carpenter.ClassCarpenterImpl
import org.junit.Test
import java.lang.reflect.Type
import kotlin.test.assertEquals
class ClassCarpentingTypeLoaderTests {
val carpenter = ClassCarpenterImpl(AllWhitelist)
val remoteTypeCarpenter = SchemaBuildingRemoteTypeCarpenter(carpenter)
val typeLoader = ClassCarpentingTypeLoader(remoteTypeCarpenter, carpenter.classloader)
@Test
fun `carpent some related classes`() {
val addressInformation = RemoteTypeInformation.Composable(
"address",
typeIdentifierOf("net.corda.test.Address"),
mapOf(
"addressLines" to remoteType<Array<String>>().mandatory,
"postcode" to remoteType<String>().optional
), emptyList(), emptyList()
)
val listOfAddresses = RemoteTypeInformation.Parameterised(
"list<Address>",
TypeIdentifier.Parameterised(
"java.util.List",
null,
listOf(addressInformation.typeIdentifier)),
listOf(addressInformation))
val personInformation = RemoteTypeInformation.Composable(
"person",
typeIdentifierOf("net.corda.test.Person"),
mapOf(
"name" to remoteType<String>().mandatory,
"age" to remoteType(TypeIdentifier.forClass(Int::class.javaPrimitiveType!!)).mandatory,
"address" to addressInformation.mandatory,
"previousAddresses" to listOfAddresses.mandatory
), emptyList(), emptyList())
val types = typeLoader.load(listOf(personInformation, addressInformation, listOfAddresses))
val addressType = types[addressInformation.typeIdentifier]!!
val personType = types[personInformation.typeIdentifier]!!
val address = addressType.make(arrayOf("23 Acacia Avenue", "Surbiton"), "VB6 5UX")
val previousAddress = addressType.make(arrayOf("99 Penguin Lane", "Doncaster"), "RA8 81T")
val person = personType.make("Arthur Putey", 42, address, listOf(previousAddress))
val personJson = ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(person)
assertEquals("""
{
"name" : "Arthur Putey",
"age" : 42,
"address" : {
"addressLines" : [ "23 Acacia Avenue", "Surbiton" ],
"postcode" : "VB6 5UX"
},
"previousAddresses" : [ {
"addressLines" : [ "99 Penguin Lane", "Doncaster" ],
"postcode" : "RA8 81T"
} ]
}
""".trimIndent(), personJson)
}
private fun Type.make(vararg params: Any): Any {
val cls = this.asClass()
val paramTypes = params.map { it::class.javaPrimitiveType ?: it::class.javaObjectType }.toTypedArray()
val constructor = cls.constructors.find { it.parameterTypes.zip(paramTypes).all {
(expected, actual) -> expected.isAssignableFrom(actual)
} }!!
return constructor.newInstance(*params)
}
private fun typeIdentifierOf(typeName: String, vararg parameters: TypeIdentifier) =
if (parameters.isEmpty()) TypeIdentifier.Unparameterised(typeName)
else TypeIdentifier.Parameterised(typeName, null, parameters.toList())
private inline fun <reified T> typeOf(): Type = object : TypeToken<T>() {}.type
private inline fun <reified T> typeIdentifierOf(): TypeIdentifier = TypeIdentifier.forGenericType(typeOf<T>())
private inline fun <reified T> remoteType(): RemoteTypeInformation = remoteType(typeIdentifierOf<T>())
private fun remoteType(typeIdentifier: TypeIdentifier): RemoteTypeInformation =
when (typeIdentifier) {
is TypeIdentifier.Unparameterised -> RemoteTypeInformation.Unparameterised(typeIdentifier.prettyPrint(), typeIdentifier)
is TypeIdentifier.Parameterised -> RemoteTypeInformation.Parameterised(
typeIdentifier.prettyPrint(),
typeIdentifier,
typeIdentifier.parameters.map { remoteType(it) })
is TypeIdentifier.ArrayOf -> RemoteTypeInformation.AnArray(
typeIdentifier.prettyPrint(),
typeIdentifier,
remoteType(typeIdentifier.componentType))
is TypeIdentifier.Erased -> RemoteTypeInformation.Unparameterised(
typeIdentifier.prettyPrint(),
TypeIdentifier.Unparameterised(typeIdentifier.name))
is TypeIdentifier.TopType -> RemoteTypeInformation.Top
is TypeIdentifier.UnknownType -> RemoteTypeInformation.Unknown
}
private val RemoteTypeInformation.optional: RemotePropertyInformation get() =
RemotePropertyInformation(this, false)
private val RemoteTypeInformation.mandatory: RemotePropertyInformation get() =
RemotePropertyInformation(this, true)
}