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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 554 additions and 204 deletions

View File

@ -98,7 +98,7 @@ private class CordaSerializableBeanSerializerModifier : BeanSerializerModifier()
val ctor = constructorForDeserialization(beanClass)
val amqpProperties = propertiesForSerialization(ctor, beanClass, serializerFactory)
.serializationOrder
.map { it.serializer.name }
.mapNotNull { if (it.isCalculated) null else it.serializer.name }
val propertyRenames = beanDesc.findProperties().associateBy({ it.name }, { it.internalName })
(amqpProperties - propertyRenames.values).let {
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.flows.FlowException
import net.corda.core.identity.AbstractParty
import net.corda.core.serialization.SerializableCalculatedProperty
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
* owner to sign, some (i.e. cash) also require the issuer.
*/
@get:SerializableCalculatedProperty
val exitKeys: Collection<PublicKey>
/**

View File

@ -19,4 +19,12 @@ import java.lang.annotation.Inherited
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@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.
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,
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:
#. 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.PersistentState
import net.corda.core.schemas.QueryableState
import net.corda.core.serialization.SerializableCalculatedProperty
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
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 org.junit.After
import org.junit.Test
import java.security.PublicKey
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.reflect.jvm.jvmName
@ -127,7 +128,7 @@ class VaultSoftLockManagerTest {
override val owner get() = participants[0]
override fun withNewOwner(newOwner: AbstractParty) = throw UnsupportedOperationException()
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 equals(other: Any?) = other is FungibleAssetImpl && participants == other.participants
override fun hashCode() = participants.hashCode()

View File

@ -117,7 +117,7 @@ class DeserializationInput constructor(
des {
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, context))
@ -149,7 +149,7 @@ class DeserializationInput constructor(
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.
val objectIndex = (obj.described as UnsignedInteger).toInt()
if (objectIndex !in 0..objectHistory.size)
if (objectIndex >= objectHistory.size)
throw AMQPNotSerializableException(
type,
"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
// compiler nullability
val isKotlin = (new.type.javaClass.declaredAnnotations.any {
it.annotationClass.qualifiedName == "kotlin.Metadata"
it.annotationClass.qualifiedName == "kotlin.Metadata"
})
constructor.parameters.withIndex().forEach {
@ -270,8 +270,8 @@ class EvolutionSerializerViaSetters(
* be an object that returns an [EvolutionSerializer]. Of course, any implementation that
* extends this class can be written to invoke whatever behaviour is desired.
*/
abstract class EvolutionSerializerGetterBase {
abstract fun getEvolutionSerializer(
interface EvolutionSerializerProvider {
fun getEvolutionSerializer(
factory: SerializerFactory,
typeNotation: TypeNotation,
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,
*/
@KeepForDJVM
class EvolutionSerializerGetter : EvolutionSerializerGetterBase() {
object DefaultEvolutionSerializerProvider : EvolutionSerializerProvider {
override fun getEvolutionSerializer(factory: SerializerFactory,
typeNotation: TypeNotation,
newSerializer: AMQPSerializer<Any>,
schemas: SerializationSchemas): AMQPSerializer<Any> {
return factory.serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) {
return factory.registerByDescriptor(typeNotation.descriptor.name!!) {
when (typeNotation) {
is CompositeType -> EvolutionSerializer.make(typeNotation, newSerializer as ObjectSerializer, factory)
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.serialization.SerializationContext
import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.trace
import net.corda.serialization.internal.amqp.SerializerFactory.Companion.nameForType
@ -61,7 +62,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
context: SerializationContext,
debugIndent: Int) = ifThrowsAppend({ clazz.typeName }
) {
if (propertySerializers.size != javaConstructor?.parameterCount &&
if (propertySerializers.deserializableSize != javaConstructor?.parameterCount &&
javaConstructor?.parameterCount ?: 0 > 0
) {
throw AMQPNotSerializableException(type, "Serialization constructor for class $type expects "
@ -86,8 +87,9 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
input: DeserializationInput,
context: SerializationContext): Any = ifThrowsAppend({ clazz.typeName }) {
if (obj is List<*>) {
if (obj.size > propertySerializers.size) {
throw AMQPNotSerializableException(type, "Too many properties in described type $typeName")
if (obj.size != propertySerializers.size) {
throw AMQPNotSerializableException(type, "${obj.size} objects to deserialize, but " +
"${propertySerializers.size} properties in described type $typeName")
}
return if (propertySerializers.byConstructor) {
@ -109,8 +111,19 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
return construct(propertySerializers.serializationOrder
.zip(obj)
.map { Pair(it.first.initialPosition, it.first.serializer.readProperty(it.second, schemas, input, context)) }
.sortedWith(compareBy({ it.first }))
.mapNotNull { (accessor, obj) ->
// 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 })
}

View File

@ -3,6 +3,7 @@ package net.corda.serialization.internal.amqp
import com.google.common.reflect.TypeToken
import net.corda.core.KeepForDJVM
import net.corda.core.internal.isPublic
import net.corda.core.serialization.SerializableCalculatedProperty
import net.corda.serialization.internal.amqp.MethodClassifier.*
import java.lang.reflect.Field
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
* 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()
return superclassChain().declaredMethods()
@ -96,9 +97,23 @@ fun Class<out Any?>.propertyDescriptors(): Map<String, PropertyDescriptor> {
.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.
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.
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.
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" }
// 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
private fun Sequence<Method>.thatArePropertyMethods() = map { method ->
propertyMethodRegex.find(method.name)?.let { result ->
private fun Sequence<Method>.thatArePropertyMethods() = mapNotNull(::getPropertyNamedMethod)
private fun getPropertyNamedMethod(method: Method): PropertyNamedMethod? {
return propertyMethodRegex.find(method.name)?.let { result ->
PropertyNamedMethod(
result.groups[2]!!.value,
MethodClassifier.valueOf(result.groups[1]!!.value.toUpperCase()),
method)
}
}.filterNotNull()
}
// Pick only those methods whose signatures are valid, discarding the remainder without warning.
private fun Sequence<PropertyNamedMethod>.withValidSignature() = filter { it.hasValidSignature() }

View File

@ -1,6 +1,7 @@
package net.corda.serialization.internal.amqp
import net.corda.core.KeepForDJVM
import net.corda.core.serialization.SerializableCalculatedProperty
import net.corda.core.utilities.loggerFor
import java.io.NotSerializableException
import java.lang.reflect.Field
@ -19,9 +20,9 @@ abstract class PropertyReader {
* Accessor for those properties of a class that have defined getter functions.
*/
@KeepForDJVM
class PublicPropertyReader(private val readMethod: Method?) : PropertyReader() {
class PublicPropertyReader(private val readMethod: Method) : PropertyReader() {
init {
readMethod?.isAccessible = true
readMethod.isAccessible = true
}
private fun Method.returnsNullable(): Boolean {
@ -47,10 +48,10 @@ class PublicPropertyReader(private val readMethod: Method?) : PropertyReader() {
}
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.
*/
abstract class PropertyAccessor(
val initialPosition: Int,
open val serializer: PropertySerializer) {
companion object : Comparator<PropertyAccessor> {
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.
*/
abstract fun set(instance: Any, obj: Any?)
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.
*/
class PropertyAccessorGetterSetter(
initialPosition: Int,
getter: PropertySerializer,
private val setter: Method) : PropertyAccessor(initialPosition, getter) {
private val setter: Method) : PropertyAccessor(getter) {
init {
/**
* 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.
*/
class PropertyAccessorConstructor(
initialPosition: Int,
override val serializer: PropertySerializer) : PropertyAccessor(initialPosition, serializer) {
val initialPosition: Int,
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.
*/
override fun set(instance: Any, obj: Any?) {
NotSerializableException("Attempting to access a setter on an object being instantiated " +
"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>) {
companion object {
fun make(serializationOrder: List<PropertyAccessor>) =
when (serializationOrder.firstOrNull()) {
when (serializationOrder.find { !it.isCalculated }) {
is PropertyAccessorConstructor -> PropertySerializersConstructor(serializationOrder)
is PropertyAccessorGetterSetter -> PropertySerializersSetter(serializationOrder)
null -> PropertySerializersNoProperties()
@ -198,6 +217,7 @@ abstract class PropertySerializers(
val size get() = serializationOrder.size
abstract val byConstructor: Boolean
val deserializableSize = serializationOrder.count { !it.isCalculated }
}
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.reflect.TypeToken
import net.corda.core.internal.isConcreteClass
import net.corda.core.serialization.ClassWhitelist
import net.corda.core.serialization.ConstructorForDeserialization
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.*
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.*
import java.lang.reflect.Field
@ -68,12 +65,33 @@ fun <T : Any> propertiesForSerialization(
kotlinConstructor: KFunction<T>?,
type: Type,
factory: SerializerFactory): PropertySerializers = PropertySerializers.make(
if (kotlinConstructor != null) {
propertiesForSerializationFromConstructor(kotlinConstructor, type, factory)
} else {
propertiesForSerializationFromAbstract(type.asClass(), type, factory)
}.sortedWith(PropertyAccessor)
)
getValueProperties(kotlinConstructor, type, factory)
.addCalculatedProperties(factory, type)
.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.
@ -145,7 +163,7 @@ fun propertiesForSerializationFromSetters(
properties: Map<String, PropertyDescriptor>,
type: Type,
factory: SerializerFactory): List<PropertyAccessor> =
properties.asSequence().withIndex().map { (index, entry) ->
properties.asSequence().map { entry ->
val (name, property) = entry
val getter = property.getter
@ -154,7 +172,6 @@ fun propertiesForSerializationFromSetters(
if (getter == null || setter == null) return@map null
PropertyAccessorGetterSetter(
index,
PropertySerializer.make(
name,
PublicPropertyReader(getter),
@ -191,9 +208,9 @@ private fun propertiesForSerializationFromAbstract(
clazz: Class<*>,
type: Type,
factory: SerializerFactory): List<PropertyAccessor> =
clazz.propertyDescriptors().asSequence().withIndex().map { (index, entry) ->
clazz.propertyDescriptors().asSequence().withIndex().mapNotNull { (index, 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 returnType = resolveTypeVariables(getter.genericReturnType, type)
@ -201,7 +218,7 @@ private fun propertiesForSerializationFromAbstract(
PropertyAccessorConstructor(
index,
PropertySerializer.make(name, PublicPropertyReader(getter), returnType, factory))
}.filterNotNull().toList()
}.toList()
internal fun interfacesForSerialization(type: Type, serializerFactory: SerializerFactory): List<Type> =
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.uncheckedCast
import net.corda.core.serialization.ClassWhitelist
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.trace
import net.corda.core.utilities.*
import net.corda.serialization.internal.carpenter.*
import org.apache.qpid.proton.amqp.*
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.
*
* @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
* can be altered to fit the requirements of the test.
* @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(
val whitelist: ClassWhitelist,
val classCarpenter: ClassCarpenter,
private val evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(),
private val evolutionSerializerProvider: EvolutionSerializerProvider = DefaultEvolutionSerializerProvider,
val fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter,
private val serializersByType: MutableMap<Type, AMQPSerializer<Any>>,
val serializersByDescriptor: MutableMap<Any, AMQPSerializer<Any>>,
@ -64,13 +61,13 @@ open class SerializerFactory(
@DeleteForDJVM
constructor(whitelist: ClassWhitelist,
classCarpenter: ClassCarpenter,
evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(),
evolutionSerializerProvider: EvolutionSerializerProvider = DefaultEvolutionSerializerProvider,
fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter,
onlyCustomSerializers: Boolean = false
) : this(
whitelist,
classCarpenter,
evolutionSerializerGetter,
evolutionSerializerProvider,
fingerPrinterConstructor,
ConcurrentHashMap(),
ConcurrentHashMap(),
@ -84,13 +81,13 @@ open class SerializerFactory(
constructor(whitelist: ClassWhitelist,
carpenterClassLoader: ClassLoader,
lenientCarpenter: Boolean = false,
evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(),
evolutionSerializerProvider: EvolutionSerializerProvider = DefaultEvolutionSerializerProvider,
fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter,
onlyCustomSerializers: Boolean = false
) : this(
whitelist,
ClassCarpenterImpl(whitelist, carpenterClassLoader, lenientCarpenter),
evolutionSerializerGetter,
evolutionSerializerProvider,
fingerPrinterConstructor,
onlyCustomSerializers)
@ -98,12 +95,6 @@ open class SerializerFactory(
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.
*
@ -117,11 +108,11 @@ open class SerializerFactory(
val declaredClass = declaredType.asClass()
val actualType: Type = if (actualClass == null) declaredType
else inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType
else inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType
val serializer = when {
// Declared class may not be set to Collection, but actual class could be a collection.
// In this case use of CollectionSerializer is perfectly appropriate.
// Declared class may not be set to Collection, but actual class could be a collection.
// In this case use of CollectionSerializer is perfectly appropriate.
(Collection::class.java.isAssignableFrom(declaredClass) ||
(actualClass != null && Collection::class.java.isAssignableFrom(actualClass))) &&
!EnumSet::class.java.isAssignableFrom(actualClass ?: declaredClass) -> {
@ -130,8 +121,8 @@ open class SerializerFactory(
CollectionSerializer(declaredTypeAmended, this)
}
}
// Declared class may not be set to Map, but actual class could be a map.
// In this case use of MapSerializer is perfectly appropriate.
// Declared class may not be set to Map, but actual class could be a map.
// In this case use of MapSerializer is perfectly appropriate.
(Map::class.java.isAssignableFrom(declaredClass) ||
(actualClass != null && Map::class.java.isAssignableFrom(actualClass))) -> {
val declaredTypeAmended = MapSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass)
@ -142,8 +133,8 @@ open class SerializerFactory(
Enum::class.java.isAssignableFrom(actualClass ?: declaredClass) -> {
logger.trace {
"class=[${actualClass?.simpleName} | $declaredClass] is an enumeration " +
"declaredType=${declaredType.typeName} " +
"isEnum=${declaredType::class.java.isEnum}"
"declaredType=${declaredType.typeName} " +
"isEnum=${declaredType::class.java.isEnum}"
}
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.
*/
private fun processSchema(schemaAndDescriptor: FactorySchemaAndDescriptor, sentinel: Boolean = false) {
val metaSchema = CarpenterMetaSchema.newInstance()
for (typeNotation in schemaAndDescriptor.schemas.schema.types) {
logger.trace { "descriptor=${schemaAndDescriptor.typeDescriptor}, typeNotation=${typeNotation.name}" }
val requiringCarpentry = schemaAndDescriptor.schemas.schema.types.mapNotNull { typeNotation ->
try {
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 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)
}
getOrRegisterSerializer(schemaAndDescriptor, typeNotation)
return@mapNotNull null
} catch (e: ClassNotFoundException) {
if (sentinel) {
logger.error("typeNotation=${typeNotation.name} error=\"after Carpentry attempt failed to load\"")
throw e
}
else {
logger.trace { "typeNotation=\"${typeNotation.name}\" action=\"carpentry required\"" }
}
metaSchema.buildFor(typeNotation, classloader)
logger.trace { "typeNotation=\"${typeNotation.name}\" action=\"carpentry required\"" }
return@mapNotNull typeNotation
}
}.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")
}
}
if (metaSchema.isNotEmpty()) {
runCarpentry(schemaAndDescriptor, metaSchema)
}
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)
}
@StubOutForDJVM
@ -251,31 +295,6 @@ open class SerializerFactory(
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(
clazz: Class<*>,
type: Type,
@ -352,6 +371,9 @@ open class SerializerFactory(
return MapSerializer(declaredType, this)
}
fun registerByDescriptor(name: Symbol, serializerCreator: () -> AMQPSerializer<Any>): AMQPSerializer<Any> =
serializersByDescriptor.computeIfAbsent(name) { _ -> serializerCreator() }
companion object {
private val logger = contextLogger()
@ -405,35 +427,6 @@ open class SerializerFactory(
is TypeVariable<*> -> "?"
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 {
@ -443,4 +436,4 @@ open class SerializerFactory(
override fun toString(): String = "?"
}
}
}

View File

@ -32,6 +32,11 @@ data class CarpenterMetaSchema(
val dependencies: MutableMap<String, Pair<TypeNotation, MutableList<String>>>,
val dependsOn: MutableMap<String, MutableList<String>>) {
companion object CarpenterSchemaConstructor {
fun buildWith(classLoader: ClassLoader, types: List<TypeNotation>) =
newInstance().apply {
types.forEach { buildFor(it, classLoader) }
}
fun newInstance(): CarpenterMetaSchema {
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
/**
* 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
* 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.
*/
class EvolutionSerializerGetterTesting : EvolutionSerializerGetterBase() {
object FailIfEvolutionAttempted : EvolutionSerializerProvider {
override fun getEvolutionSerializer(factory: SerializerFactory,
typeNotation: TypeNotation,
newSerializer: AMQPSerializer<Any>,

View File

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

View File

@ -1,6 +1,7 @@
package net.corda.serialization.internal.amqp
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.serialize
import net.corda.serialization.internal.amqp.testutils.testDefaultFactoryNoEvolution
@ -61,4 +62,62 @@ class RoundTripTests {
val newC = DeserializationInput(factory).deserialize(bytes)
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(
AllWhitelist,
ClassLoader.getSystemClassLoader(),
evolutionSerializerGetter = EvolutionSerializerGetterTesting()
evolutionSerializerProvider = FailIfEvolutionAttempted
)
}

View File

@ -23,7 +23,7 @@ fun testDefaultFactoryNoEvolution(): SerializerFactory {
return SerializerFactory(
AllWhitelist,
ClassLoader.getSystemClassLoader(),
evolutionSerializerGetter = EvolutionSerializerGetterTesting())
evolutionSerializerProvider = FailIfEvolutionAttempted)
}
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
import net.corda.core.serialization.ClassWhitelist
import net.corda.core.serialization.SerializedBytes
import net.corda.serialization.internal.amqp.*
import net.corda.serialization.internal.amqp.Field
import net.corda.serialization.internal.amqp.Schema
@ -47,7 +48,7 @@ open class AmqpCarpenterBase(whitelist: ClassWhitelist) {
var cc = ClassCarpenterImpl(whitelist = whitelist)
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")
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 obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b))
require(obj.obj is B)
val amqpObj = obj.obj as B
val amqpObj = obj.obj
assertEquals(testB, amqpObj.b)
assertEquals(testA, amqpObj.a.a)
@ -92,8 +90,6 @@ class CompositeMembers : AmqpCarpenterBase(AllWhitelist) {
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b))
val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A")))
require(obj.obj is B)
amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
}
@ -111,8 +107,6 @@ class CompositeMembers : AmqpCarpenterBase(AllWhitelist) {
val b = B(A(testA), testB)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b))
require(obj.obj is B)
val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("B")))
val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
@ -138,9 +132,6 @@ class CompositeMembers : AmqpCarpenterBase(AllWhitelist) {
val b = B(A(testA), testB)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b))
require(obj.obj is B)
val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B")))
val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
@ -198,8 +189,6 @@ class CompositeMembers : AmqpCarpenterBase(AllWhitelist) {
val c = C(B(testA, testB), testC)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(c))
require(obj.obj is C)
val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B")))
amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
@ -224,8 +213,6 @@ class CompositeMembers : AmqpCarpenterBase(AllWhitelist) {
val c = C(B(testA, testB), testC)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(c))
require(obj.obj is C)
val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B")))
amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
@ -250,8 +237,6 @@ class CompositeMembers : AmqpCarpenterBase(AllWhitelist) {
val c = C(B(testA, testB), testC)
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(c))
require(obj.obj is C)
val carpenterSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B")))
TestMetaCarpenter(carpenterSchema.carpenterSchema(
ClassLoader.getSystemClassLoader()), ClassCarpenterImpl(whitelist = AllWhitelist))

View File

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

View File

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

View File

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