mirror of
https://github.com/corda/corda.git
synced 2024-12-19 21:17:58 +00:00
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:
parent
8ea6f1c7c5
commit
f66944cac5
@ -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!!)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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() }))
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user