CORDA-786 - Use reflection to infer proxy and proxied types in CorDapp custom serializers

This removes any need for the user implement and override types from the
super class

    * CORDA-786 - Docs update
    * CORDA-786 - Remove unneeded second annotation on the proxy objects
    * Fix merge conflicts
This commit is contained in:
Katelyn Baker 2017-12-04 13:10:02 +00:00
parent d25b7f560c
commit fcec60e232
18 changed files with 80 additions and 110 deletions

View File

@ -38,7 +38,6 @@ interface Cordapp {
val schedulableFlows: List<Class<out FlowLogic<*>>>
val services: List<Class<out SerializeAsToken>>
val serializationWhitelists: List<SerializationWhitelist>
val serializationCustomSerializerProxies: List<Class<*>>
val serializationCustomSerializers: List<Class<out SerializationCustomSerializer<*, *>>>
val customSchemas: Set<MappedSchema>
val jarPath: URL

View File

@ -17,7 +17,6 @@ data class CordappImpl(
override val schedulableFlows: List<Class<out FlowLogic<*>>>,
override val services: List<Class<out SerializeAsToken>>,
override val serializationWhitelists: List<SerializationWhitelist>,
override val serializationCustomSerializerProxies: List<Class<*>>,
override val serializationCustomSerializers: List<Class<out SerializationCustomSerializer<*, *>>>,
override val customSchemas: Set<MappedSchema>,
override val jarPath: URL) : Cordapp {

View File

@ -8,11 +8,3 @@ package net.corda.core.serialization
@Target(AnnotationTarget.CLASS)
annotation class CordaCustomSerializer
/**
* This annotation marks a class as being a proxy for some third party class and used by some
* implementation of [SerializationCustomSerializer]. Such classes must be annotated to allow
* them to be discovered in a CorDapp jar and loaded
*/
@Target(AnnotationTarget.CLASS)
annotation class CordaCustomSerializerProxy

View File

@ -17,9 +17,6 @@ import java.lang.reflect.Type
*
* Failing to apply either annotation will result in the class not being loaded by Corda and thus serialization
* failing
*
* @property type the type of the object that this class is proxying
* @property ptype the type of the proxy object used as an intermediate representation of [type]
*/
interface SerializationCustomSerializer<OBJ, PROXY> {
/**
@ -33,14 +30,4 @@ interface SerializationCustomSerializer<OBJ, PROXY> {
* unserializable type
*/
fun fromProxy(proxy: PROXY) : OBJ
/**
* Should be set to the type of the object being proxied
*/
val type: Type
/**
* Should be set to the proxy objects type
*/
val ptype: Type
}

View File

@ -1,16 +1,18 @@
Pluggable Serializers for CorDapps
==================================
To be serializable by Corda Java classes must be compiled with the -parameter switch to enable matching of it's properties
to constructor parameters. This is important because Corda's internal AMQP serialization scheme will only constuct
.. contents::
To be serializable by Corda Java classes must be compiled with the -parameters switch to enable matching of it's properties
to constructor parameters. This is important because Corda's internal AMQP serialization scheme will only construct
objects using their constructors. However, when recompilation isn't possible, or classes are built in such a way that
they cannot be easily modified for simple serailization, CorDapps can provide custom proxy serializers that Corda
can use to move from types it cannot serializer to an interim representation that it can with the transformation to and
can use to move from types it cannot serialize to an interim representation that it can with the transformation to and
from this proxy object being handled by the supplied serializer.
Serializer Location
-------------------
Custom serializers should be placed in the plugins directory of a CorDapp or a sub directory thereof. These
classes will be scanned and loaded by the CorDapp loading process.
Custom serializer classes should follow the rules for including classes found in :doc:`cordapp-build-systems`
Writing a Custom Serializer
---------------------------
@ -26,7 +28,9 @@ Example
-------
Consider this example class
.. sourcecode:: java
public final class Example {
private final Int a
private final Int b
@ -42,33 +46,31 @@ Consider this example class
public int getB() { return b; }
}
Without a custom serializer we cannot serialise this class as there is no public constructor that facilitates the
initialisation of al of its's properties.
Without a custom serializer we cannot serialize this class as there is no public constructor that facilitates the
initialisation of all of its's properties.
To be serializable by Corda this would require a custom serializer as follows
.. sourcecode:: kotlin
@CordaCustomSerializer
class ExampleSerializer : SerializationCustomSerializer {
class ExampleSerializer : SerializationCustomSerializer<Example, ExampleSerializer.Proxy> {
@CordaCustomSerializerProxy
data class Proxy(val a: Int, val b: Int)
override fun toProxy(obj: Any): Any = Proxy((obj as Example).a, obj.b)
override fun toProxy(obj: Example) = Proxy(obj.a, obj.b)
override fun fromProxy(proxy: Any): Any {
override fun fromProxy(proxy: Proxy) : Example {
val constructorArg = IntArray(2);
constructorArg[0] = (proxy as Proxy).a
constructorArg[0] = proxy.a
constructorArg[1] = proxy.b
return Example.create(constructorArg)
}
override val type: Type get() = Example::class.java
override val ptype: Type get() = Proxy::class.java
}
Whitelisting
------------
By writing a custom serializer for a class it has the effect of adding that class to the whitelist, meaning such
classes don't need explicitly adding to the CorDapp's whitelist
classes don't need explicitly adding to the CorDapp's whitelist.

View File

@ -5,24 +5,70 @@ import net.corda.core.serialization.SerializationCustomSerializer
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion.nameForType
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException
import java.lang.reflect.Type
import kotlin.reflect.jvm.javaType
import kotlin.reflect.jvm.jvmErasure
/**
* Index into the types list of the parent type of the serializer object, should be the
* type that this object proxies for
*/
const val CORDAPP_TYPE = 0
/**
* Index into the types list of the parent type of the serializer object, should be the
* type of the proxy object that we're using to represent the object we're proxying for
*/
const val PROXY_TYPE = 1
/**
* Wrapper class for user provided serializers
*
* Through the CorDapp JAR scanner we will have a list of custom serializer types that implement
* the toProxy and fromProxy methods. This class takes an instance of one of those objects and
* embeds it within a serialization context associated with a serializer factory by creating
* and instance of this class and registering that with a [SerializerFactory]
*
* Proxy serializers should transform an unserializable class into a representation that we can serialize
*
* @property serializer in instance of a user written serialization proxy, normally scanned and loaded
* automatically
* @property type the Java [Type] of the class which this serializes, inferred via reflection of the
* [serializer]'s super type
* @property proxyType the Java [Type] of the class into which instances of [type] are proxied for use byt
* the underlying serialisation engine
*
* @param factory a [SerializerFactory] belonging to the context this serializer is being instantiated
* for
*/
class CorDappCustomSerializer(
private val serialiser: SerializationCustomSerializer<*, *>,
factory: SerializerFactory)
: AMQPSerializer<Any>, SerializerFor {
private val serializer: SerializationCustomSerializer<*, *>,
factory: SerializerFactory) : AMQPSerializer<Any>, SerializerFor {
override val revealSubclassesInSchema: Boolean get() = false
override val type: Type get() = serialiser.type
private val types = serializer::class.supertypes.filter { it.jvmErasure == SerializationCustomSerializer::class }
.flatMap { it.arguments }
.map { it.type!!.javaType }
init {
if (types.size != 2) {
throw NotSerializableException("Unable to determine serializer parent types")
}
}
override val type = types[CORDAPP_TYPE]
val proxyType = types[PROXY_TYPE]
override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${nameForType(type)}")
val descriptor: Descriptor = Descriptor(typeDescriptor)
private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer(serialiser.ptype, factory) }
private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer(proxyType, factory) }
override fun writeClassInfo(output: SerializationOutput) {}
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
@Suppress("UNCHECKED_CAST")
val proxy = (serialiser as SerializationCustomSerializer<Any?,Any?>).toProxy(obj)
val proxy = uncheckedCast<SerializationCustomSerializer<*, *>,
SerializationCustomSerializer<Any?,Any?>> (serializer).toProxy(obj)
data.withDescribed(descriptor) {
data.withList {
@ -33,11 +79,10 @@ class CorDappCustomSerializer(
}
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput) =
@Suppress("UNCHECKED_CAST")
(serialiser as SerializationCustomSerializer<Any?,Any?>).fromProxy(
uncheckedCast(proxySerializer.readObject(obj, schema, input)))!!
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput) =
uncheckedCast<SerializationCustomSerializer<*, *>, SerializationCustomSerializer<Any?,Any?>> (
serializer).fromProxy(uncheckedCast(proxySerializer.readObject(obj, schemas, input)))!!
override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == type
override fun isSerializerFor(clazz: Class<*>) = clazz == type
}

View File

@ -204,8 +204,8 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
}
/**
* Iterate over an AMQP schema, for each type ascertain weather it's on ClassPath of [classloader] amd
* if not use the [ClassCarpenter] to generate a class to use in it's place
* Iterate over an AMQP schema, for each type ascertain whether it's on ClassPath of [classloader] and,
* if not, use the [ClassCarpenter] to generate a class to use in it's place.
*/
private fun processSchema(schemaAndDescriptor: FactorySchemaAndDescriptor, sentinel: Boolean = false) {
val metaSchema = CarpenterMetaSchema.newInstance()

View File

@ -2,27 +2,21 @@ package net.corda.nodeapi.internal.serialization.amqp
import org.junit.Test
import net.corda.core.serialization.CordaCustomSerializer
import net.corda.core.serialization.CordaCustomSerializerProxy
import net.corda.core.serialization.ClassWhitelist
import net.corda.core.serialization.SerializationCustomSerializer
import org.assertj.core.api.Assertions
import java.io.NotSerializableException
import java.lang.reflect.Type
import kotlin.test.assertEquals
class CorDappSerializerTests {
data class NeedsProxy (val a: String)
@CordaCustomSerializer
class NeedsProxyProxySerializer : SerializationCustomSerializer {
@CordaCustomSerializerProxy
class NeedsProxyProxySerializer : SerializationCustomSerializer<NeedsProxy, NeedsProxyProxySerializer.Proxy> {
data class Proxy(val proxy_a_: String)
override val type: Type get() = NeedsProxy::class.java
override val ptype: Type get() = Proxy::class.java
override fun fromProxy(proxy: Any) : Any = NeedsProxy((proxy as Proxy).proxy_a_)
override fun toProxy(obj: Any) : Any = Proxy((obj as NeedsProxy).a)
override fun fromProxy(proxy: Proxy) = NeedsProxy(proxy.proxy_a_)
override fun toProxy(obj: NeedsProxy) = Proxy(obj.a)
}
// Standard proxy serializer used internally, here for comparison purposes
@ -34,12 +28,10 @@ class CorDappSerializerTests {
data class Proxy(val proxy_a_: String)
override fun toProxy(obj: NeedsProxy): Proxy {
println ("InternalProxySerializer - toProxy")
return Proxy(obj.a)
}
override fun fromProxy(proxy: Proxy): NeedsProxy {
println ("InternalProxySerializer - fromProxy")
return NeedsProxy(proxy.proxy_a_)
}
}

View File

@ -181,7 +181,6 @@ class CordappLoader private constructor(private val cordappJarPaths: List<Restri
schedulableFlows = listOf(),
services = listOf(),
serializationWhitelists = listOf(),
serializationCustomSerializerProxies = listOf(),
serializationCustomSerializers = listOf(),
customSchemas = setOf(),
jarPath = ContractUpgradeFlow.javaClass.protectionDomain.codeSource.location // Core JAR location
@ -198,7 +197,6 @@ class CordappLoader private constructor(private val cordappJarPaths: List<Restri
findSchedulableFlows(scanResult),
findServices(scanResult),
findPlugins(it),
findSerialiserProxies(scanResult),
findSerialzers(scanResult),
findCustomSchemas(scanResult),
it.url)
@ -250,10 +248,6 @@ class CordappLoader private constructor(private val cordappJarPaths: List<Restri
} + DefaultWhitelist // Always add the DefaultWhitelist to the whitelist for an app.
}
private fun findSerialiserProxies(scanResult: RestrictedScanResult) : List<Class<*>> {
return scanResult.getClassesWithAnnotation(Class::class, CordaCustomSerializerProxy::class)
}
private fun findSerialzers(scanResult: RestrictedScanResult) : List<Class<out SerializationCustomSerializer<*, *>>> {
return scanResult.getClassesWithAnnotation(SerializationCustomSerializer::class, CordaCustomSerializer::class)
}

View File

@ -3,20 +3,14 @@ package net.corda.vega.plugin.customserializers
import com.opengamma.strata.market.param.CurrencyParameterSensitivities
import com.opengamma.strata.market.param.CurrencyParameterSensitivity
import net.corda.core.serialization.CordaCustomSerializer
import net.corda.core.serialization.CordaCustomSerializerProxy
import net.corda.core.serialization.SerializationCustomSerializer
import java.lang.reflect.Type
@CordaCustomSerializer
@Suppress("UNUSED")
class CurrencyParameterSensitivitiesSerializer :
SerializationCustomSerializer<CurrencyParameterSensitivities, CurrencyParameterSensitivitiesSerializer.Proxy> {
@CordaCustomSerializerProxy
data class Proxy(val sensitivities: List<CurrencyParameterSensitivity>)
override val type: Type get() = CurrencyParameterSensitivities::class.java
override val ptype: Type get() = Proxy::class.java
override fun fromProxy(proxy: Proxy) = CurrencyParameterSensitivities.of(proxy.sensitivities)
override fun toProxy(obj: CurrencyParameterSensitivities) = Proxy(obj.sensitivities)
}

View File

@ -6,22 +6,16 @@ import com.opengamma.strata.data.MarketDataName
import com.opengamma.strata.collect.array.DoubleArray
import com.opengamma.strata.basics.currency.Currency
import net.corda.core.serialization.CordaCustomSerializer
import net.corda.core.serialization.CordaCustomSerializerProxy
import net.corda.core.serialization.SerializationCustomSerializer
import java.lang.reflect.Type
@CordaCustomSerializer
@Suppress("UNUSED")
class CurrencyParameterSensitivitySerializer :
SerializationCustomSerializer<CurrencyParameterSensitivity, CurrencyParameterSensitivitySerializer.Proxy> {
@CordaCustomSerializerProxy
data class Proxy(val currency: Currency, val marketDataName: MarketDataName<*>,
val parameterMetadata: List<ParameterMetadata>,
val sensitivity: DoubleArray)
override val type: Type get() = CurrencyParameterSensitivity::class.java
override val ptype: Type get() = Proxy::class.java
override fun fromProxy(proxy: CurrencyParameterSensitivitySerializer.Proxy) =
CurrencyParameterSensitivity.of(
proxy.marketDataName,

View File

@ -2,19 +2,13 @@ package net.corda.vega.plugin.customserializers
import com.opengamma.strata.basics.currency.Currency
import net.corda.core.serialization.CordaCustomSerializer
import net.corda.core.serialization.CordaCustomSerializerProxy
import net.corda.core.serialization.SerializationCustomSerializer
import java.lang.reflect.Type
@CordaCustomSerializer
@Suppress("UNUSED")
class CurrencySerializer : SerializationCustomSerializer<Currency, CurrencySerializer.Proxy> {
@CordaCustomSerializerProxy
data class Proxy(val currency: String)
override val type: Type get() = Currency::class.java
override val ptype: Type get() = Proxy::class.java
override fun fromProxy(proxy: Proxy) = Currency.parse(proxy.currency)
override fun toProxy(obj: Currency) = Proxy(obj.toString())
}

View File

@ -1,20 +1,14 @@
package net.corda.vega.plugin.customserializers
import net.corda.core.serialization.CordaCustomSerializer
import net.corda.core.serialization.CordaCustomSerializerProxy
import net.corda.core.serialization.SerializationCustomSerializer
import com.opengamma.strata.collect.array.DoubleArray
import java.lang.reflect.Type
@CordaCustomSerializer
@Suppress("UNUSED")
class DoubleArraySerializer : SerializationCustomSerializer<DoubleArray, DoubleArraySerializer.Proxy> {
@CordaCustomSerializerProxy
data class Proxy(val amount: kotlin.DoubleArray)
override val type: Type get() = DoubleArray::class.java
override val ptype: Type get() = Proxy::class.java
override fun fromProxy(proxy: Proxy) = DoubleArray.copyOf(proxy.amount)
override fun toProxy(obj: DoubleArray) = Proxy(obj.toArray())
}

View File

@ -3,20 +3,15 @@ package net.corda.vega.plugin.customserializers
import com.opengamma.strata.basics.currency.MultiCurrencyAmount
import com.opengamma.strata.basics.currency.Currency
import net.corda.core.serialization.*
import java.lang.reflect.Type
@CordaCustomSerializer
@Suppress("UNUSED")
class MultiCurrencyAmountSerializer :
SerializationCustomSerializer<MultiCurrencyAmount, MultiCurrencyAmountSerializer.Proxy> {
@CordaCustomSerializerProxy
data class Proxy(val curencies : Map<Currency, Double>)
override fun toProxy(obj: MultiCurrencyAmount) = Proxy(obj.toMap())
override fun fromProxy(proxy: Proxy) = MultiCurrencyAmount.of(proxy.curencies)
override val type: Type get() = MultiCurrencyAmount::class.java
override val ptype: Type get() = Proxy::class.java
}

View File

@ -3,20 +3,14 @@ package net.corda.vega.plugin.customserializers
import com.opengamma.strata.basics.date.Tenor
import com.opengamma.strata.market.param.TenorDateParameterMetadata
import net.corda.core.serialization.*
import java.lang.reflect.Type
import java.time.LocalDate
@CordaCustomSerializer
@Suppress("UNUSED")
class TenorDateParameterMetadataSerializer :
SerializationCustomSerializer<TenorDateParameterMetadata, TenorDateParameterMetadataSerializer.Proxy> {
@CordaCustomSerializerProxy
data class Proxy(val tenor: Tenor, val date: LocalDate, val identifier: Tenor, val label: String)
override val type: Type get() = TenorDateParameterMetadata::class.java
override val ptype: Type get() = Proxy::class.java
override fun toProxy(obj: TenorDateParameterMetadata) = Proxy(obj.tenor, obj.date, obj.identifier, obj.label)
override fun fromProxy(proxy: Proxy) = TenorDateParameterMetadata.of(proxy.date, proxy.tenor, proxy.label)
}

View File

@ -2,18 +2,13 @@ package net.corda.vega.plugin.customserializers
import com.opengamma.strata.basics.date.Tenor
import net.corda.core.serialization.*
import java.lang.reflect.Type
import java.time.Period
@CordaCustomSerializer
@Suppress("UNUSED")
class TenorSerializer : SerializationCustomSerializer<Tenor, TenorSerializer.Proxy> {
@CordaCustomSerializerProxy
data class Proxy(val years: Int, val months: Int, val days: Int, val name: String)
override val type: Type get() = Tenor::class.java
override val ptype: Type get() = Proxy::class.java
override fun toProxy(obj: Tenor) = Proxy(obj.period.years, obj.period.months, obj.period.days, obj.toString())
override fun fromProxy(proxy: Proxy) = Tenor.of (Period.of(proxy.years, proxy.months, proxy.days))
}

View File

@ -104,6 +104,7 @@ private fun createTestSerializationEnv(label: String) = object : SerializationEn
},
AMQP_P2P_CONTEXT,
KRYO_RPC_SERVER_CONTEXT,
KRYO_RPC_CLIENT_CONTEXT,
AMQP_STORAGE_CONTEXT,
KRYO_CHECKPOINT_CONTEXT) {
override fun toString() = "testSerializationEnv($label)"

View File

@ -22,7 +22,6 @@ class MockCordappProvider(cordappLoader: CordappLoader, attachmentStorage: Attac
schedulableFlows = emptyList(),
services = emptyList(),
serializationWhitelists = emptyList(),
serializationCustomSerializerProxies = emptyList(),
serializationCustomSerializers = emptyList(),
customSchemas = emptySet(),
jarPath = Paths.get(".").toUri().toURL())