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
11 changed files with 1449 additions and 20 deletions

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