mirror of
https://github.com/corda/corda.git
synced 2025-06-23 01:19:00 +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:
@ -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)
|
||||
}
|
Reference in New Issue
Block a user