Corda 1922 serialize states with calculated values (#3938)

* Introduce SerializeForCarpenter annotation

* Apply SerializableComputedProperty annotation to Cash.exitKeys, fix bugs

* info -> trace

* Remove annotation from FungibleAsset, as we do not know whether all implementing classes will provide the property as a calculated value

* Remove redundant import

* Explicit lambda params

* Restore explicit import for Enum valueOf

* Moving and rescoping

* More meaningful error message

* Add java test and documentation

* Fix accidentally broken unit test

* Ignore superclass annotation if property not calculated in implementing class

* Exclude calculated properties from Jackson serialisation

* Fix broken test
This commit is contained in:
Dominic Fox
2018-10-09 14:54:31 +01:00
committed by GitHub
parent 9c8a1cd14a
commit b6f2532ce6
26 changed files with 554 additions and 204 deletions

View File

@ -98,7 +98,7 @@ private class CordaSerializableBeanSerializerModifier : BeanSerializerModifier()
val ctor = constructorForDeserialization(beanClass) val ctor = constructorForDeserialization(beanClass)
val amqpProperties = propertiesForSerialization(ctor, beanClass, serializerFactory) val amqpProperties = propertiesForSerialization(ctor, beanClass, serializerFactory)
.serializationOrder .serializationOrder
.map { it.serializer.name } .mapNotNull { if (it.isCalculated) null else it.serializer.name }
val propertyRenames = beanDesc.findProperties().associateBy({ it.name }, { it.internalName }) val propertyRenames = beanDesc.findProperties().associateBy({ it.name }, { it.internalName })
(amqpProperties - propertyRenames.values).let { (amqpProperties - propertyRenames.values).let {
check(it.isEmpty()) { "Jackson didn't provide serialisers for $it" } check(it.isEmpty()) { "Jackson didn't provide serialisers for $it" }

View File

@ -3,6 +3,7 @@ package net.corda.core.contracts
import net.corda.core.KeepForDJVM import net.corda.core.KeepForDJVM
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.serialization.SerializableCalculatedProperty
import java.security.PublicKey import java.security.PublicKey
/** /**
@ -38,6 +39,7 @@ interface FungibleAsset<T : Any> : OwnableState {
* There must be an ExitCommand signed by these keys to destroy the amount. While all states require their * There must be an ExitCommand signed by these keys to destroy the amount. While all states require their
* owner to sign, some (i.e. cash) also require the issuer. * owner to sign, some (i.e. cash) also require the issuer.
*/ */
@get:SerializableCalculatedProperty
val exitKeys: Collection<PublicKey> val exitKeys: Collection<PublicKey>
/** /**

View File

@ -19,4 +19,12 @@ import java.lang.annotation.Inherited
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
@Inherited @Inherited
annotation class CordaSerializable annotation class CordaSerializable
/**
* Used to annotate methods which expose calculated values that we want to be serialized for use by the class carpenter.
*/
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.FUNCTION)
annotation class SerializableCalculatedProperty

View File

@ -556,10 +556,33 @@ be able to use reflection over the deserialized data, for scripting languages th
ensuring classes not on the classpath can be deserialized without loading potentially malicious code. ensuring classes not on the classpath can be deserialized without loading potentially malicious code.
If the original class implements some interfaces then the carpenter will make sure that all of the interface methods are If the original class implements some interfaces then the carpenter will make sure that all of the interface methods are
backed by feilds. If that's not the case then an exception will be thrown during deserialization. This check can backed by fields. If that's not the case then an exception will be thrown during deserialization. This check can
be turned off with ``SerializationContext.withLenientCarpenter``. This can be useful if only the field getters are needed, be turned off with ``SerializationContext.withLenientCarpenter``. This can be useful if only the field getters are needed,
say in an object viewer. say in an object viewer.
Calculated values
`````````````````
In some cases, for example the `exitKeys` field in ``FungibleState``, a property in an interface may normally be implemented
as a *calculated* value, with a "getter" method for reading it but neither a corresponding constructor parameter nor a
"setter" method for writing it. In this case, it will not automatically be included among the properties to be serialized,
since the receiving class would ordinarily be able to re-calculate it on demand. However, a synthesized class will not
have the method implementation which knows how to calculate the value, and a cast to the interface will fail because the
property is not serialized and so the "getter" method present in the interface will not be synthesized.
The solution is to annotate the method with the ``SerializableCalculatedProperty`` annotation, which will cause the value
exposed by the method to be read and transmitted during serialization, but discarded during normal deserialization. The
synthesized class will then include a backing field together with a "getter" for the serialized calculated value, and will
remain compatible with the interface.
If the annotation is added to the method in the *interface*, then all implementing classes must calculate the value and
none may have a corresponding backing field; alternatively, it can be added to the overriding method on each implementing
class where the value is calculated and there is no backing field. If the field is a Kotlin ``val``, then the annotation
should be targeted at its getter method, e.g. ``@get:SerializableCalculatedProperty``.
Future enhancements
```````````````````
Possible future enhancements include: Possible future enhancements include:
#. Java singleton support. We will add support for identifying classes which are singletons and identifying the #. Java singleton support. We will add support for identifying classes which are singletons and identifying the

View File

@ -16,6 +16,7 @@ import net.corda.core.node.ServiceHub
import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.QueryableState import net.corda.core.schemas.QueryableState
import net.corda.core.serialization.SerializableCalculatedProperty
import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.finance.contracts.asset.cash.selection.AbstractCashSelection import net.corda.finance.contracts.asset.cash.selection.AbstractCashSelection

View File

@ -34,6 +34,7 @@ import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.startFlow import net.corda.testing.node.internal.startFlow
import org.junit.After import org.junit.After
import org.junit.Test import org.junit.Test
import java.security.PublicKey
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.reflect.jvm.jvmName import kotlin.reflect.jvm.jvmName
@ -127,7 +128,7 @@ class VaultSoftLockManagerTest {
override val owner get() = participants[0] override val owner get() = participants[0]
override fun withNewOwner(newOwner: AbstractParty) = throw UnsupportedOperationException() override fun withNewOwner(newOwner: AbstractParty) = throw UnsupportedOperationException()
override val amount get() = Amount(1, Issued(PartyAndReference(owner, OpaqueBytes.of(1)), Unit)) override val amount get() = Amount(1, Issued(PartyAndReference(owner, OpaqueBytes.of(1)), Unit))
override val exitKeys get() = throw UnsupportedOperationException() override val exitKeys get() = emptyList<PublicKey>()
override fun withNewOwnerAndAmount(newAmount: Amount<Issued<Unit>>, newOwner: AbstractParty) = throw UnsupportedOperationException() override fun withNewOwnerAndAmount(newAmount: Amount<Issued<Unit>>, newOwner: AbstractParty) = throw UnsupportedOperationException()
override fun equals(other: Any?) = other is FungibleAssetImpl && participants == other.participants override fun equals(other: Any?) = other is FungibleAssetImpl && participants == other.participants
override fun hashCode() = participants.hashCode() override fun hashCode() = participants.hashCode()

View File

@ -117,7 +117,7 @@ class DeserializationInput constructor(
des { des {
val envelope = getEnvelope(bytes, context.encodingWhitelist) val envelope = getEnvelope(bytes, context.encodingWhitelist)
logger.trace("deserialize blob scheme=\"${envelope.schema.toString()}\"") logger.trace("deserialize blob scheme=\"${envelope.schema}\"")
clazz.cast(readObjectOrNull(envelope.obj, SerializationSchemas(envelope.schema, envelope.transformsSchema), clazz.cast(readObjectOrNull(envelope.obj, SerializationSchemas(envelope.schema, envelope.transformsSchema),
clazz, context)) clazz, context))
@ -149,7 +149,7 @@ class DeserializationInput constructor(
if (obj is DescribedType && ReferencedObject.DESCRIPTOR == obj.descriptor) { if (obj is DescribedType && ReferencedObject.DESCRIPTOR == obj.descriptor) {
// It must be a reference to an instance that has already been read, cheaply and quickly returning it by reference. // It must be a reference to an instance that has already been read, cheaply and quickly returning it by reference.
val objectIndex = (obj.described as UnsignedInteger).toInt() val objectIndex = (obj.described as UnsignedInteger).toInt()
if (objectIndex !in 0..objectHistory.size) if (objectIndex >= objectHistory.size)
throw AMQPNotSerializableException( throw AMQPNotSerializableException(
type, type,
"Retrieval of existing reference failed. Requested index $objectIndex " + "Retrieval of existing reference failed. Requested index $objectIndex " +

View File

@ -125,7 +125,7 @@ abstract class EvolutionSerializer(
// any particular NonNullable annotation type to indicate cross // any particular NonNullable annotation type to indicate cross
// compiler nullability // compiler nullability
val isKotlin = (new.type.javaClass.declaredAnnotations.any { val isKotlin = (new.type.javaClass.declaredAnnotations.any {
it.annotationClass.qualifiedName == "kotlin.Metadata" it.annotationClass.qualifiedName == "kotlin.Metadata"
}) })
constructor.parameters.withIndex().forEach { constructor.parameters.withIndex().forEach {
@ -270,8 +270,8 @@ class EvolutionSerializerViaSetters(
* be an object that returns an [EvolutionSerializer]. Of course, any implementation that * be an object that returns an [EvolutionSerializer]. Of course, any implementation that
* extends this class can be written to invoke whatever behaviour is desired. * extends this class can be written to invoke whatever behaviour is desired.
*/ */
abstract class EvolutionSerializerGetterBase { interface EvolutionSerializerProvider {
abstract fun getEvolutionSerializer( fun getEvolutionSerializer(
factory: SerializerFactory, factory: SerializerFactory,
typeNotation: TypeNotation, typeNotation: TypeNotation,
newSerializer: AMQPSerializer<Any>, newSerializer: AMQPSerializer<Any>,
@ -283,12 +283,12 @@ abstract class EvolutionSerializerGetterBase {
* between the received schema and the class as it exists now on the class path, * between the received schema and the class as it exists now on the class path,
*/ */
@KeepForDJVM @KeepForDJVM
class EvolutionSerializerGetter : EvolutionSerializerGetterBase() { object DefaultEvolutionSerializerProvider : EvolutionSerializerProvider {
override fun getEvolutionSerializer(factory: SerializerFactory, override fun getEvolutionSerializer(factory: SerializerFactory,
typeNotation: TypeNotation, typeNotation: TypeNotation,
newSerializer: AMQPSerializer<Any>, newSerializer: AMQPSerializer<Any>,
schemas: SerializationSchemas): AMQPSerializer<Any> { schemas: SerializationSchemas): AMQPSerializer<Any> {
return factory.serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) { return factory.registerByDescriptor(typeNotation.descriptor.name!!) {
when (typeNotation) { when (typeNotation) {
is CompositeType -> EvolutionSerializer.make(typeNotation, newSerializer as ObjectSerializer, factory) is CompositeType -> EvolutionSerializer.make(typeNotation, newSerializer as ObjectSerializer, factory)
is RestrictedType -> { is RestrictedType -> {
@ -310,5 +310,4 @@ class EvolutionSerializerGetter : EvolutionSerializerGetterBase() {
} }
} }
} }
} }

View File

@ -2,6 +2,7 @@ package net.corda.serialization.internal.amqp
import net.corda.core.internal.isConcreteClass import net.corda.core.internal.isConcreteClass
import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.trace import net.corda.core.utilities.trace
import net.corda.serialization.internal.amqp.SerializerFactory.Companion.nameForType import net.corda.serialization.internal.amqp.SerializerFactory.Companion.nameForType
@ -61,7 +62,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
context: SerializationContext, context: SerializationContext,
debugIndent: Int) = ifThrowsAppend({ clazz.typeName } debugIndent: Int) = ifThrowsAppend({ clazz.typeName }
) { ) {
if (propertySerializers.size != javaConstructor?.parameterCount && if (propertySerializers.deserializableSize != javaConstructor?.parameterCount &&
javaConstructor?.parameterCount ?: 0 > 0 javaConstructor?.parameterCount ?: 0 > 0
) { ) {
throw AMQPNotSerializableException(type, "Serialization constructor for class $type expects " throw AMQPNotSerializableException(type, "Serialization constructor for class $type expects "
@ -86,8 +87,9 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
input: DeserializationInput, input: DeserializationInput,
context: SerializationContext): Any = ifThrowsAppend({ clazz.typeName }) { context: SerializationContext): Any = ifThrowsAppend({ clazz.typeName }) {
if (obj is List<*>) { if (obj is List<*>) {
if (obj.size > propertySerializers.size) { if (obj.size != propertySerializers.size) {
throw AMQPNotSerializableException(type, "Too many properties in described type $typeName") throw AMQPNotSerializableException(type, "${obj.size} objects to deserialize, but " +
"${propertySerializers.size} properties in described type $typeName")
} }
return if (propertySerializers.byConstructor) { return if (propertySerializers.byConstructor) {
@ -109,8 +111,19 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
return construct(propertySerializers.serializationOrder return construct(propertySerializers.serializationOrder
.zip(obj) .zip(obj)
.map { Pair(it.first.initialPosition, it.first.serializer.readProperty(it.second, schemas, input, context)) } .mapNotNull { (accessor, obj) ->
.sortedWith(compareBy({ it.first })) // Ensure values get read out of input no matter what
val value = accessor.serializer.readProperty(obj, schemas, input, context)
when(accessor) {
is PropertyAccessorConstructor -> accessor.initialPosition to value
is CalculatedPropertyAccessor -> null
else -> throw UnsupportedOperationException(
"${accessor::class.simpleName} accessor not supported " +
"for constructor-based object building")
}
}
.sortedWith(compareBy { it.first })
.map { it.second }) .map { it.second })
} }

View File

@ -3,6 +3,7 @@ package net.corda.serialization.internal.amqp
import com.google.common.reflect.TypeToken import com.google.common.reflect.TypeToken
import net.corda.core.KeepForDJVM import net.corda.core.KeepForDJVM
import net.corda.core.internal.isPublic import net.corda.core.internal.isPublic
import net.corda.core.serialization.SerializableCalculatedProperty
import net.corda.serialization.internal.amqp.MethodClassifier.* import net.corda.serialization.internal.amqp.MethodClassifier.*
import java.lang.reflect.Field import java.lang.reflect.Field
import java.lang.reflect.Method import java.lang.reflect.Method
@ -84,7 +85,7 @@ private val propertyMethodRegex = Regex("(?<type>get|set|is)(?<var>\\p{Lu}.*)")
* take a single parameter of a type compatible with exampleProperty and isExampleProperty must * take a single parameter of a type compatible with exampleProperty and isExampleProperty must
* return a boolean * return a boolean
*/ */
fun Class<out Any?>.propertyDescriptors(): Map<String, PropertyDescriptor> { internal fun Class<out Any?>.propertyDescriptors(): Map<String, PropertyDescriptor> {
val fieldProperties = superclassChain().declaredFields().byFieldName() val fieldProperties = superclassChain().declaredFields().byFieldName()
return superclassChain().declaredMethods() return superclassChain().declaredMethods()
@ -96,9 +97,23 @@ fun Class<out Any?>.propertyDescriptors(): Map<String, PropertyDescriptor> {
.validated() .validated()
} }
/**
* Obtain [PropertyDescriptor]s for those calculated properties of a class which are annotated with
* [SerializableCalculatedProperty]
*/
internal fun Class<out Any?>.calculatedPropertyDescriptors(): Map<String, PropertyDescriptor> =
superclassChain().withInterfaces().declaredMethods()
.thatArePublic()
.thatAreCalculated()
.toCalculatedProperties()
// Generate the sequence of classes starting with this class and ascending through it superclasses. // Generate the sequence of classes starting with this class and ascending through it superclasses.
private fun Class<*>.superclassChain() = generateSequence(this, Class<*>::getSuperclass) private fun Class<*>.superclassChain() = generateSequence(this, Class<*>::getSuperclass)
private fun Sequence<Class<*>>.withInterfaces() = flatMap {
sequenceOf(it) + it.genericInterfaces.asSequence().map { it.asClass() }
}
// Obtain the fields declared by all classes in this sequence of classes. // Obtain the fields declared by all classes in this sequence of classes.
private fun Sequence<Class<*>>.declaredFields() = flatMap { it.declaredFields.asSequence() } private fun Sequence<Class<*>>.declaredFields() = flatMap { it.declaredFields.asSequence() }
@ -108,18 +123,45 @@ private fun Sequence<Class<*>>.declaredMethods() = flatMap { it.declaredMethods.
// Map a sequence of fields by field name. // Map a sequence of fields by field name.
private fun Sequence<Field>.byFieldName() = map { it.name to it }.toMap() private fun Sequence<Field>.byFieldName() = map { it.name to it }.toMap()
// Select only those methods that are public (and are not the "getClass" method) // Select only those methods that are public (and are not the "getClass" method).
private fun Sequence<Method>.thatArePublic() = filter { it.isPublic && it.name != "getClass" } private fun Sequence<Method>.thatArePublic() = filter { it.isPublic && it.name != "getClass" }
// Select only those methods that are annotated with [SerializableCalculatedProperty].
private fun Sequence<Method>.thatAreCalculated() = filter {
it.isAnnotationPresent(SerializableCalculatedProperty::class.java)
}
// Convert a sequence of calculated property methods to a map of property descriptors by property name.
private fun Sequence<Method>.toCalculatedProperties(): Map<String, PropertyDescriptor> {
val methodsByName = mutableMapOf<String, Method>()
for (method in this) {
val propertyNamedMethod = getPropertyNamedMethod(method)
?: throw IllegalArgumentException("Calculated property method must have a name beginning with 'get' or 'is'")
require(propertyNamedMethod.hasValidSignature()) {
"Calculated property name must have no parameters, and a non-void return type"
}
val propertyName = propertyNamedMethod.fieldName.decapitalize()
methodsByName.compute(propertyName) { _, existingMethod ->
if (existingMethod == null) method
else leastGenericBy({ genericReturnType }, existingMethod, method)
}
}
return methodsByName.mapValues { (_, method) -> PropertyDescriptor(null, null, method) }
}
// Select only those methods that are isX/getX/setX methods // Select only those methods that are isX/getX/setX methods
private fun Sequence<Method>.thatArePropertyMethods() = map { method -> private fun Sequence<Method>.thatArePropertyMethods() = mapNotNull(::getPropertyNamedMethod)
propertyMethodRegex.find(method.name)?.let { result ->
private fun getPropertyNamedMethod(method: Method): PropertyNamedMethod? {
return propertyMethodRegex.find(method.name)?.let { result ->
PropertyNamedMethod( PropertyNamedMethod(
result.groups[2]!!.value, result.groups[2]!!.value,
MethodClassifier.valueOf(result.groups[1]!!.value.toUpperCase()), MethodClassifier.valueOf(result.groups[1]!!.value.toUpperCase()),
method) method)
} }
}.filterNotNull() }
// Pick only those methods whose signatures are valid, discarding the remainder without warning. // Pick only those methods whose signatures are valid, discarding the remainder without warning.
private fun Sequence<PropertyNamedMethod>.withValidSignature() = filter { it.hasValidSignature() } private fun Sequence<PropertyNamedMethod>.withValidSignature() = filter { it.hasValidSignature() }

View File

@ -1,6 +1,7 @@
package net.corda.serialization.internal.amqp package net.corda.serialization.internal.amqp
import net.corda.core.KeepForDJVM import net.corda.core.KeepForDJVM
import net.corda.core.serialization.SerializableCalculatedProperty
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import java.io.NotSerializableException import java.io.NotSerializableException
import java.lang.reflect.Field import java.lang.reflect.Field
@ -19,9 +20,9 @@ abstract class PropertyReader {
* Accessor for those properties of a class that have defined getter functions. * Accessor for those properties of a class that have defined getter functions.
*/ */
@KeepForDJVM @KeepForDJVM
class PublicPropertyReader(private val readMethod: Method?) : PropertyReader() { class PublicPropertyReader(private val readMethod: Method) : PropertyReader() {
init { init {
readMethod?.isAccessible = true readMethod.isAccessible = true
} }
private fun Method.returnsNullable(): Boolean { private fun Method.returnsNullable(): Boolean {
@ -47,10 +48,10 @@ class PublicPropertyReader(private val readMethod: Method?) : PropertyReader() {
} }
override fun read(obj: Any?): Any? { override fun read(obj: Any?): Any? {
return readMethod!!.invoke(obj) return readMethod.invoke(obj)
} }
override fun isNullable(): Boolean = readMethod?.returnsNullable() ?: false override fun isNullable(): Boolean = readMethod.returnsNullable()
} }
/** /**
@ -112,7 +113,6 @@ class EvolutionPropertyReader : PropertyReader() {
* making the property accessible. * making the property accessible.
*/ */
abstract class PropertyAccessor( abstract class PropertyAccessor(
val initialPosition: Int,
open val serializer: PropertySerializer) { open val serializer: PropertySerializer) {
companion object : Comparator<PropertyAccessor> { companion object : Comparator<PropertyAccessor> {
override fun compare(p0: PropertyAccessor?, p1: PropertyAccessor?): Int { override fun compare(p0: PropertyAccessor?, p1: PropertyAccessor?): Int {
@ -120,13 +120,15 @@ abstract class PropertyAccessor(
} }
} }
open val isCalculated get() = false
/** /**
* Override to control how the property is set on the object. * Override to control how the property is set on the object.
*/ */
abstract fun set(instance: Any, obj: Any?) abstract fun set(instance: Any, obj: Any?)
override fun toString(): String { override fun toString(): String {
return "${serializer.name}($initialPosition)" return serializer.name
} }
} }
@ -135,9 +137,8 @@ abstract class PropertyAccessor(
* is serialized and deserialized via JavaBean getter and setter style methods. * is serialized and deserialized via JavaBean getter and setter style methods.
*/ */
class PropertyAccessorGetterSetter( class PropertyAccessorGetterSetter(
initialPosition: Int,
getter: PropertySerializer, getter: PropertySerializer,
private val setter: Method) : PropertyAccessor(initialPosition, getter) { private val setter: Method) : PropertyAccessor(getter) {
init { init {
/** /**
* Play nicely with Java interop, public methods aren't marked as accessible * Play nicely with Java interop, public methods aren't marked as accessible
@ -159,16 +160,34 @@ class PropertyAccessorGetterSetter(
* of the object the property belongs to. * of the object the property belongs to.
*/ */
class PropertyAccessorConstructor( class PropertyAccessorConstructor(
initialPosition: Int, val initialPosition: Int,
override val serializer: PropertySerializer) : PropertyAccessor(initialPosition, serializer) { override val serializer: PropertySerializer) : PropertyAccessor(serializer) {
/** /**
* Because the property should be being set on the obejct through the constructor any * Because the property should be being set on the object through the constructor any
* calls to the explicit setter should be an error. * calls to the explicit setter should be an error.
*/ */
override fun set(instance: Any, obj: Any?) { override fun set(instance: Any, obj: Any?) {
NotSerializableException("Attempting to access a setter on an object being instantiated " + NotSerializableException("Attempting to access a setter on an object being instantiated " +
"via its constructor.") "via its constructor.")
} }
override fun toString(): String =
"${serializer.name}($initialPosition)"
}
/**
* Implementation of [PropertyAccessor] representing a calculated property of an object that is serialized
* so that it can be used by the class carpenter, but ignored on deserialisation as there is no setter or
* constructor parameter to receive its value.
*
* This will only be created for calculated properties that are accessible via no-argument methods annotated
* with [SerializableCalculatedProperty].
*/
class CalculatedPropertyAccessor(override val serializer: PropertySerializer): PropertyAccessor(serializer) {
override val isCalculated: Boolean
get() = true
override fun set(instance: Any, obj: Any?) = Unit // do nothing, as it's a calculated value
} }
/** /**
@ -186,7 +205,7 @@ abstract class PropertySerializers(
val serializationOrder: List<PropertyAccessor>) { val serializationOrder: List<PropertyAccessor>) {
companion object { companion object {
fun make(serializationOrder: List<PropertyAccessor>) = fun make(serializationOrder: List<PropertyAccessor>) =
when (serializationOrder.firstOrNull()) { when (serializationOrder.find { !it.isCalculated }) {
is PropertyAccessorConstructor -> PropertySerializersConstructor(serializationOrder) is PropertyAccessorConstructor -> PropertySerializersConstructor(serializationOrder)
is PropertyAccessorGetterSetter -> PropertySerializersSetter(serializationOrder) is PropertyAccessorGetterSetter -> PropertySerializersSetter(serializationOrder)
null -> PropertySerializersNoProperties() null -> PropertySerializersNoProperties()
@ -198,6 +217,7 @@ abstract class PropertySerializers(
val size get() = serializationOrder.size val size get() = serializationOrder.size
abstract val byConstructor: Boolean abstract val byConstructor: Boolean
val deserializableSize = serializationOrder.count { !it.isCalculated }
} }
class PropertySerializersNoProperties : PropertySerializers(emptyList()) { class PropertySerializersNoProperties : PropertySerializers(emptyList()) {

View File

@ -3,10 +3,7 @@ package net.corda.serialization.internal.amqp
import com.google.common.primitives.Primitives import com.google.common.primitives.Primitives
import com.google.common.reflect.TypeToken import com.google.common.reflect.TypeToken
import net.corda.core.internal.isConcreteClass import net.corda.core.internal.isConcreteClass
import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.*
import net.corda.core.serialization.ConstructorForDeserialization
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializationContext
import org.apache.qpid.proton.codec.Data import org.apache.qpid.proton.codec.Data
import java.lang.reflect.* import java.lang.reflect.*
import java.lang.reflect.Field import java.lang.reflect.Field
@ -68,12 +65,33 @@ fun <T : Any> propertiesForSerialization(
kotlinConstructor: KFunction<T>?, kotlinConstructor: KFunction<T>?,
type: Type, type: Type,
factory: SerializerFactory): PropertySerializers = PropertySerializers.make( factory: SerializerFactory): PropertySerializers = PropertySerializers.make(
if (kotlinConstructor != null) { getValueProperties(kotlinConstructor, type, factory)
propertiesForSerializationFromConstructor(kotlinConstructor, type, factory) .addCalculatedProperties(factory, type)
} else { .sortedWith(PropertyAccessor))
propertiesForSerializationFromAbstract(type.asClass(), type, factory)
}.sortedWith(PropertyAccessor) fun <T : Any> getValueProperties(kotlinConstructor: KFunction<T>?, type: Type, factory: SerializerFactory)
) : List<PropertyAccessor> =
if (kotlinConstructor != null) {
propertiesForSerializationFromConstructor(kotlinConstructor, type, factory)
} else {
propertiesForSerializationFromAbstract(type.asClass(), type, factory)
}
private fun List<PropertyAccessor>.addCalculatedProperties(factory: SerializerFactory, type: Type)
: List<PropertyAccessor> {
val nonCalculated = map { it.serializer.name }.toSet()
return this + type.asClass().calculatedPropertyDescriptors().mapNotNull { (name, descriptor) ->
if (name in nonCalculated) null else {
val calculatedPropertyMethod = descriptor.getter
?: throw IllegalStateException("Property $name is not a calculated property")
CalculatedPropertyAccessor(PropertySerializer.make(
name,
PublicPropertyReader(calculatedPropertyMethod),
calculatedPropertyMethod.genericReturnType,
factory))
}
}
}
/** /**
* From a constructor, determine which properties of a class are to be serialized. * From a constructor, determine which properties of a class are to be serialized.
@ -145,7 +163,7 @@ fun propertiesForSerializationFromSetters(
properties: Map<String, PropertyDescriptor>, properties: Map<String, PropertyDescriptor>,
type: Type, type: Type,
factory: SerializerFactory): List<PropertyAccessor> = factory: SerializerFactory): List<PropertyAccessor> =
properties.asSequence().withIndex().map { (index, entry) -> properties.asSequence().map { entry ->
val (name, property) = entry val (name, property) = entry
val getter = property.getter val getter = property.getter
@ -154,7 +172,6 @@ fun propertiesForSerializationFromSetters(
if (getter == null || setter == null) return@map null if (getter == null || setter == null) return@map null
PropertyAccessorGetterSetter( PropertyAccessorGetterSetter(
index,
PropertySerializer.make( PropertySerializer.make(
name, name,
PublicPropertyReader(getter), PublicPropertyReader(getter),
@ -191,9 +208,9 @@ private fun propertiesForSerializationFromAbstract(
clazz: Class<*>, clazz: Class<*>,
type: Type, type: Type,
factory: SerializerFactory): List<PropertyAccessor> = factory: SerializerFactory): List<PropertyAccessor> =
clazz.propertyDescriptors().asSequence().withIndex().map { (index, entry) -> clazz.propertyDescriptors().asSequence().withIndex().mapNotNull { (index, entry) ->
val (name, property) = entry val (name, property) = entry
if (property.getter == null || property.field == null) return@map null if (property.getter == null || property.field == null) return@mapNotNull null
val getter = property.getter val getter = property.getter
val returnType = resolveTypeVariables(getter.genericReturnType, type) val returnType = resolveTypeVariables(getter.genericReturnType, type)
@ -201,7 +218,7 @@ private fun propertiesForSerializationFromAbstract(
PropertyAccessorConstructor( PropertyAccessorConstructor(
index, index,
PropertySerializer.make(name, PublicPropertyReader(getter), returnType, factory)) PropertySerializer.make(name, PublicPropertyReader(getter), returnType, factory))
}.filterNotNull().toList() }.toList()
internal fun interfacesForSerialization(type: Type, serializerFactory: SerializerFactory): List<Type> = internal fun interfacesForSerialization(type: Type, serializerFactory: SerializerFactory): List<Type> =
exploreType(type, serializerFactory).toList() exploreType(type, serializerFactory).toList()

View File

@ -7,10 +7,7 @@ import net.corda.core.StubOutForDJVM
import net.corda.core.internal.kotlinObjectInstance import net.corda.core.internal.kotlinObjectInstance
import net.corda.core.internal.uncheckedCast import net.corda.core.internal.uncheckedCast
import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.ClassWhitelist
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.*
import net.corda.core.utilities.debug
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.trace
import net.corda.serialization.internal.carpenter.* import net.corda.serialization.internal.carpenter.*
import org.apache.qpid.proton.amqp.* import org.apache.qpid.proton.amqp.*
import java.io.NotSerializableException import java.io.NotSerializableException
@ -30,7 +27,7 @@ data class CustomSerializersCacheKey(val clazz: Class<*>, val declaredType: Type
/** /**
* Factory of serializers designed to be shared across threads and invocations. * Factory of serializers designed to be shared across threads and invocations.
* *
* @property evolutionSerializerGetter controls how evolution serializers are generated by the factory. The normal * @property evolutionSerializerProvider controls how evolution serializers are generated by the factory. The normal
* use case is an [EvolutionSerializer] type is returned. However, in some scenarios, primarily testing, this * use case is an [EvolutionSerializer] type is returned. However, in some scenarios, primarily testing, this
* can be altered to fit the requirements of the test. * can be altered to fit the requirements of the test.
* @property onlyCustomSerializers used for testing, when set will cause the factory to throw a * @property onlyCustomSerializers used for testing, when set will cause the factory to throw a
@ -52,7 +49,7 @@ data class CustomSerializersCacheKey(val clazz: Class<*>, val declaredType: Type
open class SerializerFactory( open class SerializerFactory(
val whitelist: ClassWhitelist, val whitelist: ClassWhitelist,
val classCarpenter: ClassCarpenter, val classCarpenter: ClassCarpenter,
private val evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), private val evolutionSerializerProvider: EvolutionSerializerProvider = DefaultEvolutionSerializerProvider,
val fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter, val fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter,
private val serializersByType: MutableMap<Type, AMQPSerializer<Any>>, private val serializersByType: MutableMap<Type, AMQPSerializer<Any>>,
val serializersByDescriptor: MutableMap<Any, AMQPSerializer<Any>>, val serializersByDescriptor: MutableMap<Any, AMQPSerializer<Any>>,
@ -64,13 +61,13 @@ open class SerializerFactory(
@DeleteForDJVM @DeleteForDJVM
constructor(whitelist: ClassWhitelist, constructor(whitelist: ClassWhitelist,
classCarpenter: ClassCarpenter, classCarpenter: ClassCarpenter,
evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), evolutionSerializerProvider: EvolutionSerializerProvider = DefaultEvolutionSerializerProvider,
fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter, fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter,
onlyCustomSerializers: Boolean = false onlyCustomSerializers: Boolean = false
) : this( ) : this(
whitelist, whitelist,
classCarpenter, classCarpenter,
evolutionSerializerGetter, evolutionSerializerProvider,
fingerPrinterConstructor, fingerPrinterConstructor,
ConcurrentHashMap(), ConcurrentHashMap(),
ConcurrentHashMap(), ConcurrentHashMap(),
@ -84,13 +81,13 @@ open class SerializerFactory(
constructor(whitelist: ClassWhitelist, constructor(whitelist: ClassWhitelist,
carpenterClassLoader: ClassLoader, carpenterClassLoader: ClassLoader,
lenientCarpenter: Boolean = false, lenientCarpenter: Boolean = false,
evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), evolutionSerializerProvider: EvolutionSerializerProvider = DefaultEvolutionSerializerProvider,
fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter, fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter,
onlyCustomSerializers: Boolean = false onlyCustomSerializers: Boolean = false
) : this( ) : this(
whitelist, whitelist,
ClassCarpenterImpl(whitelist, carpenterClassLoader, lenientCarpenter), ClassCarpenterImpl(whitelist, carpenterClassLoader, lenientCarpenter),
evolutionSerializerGetter, evolutionSerializerProvider,
fingerPrinterConstructor, fingerPrinterConstructor,
onlyCustomSerializers) onlyCustomSerializers)
@ -98,12 +95,6 @@ open class SerializerFactory(
val classloader: ClassLoader get() = classCarpenter.classloader val classloader: ClassLoader get() = classCarpenter.classloader
private fun getEvolutionSerializer(typeNotation: TypeNotation,
newSerializer: AMQPSerializer<Any>,
schemas: SerializationSchemas): AMQPSerializer<Any> {
return evolutionSerializerGetter.getEvolutionSerializer(this, typeNotation, newSerializer, schemas)
}
/** /**
* Look up, and manufacture if necessary, a serializer for the given type. * Look up, and manufacture if necessary, a serializer for the given type.
* *
@ -117,11 +108,11 @@ open class SerializerFactory(
val declaredClass = declaredType.asClass() val declaredClass = declaredType.asClass()
val actualType: Type = if (actualClass == null) declaredType val actualType: Type = if (actualClass == null) declaredType
else inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType else inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType
val serializer = when { val serializer = when {
// Declared class may not be set to Collection, but actual class could be a collection. // Declared class may not be set to Collection, but actual class could be a collection.
// In this case use of CollectionSerializer is perfectly appropriate. // In this case use of CollectionSerializer is perfectly appropriate.
(Collection::class.java.isAssignableFrom(declaredClass) || (Collection::class.java.isAssignableFrom(declaredClass) ||
(actualClass != null && Collection::class.java.isAssignableFrom(actualClass))) && (actualClass != null && Collection::class.java.isAssignableFrom(actualClass))) &&
!EnumSet::class.java.isAssignableFrom(actualClass ?: declaredClass) -> { !EnumSet::class.java.isAssignableFrom(actualClass ?: declaredClass) -> {
@ -130,8 +121,8 @@ open class SerializerFactory(
CollectionSerializer(declaredTypeAmended, this) CollectionSerializer(declaredTypeAmended, this)
} }
} }
// Declared class may not be set to Map, but actual class could be a map. // Declared class may not be set to Map, but actual class could be a map.
// In this case use of MapSerializer is perfectly appropriate. // In this case use of MapSerializer is perfectly appropriate.
(Map::class.java.isAssignableFrom(declaredClass) || (Map::class.java.isAssignableFrom(declaredClass) ||
(actualClass != null && Map::class.java.isAssignableFrom(actualClass))) -> { (actualClass != null && Map::class.java.isAssignableFrom(actualClass))) -> {
val declaredTypeAmended = MapSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass) val declaredTypeAmended = MapSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass)
@ -142,8 +133,8 @@ open class SerializerFactory(
Enum::class.java.isAssignableFrom(actualClass ?: declaredClass) -> { Enum::class.java.isAssignableFrom(actualClass ?: declaredClass) -> {
logger.trace { logger.trace {
"class=[${actualClass?.simpleName} | $declaredClass] is an enumeration " + "class=[${actualClass?.simpleName} | $declaredClass] is an enumeration " +
"declaredType=${declaredType.typeName} " + "declaredType=${declaredType.typeName} " +
"isEnum=${declaredType::class.java.isEnum}" "isEnum=${declaredType::class.java.isEnum}"
} }
serializersByType.computeIfAbsent(actualClass ?: declaredClass) { serializersByType.computeIfAbsent(actualClass ?: declaredClass) {
@ -203,33 +194,86 @@ open class SerializerFactory(
* if not, use the [ClassCarpenter] to generate a class to use in its place. * if not, use the [ClassCarpenter] to generate a class to use in its place.
*/ */
private fun processSchema(schemaAndDescriptor: FactorySchemaAndDescriptor, sentinel: Boolean = false) { private fun processSchema(schemaAndDescriptor: FactorySchemaAndDescriptor, sentinel: Boolean = false) {
val metaSchema = CarpenterMetaSchema.newInstance() val requiringCarpentry = schemaAndDescriptor.schemas.schema.types.mapNotNull { typeNotation ->
for (typeNotation in schemaAndDescriptor.schemas.schema.types) {
logger.trace { "descriptor=${schemaAndDescriptor.typeDescriptor}, typeNotation=${typeNotation.name}" }
try { try {
val serialiser = processSchemaEntry(typeNotation) getOrRegisterSerializer(schemaAndDescriptor, typeNotation)
// if we just successfully built a serializer for the type but the type fingerprint return@mapNotNull null
// doesn't match that of the serialised object then we are dealing with different
// instance of the class, as such we need to build an EvolutionSerializer
if (serialiser.typeDescriptor != typeNotation.descriptor.name) {
logger.trace { "typeNotation=${typeNotation.name} action=\"requires Evolution\"" }
getEvolutionSerializer(typeNotation, serialiser, schemaAndDescriptor.schemas)
}
} catch (e: ClassNotFoundException) { } catch (e: ClassNotFoundException) {
if (sentinel) { if (sentinel) {
logger.error("typeNotation=${typeNotation.name} error=\"after Carpentry attempt failed to load\"") logger.error("typeNotation=${typeNotation.name} error=\"after Carpentry attempt failed to load\"")
throw e throw e
} }
else { logger.trace { "typeNotation=\"${typeNotation.name}\" action=\"carpentry required\"" }
logger.trace { "typeNotation=\"${typeNotation.name}\" action=\"carpentry required\"" } return@mapNotNull typeNotation
} }
metaSchema.buildFor(typeNotation, classloader) }.toList()
if (requiringCarpentry.isEmpty()) return
runCarpentry(schemaAndDescriptor, CarpenterMetaSchema.buildWith(classloader, requiringCarpentry))
}
private fun getOrRegisterSerializer(schemaAndDescriptor: FactorySchemaAndDescriptor, typeNotation: TypeNotation) {
logger.trace { "descriptor=${schemaAndDescriptor.typeDescriptor}, typeNotation=${typeNotation.name}" }
val serialiser = processSchemaEntry(typeNotation)
// if we just successfully built a serializer for the type but the type fingerprint
// doesn't match that of the serialised object then we may be dealing with different
// instance of the class, and such we need to build an EvolutionSerializer
if (serialiser.typeDescriptor == typeNotation.descriptor.name) return
logger.trace { "typeNotation=${typeNotation.name} action=\"requires Evolution\"" }
evolutionSerializerProvider.getEvolutionSerializer(this, typeNotation, serialiser, schemaAndDescriptor.schemas)
}
private fun processSchemaEntry(typeNotation: TypeNotation) = when (typeNotation) {
// java.lang.Class (whether a class or interface)
is CompositeType -> {
logger.trace("typeNotation=${typeNotation.name} amqpType=CompositeType")
processCompositeType(typeNotation)
}
// Collection / Map, possibly with generics
is RestrictedType -> {
logger.trace("typeNotation=${typeNotation.name} amqpType=RestrictedType")
processRestrictedType(typeNotation)
}
}
// TODO: class loader logic, and compare the schema.
private fun processRestrictedType(typeNotation: RestrictedType) =
get(null, typeForName(typeNotation.name, classloader))
private fun processCompositeType(typeNotation: CompositeType): AMQPSerializer<Any> {
// TODO: class loader logic, and compare the schema.
val type = typeForName(typeNotation.name, classloader)
return get(type.asClass(), type)
}
private fun typeForName(name: String, classloader: ClassLoader): Type = when {
name.endsWith("[]") -> {
val elementType = typeForName(name.substring(0, name.lastIndex - 1), classloader)
if (elementType is ParameterizedType || elementType is GenericArrayType) {
DeserializedGenericArrayType(elementType)
} else if (elementType is Class<*>) {
java.lang.reflect.Array.newInstance(elementType, 0).javaClass
} else {
throw AMQPNoTypeNotSerializableException("Not able to deserialize array type: $name")
} }
} }
name.endsWith("[p]") -> // There is no need to handle the ByteArray case as that type is coercible automatically
if (metaSchema.isNotEmpty()) { // to the binary type and is thus handled by the main serializer and doesn't need a
runCarpentry(schemaAndDescriptor, metaSchema) // special case for a primitive array of bytes
} when (name) {
"int[p]" -> IntArray::class.java
"char[p]" -> CharArray::class.java
"boolean[p]" -> BooleanArray::class.java
"float[p]" -> FloatArray::class.java
"double[p]" -> DoubleArray::class.java
"short[p]" -> ShortArray::class.java
"long[p]" -> LongArray::class.java
else -> throw AMQPNoTypeNotSerializableException("Not able to deserialize array type: $name")
}
else -> DeserializedParameterizedType.make(name, classloader)
} }
@StubOutForDJVM @StubOutForDJVM
@ -251,31 +295,6 @@ open class SerializerFactory(
processSchema(schemaAndDescriptor, true) processSchema(schemaAndDescriptor, true)
} }
private fun processSchemaEntry(typeNotation: TypeNotation) = when (typeNotation) {
// java.lang.Class (whether a class or interface)
is CompositeType -> {
logger.trace("typeNotation=${typeNotation.name} amqpType=CompositeType")
processCompositeType(typeNotation)
}
// Collection / Map, possibly with generics
is RestrictedType -> {
logger.trace("typeNotation=${typeNotation.name} amqpType=RestrictedType")
processRestrictedType(typeNotation)
}
}
// TODO: class loader logic, and compare the schema.
private fun processRestrictedType(typeNotation: RestrictedType) = get(null,
typeForName(typeNotation.name, classloader))
private fun processCompositeType(typeNotation: CompositeType): AMQPSerializer<Any> {
// TODO: class loader logic, and compare the schema.
val type = typeForName(typeNotation.name, classloader)
return get(
type.asClass(),
type)
}
private fun makeClassSerializer( private fun makeClassSerializer(
clazz: Class<*>, clazz: Class<*>,
type: Type, type: Type,
@ -352,6 +371,9 @@ open class SerializerFactory(
return MapSerializer(declaredType, this) return MapSerializer(declaredType, this)
} }
fun registerByDescriptor(name: Symbol, serializerCreator: () -> AMQPSerializer<Any>): AMQPSerializer<Any> =
serializersByDescriptor.computeIfAbsent(name) { _ -> serializerCreator() }
companion object { companion object {
private val logger = contextLogger() private val logger = contextLogger()
@ -405,35 +427,6 @@ open class SerializerFactory(
is TypeVariable<*> -> "?" is TypeVariable<*> -> "?"
else -> throw AMQPNotSerializableException(type, "Unable to render type $type to a string.") else -> throw AMQPNotSerializableException(type, "Unable to render type $type to a string.")
} }
private fun typeForName(name: String, classloader: ClassLoader): Type {
return if (name.endsWith("[]")) {
val elementType = typeForName(name.substring(0, name.lastIndex - 1), classloader)
if (elementType is ParameterizedType || elementType is GenericArrayType) {
DeserializedGenericArrayType(elementType)
} else if (elementType is Class<*>) {
java.lang.reflect.Array.newInstance(elementType, 0).javaClass
} else {
throw AMQPNoTypeNotSerializableException("Not able to deserialize array type: $name")
}
} else if (name.endsWith("[p]")) {
// There is no need to handle the ByteArray case as that type is coercible automatically
// to the binary type and is thus handled by the main serializer and doesn't need a
// special case for a primitive array of bytes
when (name) {
"int[p]" -> IntArray::class.java
"char[p]" -> CharArray::class.java
"boolean[p]" -> BooleanArray::class.java
"float[p]" -> FloatArray::class.java
"double[p]" -> DoubleArray::class.java
"short[p]" -> ShortArray::class.java
"long[p]" -> LongArray::class.java
else -> throw AMQPNoTypeNotSerializableException("Not able to deserialize array type: $name")
}
} else {
DeserializedParameterizedType.make(name, classloader)
}
}
} }
object AnyType : WildcardType { object AnyType : WildcardType {
@ -443,4 +436,4 @@ open class SerializerFactory(
override fun toString(): String = "?" override fun toString(): String = "?"
} }
} }

View File

@ -32,6 +32,11 @@ data class CarpenterMetaSchema(
val dependencies: MutableMap<String, Pair<TypeNotation, MutableList<String>>>, val dependencies: MutableMap<String, Pair<TypeNotation, MutableList<String>>>,
val dependsOn: MutableMap<String, MutableList<String>>) { val dependsOn: MutableMap<String, MutableList<String>>) {
companion object CarpenterSchemaConstructor { companion object CarpenterSchemaConstructor {
fun buildWith(classLoader: ClassLoader, types: List<TypeNotation>) =
newInstance().apply {
types.forEach { buildFor(it, classLoader) }
}
fun newInstance(): CarpenterMetaSchema { fun newInstance(): CarpenterMetaSchema {
return CarpenterMetaSchema(mutableListOf(), mutableMapOf(), mutableMapOf()) return CarpenterMetaSchema(mutableListOf(), mutableMapOf(), mutableMapOf())
} }

View File

@ -0,0 +1,107 @@
package net.corda.serialization.internal.carpenter;
import net.corda.core.serialization.SerializableCalculatedProperty;
import net.corda.core.serialization.SerializationContext;
import net.corda.core.serialization.SerializationFactory;
import net.corda.core.serialization.SerializedBytes;
import net.corda.serialization.internal.AllWhitelist;
import net.corda.serialization.internal.amqp.*;
import net.corda.serialization.internal.amqp.Schema;
import net.corda.testing.core.SerializationEnvironmentRule;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import static java.util.Collections.singletonList;
import static net.corda.serialization.internal.amqp.testutils.AMQPTestUtilsKt.testDefaultFactoryNoEvolution;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class JavaCalculatedValuesToClassCarpenterTest extends AmqpCarpenterBase {
public JavaCalculatedValuesToClassCarpenterTest() {
super(AllWhitelist.INSTANCE);
}
public interface Parent {
@SerializableCalculatedProperty
int getDoubled();
}
public static final class C implements Parent {
private final int i;
public C(int i) {
this.i = i;
}
@SerializableCalculatedProperty
public String getSquared() {
return Integer.toString(i * i);
}
@Override
public int getDoubled() {
return i * 2;
}
public int getI() {
return i;
}
}
@Rule
public final SerializationEnvironmentRule serializationEnvironmentRule = new SerializationEnvironmentRule();
private SerializationContext context;
@Before
public void initSerialization() {
SerializationFactory factory = serializationEnvironmentRule.getSerializationFactory();
context = factory.getDefaultContext();
}
@Test
public void calculatedValues() throws Exception {
SerializerFactory factory = testDefaultFactoryNoEvolution();
SerializedBytes<C> serialized = serialise(new C(2));
ObjectAndEnvelope<C> objAndEnv = new DeserializationInput(factory)
.deserializeAndReturnEnvelope(serialized, C.class, context);
C amqpObj = objAndEnv.getObj();
Schema schema = objAndEnv.getEnvelope().getSchema();
assertEquals(2, amqpObj.getI());
assertEquals("4", amqpObj.getSquared());
assertEquals(2, schema.getTypes().size());
assertTrue(schema.getTypes().get(0) instanceof CompositeType);
CompositeType concrete = (CompositeType) schema.getTypes().get(0);
assertEquals(3, concrete.getFields().size());
assertEquals("doubled", concrete.getFields().get(0).getName());
assertEquals("int", concrete.getFields().get(0).getType());
assertEquals("i", concrete.getFields().get(1).getName());
assertEquals("int", concrete.getFields().get(1).getType());
assertEquals("squared", concrete.getFields().get(2).getName());
assertEquals("string", concrete.getFields().get(2).getType());
assertEquals(0, AMQPSchemaExtensions.carpenterSchema(schema, ClassLoader.getSystemClassLoader()).getSize());
Schema mangledSchema = ClassCarpenterTestUtilsKt.mangleNames(schema, singletonList(C.class.getTypeName()));
CarpenterMetaSchema l2 = AMQPSchemaExtensions.carpenterSchema(mangledSchema, ClassLoader.getSystemClassLoader());
String mangledClassName = ClassCarpenterTestUtilsKt.mangleName(C.class.getTypeName());
assertEquals(1, l2.getSize());
net.corda.serialization.internal.carpenter.Schema carpenterSchema = l2.getCarpenterSchemas().stream()
.filter(s -> s.getName().equals(mangledClassName))
.findFirst()
.orElseThrow(() -> new IllegalStateException("No schema found for mangled class name " + mangledClassName));
Class<?> pinochio = new ClassCarpenterImpl(AllWhitelist.INSTANCE).build(carpenterSchema);
Object p = pinochio.getConstructors()[0].newInstance(4, 2, "4");
assertEquals(pinochio.getMethod("getI").invoke(p), amqpObj.getI());
assertEquals(pinochio.getMethod("getSquared").invoke(p), amqpObj.getSquared());
assertEquals(pinochio.getMethod("getDoubled").invoke(p), amqpObj.getDoubled());
Parent upcast = (Parent) p;
assertEquals(upcast.getDoubled(), amqpObj.getDoubled());
}
}

View File

@ -3,12 +3,12 @@ package net.corda.serialization.internal.amqp
import java.io.NotSerializableException import java.io.NotSerializableException
/** /**
* An implementation of [EvolutionSerializerGetterBase] that disables all evolution within a * An implementation of [EvolutionSerializerProvider] that disables all evolution within a
* [SerializerFactory]. This is most useful in testing where it is known that evolution should not be * [SerializerFactory]. This is most useful in testing where it is known that evolution should not be
* occurring and where bugs may be hidden by transparent invocation of an [EvolutionSerializer]. This * occurring and where bugs may be hidden by transparent invocation of an [EvolutionSerializer]. This
* prevents that by simply throwing an exception whenever such a serializer is requested. * prevents that by simply throwing an exception whenever such a serializer is requested.
*/ */
class EvolutionSerializerGetterTesting : EvolutionSerializerGetterBase() { object FailIfEvolutionAttempted : EvolutionSerializerProvider {
override fun getEvolutionSerializer(factory: SerializerFactory, override fun getEvolutionSerializer(factory: SerializerFactory,
typeNotation: TypeNotation, typeNotation: TypeNotation,
newSerializer: AMQPSerializer<Any>, newSerializer: AMQPSerializer<Any>,

View File

@ -42,7 +42,7 @@ class FingerPrinterTestingTests {
val factory = SerializerFactory( val factory = SerializerFactory(
AllWhitelist, AllWhitelist,
ClassLoader.getSystemClassLoader(), ClassLoader.getSystemClassLoader(),
evolutionSerializerGetter = EvolutionSerializerGetterTesting(), evolutionSerializerProvider = FailIfEvolutionAttempted,
fingerPrinterConstructor = { _ -> FingerPrinterTesting() }) fingerPrinterConstructor = { _ -> FingerPrinterTesting() })
val blob = TestSerializationOutput(VERBOSE, factory).serializeAndReturnSchema(C(1, 2L)) val blob = TestSerializationOutput(VERBOSE, factory).serializeAndReturnSchema(C(1, 2L))

View File

@ -1,6 +1,7 @@
package net.corda.serialization.internal.amqp package net.corda.serialization.internal.amqp
import net.corda.core.serialization.ConstructorForDeserialization import net.corda.core.serialization.ConstructorForDeserialization
import net.corda.core.serialization.SerializableCalculatedProperty
import net.corda.serialization.internal.amqp.testutils.deserialize import net.corda.serialization.internal.amqp.testutils.deserialize
import net.corda.serialization.internal.amqp.testutils.serialize import net.corda.serialization.internal.amqp.testutils.serialize
import net.corda.serialization.internal.amqp.testutils.testDefaultFactoryNoEvolution import net.corda.serialization.internal.amqp.testutils.testDefaultFactoryNoEvolution
@ -61,4 +62,62 @@ class RoundTripTests {
val newC = DeserializationInput(factory).deserialize(bytes) val newC = DeserializationInput(factory).deserialize(bytes)
newC.copy(l = (newC.l + "d")) newC.copy(l = (newC.l + "d"))
} }
@Test
fun calculatedValues() {
data class C(val i: Int) {
@get:SerializableCalculatedProperty
val squared = i * i
}
val factory = testDefaultFactoryNoEvolution()
val bytes = SerializationOutput(factory).serialize(C(2))
val deserialized = DeserializationInput(factory).deserialize(bytes)
assertThat(deserialized.squared).isEqualTo(4)
}
@Test
fun calculatedFunction() {
class C {
var i: Int = 0
@SerializableCalculatedProperty
fun getSquared() = i * i
}
val instance = C().apply { i = 2 }
val factory = testDefaultFactoryNoEvolution()
val bytes = SerializationOutput(factory).serialize(instance)
val deserialized = DeserializationInput(factory).deserialize(bytes)
assertThat(deserialized.getSquared()).isEqualTo(4)
}
interface I {
@get:SerializableCalculatedProperty
val squared: Int
}
@Test
fun inheritedCalculatedFunction() {
class C: I {
var i: Int = 0
override val squared get() = i * i
}
val instance = C().apply { i = 2 }
val factory = testDefaultFactoryNoEvolution()
val bytes = SerializationOutput(factory).serialize(instance)
val deserialized = DeserializationInput(factory).deserialize(bytes) as I
assertThat(deserialized.squared).isEqualTo(4)
}
@Test
fun inheritedCalculatedFunctionIsNotCalculated() {
class C(override val squared: Int): I
val instance = C(2)
val factory = testDefaultFactoryNoEvolution()
val bytes = SerializationOutput(factory).serialize(instance)
val deserialized = DeserializationInput(factory).deserialize(bytes) as I
assertThat(deserialized.squared).isEqualTo(2)
}
} }

View File

@ -210,7 +210,7 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi
return SerializerFactory( return SerializerFactory(
AllWhitelist, AllWhitelist,
ClassLoader.getSystemClassLoader(), ClassLoader.getSystemClassLoader(),
evolutionSerializerGetter = EvolutionSerializerGetterTesting() evolutionSerializerProvider = FailIfEvolutionAttempted
) )
} }

View File

@ -23,7 +23,7 @@ fun testDefaultFactoryNoEvolution(): SerializerFactory {
return SerializerFactory( return SerializerFactory(
AllWhitelist, AllWhitelist,
ClassLoader.getSystemClassLoader(), ClassLoader.getSystemClassLoader(),
evolutionSerializerGetter = EvolutionSerializerGetterTesting()) evolutionSerializerProvider = FailIfEvolutionAttempted)
} }
fun testDefaultFactoryWithWhitelist() = SerializerFactory(EmptyWhitelist, ClassLoader.getSystemClassLoader()) fun testDefaultFactoryWithWhitelist() = SerializerFactory(EmptyWhitelist, ClassLoader.getSystemClassLoader())

View File

@ -0,0 +1,101 @@
package net.corda.serialization.internal.carpenter
import net.corda.core.serialization.SerializableCalculatedProperty
import net.corda.serialization.internal.AllWhitelist
import net.corda.serialization.internal.amqp.CompositeType
import net.corda.serialization.internal.amqp.DeserializationInput
import net.corda.serialization.internal.amqp.testutils.deserializeAndReturnEnvelope
import net.corda.serialization.internal.amqp.testutils.testDefaultFactoryNoEvolution
import org.junit.Test
import kotlin.test.assertEquals
class CalculatedValuesToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) {
interface Parent {
@get:SerializableCalculatedProperty
val doubled: Int
}
@Test
fun calculatedValues() {
data class C(val i: Int): Parent {
@get:SerializableCalculatedProperty
val squared = (i * i).toString()
override val doubled get() = i * 2
}
val factory = testDefaultFactoryNoEvolution()
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(C(2)))
val amqpObj = obj.obj
val serSchema = obj.envelope.schema
assertEquals(2, amqpObj.i)
assertEquals("4", amqpObj.squared)
assertEquals(2, serSchema.types.size)
require(serSchema.types[0] is CompositeType)
val concrete = serSchema.types[0] as CompositeType
assertEquals(3, concrete.fields.size)
assertEquals("doubled", concrete.fields[0].name)
assertEquals("int", concrete.fields[0].type)
assertEquals("i", concrete.fields[1].name)
assertEquals("int", concrete.fields[1].type)
assertEquals("squared", concrete.fields[2].name)
assertEquals("string", concrete.fields[2].type)
val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
assertEquals(0, l1.size)
val mangleSchema = serSchema.mangleNames(listOf((classTestName("C"))))
val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
val aName = mangleName(classTestName("C"))
assertEquals(1, l2.size)
val aSchema = l2.carpenterSchemas.find { it.name == aName }!!
val pinochio = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema)
val p = pinochio.constructors[0].newInstance(4, 2, "4")
assertEquals(pinochio.getMethod("getI").invoke(p), amqpObj.i)
assertEquals(pinochio.getMethod("getSquared").invoke(p), amqpObj.squared)
assertEquals(pinochio.getMethod("getDoubled").invoke(p), amqpObj.doubled)
val upcast = p as Parent
assertEquals(upcast.doubled, amqpObj.doubled)
}
@Test
fun implementingClassDoesNotCalculateValue() {
class C(override val doubled: Int): Parent
val factory = testDefaultFactoryNoEvolution()
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(C(5)))
val amqpObj = obj.obj
val serSchema = obj.envelope.schema
assertEquals(2, serSchema.types.size)
require(serSchema.types[0] is CompositeType)
val concrete = serSchema.types[0] as CompositeType
assertEquals(1, concrete.fields.size)
assertEquals("doubled", concrete.fields[0].name)
assertEquals("int", concrete.fields[0].type)
val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
assertEquals(0, l1.size)
val mangleSchema = serSchema.mangleNames(listOf((classTestName("C"))))
val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
val aName = mangleName(classTestName("C"))
assertEquals(1, l2.size)
val aSchema = l2.carpenterSchemas.find { it.name == aName }!!
val pinochio = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema)
val p = pinochio.constructors[0].newInstance(5)
assertEquals(pinochio.getMethod("getDoubled").invoke(p), amqpObj.doubled)
val upcast = p as Parent
assertEquals(upcast.doubled, amqpObj.doubled)
}
}

View File

@ -1,6 +1,7 @@
package net.corda.serialization.internal.carpenter package net.corda.serialization.internal.carpenter
import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.ClassWhitelist
import net.corda.core.serialization.SerializedBytes
import net.corda.serialization.internal.amqp.* import net.corda.serialization.internal.amqp.*
import net.corda.serialization.internal.amqp.Field import net.corda.serialization.internal.amqp.Field
import net.corda.serialization.internal.amqp.Schema import net.corda.serialization.internal.amqp.Schema
@ -47,7 +48,7 @@ open class AmqpCarpenterBase(whitelist: ClassWhitelist) {
var cc = ClassCarpenterImpl(whitelist = whitelist) var cc = ClassCarpenterImpl(whitelist = whitelist)
var factory = SerializerFactoryExternalCarpenter(cc) var factory = SerializerFactoryExternalCarpenter(cc)
fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz) fun <T: Any> serialise(obj: T): SerializedBytes<T> = SerializationOutput(factory).serialize(obj)
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
inline fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz" inline fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz"
} }

View File

@ -30,9 +30,7 @@ class CompositeMembers : AmqpCarpenterBase(AllWhitelist) {
val b = B(A(testA), testB) val b = B(A(testA), testB)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b))
require(obj.obj is B) val amqpObj = obj.obj
val amqpObj = obj.obj as B
assertEquals(testB, amqpObj.b) assertEquals(testB, amqpObj.b)
assertEquals(testA, amqpObj.a.a) assertEquals(testA, amqpObj.a.a)
@ -92,8 +90,6 @@ class CompositeMembers : AmqpCarpenterBase(AllWhitelist) {
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b))
val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"))) val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A")))
require(obj.obj is B)
amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
} }
@ -111,8 +107,6 @@ class CompositeMembers : AmqpCarpenterBase(AllWhitelist) {
val b = B(A(testA), testB) val b = B(A(testA), testB)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b))
require(obj.obj is B)
val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("B"))) val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("B")))
val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
@ -138,9 +132,6 @@ class CompositeMembers : AmqpCarpenterBase(AllWhitelist) {
val b = B(A(testA), testB) val b = B(A(testA), testB)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b))
require(obj.obj is B)
val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B")))
val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
@ -198,8 +189,6 @@ class CompositeMembers : AmqpCarpenterBase(AllWhitelist) {
val c = C(B(testA, testB), testC) val c = C(B(testA, testB), testC)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(c)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(c))
require(obj.obj is C)
val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B")))
amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
@ -224,8 +213,6 @@ class CompositeMembers : AmqpCarpenterBase(AllWhitelist) {
val c = C(B(testA, testB), testC) val c = C(B(testA, testB), testC)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(c)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(c))
require(obj.obj is C)
val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B")))
amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
@ -250,8 +237,6 @@ class CompositeMembers : AmqpCarpenterBase(AllWhitelist) {
val c = C(B(testA, testB), testC) val c = C(B(testA, testB), testC)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(c)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(c))
require(obj.obj is C)
val carpenterSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) val carpenterSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B")))
TestMetaCarpenter(carpenterSchema.carpenterSchema( TestMetaCarpenter(carpenterSchema.carpenterSchema(
ClassLoader.getSystemClassLoader()), ClassCarpenterImpl(whitelist = AllWhitelist)) ClassLoader.getSystemClassLoader()), ClassCarpenterImpl(whitelist = AllWhitelist))

View File

@ -44,7 +44,6 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) {
assertEquals(testJ, a.j) assertEquals(testJ, a.j)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
assertTrue(obj.obj is A)
val serSchema = obj.envelope.schema val serSchema = obj.envelope.schema
assertEquals(2, serSchema.types.size) assertEquals(2, serSchema.types.size)
val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
@ -84,8 +83,6 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) {
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
assertTrue(obj.obj is A)
val serSchema = obj.envelope.schema val serSchema = obj.envelope.schema
assertEquals(2, serSchema.types.size) assertEquals(2, serSchema.types.size)
@ -128,8 +125,6 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) {
val a = A(testI, testII) val a = A(testI, testII)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
assertTrue(obj.obj is A)
val serSchema = obj.envelope.schema val serSchema = obj.envelope.schema
assertEquals(3, serSchema.types.size) assertEquals(3, serSchema.types.size)
@ -176,8 +171,6 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) {
val a = A(testI, testIII) val a = A(testI, testIII)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
assertTrue(obj.obj is A)
val serSchema = obj.envelope.schema val serSchema = obj.envelope.schema
assertEquals(3, serSchema.types.size) assertEquals(3, serSchema.types.size)
@ -225,8 +218,6 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) {
val b = B(a, testIIII) val b = B(a, testIIII)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b))
assertTrue(obj.obj is B)
val serSchema = obj.envelope.schema val serSchema = obj.envelope.schema
// Expected classes are // Expected classes are
@ -281,8 +272,6 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) {
val b = B(a, testIIII) val b = B(a, testIIII)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b))
assertTrue(obj.obj is B)
val serSchema = obj.envelope.schema val serSchema = obj.envelope.schema
// The classes we're expecting to find: // The classes we're expecting to find:
@ -305,8 +294,6 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) {
val a = A(testI) val a = A(testI)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
assertTrue(obj.obj is A)
val serSchema = obj.envelope.schema val serSchema = obj.envelope.schema
// The classes we're expecting to find: // The classes we're expecting to find:

View File

@ -21,8 +21,7 @@ class MultiMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhi
val a = A(testA, testB) val a = A(testA, testB)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
require(obj.obj is A) val amqpObj = obj.obj
val amqpObj = obj.obj as A
assertEquals(testA, amqpObj.a) assertEquals(testA, amqpObj.a)
assertEquals(testB, amqpObj.b) assertEquals(testB, amqpObj.b)
@ -65,8 +64,7 @@ class MultiMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhi
val a = A(testA, testB) val a = A(testA, testB)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
require(obj.obj is A) val amqpObj = obj.obj
val amqpObj = obj.obj as A
assertEquals(testA, amqpObj.a) assertEquals(testA, amqpObj.a)
assertEquals(testB, amqpObj.b) assertEquals(testB, amqpObj.b)

View File

@ -17,9 +17,7 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWh
val test = 10 val test = 10
val a = A(test) val a = A(test)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
val amqpObj = obj.obj
require(obj.obj is A)
val amqpObj = obj.obj as A
assertEquals(test, amqpObj.a) assertEquals(test, amqpObj.a)
assertEquals(1, obj.envelope.schema.types.size) assertEquals(1, obj.envelope.schema.types.size)
@ -53,9 +51,7 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWh
val a = A(test) val a = A(test)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
val amqpObj = obj.obj
require(obj.obj is A)
val amqpObj = obj.obj as A
assertEquals(test, amqpObj.a) assertEquals(test, amqpObj.a)
assertEquals(1, obj.envelope.schema.types.size) assertEquals(1, obj.envelope.schema.types.size)
@ -83,9 +79,7 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWh
val test = 10L val test = 10L
val a = A(test) val a = A(test)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
val amqpObj = obj.obj
require(obj.obj is A)
val amqpObj = obj.obj as A
assertEquals(test, amqpObj.a) assertEquals(test, amqpObj.a)
assertEquals(1, obj.envelope.schema.types.size) assertEquals(1, obj.envelope.schema.types.size)
@ -118,9 +112,7 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWh
val test = 10.toShort() val test = 10.toShort()
val a = A(test) val a = A(test)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
val amqpObj = obj.obj
require(obj.obj is A)
val amqpObj = obj.obj as A
assertEquals(test, amqpObj.a) assertEquals(test, amqpObj.a)
assertEquals(1, obj.envelope.schema.types.size) assertEquals(1, obj.envelope.schema.types.size)
@ -153,9 +145,7 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWh
val test = 10.0 val test = 10.0
val a = A(test) val a = A(test)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
val amqpObj = obj.obj
require(obj.obj is A)
val amqpObj = obj.obj as A
assertEquals(test, amqpObj.a) assertEquals(test, amqpObj.a)
assertEquals(1, obj.envelope.schema.types.size) assertEquals(1, obj.envelope.schema.types.size)
@ -188,9 +178,7 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWh
val test = 10.0F val test = 10.0F
val a = A(test) val a = A(test)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
val amqpObj = obj.obj
require(obj.obj is A)
val amqpObj = obj.obj as A
assertEquals(test, amqpObj.a) assertEquals(test, amqpObj.a)
assertEquals(1, obj.envelope.schema.types.size) assertEquals(1, obj.envelope.schema.types.size)