Add the carpenter into the deserializer

Deseializing any blob that contains a type unknown to the classloader
will cause a class object to be carpented up and a suitable object
returned
This commit is contained in:
Katelyn Baker 2017-07-12 16:41:01 +01:00
parent 28610868c4
commit 4cfa376d7d
6 changed files with 157 additions and 10 deletions

View File

@ -69,6 +69,7 @@ private fun isConcrete(clazz: Class<*>): Boolean = !(clazz.isInterface || Modifi
private fun <T : Any> propertiesForSerializationFromConstructor(kotlinConstructor: KFunction<T>, type: Type, factory: SerializerFactory): Collection<PropertySerializer> {
val clazz = (kotlinConstructor.returnType.classifier as KClass<*>).javaObjectType
// Kotlin reflection doesn't work with Java getters the way you might expect, so we drop back to good ol' beans.
println (Introspector.getBeanInfo(clazz).propertyDescriptors)
val properties = Introspector.getBeanInfo(clazz).propertyDescriptors.filter { it.name != "class" }.groupBy { it.name }.mapValues { it.value[0] }
val rc: MutableList<PropertySerializer> = ArrayList(kotlinConstructor.parameters.size)
for (param in kotlinConstructor.parameters) {

View File

@ -5,6 +5,10 @@ import com.google.common.reflect.TypeResolver
import net.corda.core.serialization.ClassWhitelist
import net.corda.core.serialization.CordaSerializable
import net.corda.nodeapi.internal.serialization.AllWhitelist
import net.corda.nodeapi.internal.serialization.carpenter.CarpenterSchemas
import net.corda.nodeapi.internal.serialization.carpenter.ClassCarpenter
import net.corda.nodeapi.internal.serialization.carpenter.MetaCarpenter
import net.corda.nodeapi.internal.serialization.carpenter.carpenterSchema
import org.apache.qpid.proton.amqp.*
import java.io.NotSerializableException
import java.lang.reflect.GenericArrayType
@ -167,15 +171,38 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
}
}
private fun processSchema(schema: Schema) {
private fun processSchema(
schema: Schema,
cl: ClassLoader = DeserializedParameterizedType::class.java.classLoader) {
println ("processSchema cl ${cl::class.java}")
val retry = cl != DeserializedParameterizedType::class.java.classLoader
val carpenterSchemas = CarpenterSchemas.newInstance()
for (typeNotation in schema.types) {
processSchemaEntry(typeNotation)
try {
processSchemaEntry(typeNotation, cl)
}
catch (e: java.lang.ClassNotFoundException) {
println (" CLASS NOT FOUND - $retry")
if (retry) {
throw e
}
println ("add schema ${typeNotation.name}")
(typeNotation as CompositeType).carpenterSchema(carpenterSchemas = carpenterSchemas)
}
}
private fun processSchemaEntry(typeNotation: TypeNotation) {
if (carpenterSchemas.isEmpty()) return
val mc = MetaCarpenter(carpenterSchemas)
mc.build()
processSchema(schema, mc.classloader)
}
private fun processSchemaEntry(typeNotation: TypeNotation,
cl: ClassLoader = DeserializedParameterizedType::class.java.classLoader) {
when (typeNotation) {
is CompositeType -> processCompositeType(typeNotation) // java.lang.Class (whether a class or interface)
is CompositeType -> processCompositeType(typeNotation, cl) // java.lang.Class (whether a class or interface)
is RestrictedType -> processRestrictedType(typeNotation) // Collection / Map, possibly with generics
}
}
@ -188,10 +215,13 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
}
}
private fun processCompositeType(typeNotation: CompositeType) {
private fun processCompositeType(typeNotation: CompositeType,
cl: ClassLoader = DeserializedParameterizedType::class.java.classLoader) {
println ("processCompositeType ${typeNotation.name}")
serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) {
println (" no such type")
// TODO: class loader logic, and compare the schema.
val type = typeForName(typeNotation.name)
val type = typeForName(typeNotation.name, cl)
get(type.asClass() ?: throw NotSerializableException("Unable to build composite type for $type"), type)
}
}
@ -304,7 +334,9 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
else -> throw NotSerializableException("Unable to render type $type to a string.")
}
private fun typeForName(name: String): Type {
private fun typeForName(
name: String,
cl: ClassLoader = DeserializedParameterizedType::class.java.classLoader): Type {
return if (name.endsWith("[]")) {
val elementType = typeForName(name.substring(0, name.lastIndex - 1))
if (elementType is ParameterizedType || elementType is GenericArrayType) {
@ -329,7 +361,8 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
else -> throw NotSerializableException("Not able to deserialize array type: $name")
}
} else {
DeserializedParameterizedType.make(name)
println ("typeForName: name = $name")
DeserializedParameterizedType.make(name, cl)
}
}
}

View File

@ -297,8 +297,10 @@ class ClassCarpenter {
// fact that we didn't implement the interface we said we would at the moment the missing method is
// actually called, which is a bit too dynamic for my tastes.
val allFields = schema.fieldsIncludingSuperclasses()
println (" interfaces = ${schema.interfaces}")
for (itf in schema.interfaces) {
itf.methods.forEach {
println (" param ${it.name}")
val fieldNameFromItf = when {
it.name.startsWith("get") -> it.name.substring(3).decapitalize()
else -> throw InterfaceMismatchException(

View File

@ -42,6 +42,8 @@ data class CarpenterSchemas (
val size
get() = carpenterSchemas.size
fun isEmpty() = carpenterSchemas.isEmpty()
}
/**
@ -80,6 +82,9 @@ abstract class MetaCarpenterBase (val schemas : CarpenterSchemas) {
}
abstract fun build()
val classloader : ClassLoader
get() = cc.classloader
}
class MetaCarpenter(schemas: CarpenterSchemas) : MetaCarpenterBase(schemas) {

View File

@ -0,0 +1,105 @@
package net.corda.core.serialization.amqp
import org.junit.Test
import kotlin.test.*
import net.corda.core.serialization.carpenter.ClassCarpenter
import net.corda.core.serialization.carpenter.ClassSchema
import net.corda.core.serialization.carpenter.NonNullableField
interface I {
fun getName() : String
}
class DeserializeNeedingCarpentryTests {
@Test
fun verySimpleType() {
val testVal = 10
val cc = ClassCarpenter()
val schema = ClassSchema("oneType", mapOf("a" to NonNullableField(Int::class.java)))
val clazz = cc.build (schema)
val classInstance = clazz.constructors[0].newInstance(testVal)
val serialisedBytes = SerializationOutput().serialize(classInstance)
val deserializedObj = DeserializationInput().deserialize(serialisedBytes)
assertEquals (testVal, deserializedObj::class.java.getMethod("getA").invoke(deserializedObj))
}
@Test
fun simpleTypeKnownInterface() {
val cc = ClassCarpenter()
val schema = ClassSchema("oneType", mapOf("name" to NonNullableField(String::class.java)),
interfaces = listOf (I::class.java))
val clazz = cc.build (schema)
val testVal = "Andrew Person"
val classInstance = clazz.constructors[0].newInstance(testVal)
val serialisedBytes = SerializationOutput().serialize(classInstance)
val deserializedObj = DeserializationInput().deserialize(serialisedBytes)
assertTrue(deserializedObj is I)
assertEquals(testVal, (deserializedObj as I).getName())
}
@Test
fun nestedTypes() {
val cc = ClassCarpenter()
val nestedClass = cc.build (
ClassSchema("nestedType",
mapOf("name" to NonNullableField(String::class.java))))
val outerClass = cc.build (
ClassSchema("outerType",
mapOf("inner" to NonNullableField(nestedClass))))
val classInstance = outerClass.constructors.first().newInstance(nestedClass.constructors.first().newInstance("name"))
val serialisedBytes = SerializationOutput().serialize(classInstance)
val deserializedObj = DeserializationInput().deserialize(serialisedBytes)
val inner = deserializedObj::class.java.getMethod("getInner").invoke(deserializedObj)
assertEquals("name", inner::class.java.getMethod("getName").invoke(inner))
}
@Test
fun repeatedNestedTypes() {
val cc = ClassCarpenter()
val nestedClass = cc.build (
ClassSchema("nestedType",
mapOf("name" to NonNullableField(String::class.java))))
data class outer(val a: Any, val b: Any)
val classInstance = outer (
nestedClass.constructors.first().newInstance("foo"),
nestedClass.constructors.first().newInstance("bar"))
val serialisedBytes = SerializationOutput().serialize(classInstance)
val deserializedObj = DeserializationInput().deserialize(serialisedBytes)
assertEquals ("foo", deserializedObj.a::class.java.getMethod("getName").invoke(deserializedObj.a))
assertEquals ("bar", deserializedObj.b::class.java.getMethod("getName").invoke(deserializedObj.b))
}
@Test
fun listOfType() {
val cc = ClassCarpenter()
val unknownClass = cc.build (ClassSchema("unknownClass", mapOf(
"v1" to NonNullableField(Int::class.java),
"v2" to NonNullableField(Int::class.java))))
data class outer (val l : List<Any>)
val toSerialise = outer (listOf (
unknownClass.constructors.first().newInstance(1, 2),
unknownClass.constructors.first().newInstance(3, 4),
unknownClass.constructors.first().newInstance(5, 6),
unknownClass.constructors.first().newInstance(7, 8)))
val serialisedBytes = SerializationOutput().serialize(toSerialise)
val deserializedObj = DeserializationInput().deserialize(serialisedBytes)
var sentinel = 1
deserializedObj.l.forEach {
assertEquals(sentinel++, it::class.java.getMethod("getV1").invoke(it))
assertEquals(sentinel++, it::class.java.getMethod("getV2").invoke(it))
}
}
}

View File

@ -1,8 +1,9 @@
package net.corda.nodeapi.internal.serialization.carpenter
import net.corda.nodeapi.internal.serialization.amqp.*
import net.corda.nodeapi.internal.serialization.amqp.Schema
import net.corda.nodeapi.internal.serialization.amqp.Field
import net.corda.nodeapi.internal.serialization.amqp.Schema
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
import net.corda.nodeapi.internal.serialization.amqp.SerializationOutput
fun mangleName(name: String) = "${name}__carpenter"