From 0f36e2231462c2e3697f33b79c411f925dc7c948 Mon Sep 17 00:00:00 2001 From: Dominic Fox <40790090+distributedleetravis@users.noreply.github.com> Date: Thu, 30 Aug 2018 10:18:02 +0100 Subject: [PATCH] Corda-1869 serialisation refactor (#3780) * Pull out and tidy type parameter inference * Contain null proliferation * Extract fingerprinter state * SerializerFingerPrinter is always initialised with a SerializerFactory * Move non-recursive state transition functions into state * Move all state transition functions into state * Simplify and optimise with mutable state * Move TypeParameterUtils back into internal.amqp * Clarify behaviour of constructorForDeserialisation * constructorForDeserialization no longer returns null * Capture field properties * Narrow PropertyDescriptor * Use map rather than apply on a mutable list * Remove printStackTrace added for debugging * CORDA-1869 minor tweaks * Use groupingBy to avoid creating an intermediate map * Convert some functional origami to plain old for-loops * Eliminate nested lambda to unbreak pre-serialisation * Use EnumMap for map of Enums --- .../internal/amqp/ArraySerializer.kt | 12 +- .../internal/amqp/CorDappCustomSerializer.kt | 6 +- .../internal/amqp/CustomSerializer.kt | 2 +- .../internal/amqp/DeserializationInput.kt | 2 +- .../internal/amqp/EnumEvolutionSerializer.kt | 4 +- .../internal/amqp/EnumSerializer.kt | 2 +- .../internal/amqp/EvolutionSerializer.kt | 10 +- .../internal/amqp/FingerPrinter.kt | 311 ++++++------ .../internal/amqp/ObjectSerializer.kt | 3 +- .../internal/amqp/PropertyDescriptor.kt | 205 ++++++++ .../internal/amqp/PropertySerializer.kt | 4 +- .../internal/amqp/SerializationHelper.kt | 456 ++++++------------ .../internal/amqp/SerializerFactory.kt | 99 +--- .../internal/amqp/TypeParameterUtils.kt | 94 ++++ .../amqp/custom/ThrowableSerializer.kt | 4 +- .../internal/amqp/FingerPrinterTesting.kt | 6 +- 16 files changed, 627 insertions(+), 593 deletions(-) create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertyDescriptor.kt create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TypeParameterUtils.kt diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt index 913accf079..6869e71d45 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt @@ -49,7 +49,7 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory) "$typeName[]" } else { - val arrayType = if (type.asClass()!!.componentType.isPrimitive) "[p]" else "[]" + val arrayType = if (type.asClass().componentType.isPrimitive) "[p]" else "[]" "${type.componentType().typeName}$arrayType" } } @@ -93,7 +93,7 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory) } open fun List.toArrayOfType(type: Type): Any { - val elementType = type.asClass() ?: throw AMQPNotSerializableException(type, "Unexpected array element type $type") + val elementType = type.asClass() val list = this return java.lang.reflect.Array.newInstance(elementType, this.size).apply { (0..lastIndex).forEach { java.lang.reflect.Array.set(this, it, list[it]) } @@ -105,7 +105,7 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory) // the array since Kotlin won't allow an implicit cast from Int (as they're stored as 16bit ints) to Char class CharArraySerializer(factory: SerializerFactory) : ArraySerializer(Array::class.java, factory) { override fun List.toArrayOfType(type: Type): Any { - val elementType = type.asClass() ?: throw AMQPNotSerializableException(type, "Unexpected array element type $type") + val elementType = type.asClass() val list = this return java.lang.reflect.Array.newInstance(elementType, this.size).apply { (0..lastIndex).forEach { java.lang.reflect.Array.set(this, it, (list[it] as Int).toChar()) } @@ -159,11 +159,7 @@ class PrimCharArraySerializer(factory: SerializerFactory) : PrimArraySerializer( } override fun List.toArrayOfType(type: Type): Any { - val elementType = type.asClass() ?: throw AMQPNotSerializableException( - type, - "Unexpected array element type $type", - "blob is corrupt") - + val elementType = type.asClass() val list = this return java.lang.reflect.Array.newInstance(elementType, this.size).apply { val array = this diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CorDappCustomSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CorDappCustomSerializer.kt index 9506fbd510..f1b509c940 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CorDappCustomSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CorDappCustomSerializer.kt @@ -93,8 +93,8 @@ class CorDappCustomSerializer( * For 3rd party plugin serializers we are going to exist on exact type matching. i.e. we will * not support base class serializers for derivedtypes */ - override fun isSerializerFor(clazz: Class<*>) : Boolean { - return type.asClass()?.let { TypeToken.of(it) == TypeToken.of(clazz) } ?: false - } + override fun isSerializerFor(clazz: Class<*>) = + TypeToken.of(type.asClass()) == TypeToken.of(clazz) + } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt index f99a842758..cbd54f08c2 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt @@ -67,7 +67,7 @@ abstract class CustomSerializer : AMQPSerializer, SerializerFor { override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.clazz override val type: Type get() = clazz override val typeDescriptor: Symbol by lazy { - Symbol.valueOf("$DESCRIPTOR_DOMAIN:${SerializerFingerPrinter().fingerprintForDescriptors(superClassSerializer.typeDescriptor.toString(), nameForType(clazz))}") + Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForDescriptors(superClassSerializer.typeDescriptor.toString(), nameForType(clazz))}") } private val typeNotation: TypeNotation = RestrictedType( SerializerFactory.nameForType(clazz), diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt index 6d75e3a553..b9c50f7250 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt @@ -156,7 +156,7 @@ class DeserializationInput constructor( "is outside of the bounds for the list of size: ${objectHistory.size}") val objectRetrieved = objectHistory[objectIndex] - if (!objectRetrieved::class.java.isSubClassOf(type.asClass()!!)) { + if (!objectRetrieved::class.java.isSubClassOf(type.asClass())) { throw AMQPNotSerializableException( type, "Existing reference type mismatch. Expected: '$type', found: '${objectRetrieved::class.java}' " + diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt index 2dfb36b503..5e7010c71c 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt @@ -80,7 +80,7 @@ class EnumEvolutionSerializer( val renameRules: List? = uncheckedCast(transforms[TransformTypes.Rename]) // What values exist on the enum as it exists on the class path - val localValues = new.type.asClass()!!.enumConstants.map { it.toString() } + val localValues = new.type.asClass().enumConstants.map { it.toString() } val conversions: MutableMap = localValues .union(defaultRules?.map { it.new }?.toSet() ?: emptySet()) @@ -130,7 +130,7 @@ class EnumEvolutionSerializer( throw AMQPNotSerializableException(type, "No rule to evolve enum constant $type::$enumName") } - return type.asClass()!!.enumConstants[ordinals[conversions[enumName]]!!] + return type.asClass().enumConstants[ordinals[conversions[enumName]]!!] } override fun writeClassInfo(output: SerializationOutput) { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumSerializer.kt index 34b6697901..1bb12190f2 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumSerializer.kt @@ -34,7 +34,7 @@ class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: Seria ): Any { val enumName = (obj as List<*>)[0] as String val enumOrd = obj[1] as Int - val fromOrd = type.asClass()!!.enumConstants[enumOrd] as Enum<*>? + val fromOrd = type.asClass().enumConstants[enumOrd] as Enum<*>? if (enumName != fromOrd?.name) { throw AMQPNotSerializableException( diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt index 0aec3e6832..79a3f85c00 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt @@ -32,7 +32,7 @@ abstract class EvolutionSerializer( clazz: Type, factory: SerializerFactory, protected val oldReaders: Map, - override val kotlinConstructor: KFunction? + override val kotlinConstructor: KFunction ) : ObjectSerializer(clazz, factory) { // explicitly set as empty to indicate it's unused by this type of serializer override val propertySerializers = PropertySerializersEvolution() @@ -74,7 +74,7 @@ abstract class EvolutionSerializer( * TODO: rename annotation */ private fun getEvolverConstructor(type: Type, oldArgs: Map): KFunction? { - val clazz: Class<*> = type.asClass()!! + val clazz: Class<*> = type.asClass() if (!clazz.isConcreteClass) return null @@ -189,7 +189,7 @@ abstract class EvolutionSerializer( // return the synthesised object which is, given the absence of a constructor, a no op val constructor = getEvolverConstructor(new.type, readersAsSerialized) ?: return new - val classProperties = new.type.asClass()?.propertyDescriptors() ?: emptyMap() + val classProperties = new.type.asClass().propertyDescriptors() return if (classProperties.isNotEmpty() && constructor.parameters.isEmpty()) { makeWithSetters(new, factory, constructor, readersAsSerialized, classProperties) @@ -210,7 +210,7 @@ class EvolutionSerializerViaConstructor( clazz: Type, factory: SerializerFactory, oldReaders: Map, - kotlinConstructor: KFunction?, + kotlinConstructor: KFunction, private val constructorArgs: Array) : EvolutionSerializer(clazz, factory, oldReaders, kotlinConstructor) { /** * Unlike a normal [readObject] call where we simply apply the parameter deserialisers @@ -242,7 +242,7 @@ class EvolutionSerializerViaSetters( clazz: Type, factory: SerializerFactory, oldReaders: Map, - kotlinConstructor: KFunction?, + kotlinConstructor: KFunction, private val setters: Map) : EvolutionSerializer(clazz, factory, oldReaders, kotlinConstructor) { override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt index 6b698dd057..d03e3e6da1 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt @@ -3,15 +3,15 @@ package net.corda.serialization.internal.amqp import com.google.common.hash.Hasher import com.google.common.hash.Hashing import net.corda.core.KeepForDJVM +import net.corda.core.internal.isConcreteClass import net.corda.core.internal.kotlinObjectInstance -import net.corda.core.utilities.loggerFor import net.corda.core.utilities.toBase64 -import java.io.NotSerializableException +import net.corda.serialization.internal.amqp.SerializerFactory.Companion.isPrimitive import java.lang.reflect.* import java.util.* /** - * Should be implemented by classes which wish to provide plugable fingerprinting og types for a [SerializerFactory] + * Should be implemented by classes which wish to provide pluggable fingerprinting on types for a [SerializerFactory] */ @KeepForDJVM interface FingerPrinter { @@ -20,34 +20,13 @@ interface FingerPrinter { * of said type such that any modification to any sub element wll generate a different fingerprint */ fun fingerprint(type: Type): String - - /** - * If required, associate an instance of the fingerprinter with a specific serializer factory - */ - fun setOwner(factory: SerializerFactory) } /** * Implementation of the finger printing mechanism used by default */ @KeepForDJVM -class SerializerFingerPrinter : FingerPrinter { - private var factory: SerializerFactory? = null - - private val ARRAY_HASH: String = "Array = true" - private val ENUM_HASH: String = "Enum = true" - private val ALREADY_SEEN_HASH: String = "Already seen = true" - private val NULLABLE_HASH: String = "Nullable = true" - private val NOT_NULLABLE_HASH: String = "Nullable = false" - private val ANY_TYPE_HASH: String = "Any type = true" - private val TYPE_VARIABLE_HASH: String = "Type variable = true" - private val WILDCARD_TYPE_HASH: String = "Wild card = true" - - private val logger by lazy { loggerFor() } - - override fun setOwner(factory: SerializerFactory) { - this.factory = factory - } +class SerializerFingerPrinter(val factory: SerializerFactory) : FingerPrinter { /** * The method generates a fingerprint for a given JVM [Type] that should be unique to the schema representation. @@ -57,147 +36,167 @@ class SerializerFingerPrinter : FingerPrinter { * The idea being that even for two classes that share the same name but differ in a minor way, the fingerprint will be * different. */ - override fun fingerprint(type: Type): String { - return fingerprintForType( - type, null, HashSet(), Hashing.murmur3_128().newHasher(), debugIndent = 1).hash().asBytes().toBase64() + override fun fingerprint(type: Type): String = FingerPrintingState(factory).fingerprint(type) +} + +// Representation of the current state of fingerprinting +internal class FingerPrintingState(private val factory: SerializerFactory) { + + companion object { + private const val ARRAY_HASH: String = "Array = true" + private const val ENUM_HASH: String = "Enum = true" + private const val ALREADY_SEEN_HASH: String = "Already seen = true" + private const val NULLABLE_HASH: String = "Nullable = true" + private const val NOT_NULLABLE_HASH: String = "Nullable = false" + private const val ANY_TYPE_HASH: String = "Any type = true" } - private fun isCollectionOrMap(type: Class<*>) = - (Collection::class.java.isAssignableFrom(type) || Map::class.java.isAssignableFrom(type)) - && !EnumSet::class.java.isAssignableFrom(type) + private val typesSeen: MutableSet = mutableSetOf() + private var currentContext: Type? = null + private var hasher: Hasher = newDefaultHasher() - internal fun fingerprintForDescriptors(vararg typeDescriptors: String): String { - val hasher = Hashing.murmur3_128().newHasher() - for (typeDescriptor in typeDescriptors) { - hasher.putUnencodedChars(typeDescriptor) - } - return hasher.hash().asBytes().toBase64() - } - - private fun Hasher.fingerprintWithCustomSerializerOrElse( - factory: SerializerFactory, - clazz: Class<*>, - declaredType: Type, - block: () -> Hasher): Hasher { - // Need to check if a custom serializer is applicable - val customSerializer = factory.findCustomSerializer(clazz, declaredType) - return if (customSerializer != null) { - putUnencodedChars(customSerializer.typeDescriptor) - } else { - block() - } - } + // Fingerprint the type recursively, and return the encoded fingerprint written into the hasher. + fun fingerprint(type: Type) = fingerprintType(type).hasher.fingerprint // This method concatenates various elements of the types recursively as unencoded strings into the hasher, // effectively creating a unique string for a type which we then hash in the calling function above. - private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: MutableSet, - hasher: Hasher, debugIndent: Int = 1): Hasher { - // We don't include Example and Example where type is ? or T in this otherwise we - // generate different fingerprints for class Outer(val a: Inner) when serialising - // and deserializing (assuming deserialization is occurring in a factory that didn't - // serialise the object in the first place (and thus the cache lookup fails). This is also - // true of Any, where we need Example and Example to have the same fingerprint - return if ((type in alreadySeen) - && (type !== SerializerFactory.AnyType) - && (type !is TypeVariable<*>) - && (type !is WildcardType) - ) { - hasher.putUnencodedChars(ALREADY_SEEN_HASH) - } else { - alreadySeen += type - ifThrowsAppend({ type.typeName }) { - when (type) { - is ParameterizedType -> { - // Hash the rawType + params - val clazz = type.rawType as Class<*> + private fun fingerprintType(type: Type): FingerPrintingState = apply { + // Don't go round in circles. + if (hasSeen(type)) append(ALREADY_SEEN_HASH) + else ifThrowsAppend( + { type.typeName }, + { + typesSeen.add(type) + currentContext = type + fingerprintNewType(type) + }) + } - val startingHash = if (isCollectionOrMap(clazz)) { - hasher.putUnencodedChars(clazz.name) - } else { - hasher.fingerprintWithCustomSerializerOrElse(factory!!, clazz, type) { - fingerprintForObject(type, type, alreadySeen, hasher, factory!!, debugIndent + 1) - } - } + // For a type we haven't seen before, determine the correct path depending on the type of type it is. + private fun fingerprintNewType(type: Type) = when (type) { + is ParameterizedType -> fingerprintParameterizedType(type) + // Previously, we drew a distinction between TypeVariable, WildcardType, and AnyType, changing + // the signature of the fingerprinted object. This, however, doesn't work as it breaks bi- + // directional fingerprints. That is, fingerprinting a concrete instance of a generic + // type (Example), creates a different fingerprint from the generic type itself (Example) + // + // On serialization Example is treated as Example, a TypeVariable + // On deserialisation it is seen as Example, A WildcardType *and* a TypeVariable + // Note: AnyType is a special case of WildcardType used in other parts of the + // serializer so both cases need to be dealt with here + // + // If we treat these types as fundamentally different and alter the fingerprint we will + // end up breaking into the evolver when we shouldn't or, worse, evoking the carpenter. + is SerializerFactory.AnyType, + is WildcardType, + is TypeVariable<*> -> append("?$ANY_TYPE_HASH") + is Class<*> -> fingerprintClass(type) + is GenericArrayType -> fingerprintType(type.genericComponentType).append(ARRAY_HASH) + else -> throw AMQPNotSerializableException(type, "Don't know how to hash") + } - // ... and concatenate the type data for each parameter type. - type.actualTypeArguments.fold(startingHash) { orig, paramType -> - fingerprintForType(paramType, type, alreadySeen, orig, debugIndent + 1) - } - } - // Previously, we drew a distinction between TypeVariable, WildcardType, and AnyType, changing - // the signature of the fingerprinted object. This, however, doesn't work as it breaks bi- - // directional fingerprints. That is, fingerprinting a concrete instance of a generic - // type (Example), creates a different fingerprint from the generic type itself (Example) - // - // On serialization Example is treated as Example, a TypeVariable - // On deserialisation it is seen as Example, A WildcardType *and* a TypeVariable - // Note: AnyType is a special case of WildcardType used in other parts of the - // serializer so both cases need to be dealt with here - // - // If we treat these types as fundamentally different and alter the fingerprint we will - // end up breaking into the evolver when we shouldn't or, worse, evoking the carpenter. - is SerializerFactory.AnyType, - is WildcardType, - is TypeVariable<*> -> { - hasher.putUnencodedChars("?").putUnencodedChars(ANY_TYPE_HASH) - } - is Class<*> -> { - if (type.isArray) { - fingerprintForType(type.componentType, contextType, alreadySeen, hasher, debugIndent + 1) - .putUnencodedChars(ARRAY_HASH) - } else if (SerializerFactory.isPrimitive(type)) { - hasher.putUnencodedChars(type.name) - } else if (isCollectionOrMap(type)) { - hasher.putUnencodedChars(type.name) - } else if (type.isEnum) { - // ensures any change to the enum (adding constants) will trigger the need for evolution - hasher.apply { - type.enumConstants.forEach { - putUnencodedChars(it.toString()) - } - }.putUnencodedChars(type.name).putUnencodedChars(ENUM_HASH) - } else { - hasher.fingerprintWithCustomSerializerOrElse(factory!!, type, type) { - if (type.kotlinObjectInstance != null) { - // TODO: name collision is too likely for kotlin objects, we need to introduce some - // reference to the CorDapp but maybe reference to the JAR in the short term. - hasher.putUnencodedChars(type.name) - } else { - fingerprintForObject(type, type, alreadySeen, hasher, factory!!, debugIndent + 1) - } - } - } - } - // Hash the element type + some array hash - is GenericArrayType -> { - fingerprintForType(type.genericComponentType, contextType, alreadySeen, - hasher, debugIndent + 1).putUnencodedChars(ARRAY_HASH) - } - else -> throw AMQPNotSerializableException(type, "Don't know how to hash") - } - } + private fun fingerprintClass(type: Class<*>) = when { + type.isArray -> fingerprintType(type.componentType).append(ARRAY_HASH) + type.isPrimitiveOrCollection -> append(type.name) + type.isEnum -> fingerprintEnum(type) + else -> fingerprintWithCustomSerializerOrElse(type, type) { + if (type.kotlinObjectInstance != null) append(type.name) + else fingerprintObject(type) } } - private fun fingerprintForObject( - type: Type, - contextType: Type?, - alreadySeen: MutableSet, - hasher: Hasher, - factory: SerializerFactory, - debugIndent: Int = 0): Hasher { - // Hash the class + properties + interfaces - val name = type.asClass()?.name - ?: throw AMQPNotSerializableException(type, "Expected only Class or ParameterizedType but found $type") + private fun fingerprintParameterizedType(type: ParameterizedType) { + // Hash the rawType + params + type.asClass().let { clazz -> + if (clazz.isCollectionOrMap) append(clazz.name) + else fingerprintWithCustomSerializerOrElse(clazz, type) { + fingerprintObject(type) + } + } - propertiesForSerialization(constructorForDeserialization(type), contextType ?: type, factory) - .serializationOrder - .fold(hasher.putUnencodedChars(name)) { orig, prop -> - fingerprintForType(prop.serializer.resolvedType, type, alreadySeen, orig, debugIndent + 1) - .putUnencodedChars(prop.serializer.name) - .putUnencodedChars(if (prop.serializer.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH) - } - interfacesForSerialization(type, factory).map { fingerprintForType(it, type, alreadySeen, hasher, debugIndent + 1) } - return hasher + // ...and concatenate the type data for each parameter type. + type.actualTypeArguments.forEach { paramType -> + fingerprintType(paramType) + } } -} \ No newline at end of file + + private fun fingerprintObject(type: Type) { + // Hash the class + properties + interfaces + append(type.asClass().name) + + orderedPropertiesForSerialization(type).forEach { prop -> + fingerprintType(prop.serializer.resolvedType) + fingerprintPropSerialiser(prop) + } + + interfacesForSerialization(type, factory).forEach { iface -> + fingerprintType(iface) + } + } + + // ensures any change to the enum (adding constants) will trigger the need for evolution + private fun fingerprintEnum(type: Class<*>) { + append(type.enumConstants.joinToString()) + append(type.name) + append(ENUM_HASH) + } + + private fun fingerprintPropSerialiser(prop: PropertyAccessor) { + append(prop.serializer.name) + append(if (prop.serializer.mandatory) NOT_NULLABLE_HASH + else NULLABLE_HASH) + } + + // Write the given character sequence into the hasher. + private fun append(chars: CharSequence) { + hasher = hasher.putUnencodedChars(chars) + } + + // Give any custom serializers loaded into the factory the chance to supply their own type-descriptors + private fun fingerprintWithCustomSerializerOrElse( + clazz: Class<*>, + declaredType: Type, + defaultAction: () -> Unit) + : Unit = factory.findCustomSerializer(clazz, declaredType)?.let { + append(it.typeDescriptor) + } ?: defaultAction() + + // Test whether we are in a state in which we have already seen the given type. + // + // We don't include Example and Example where type is ? or T in this otherwise we + // generate different fingerprints for class Outer(val a: Inner) when serialising + // and deserializing (assuming deserialization is occurring in a factory that didn't + // serialise the object in the first place (and thus the cache lookup fails). This is also + // true of Any, where we need Example and Example to have the same fingerprint + private fun hasSeen(type: Type) = (type in typesSeen) + && (type !== SerializerFactory.AnyType) + && (type !is TypeVariable<*>) + && (type !is WildcardType) + + private fun orderedPropertiesForSerialization(type: Type): List { + return propertiesForSerialization( + if (type.asClass().isConcreteClass) constructorForDeserialization(type) else null, + currentContext ?: type, + factory).serializationOrder + } + +} + +// region Utility functions + +// Create a new instance of the [Hasher] used for fingerprinting by the default [SerializerFingerPrinter] +private fun newDefaultHasher() = Hashing.murmur3_128().newHasher() + +// We obtain a fingerprint from a [Hasher] by taking the Base 64 encoding of its hash bytes +private val Hasher.fingerprint get() = hash().asBytes().toBase64() + +internal fun fingerprintForDescriptors(vararg typeDescriptors: String): String = + newDefaultHasher().putUnencodedChars(typeDescriptors.joinToString()).fingerprint + +private val Class<*>.isCollectionOrMap get() = + (Collection::class.java.isAssignableFrom(this) || Map::class.java.isAssignableFrom(this)) + && !EnumSet::class.java.isAssignableFrom(this) + +private val Class<*>.isPrimitiveOrCollection get() = + isPrimitive(this) || isCollectionOrMap +// endregion diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt index 7a8f0d2334..dc142fce16 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt @@ -1,5 +1,6 @@ package net.corda.serialization.internal.amqp +import net.corda.core.internal.isConcreteClass import net.corda.core.serialization.SerializationContext import net.corda.core.utilities.contextLogger import net.corda.core.utilities.trace @@ -18,7 +19,7 @@ import kotlin.reflect.jvm.javaConstructor */ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerializer { override val type: Type get() = clazz - open val kotlinConstructor = constructorForDeserialization(clazz) + open val kotlinConstructor = if (clazz.asClass().isConcreteClass) constructorForDeserialization(clazz) else null val javaConstructor by lazy { kotlinConstructor?.javaConstructor } companion object { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertyDescriptor.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertyDescriptor.kt new file mode 100644 index 0000000000..882f067eba --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertyDescriptor.kt @@ -0,0 +1,205 @@ +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.serialization.internal.amqp.MethodClassifier.* +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.lang.reflect.Type +import java.util.* + +/** + * Encapsulates the property of a class and its potential getter and setter methods. + * + * @property field a property of a class. + * @property setter the method of a class that sets the field. Determined by locating + * a function called setXyz on the class for the property named in field as xyz. + * @property getter the method of a class that returns a fields value. Determined by + * locating a function named getXyz for the property named in field as xyz. + */ +@KeepForDJVM +data class PropertyDescriptor(val field: Field?, val setter: Method?, val getter: Method?) { + override fun toString() = StringBuilder("").apply { + appendln("Property - ${field?.name ?: "null field"}\n") + appendln(" getter - ${getter?.name ?: "no getter"}") + appendln(" setter - ${setter?.name ?: "no setter"}") + }.toString() + + /** + * Check the types of the field, getter and setter methods against each other. + */ + fun validate() { + getter?.apply { + val getterType = genericReturnType + field?.apply { + if (!getterType.isSupertypeOf(genericReturnType)) + throw AMQPNotSerializableException( + declaringClass, + "Defined getter for parameter $name returns type $getterType " + + "yet underlying type is $genericType") + } + } + + setter?.apply { + val setterType = genericParameterTypes[0]!! + + field?.apply { + if (!genericType.isSupertypeOf(setterType)) + throw AMQPNotSerializableException( + declaringClass, + "Defined setter for parameter $name takes parameter of type $setterType " + + "yet underlying type is $genericType") + } + + getter?.apply { + if (!genericReturnType.isSupertypeOf(setterType)) + throw AMQPNotSerializableException( + declaringClass, + "Defined setter for parameter $name takes parameter of type $setterType, " + + "but getter returns $genericReturnType") + } + } + } +} + +private fun Type.isSupertypeOf(that: Type) = TypeToken.of(this).isSupertypeOf(that) + +// match an uppercase letter that also has a corresponding lower case equivalent +private val propertyMethodRegex = Regex("(?get|set|is)(?\\p{Lu}.*)") + +/** + * Collate the properties of a class and match them with their getter and setter + * methods as per a JavaBean. + * + * for a property + * exampleProperty + * + * We look for methods + * setExampleProperty + * getExampleProperty + * isExampleProperty + * + * Where getExampleProperty must return a type compatible with exampleProperty, setExampleProperty must + * take a single parameter of a type compatible with exampleProperty and isExampleProperty must + * return a boolean + */ +fun Class.propertyDescriptors(): Map { + val fieldProperties = superclassChain().declaredFields().byFieldName() + + return superclassChain().declaredMethods() + .thatArePublic() + .thatArePropertyMethods() + .withValidSignature() + .byNameAndClassifier(fieldProperties.keys) + .toClassProperties(fieldProperties) + .validated() +} + +// Generate the sequence of classes starting with this class and ascending through it superclasses. +private fun Class<*>.superclassChain() = generateSequence(this, Class<*>::getSuperclass) + +// Obtain the fields declared by all classes in this sequence of classes. +private fun Sequence>.declaredFields() = flatMap { it.declaredFields.asSequence() } + +// Obtain the methods declared by all classes in this sequence of classes. +private fun Sequence>.declaredMethods() = flatMap { it.declaredMethods.asSequence() } + +// Map a sequence of fields by field name. +private fun Sequence.byFieldName() = map { it.name to it }.toMap() + +// Select only those methods that are public (and are not the "getClass" method) +private fun Sequence.thatArePublic() = filter { it.isPublic && it.name != "getClass" } + +// Select only those methods that are isX/getX/setX methods +private fun Sequence.thatArePropertyMethods() = map { method -> + 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.withValidSignature() = filter { it.hasValidSignature() } + +// Group methods by name and classifier, picking the method with the least generic signature if there is more than one +// of a given name and type. +private fun Sequence.byNameAndClassifier(fieldNames: Set): Map> { + val result = mutableMapOf>() + + forEach { (fieldName, classifier, method) -> + result.compute(getPropertyName(fieldName, fieldNames)) { _, byClassifier -> + (byClassifier ?: EnumMap(MethodClassifier::class.java)).merge(classifier, method) + } + } + + return result +} + +// Merge the given method into a map of methods by method classifier, picking the least generic method for each classifier. +private fun EnumMap.merge(classifier: MethodClassifier, method: Method): EnumMap { + compute(classifier) { _, existingMethod -> + if (existingMethod == null) method + else when (classifier) { + IS -> existingMethod + GET -> leastGenericBy({ genericReturnType }, existingMethod, method) + SET -> leastGenericBy({ genericParameterTypes[0] }, existingMethod, method) + } + } + return this +} + +// Make the property name conform to the underlying field name, if there is one. +private fun getPropertyName(propertyName: String, fieldNames: Set) = + if (propertyName.decapitalize() in fieldNames) propertyName.decapitalize() + else propertyName + + +// Which of the three types of property method the method is. +private enum class MethodClassifier { GET, SET, IS } + +private data class PropertyNamedMethod(val fieldName: String, val classifier: MethodClassifier, val method: Method) { + // Validate the method's signature against its classifier + fun hasValidSignature(): Boolean = method.run { + when (classifier) { + GET -> parameterCount == 0 && returnType != Void.TYPE + SET -> parameterCount == 1 && returnType == Void.TYPE + IS -> parameterCount == 0 && + (returnType == Boolean::class.java || + returnType == Boolean::class.javaObjectType) + } + } +} + +// Construct a map of PropertyDescriptors by name, by merging the raw field map with the map of classified property methods +private fun Map>.toClassProperties(fieldMap: Map): Map { + val result = mutableMapOf() + + // Fields for which we have no property methods + for ((name, field) in fieldMap) { + if (name !in keys) { + result[name] = PropertyDescriptor(field, null, null) + } + } + + for ((name, methodMap) in this) { + result[name] = PropertyDescriptor( + fieldMap[name], + methodMap[SET], + methodMap[GET] ?: methodMap[IS] + ) + } + + return result +} + +// Select the least generic of two methods by a type associated with each. +private fun leastGenericBy(feature: Method.() -> Type, first: Method, second: Method) = + if (first.feature().isSupertypeOf(second.feature())) second else first + +// Throw an exception if any property descriptor is inconsistent, e.g. the types don't match +private fun Map.validated() = apply { + forEach { _, value -> value.validate() } +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializer.kt index e83c5c4119..3b3ee33478 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializer.kt @@ -19,8 +19,8 @@ sealed class PropertySerializer(val name: String, val propertyReader: PropertyRe val default: String? = generateDefault() val mandatory: Boolean = generateMandatory() - private val isInterface: Boolean get() = resolvedType.asClass()?.isInterface == true - private val isJVMPrimitive: Boolean get() = resolvedType.asClass()?.isPrimitive == true + private val isInterface: Boolean get() = resolvedType.asClass().isInterface + private val isJVMPrimitive: Boolean get() = resolvedType.asClass().isPrimitive private fun generateType(): String { return if (isInterface || resolvedType == Any::class.java) "*" else SerializerFactory.nameForType(resolvedType) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt index f393fb7aad..f2820cc505 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt @@ -2,15 +2,12 @@ package net.corda.serialization.internal.amqp import com.google.common.primitives.Primitives import com.google.common.reflect.TypeToken -import net.corda.core.KeepForDJVM import net.corda.core.internal.isConcreteClass -import net.corda.core.internal.isPublic 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 org.apache.qpid.proton.codec.Data -import java.io.NotSerializableException import java.lang.reflect.* import java.lang.reflect.Field import java.util.* @@ -26,42 +23,37 @@ import kotlin.reflect.jvm.javaType /** * Code for finding the constructor we will use for deserialization. * - * If there's only one constructor, it selects that. If there are two and one is the default, it selects the other. - * Otherwise it starts with the primary constructor in kotlin, if there is one, and then will override this with any that is - * annotated with [@ConstructorForDeserialization]. It will report an error if more than one constructor is annotated. + * If any constructor is uniquely annotated with [@ConstructorForDeserialization], then that constructor is chosen. + * An error is reported if more than one constructor is annotated. + * + * Otherwise, if there is a Kotlin primary constructor, it selects that, and if not it selects either the unique + * constructor or, if there are two and one is the default no-argument constructor, the non-default constructor. */ -fun constructorForDeserialization(type: Type): KFunction? { - val clazz: Class<*> = type.asClass()!! - if (clazz.isConcreteClass) { - var preferredCandidate: KFunction? = clazz.kotlin.primaryConstructor - var annotatedCount = 0 - val kotlinConstructors = clazz.kotlin.constructors - val hasDefault = kotlinConstructors.any { it.parameters.isEmpty() } - - for (kotlinConstructor in kotlinConstructors) { - if (preferredCandidate == null && kotlinConstructors.size == 1) { - preferredCandidate = kotlinConstructor - } else if (preferredCandidate == null && - kotlinConstructors.size == 2 && - hasDefault && - kotlinConstructor.parameters.isNotEmpty() - ) { - preferredCandidate = kotlinConstructor - } else if (kotlinConstructor.findAnnotation() != null) { - if (annotatedCount++ > 0) { - throw AMQPNotSerializableException( - type, - "More than one constructor for $clazz is annotated with @ConstructorForDeserialization.") - } - preferredCandidate = kotlinConstructor - } - } - - return preferredCandidate?.apply { isAccessible = true } - ?: throw AMQPNotSerializableException(type, "No constructor for deserialization found for $clazz.") - } else { - return null +fun constructorForDeserialization(type: Type): KFunction { + val clazz = type.asClass().apply { + if (!isConcreteClass) throw AMQPNotSerializableException(type, + "Cannot find deserialisation constructor for non-concrete class $this") } + + val kotlinCtors = clazz.kotlin.constructors + + val annotatedCtors = kotlinCtors.filter { it.findAnnotation() != null } + if (annotatedCtors.size > 1) throw AMQPNotSerializableException( + type, + "More than one constructor for $clazz is annotated with @ConstructorForDeserialization.") + + val defaultCtor = kotlinCtors.firstOrNull { it.parameters.isEmpty() } + val nonDefaultCtors = kotlinCtors.filter { it != defaultCtor } + + val preferredCandidate = annotatedCtors.firstOrNull() ?: + clazz.kotlin.primaryConstructor ?: + when(nonDefaultCtors.size) { + 1 -> nonDefaultCtors.first() + 0 -> defaultCtor ?: throw AMQPNotSerializableException(type, "No constructor found for $clazz.") + else -> throw AMQPNotSerializableException(type, "No unique non-default constructor found for $clazz.") + } + + return preferredCandidate.apply { isAccessible = true } } /** @@ -75,145 +67,13 @@ fun constructorForDeserialization(type: Type): KFunction? { fun propertiesForSerialization( kotlinConstructor: KFunction?, type: Type, - factory: SerializerFactory): PropertySerializers { - return PropertySerializers.make( + factory: SerializerFactory): PropertySerializers = PropertySerializers.make( if (kotlinConstructor != null) { propertiesForSerializationFromConstructor(kotlinConstructor, type, factory) } else { - propertiesForSerializationFromAbstract(type.asClass()!!, type, factory) + propertiesForSerializationFromAbstract(type.asClass(), type, factory) }.sortedWith(PropertyAccessor) ) -} - -/** - * Encapsulates the property of a class and its potential getter and setter methods. - * - * @property field a property of a class. - * @property setter the method of a class that sets the field. Determined by locating - * a function called setXyz on the class for the property named in field as xyz. - * @property getter the method of a class that returns a fields value. Determined by - * locating a function named getXyz for the property named in field as xyz. - */ -@KeepForDJVM -data class PropertyDescriptor(var field: Field?, var setter: Method?, var getter: Method?, var iser: Method?) { - override fun toString() = StringBuilder("").apply { - appendln("Property - ${field?.name ?: "null field"}\n") - appendln(" getter - ${getter?.name ?: "no getter"}") - appendln(" setter - ${setter?.name ?: "no setter"}") - appendln(" iser - ${iser?.name ?: "no isXYZ defined"}") - }.toString() - - constructor() : this(null, null, null, null) - - fun preferredGetter(): Method? = getter ?: iser -} - -object PropertyDescriptorsRegex { - // match an uppercase letter that also has a corresponding lower case equivalent - val re = Regex("(?get|set|is)(?\\p{Lu}.*)") -} - -/** - * Collate the properties of a class and match them with their getter and setter - * methods as per a JavaBean. - * - * for a property - * exampleProperty - * - * We look for methods - * setExampleProperty - * getExampleProperty - * isExampleProperty - * - * Where setExampleProperty must return a type compatible with exampleProperty, getExampleProperty must - * take a single parameter of a type compatible with exampleProperty and isExampleProperty must - * return a boolean - */ -fun Class.propertyDescriptors(): Map { - val classProperties = mutableMapOf() - - var clazz: Class? = this - - do { - clazz!!.declaredFields.forEach { property -> - classProperties.computeIfAbsent(property.name) { - PropertyDescriptor() - }.apply { - this.field = property - } - } - clazz = clazz.superclass - } while (clazz != null) - - // - // Running as two loops rather than one as we need to ensure we have captured all of the properties - // before looking for interacting methods and need to cope with the class hierarchy introducing - // new properties / methods - // - clazz = this - do { - // Note: It is possible for a class to have multiple instances of a function where the types - // differ. For example: - // interface I { val a: T } - // class D(override val a: String) : I - // instances of D will have both - // getA - returning a String (java.lang.String) and - // getA - returning an Object (java.lang.Object) - // In this instance we take the most derived object - // - // In addition, only getters that take zero parameters and setters that take a single - // parameter will be considered - clazz!!.declaredMethods?.map { func -> - if (!func.isPublic) return@map - if (func.name == "getClass") return@map - - PropertyDescriptorsRegex.re.find(func.name)?.apply { - // matching means we have an func getX where the property could be x or X - // so having pre-loaded all of the properties we try to match to either case. If that - // fails the getter doesn't refer to a property directly, but may refer to a constructor - // parameter that shadows a property - val properties = - classProperties[groups[2]!!.value] ?: classProperties[groups[2]!!.value.decapitalize()] ?: - // take into account those constructor properties that don't directly map to a named - // property which are, by default, already added to the map - classProperties.computeIfAbsent(groups[2]!!.value) { PropertyDescriptor() } - - properties.apply { - when (groups[1]!!.value) { - "set" -> { - if (func.parameterCount == 1) { - if (setter == null) setter = func - else if (TypeToken.of(setter!!.genericReturnType).isSupertypeOf(func.genericReturnType)) { - setter = func - } - } - } - "get" -> { - if (func.parameterCount == 0) { - if (getter == null) getter = func - else if (TypeToken.of(getter!!.genericReturnType).isSupertypeOf(func.genericReturnType)) { - getter = func - } - } - } - "is" -> { - if (func.parameterCount == 0) { - val rtnType = TypeToken.of(func.genericReturnType) - if ((rtnType == TypeToken.of(Boolean::class.java)) - || (rtnType == TypeToken.of(Boolean::class.javaObjectType))) { - if (iser == null) iser = func - } - } - } - } - } - } - } - clazz = clazz.superclass - } while (clazz != null) - - return classProperties -} /** * From a constructor, determine which properties of a class are to be serialized. @@ -235,66 +95,48 @@ internal fun propertiesForSerializationFromConstructor( // think you could inspect the parameter and check the isSynthetic flag but that is always // false so given the naming convention is specified by the standard we can just check for // this - if (kotlinConstructor.javaConstructor?.parameterCount ?: 0 > 0 && - kotlinConstructor.javaConstructor?.parameters?.get(0)?.name == "this$0" - ) { - throw SyntheticParameterException(type) + kotlinConstructor.javaConstructor?.apply { + if (parameterCount > 0 && parameters[0].name == "this$0") throw SyntheticParameterException(type) } if (classProperties.isNotEmpty() && kotlinConstructor.parameters.isEmpty()) { return propertiesForSerializationFromSetters(classProperties, type, factory) } - return mutableListOf().apply { - kotlinConstructor.parameters.withIndex().forEach { param -> - // name cannot be null, if it is then this is a synthetic field and we will have bailed - // out prior to this - val name = param.value.name!! - - // We will already have disambiguated getA for property A or a but we still need to cope - // with the case we don't know the case of A when the parameter doesn't match a property - // but has a getter - val matchingProperty = classProperties[name] ?: classProperties[name.capitalize()] - ?: throw AMQPNotSerializableException(type, - "Constructor parameter - \"$name\" - doesn't refer to a property of \"$clazz\"") - - // If the property has a getter we'll use that to retrieve it's value from the instance, if it doesn't - // *for *know* we switch to a reflection based method - val propertyReader = if (matchingProperty.getter != null) { - val getter = matchingProperty.getter ?: throw AMQPNotSerializableException( - type, - "Property has no getter method for - \"$name\" - of \"$clazz\". If using Java and the parameter name" - + "looks anonymous, check that you have the -parameters option specified in the " - + "Java compiler. Alternately, provide a proxy serializer " - + "(SerializationCustomSerializer) if recompiling isn't an option.") - - val returnType = resolveTypeVariables(getter.genericReturnType, type) - if (!constructorParamTakesReturnTypeOfGetter(returnType, getter.genericReturnType, param.value)) { - throw AMQPNotSerializableException( - type, - "Property - \"$name\" - has type \"$returnType\" on \"$clazz\" but differs from constructor " + - "parameter type \"${param.value.type.javaType}\"") - } - - Pair(PublicPropertyReader(getter), returnType) - } else { - val field = classProperties[name]!!.field - ?: throw AMQPNotSerializableException(type, - "No property matching constructor parameter named - \"$name\" - " + - "of \"$clazz\". If using Java, check that you have the -parameters option specified " + - "in the Java compiler. Alternately, provide a proxy serializer " + - "(SerializationCustomSerializer) if recompiling isn't an option") - - Pair(PrivatePropertyReader(field, type), resolveTypeVariables(field.genericType, type)) - } - - this += PropertyAccessorConstructor( - param.index, - PropertySerializer.make(name, propertyReader.first, propertyReader.second, factory)) - } + return kotlinConstructor.parameters.withIndex().map { param -> + toPropertyAccessorConstructor(param.index, param.value, classProperties, type, clazz, factory) } } +private fun toPropertyAccessorConstructor(index: Int, param: KParameter, classProperties: Map, type: Type, clazz: Class, factory: SerializerFactory): PropertyAccessorConstructor { + // name cannot be null, if it is then this is a synthetic field and we will have bailed + // out prior to this + val name = param.name!! + + // We will already have disambiguated getA for property A or a but we still need to cope + // with the case we don't know the case of A when the parameter doesn't match a property + // but has a getter + val matchingProperty = classProperties[name] ?: classProperties[name.capitalize()] + ?: throw AMQPNotSerializableException(type, + "Constructor parameter - \"$name\" - doesn't refer to a property of \"$clazz\"") + + // If the property has a getter we'll use that to retrieve it's value from the instance, if it doesn't + // *for *now* we switch to a reflection based method + val propertyReader = matchingProperty.getter?.let { getter -> + getPublicPropertyReader(getter, type, param, name, clazz) + } ?: matchingProperty.field?.let { field -> + getPrivatePropertyReader(field, type) + } ?: throw AMQPNotSerializableException(type, + "No property matching constructor parameter named - \"$name\" - " + + "of \"${param}\". If using Java, check that you have the -parameters option specified " + + "in the Java compiler. Alternately, provide a proxy serializer " + + "(SerializationCustomSerializer) if recompiling isn't an option") + + return PropertyAccessorConstructor( + index, + PropertySerializer.make(name, propertyReader.first, propertyReader.second, factory)) +} + /** * If we determine a class has a constructor that takes no parameters then check for pairs of getters / setters * and use those @@ -302,107 +144,83 @@ internal fun propertiesForSerializationFromConstructor( fun propertiesForSerializationFromSetters( properties: Map, type: Type, - factory: SerializerFactory): List { - return mutableListOf().apply { - var idx = 0 + factory: SerializerFactory): List = + properties.asSequence().withIndex().map { (index, entry) -> + val (name, property) = entry - properties.forEach { property -> - val getter: Method? = property.value.preferredGetter() - val setter: Method? = property.value.setter + val getter = property.getter + val setter = property.setter - if (getter == null || setter == null) return@forEach + if (getter == null || setter == null) return@map null - if (setter.parameterCount != 1) { - throw AMQPNotSerializableException( - type, - "Defined setter for parameter ${property.value.field?.name} takes too many arguments") - } - - val setterType = setter.genericParameterTypes[0]!! - - if ((property.value.field != null) && - (!(TypeToken.of(property.value.field?.genericType!!).isSupertypeOf(setterType))) - ) { - throw AMQPNotSerializableException( - type, - "Defined setter for parameter ${property.value.field?.name} " + - "takes parameter of type $setterType yet underlying type is " + - "${property.value.field?.genericType!!}") - } - - // Make sure the getter returns the same type (within inheritance bounds) the setter accepts. - if (!(TypeToken.of(getter.genericReturnType).isSupertypeOf(setterType))) { - throw AMQPNotSerializableException( - type, - "Defined setter for parameter ${property.value.field?.name} " + - "takes parameter of type $setterType yet the defined getter returns a value of type " + - "${getter.returnType} [${getter.genericReturnType}]") - } - this += PropertyAccessorGetterSetter( - idx++, - PropertySerializer.make(property.key, PublicPropertyReader(getter), - resolveTypeVariables(getter.genericReturnType, type), factory), + PropertyAccessorGetterSetter( + index, + PropertySerializer.make( + name, + PublicPropertyReader(getter), + resolveTypeVariables(getter.genericReturnType, type), + factory), setter) - } - } -} + }.filterNotNull().toList() -private fun constructorParamTakesReturnTypeOfGetter( - getterReturnType: Type, - rawGetterReturnType: Type, - param: KParameter): Boolean { +private fun getPrivatePropertyReader(field: Field, type: Type) = + PrivatePropertyReader(field, type) to resolveTypeVariables(field.genericType, type) + +private fun getPublicPropertyReader(getter: Method, type: Type, param: KParameter, name: String, clazz: Class): Pair { + val returnType = resolveTypeVariables(getter.genericReturnType, type) val paramToken = TypeToken.of(param.type.javaType) val rawParamType = TypeToken.of(paramToken.rawType) - return paramToken.isSupertypeOf(getterReturnType) - || paramToken.isSupertypeOf(rawGetterReturnType) - // cope with the case where the constructor parameter is a generic type (T etc) but we - // can discover it's raw type. When bounded this wil be the bounding type, unbounded - // generics this will be object - || rawParamType.isSupertypeOf(getterReturnType) - || rawParamType.isSupertypeOf(rawGetterReturnType) + if (!(paramToken.isSupertypeOf(returnType) + || paramToken.isSupertypeOf(getter.genericReturnType) + // cope with the case where the constructor parameter is a generic type (T etc) but we + // can discover it's raw type. When bounded this wil be the bounding type, unbounded + // generics this will be object + || rawParamType.isSupertypeOf(returnType) + || rawParamType.isSupertypeOf(getter.genericReturnType))) { + throw AMQPNotSerializableException( + type, + "Property - \"$name\" - has type \"$returnType\" on \"$clazz\" " + + "but differs from constructor parameter type \"${param.type.javaType}\"") + } + + return PublicPropertyReader(getter) to returnType } private fun propertiesForSerializationFromAbstract( clazz: Class<*>, type: Type, - factory: SerializerFactory): List { - val properties = clazz.propertyDescriptors() - - return mutableListOf().apply { - properties.toList().withIndex().forEach { - val getter = it.value.second.getter ?: return@forEach - if (it.value.second.field == null) return@forEach + factory: SerializerFactory): List = + clazz.propertyDescriptors().asSequence().withIndex().map { (index, entry) -> + val (name, property) = entry + if (property.getter == null || property.field == null) return@map null + val getter = property.getter val returnType = resolveTypeVariables(getter.genericReturnType, type) - this += PropertyAccessorConstructor( - it.index, - PropertySerializer.make(it.value.first, PublicPropertyReader(getter), returnType, factory)) - } - } -} -internal fun interfacesForSerialization(type: Type, serializerFactory: SerializerFactory): List { - val interfaces = LinkedHashSet() - exploreType(type, interfaces, serializerFactory) - return interfaces.toList() -} + PropertyAccessorConstructor( + index, + PropertySerializer.make(name, PublicPropertyReader(getter), returnType, factory)) + }.filterNotNull().toList() -private fun exploreType(type: Type?, interfaces: MutableSet, serializerFactory: SerializerFactory) { - val clazz = type?.asClass() - if (clazz != null) { - if (clazz.isInterface) { - if (serializerFactory.whitelist.isNotWhitelisted(clazz)) return // We stop exploring once we reach a branch that has no `CordaSerializable` annotation or whitelisting. - else interfaces += type - } - for (newInterface in clazz.genericInterfaces) { - if (newInterface !in interfaces) { - exploreType(resolveTypeVariables(newInterface, type), interfaces, serializerFactory) - } - } - val superClass = clazz.genericSuperclass ?: return - exploreType(resolveTypeVariables(superClass, type), interfaces, serializerFactory) +internal fun interfacesForSerialization(type: Type, serializerFactory: SerializerFactory): List = + exploreType(type, serializerFactory).toList() + +private fun exploreType(type: Type, serializerFactory: SerializerFactory, interfaces: MutableSet = LinkedHashSet()): MutableSet { + val clazz = type.asClass() + + if (clazz.isInterface) { + // Ignore classes we've already seen, and stop exploring once we reach a branch that has no `CordaSerializable` + // annotation or whitelisting. + if (clazz in interfaces || serializerFactory.whitelist.isNotWhitelisted(clazz)) return interfaces + else interfaces += type } + + (clazz.genericInterfaces.asSequence() + clazz.genericSuperclass) + .filterNotNull() + .forEach { exploreType(resolveTypeVariables(it, type), serializerFactory, interfaces) } + + return interfaces } /** @@ -459,21 +277,23 @@ fun resolveTypeVariables(actualType: Type, contextType: Type?): Type { } } -internal fun Type.asClass(): Class<*>? { - return when { - this is Class<*> -> this - this is ParameterizedType -> this.rawType.asClass() - this is GenericArrayType -> this.genericComponentType.asClass()?.arrayClass() - this is TypeVariable<*> -> this.bounds.first().asClass() - this is WildcardType -> this.upperBounds.first().asClass() - else -> null +internal fun Type.asClass(): Class<*> { + return when(this) { + is Class<*> -> this + is ParameterizedType -> this.rawType.asClass() + is GenericArrayType -> this.genericComponentType.asClass().arrayClass() + is TypeVariable<*> -> this.bounds.first().asClass() + is WildcardType -> this.upperBounds.first().asClass() + // Per https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Type.html, + // there is nothing else that it can be, so this can never happen. + else -> throw UnsupportedOperationException("Cannot convert $this to class") } } internal fun Type.asArray(): Type? { - return when { - this is Class<*> -> this.arrayClass() - this is ParameterizedType -> DeserializedGenericArrayType(this) + return when(this) { + is Class<*> -> this.arrayClass() + is ParameterizedType -> DeserializedGenericArrayType(this) else -> null } } @@ -506,7 +326,7 @@ internal fun Type.isSubClassOf(type: Type): Boolean { // ByteArrays, primitives and boxed primitives are not stored in the object history internal fun suitableForObjectReference(type: Type): Boolean { val clazz = type.asClass() - return type != ByteArray::class.java && (clazz != null && !clazz.isPrimitive && !Primitives.unwrap(clazz).isPrimitive) + return type != ByteArray::class.java && (!clazz.isPrimitive && !Primitives.unwrap(clazz).isPrimitive) } /** @@ -519,7 +339,7 @@ internal enum class CommonPropertyNames { fun ClassWhitelist.requireWhitelisted(type: Type) { - if (!this.isWhitelisted(type.asClass()!!)) { + if (!this.isWhitelisted(type.asClass())) { throw AMQPNotSerializableException( type, "Class \"$type\" is not on the whitelist or annotated with @CordaSerializable.") diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt index 657c72d99b..8c869e2eda 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt @@ -1,7 +1,6 @@ package net.corda.serialization.internal.amqp import com.google.common.primitives.Primitives -import com.google.common.reflect.TypeResolver import net.corda.core.DeleteForDJVM import net.corda.core.KeepForDJVM import net.corda.core.StubOutForDJVM @@ -54,7 +53,7 @@ open class SerializerFactory( val whitelist: ClassWhitelist, val classCarpenter: ClassCarpenter, private val evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), - val fingerPrinter: FingerPrinter = SerializerFingerPrinter(), + val fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter, private val serializersByType: MutableMap>, val serializersByDescriptor: MutableMap>, private val customSerializers: MutableList, @@ -66,13 +65,13 @@ open class SerializerFactory( constructor(whitelist: ClassWhitelist, classCarpenter: ClassCarpenter, evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), - fingerPrinter: FingerPrinter = SerializerFingerPrinter(), + fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter, onlyCustomSerializers: Boolean = false ) : this( whitelist, classCarpenter, evolutionSerializerGetter, - fingerPrinter, + fingerPrinterConstructor, ConcurrentHashMap(), ConcurrentHashMap(), CopyOnWriteArrayList(), @@ -86,18 +85,16 @@ open class SerializerFactory( carpenterClassLoader: ClassLoader, lenientCarpenter: Boolean = false, evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), - fingerPrinter: FingerPrinter = SerializerFingerPrinter(), + fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter, onlyCustomSerializers: Boolean = false ) : this( whitelist, ClassCarpenterImpl(whitelist, carpenterClassLoader, lenientCarpenter), evolutionSerializerGetter, - fingerPrinter, + fingerPrinterConstructor, onlyCustomSerializers) - init { - fingerPrinter.setOwner(this) - } + val fingerPrinter by lazy { fingerPrinterConstructor(this) } val classloader: ClassLoader get() = classCarpenter.classloader @@ -118,11 +115,9 @@ open class SerializerFactory( // can be useful to enable but will be *extremely* chatty if you do logger.trace { "Get Serializer for $actualClass ${declaredType.typeName}" } - val declaredClass = declaredType.asClass() ?: throw AMQPNotSerializableException( - declaredType, - "Declared types of $declaredType are not supported.") - - val actualType: Type = inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType + val declaredClass = declaredType.asClass() + val actualType: Type = if (actualClass == null) 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. @@ -166,78 +161,6 @@ open class SerializerFactory( return serializer } - /** - * Try and infer concrete types for any generics type variables for the actual class encountered, - * based on the declared type. - */ - // TODO: test GenericArrayType - private fun inferTypeVariables(actualClass: Class<*>?, declaredClass: Class<*>, - declaredType: Type): Type? = when (declaredType) { - is ParameterizedType -> inferTypeVariables(actualClass, declaredClass, declaredType) - // Nothing to infer, otherwise we'd have ParameterizedType - is Class<*> -> actualClass - is GenericArrayType -> { - val declaredComponent = declaredType.genericComponentType - inferTypeVariables(actualClass?.componentType, declaredComponent.asClass()!!, declaredComponent)?.asArray() - } - is TypeVariable<*> -> actualClass - is WildcardType -> actualClass - else -> null - } - - /** - * Try and infer concrete types for any generics type variables for the actual class encountered, based on the declared - * type, which must be a [ParameterizedType]. - */ - private fun inferTypeVariables(actualClass: Class<*>?, declaredClass: Class<*>, declaredType: ParameterizedType): Type? { - if (actualClass == null || declaredClass == actualClass) { - return null - } else if (declaredClass.isAssignableFrom(actualClass)) { - return if (actualClass.typeParameters.isNotEmpty()) { - // The actual class can never have type variables resolved, due to the JVM's use of type erasure, so let's try and resolve them - // Search for declared type in the inheritance hierarchy and then see if that fills in all the variables - val implementationChain: List? = findPathToDeclared(actualClass, declaredType, mutableListOf()) - if (implementationChain != null) { - val start = implementationChain.last() - val rest = implementationChain.dropLast(1).drop(1) - val resolver = rest.reversed().fold(TypeResolver().where(start, declaredType)) { resolved, chainEntry -> - val newResolved = resolved.resolveType(chainEntry) - TypeResolver().where(chainEntry, newResolved) - } - // The end type is a special case as it is a Class, so we need to fake up a ParameterizedType for it to get the TypeResolver to do anything. - val endType = DeserializedParameterizedType(actualClass, actualClass.typeParameters) - val resolvedType = resolver.resolveType(endType) - resolvedType - } else throw AMQPNotSerializableException(declaredType, - "No inheritance path between actual $actualClass and declared $declaredType.") - } else actualClass - } else throw AMQPNotSerializableException( - declaredType, - "Found object of type $actualClass in a property expecting $declaredType") - } - - // Stop when reach declared type or return null if we don't find it. - private fun findPathToDeclared(startingType: Type, declaredType: Type, chain: MutableList): List? { - chain.add(startingType) - val startingClass = startingType.asClass() - if (startingClass == declaredType.asClass()) { - // We're done... - return chain - } - // Now explore potential options of superclass and all interfaces - val superClass = startingClass?.genericSuperclass - val superClassChain = if (superClass != null) { - val resolved = TypeResolver().where(startingClass.asParameterizedType(), startingType.asParameterizedType()).resolveType(superClass) - findPathToDeclared(resolved, declaredType, ArrayList(chain)) - } else null - if (superClassChain != null) return superClassChain - for (iface in startingClass?.genericInterfaces ?: emptyArray()) { - val resolved = TypeResolver().where(startingClass!!.asParameterizedType(), startingType.asParameterizedType()).resolveType(iface) - return findPathToDeclared(resolved, declaredType, ArrayList(chain)) ?: continue - } - return null - } - /** * Lookup and manufacture a serializer for the given AMQP type descriptor, assuming we also have the necessary types * contained in the [Schema]. @@ -349,7 +272,7 @@ open class SerializerFactory( // TODO: class loader logic, and compare the schema. val type = typeForName(typeNotation.name, classloader) return get( - type.asClass() ?: throw AMQPNotSerializableException(type, "Unable to build composite type for $type"), + type.asClass(), type) } @@ -402,7 +325,7 @@ open class SerializerFactory( // super type. Could be done, but do we need it? for (customSerializer in customSerializers) { if (customSerializer.isSerializerFor(clazz)) { - val declaredSuperClass = declaredType.asClass()?.superclass + val declaredSuperClass = declaredType.asClass().superclass return if (declaredSuperClass == null diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TypeParameterUtils.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TypeParameterUtils.kt new file mode 100644 index 0000000000..72720add79 --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TypeParameterUtils.kt @@ -0,0 +1,94 @@ +package net.corda.serialization.internal.amqp + +import com.google.common.reflect.TypeResolver +import java.lang.reflect.* + +/** + * Try and infer concrete types for any generics type variables for the actual class encountered, + * based on the declared type. + */ +// TODO: test GenericArrayType +fun inferTypeVariables(actualClass: Class<*>, + declaredClass: Class<*>, + declaredType: Type): Type? = when (declaredType) { + is ParameterizedType -> inferTypeVariables(actualClass, declaredClass, declaredType) + is GenericArrayType -> { + val declaredComponent = declaredType.genericComponentType + inferTypeVariables(actualClass.componentType, declaredComponent.asClass(), declaredComponent)?.asArray() + } + // Nothing to infer, otherwise we'd have ParameterizedType + is Class<*> -> actualClass + is TypeVariable<*> -> actualClass + is WildcardType -> actualClass + else -> throw UnsupportedOperationException("Cannot infer type variables for type $declaredType") +} + +/** + * Try and infer concrete types for any generics type variables for the actual class encountered, based on the declared + * type, which must be a [ParameterizedType]. + */ +private fun inferTypeVariables(actualClass: Class<*>, declaredClass: Class<*>, declaredType: ParameterizedType): Type? { + if (declaredClass == actualClass) { + return null + } + + if (!declaredClass.isAssignableFrom(actualClass)) { + throw AMQPNotSerializableException( + declaredType, + "Found object of type $actualClass in a property expecting $declaredType") + } + + if (actualClass.typeParameters.isEmpty()) { + return actualClass + } + // The actual class can never have type variables resolved, due to the JVM's use of type erasure, so let's try and resolve them + // Search for declared type in the inheritance hierarchy and then see if that fills in all the variables + val implementationChain: List = findPathToDeclared(actualClass, declaredType)?.toList() + ?: throw AMQPNotSerializableException( + declaredType, + "No inheritance path between actual $actualClass and declared $declaredType.") + + val start = implementationChain.last() + val rest = implementationChain.dropLast(1).drop(1) + val resolver = rest.reversed().fold(TypeResolver().where(start, declaredType)) { resolved, chainEntry -> + val newResolved = resolved.resolveType(chainEntry) + TypeResolver().where(chainEntry, newResolved) + } + // The end type is a special case as it is a Class, so we need to fake up a ParameterizedType for it to get the TypeResolver to do anything. + val endType = DeserializedParameterizedType(actualClass, actualClass.typeParameters) + return resolver.resolveType(endType) +} + +// Stop when reach declared type or return null if we don't find it. +private fun findPathToDeclared(startingType: Type, declaredType: Type, chain: Sequence = emptySequence()): Sequence? { + val extendedChain = chain + startingType + val startingClass = startingType.asClass() + + if (startingClass == declaredType.asClass()) { + // We're done... + return extendedChain + } + + val resolver = { type: Type -> + TypeResolver().where( + startingClass.asParameterizedType(), + startingType.asParameterizedType()) + .resolveType(type) + } + + // Now explore potential options of superclass and all interfaces + return findPathViaGenericSuperclass(startingClass, resolver, declaredType, extendedChain) + ?: findPathViaInterfaces(startingClass, resolver, declaredType, extendedChain) +} + +private fun findPathViaInterfaces(startingClass: Class<*>, resolver: (Type) -> Type, declaredType: Type, extendedChain: Sequence): Sequence? = + startingClass.genericInterfaces.asSequence().map { + findPathToDeclared(resolver(it), declaredType, extendedChain) + }.filterNotNull().firstOrNull() + + +private fun findPathViaGenericSuperclass(startingClass: Class<*>, resolver: (Type) -> Type, declaredType: Type, extendedChain: Sequence): Sequence? { + val superClass = startingClass.genericSuperclass ?: return null + return findPathToDeclared(resolver(superClass), declaredType, extendedChain) +} + diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt index 135e93710c..3b4b03800e 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt @@ -25,7 +25,7 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy + propertiesForSerializationFromConstructor(constructor, obj.javaClass, factory).forEach { property -> extraProperties[property.serializer.name] = property.serializer.propertyReader.read(obj) } } catch (e: NotSerializableException) { @@ -52,7 +52,7 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy FingerPrinterTesting() }) val blob = TestSerializationOutput(VERBOSE, factory).serializeAndReturnSchema(C(1, 2L))