From 00b90a98fbabc8b7f6ed55634c0cf08e2fee0105 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Thu, 1 Feb 2018 12:19:32 +0000 Subject: [PATCH] CORDA-943 - Cope with multiple generics at str->type conversion in AMQP Also fixes an odd bug where the inferred type of a getter wasn't matching the constructor parameter type because that was still unbounded and seen as T, looking at the raw type allows us to inspect this properly --- .../amqp/AMQPPrimitiveSerializer.kt | 2 +- .../serialization/amqp/AMQPSerializer.kt | 2 +- .../serialization/amqp/ArraySerializer.kt | 46 ++++--- .../amqp/CollectionSerializer.kt | 14 +- .../amqp/CorDappCustomSerializer.kt | 2 +- .../serialization/amqp/CustomSerializer.kt | 2 +- .../amqp/DeserializationInput.kt | 68 +++++----- .../amqp/EnumEvolutionSerializer.kt | 2 +- .../serialization/amqp/EnumSerializer.kt | 2 +- .../serialization/amqp/MapSerializer.kt | 11 +- .../serialization/amqp/ObjectSerializer.kt | 9 +- .../serialization/amqp/PropertySerializer.kt | 10 +- .../internal/serialization/amqp/Schema.kt | 27 ++-- .../serialization/amqp/SerializationHelper.kt | 21 ++- .../serialization/amqp/SerializationOutput.kt | 8 +- .../serialization/amqp/SingletonSerializer.kt | 2 +- .../carpenter/AMQPSchemaExtensions.kt | 6 +- .../serialization/amqp/GenericsTests.kt | 120 +++++++++++++++++- 18 files changed, 254 insertions(+), 100 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPPrimitiveSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPPrimitiveSerializer.kt index 8695c630f7..27db491d33 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPPrimitiveSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPPrimitiveSerializer.kt @@ -18,7 +18,7 @@ class AMQPPrimitiveSerializer(clazz: Class<*>) : AMQPSerializer { override fun writeClassInfo(output: SerializationOutput) { } - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, offset: Int) { + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) { if (obj is ByteArray) { data.putObject(Binary(obj)) } else { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPSerializer.kt index f306b31f21..9dfcd03cb7 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPSerializer.kt @@ -30,7 +30,7 @@ interface AMQPSerializer { /** * Write the given object, with declared type, to the output. */ - fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, offset: Int = 0) + fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int = 0) /** * Read the given object from the input. The envelope is provided in case the schema is required. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt index 7e3e51da97..b6b20a204e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt @@ -45,12 +45,12 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory) } } - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, offset: Int) { + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) { // Write described data.withDescribed(typeNotation.descriptor) { withList { for (entry in obj as Array<*>) { - output.writeObjectOrNull(entry, this, elementType, offset) + output.writeObjectOrNull(entry, this, elementType, debugIndent) } } } @@ -109,15 +109,19 @@ abstract class PrimArraySerializer(type: Type, factory: SerializerFactory) : Arr class PrimIntArraySerializer(factory: SerializerFactory) : PrimArraySerializer(IntArray::class.java, factory) { - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, offset: Int) { - localWriteObject(data) { (obj as IntArray).forEach { output.writeObjectOrNull(it, data, elementType, offset+4) } } + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) { + localWriteObject(data) { + (obj as IntArray).forEach { output.writeObjectOrNull(it, data, elementType, debugIndent+1) } + } } } class PrimCharArraySerializer(factory: SerializerFactory) : PrimArraySerializer(CharArray::class.java, factory) { - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, offset: Int) { - localWriteObject(data) { (obj as CharArray).forEach { output.writeObjectOrNull(it, data, elementType, offset+4) } } + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) { + localWriteObject(data) { (obj as CharArray).forEach { + output.writeObjectOrNull(it, data, elementType, debugIndent+1) } + } } override fun List.toArrayOfType(type: Type): Any { @@ -132,35 +136,45 @@ class PrimCharArraySerializer(factory: SerializerFactory) : class PrimBooleanArraySerializer(factory: SerializerFactory) : PrimArraySerializer(BooleanArray::class.java, factory) { - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, offset: Int) { - localWriteObject(data) { (obj as BooleanArray).forEach { output.writeObjectOrNull(it, data, elementType, offset+4) } } + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) { + localWriteObject(data) { + (obj as BooleanArray).forEach { output.writeObjectOrNull(it, data, elementType, debugIndent+1) } + } } } class PrimDoubleArraySerializer(factory: SerializerFactory) : PrimArraySerializer(DoubleArray::class.java, factory) { - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, offset: Int) { - localWriteObject(data) { (obj as DoubleArray).forEach { output.writeObjectOrNull(it, data, elementType, offset+4) } } + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) { + localWriteObject(data) { + (obj as DoubleArray).forEach { output.writeObjectOrNull(it, data, elementType, debugIndent+1) } + } } } class PrimFloatArraySerializer(factory: SerializerFactory) : PrimArraySerializer(FloatArray::class.java, factory) { - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, offset: Int) { - localWriteObject(data) { (obj as FloatArray).forEach { output.writeObjectOrNull(it, data, elementType, offset+4) } } + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) { + localWriteObject(data) { + (obj as FloatArray).forEach { output.writeObjectOrNull(it, data, elementType, debugIndent+1) } + } } } class PrimShortArraySerializer(factory: SerializerFactory) : PrimArraySerializer(ShortArray::class.java, factory) { - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, offset: Int) { - localWriteObject(data) { (obj as ShortArray).forEach { output.writeObjectOrNull(it, data, elementType, offset+4) } } + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) { + localWriteObject(data) { + (obj as ShortArray).forEach { output.writeObjectOrNull(it, data, elementType, debugIndent+1) } + } } } class PrimLongArraySerializer(factory: SerializerFactory) : PrimArraySerializer(LongArray::class.java, factory) { - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, offset: Int) { - localWriteObject(data) { (obj as LongArray).forEach { output.writeObjectOrNull(it, data, elementType, offset+4) } } + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) { + localWriteObject(data) { + (obj as LongArray).forEach { output.writeObjectOrNull(it, data, elementType, debugIndent+1) } + } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt index 90a3b8fe54..bd19e98599 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt @@ -66,18 +66,26 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali } } - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, offset: Int) = ifThrowsAppend({ declaredType.typeName }) { + override fun writeObject( + obj: Any, + data: Data, + type: Type, + output: SerializationOutput, + debugIndent: Int) = ifThrowsAppend({ declaredType.typeName }) { // Write described data.withDescribed(typeNotation.descriptor) { withList { for (entry in obj as Collection<*>) { - output.writeObjectOrNull(entry, this, declaredType.actualTypeArguments[0], offset) + output.writeObjectOrNull(entry, this, declaredType.actualTypeArguments[0], debugIndent) } } } } - override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) { + override fun readObject( + obj: Any, + schemas: SerializationSchemas, + input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) { // TODO: Can we verify the entries in the list? concreteBuilder((obj as List<*>).map { input.readObjectOrNull(it, schemas, declaredType.actualTypeArguments[0]) }) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CorDappCustomSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CorDappCustomSerializer.kt index 12508753f6..161c18d4cc 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CorDappCustomSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CorDappCustomSerializer.kt @@ -64,7 +64,7 @@ class CorDappCustomSerializer( override fun writeClassInfo(output: SerializationOutput) {} - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, offset: Int) { + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) { val proxy = uncheckedCast, SerializationCustomSerializer>(serializer).toProxy(obj) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt index 143b114fdc..f2ee28d01b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt @@ -40,7 +40,7 @@ abstract class CustomSerializer : AMQPSerializer, SerializerFor { */ override val revealSubclassesInSchema: Boolean get() = false - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, offset: Int) { + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) { data.withDescribed(descriptor) { writeDescribedObject(uncheckedCast(obj), data, type, output) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt index ce353f0140..ac61820218 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt @@ -1,6 +1,5 @@ package net.corda.nodeapi.internal.serialization.amqp -import com.google.common.primitives.Primitives import net.corda.core.internal.getStackTraceAsString import net.corda.core.serialization.SerializedBytes import net.corda.core.utilities.ByteSequence @@ -56,7 +55,7 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) { @Throws(NotSerializableException::class) inline internal fun deserializeAndReturnEnvelope(bytes: SerializedBytes): ObjectAndEnvelope = - deserializeAndReturnEnvelope(bytes, T::class.java) + deserializeAndReturnEnvelope(bytes, T::class.java) @Throws(NotSerializableException::class) internal fun getEnvelope(bytes: ByteSequence): Envelope { @@ -110,43 +109,42 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) { return if (obj == null) null else readObject(obj, schema, type, offset) } - internal fun readObject(obj: Any, schemas: SerializationSchemas, type: Type, offset: Int = 0): Any { - return 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) - throw NotSerializableException("Retrieval of existing reference failed. Requested index $objectIndex " + - "is outside of the bounds for the list of size: ${objectHistory.size}") + internal fun readObject(obj: Any, schemas: SerializationSchemas, type: Type, debugIndent: Int = 0): Any = + 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) + throw NotSerializableException("Retrieval of existing reference failed. Requested index $objectIndex " + + "is outside of the bounds for the list of size: ${objectHistory.size}") - val objectRetrieved = objectHistory[objectIndex] - if (!objectRetrieved::class.java.isSubClassOf(type.asClass()!!)) { - throw NotSerializableException( - "Existing reference type mismatch. Expected: '$type', found: '${objectRetrieved::class.java}' " + - "@ ${objectIndex}") - } - objectRetrieved - } else { - val objectRead = when (obj) { - is DescribedType -> { - // Look up serializer in factory by descriptor - val serializer = serializerFactory.get(obj.descriptor, schemas) - if (SerializerFactory.AnyType != type && serializer.type != type && with(serializer.type) { !isSubClassOf(type) && !materiallyEquivalentTo(type) }) - throw NotSerializableException("Described type with descriptor ${obj.descriptor} was " + - "expected to be of type $type but was ${serializer.type}") - serializer.readObject(obj.described, schemas, this) + val objectRetrieved = objectHistory[objectIndex] + if (!objectRetrieved::class.java.isSubClassOf(type.asClass()!!)) { + throw NotSerializableException( + "Existing reference type mismatch. Expected: '$type', found: '${objectRetrieved::class.java}' " + + "@ ${objectIndex}") + } + objectRetrieved + } else { + val objectRead = when (obj) { + is DescribedType -> { + // Look up serializer in factory by descriptor + val serializer = serializerFactory.get(obj.descriptor, schemas) + if (SerializerFactory.AnyType != type && serializer.type != type && with(serializer.type) { !isSubClassOf(type) && !materiallyEquivalentTo(type) }) + throw NotSerializableException("Described type with descriptor ${obj.descriptor} was " + + "expected to be of type $type but was ${serializer.type}") + serializer.readObject(obj.described, schemas, this) + } + is Binary -> obj.array + else -> obj // this will be the case for primitive types like [boolean] et al. } - is Binary -> obj.array - else -> obj // this will be the case for primitive types like [boolean] et al. - } - // Store the reference in case we need it later on. - // Skip for primitive types as they are too small and overhead of referencing them will be much higher than their content - if (suitableForObjectReference(objectRead.javaClass)) { - objectHistory.add(objectRead) + // Store the reference in case we need it later on. + // Skip for primitive types as they are too small and overhead of referencing them will be much higher than their content + if (suitableForObjectReference(objectRead.javaClass)) { + objectHistory.add(objectRead) + } + objectRead } - objectRead - } - } /** * Currently performs checks aimed at: diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt index e7d7010338..a9bc0916b1 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt @@ -130,7 +130,7 @@ class EnumEvolutionSerializer( throw UnsupportedOperationException("It should be impossible to write an evolution serializer") } - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, offset: Int) { + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) { throw UnsupportedOperationException("It should be impossible to write an evolution serializer") } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt index 203c1f8ed9..5678f094ed 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt @@ -39,7 +39,7 @@ class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: Seria return fromOrd } - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, offset: Int) { + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) { if (obj !is Enum<*>) throw NotSerializableException("Serializing $obj as enum when it isn't") data.withDescribed(typeNotation.descriptor) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt index bf7f9c6e84..5472074d62 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt @@ -73,7 +73,12 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial } } - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, offset: Int) = ifThrowsAppend({ declaredType.typeName }) { + override fun writeObject( + obj: Any, + data: Data, + type: Type, + output: SerializationOutput, + debugIndent: Int) = ifThrowsAppend({ declaredType.typeName }) { obj.javaClass.checkSupportedMapType() // Write described data.withDescribed(typeNotation.descriptor) { @@ -81,8 +86,8 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial data.putMap() data.enter() for ((key, value) in obj as Map<*, *>) { - output.writeObjectOrNull(key, data, declaredType.actualTypeArguments[0], offset) - output.writeObjectOrNull(value, data, declaredType.actualTypeArguments[1], offset) + output.writeObjectOrNull(key, data, declaredType.actualTypeArguments[0], debugIndent) + output.writeObjectOrNull(value, data, declaredType.actualTypeArguments[1], debugIndent) } data.exit() // exit map } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt index ad969bea35..fa723fd3de 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt @@ -52,13 +52,18 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS } } - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, offset: Int) = ifThrowsAppend({ clazz.typeName }) { + override fun writeObject( + obj: Any, + data: Data, + type: Type, + output: SerializationOutput, + debugIndent: Int) = ifThrowsAppend({ clazz.typeName }) { // Write described data.withDescribed(typeNotation.descriptor) { // Write list withList { propertySerializers.serializationOrder.forEach { property -> - property.getter.writeProperty(obj, this, output, offset+4) + property.getter.writeProperty(obj, this, output, debugIndent+1) } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt index e0df115ff5..f9fa3d8f83 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt @@ -9,7 +9,7 @@ import java.lang.reflect.Type */ sealed class PropertySerializer(val name: String, val propertyReader: PropertyReader, val resolvedType: Type) { abstract fun writeClassInfo(output: SerializationOutput) - abstract fun writeProperty(obj: Any?, data: Data, output: SerializationOutput, offset: Int = 0) + abstract fun writeProperty(obj: Any?, data: Data, output: SerializationOutput, debugIndent: Int = 0) abstract fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput): Any? val type: String = generateType() @@ -80,8 +80,8 @@ sealed class PropertySerializer(val name: String, val propertyReader: PropertyRe input.readObjectOrNull(obj, schemas, resolvedType) } - override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput, offset: Int) = ifThrowsAppend({ nameForDebug }) { - output.writeObjectOrNull(propertyReader.read(obj), data, resolvedType, offset) + override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput, debugIndent: Int) = ifThrowsAppend({ nameForDebug }) { + output.writeObjectOrNull(propertyReader.read(obj), data, resolvedType, debugIndent) } private val nameForDebug = "$name(${resolvedType.typeName})" @@ -100,7 +100,7 @@ sealed class PropertySerializer(val name: String, val propertyReader: PropertyRe return if (obj is Binary) obj.array else obj } - override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput, offset: Int) { + override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput, debugIndent: Int) { val value = propertyReader.read(obj) if (value is ByteArray) { data.putObject(Binary(value)) @@ -123,7 +123,7 @@ sealed class PropertySerializer(val name: String, val propertyReader: PropertyRe return if (obj == null) null else (obj as Short).toChar() } - override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput, offset: Int) { + override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput, debugIndent: Int) { val input = propertyReader.read(obj) if (input != null) data.putShort((input as Char).toShort()) else data.putNull() } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt index 6d344a0f98..372cfed9be 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt @@ -349,7 +349,7 @@ private fun Hasher.fingerprintWithCustomSerializerOrElse(factory: SerializerFact // 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, factory: SerializerFactory, offset: Int = 4): Hasher { + hasher: Hasher, factory: SerializerFactory, 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 @@ -372,23 +372,23 @@ private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: Muta hasher.putUnencodedChars(clazz.name) } else { hasher.fingerprintWithCustomSerializerOrElse(factory, clazz, type) { - fingerprintForObject(type, type, alreadySeen, hasher, factory, offset+4) + fingerprintForObject(type, type, alreadySeen, hasher, factory, debugIndent+1) } } // ... and concatenate the type data for each parameter type. type.actualTypeArguments.fold(startingHash) { orig, paramType -> - fingerprintForType(paramType, type, alreadySeen, orig, factory, offset+4) + fingerprintForType(paramType, type, alreadySeen, orig, factory, debugIndent+1) } } - // Previously, we drew a distinction between TypeVariable, Wildcard, and AnyType, changing + // 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 wildcard *and* a TypeVariable - // Note: AnyType is a special case of WildcarcType used in other parts of the + // 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 @@ -400,7 +400,7 @@ private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: Muta } is Class<*> -> { if (type.isArray) { - fingerprintForType(type.componentType, contextType, alreadySeen, hasher, factory, offset+4) + fingerprintForType(type.componentType, contextType, alreadySeen, hasher, factory, debugIndent+1) .putUnencodedChars(ARRAY_HASH) } else if (SerializerFactory.isPrimitive(type)) { hasher.putUnencodedChars(type.name) @@ -420,7 +420,7 @@ private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: Muta // to the CorDapp but maybe reference to the JAR in the short term. hasher.putUnencodedChars(type.name) } else { - fingerprintForObject(type, type, alreadySeen, hasher, factory, offset+4) + fingerprintForObject(type, type, alreadySeen, hasher, factory, debugIndent+1) } } } @@ -428,10 +428,7 @@ private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: Muta // Hash the element type + some array hash is GenericArrayType -> { fingerprintForType(type.genericComponentType, contextType, alreadySeen, - hasher, factory, offset+4).putUnencodedChars(ARRAY_HASH) - } - is WildcardType -> { - hasher.putUnencodedChars(type.typeName).putUnencodedChars(WILDCARD_TYPE_HASH) + hasher, factory, debugIndent+1).putUnencodedChars(ARRAY_HASH) } else -> throw NotSerializableException("Don't know how to hash") } @@ -452,17 +449,17 @@ private fun fingerprintForObject( alreadySeen: MutableSet, hasher: Hasher, factory: SerializerFactory, - offset: Int = 0): Hasher { + debugIndent: Int = 0): Hasher { // Hash the class + properties + interfaces val name = type.asClass()?.name ?: throw NotSerializableException("Expected only Class or ParameterizedType but found $type") propertiesForSerialization(constructorForDeserialization(type), contextType ?: type, factory) .serializationOrder .fold(hasher.putUnencodedChars(name)) { orig, prop -> - fingerprintForType(prop.getter.resolvedType, type, alreadySeen, orig, factory, offset+4) + fingerprintForType(prop.getter.resolvedType, type, alreadySeen, orig, factory, debugIndent+1) .putUnencodedChars(prop.getter.name) .putUnencodedChars(if (prop.getter.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH) } - interfacesForSerialization(type, factory).map { fingerprintForType(it, type, alreadySeen, hasher, factory, offset+4) } + interfacesForSerialization(type, factory).map { fingerprintForType(it, type, alreadySeen, hasher, factory, debugIndent+4) } return hasher } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt index 92aa09fe57..9dd0fb30cf 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt @@ -114,8 +114,8 @@ internal fun propertiesForSerializationFromConstructor( val returnType = resolveTypeVariables(getter.genericReturnType, type) if (!constructorParamTakesReturnTypeOfGetter(returnType, getter.genericReturnType, param.value)) { throw NotSerializableException( - "Property type $returnType for $name of $clazz differs from constructor parameter " - + "type ${param.value.type.javaType}") + "Property type '$returnType' for '$name' of '$clazz' differs from constructor parameter " + + "type '${param.value.type.javaType}'") } Pair(PublicPropertyReader(getter), returnType) @@ -166,9 +166,20 @@ private fun propertiesForSerializationFromSetters( } } -private fun constructorParamTakesReturnTypeOfGetter(getterReturnType: Type, rawGetterReturnType: Type, param: KParameter): Boolean { - val typeToken = TypeToken.of(param.type.javaType) - return typeToken.isSupertypeOf(getterReturnType) || typeToken.isSupertypeOf(rawGetterReturnType) +private fun constructorParamTakesReturnTypeOfGetter( + getterReturnType: Type, + rawGetterReturnType: Type, + param: KParameter): Boolean { + 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) } private fun propertiesForSerializationFromAbstract( diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt index ff20b8ab68..94b17f6efd 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt @@ -86,15 +86,15 @@ open class SerializationOutput(internal val serializerFactory: SerializerFactory data.putObject(transformsSchema) } - internal fun writeObjectOrNull(obj: Any?, data: Data, type: Type, offset: Int) { + internal fun writeObjectOrNull(obj: Any?, data: Data, type: Type, debugIndent: Int) { if (obj == null) { data.putNull() } else { - writeObject(obj, data, if (type == SerializerFactory.AnyType) obj.javaClass else type, offset) + writeObject(obj, data, if (type == SerializerFactory.AnyType) obj.javaClass else type, debugIndent) } } - internal fun writeObject(obj: Any, data: Data, type: Type, offset: Int = 0) { + internal fun writeObject(obj: Any, data: Data, type: Type, debugIndent: Int = 0) { val serializer = serializerFactory.get(obj.javaClass, type) if (serializer !in serializerHistory) { serializerHistory.add(serializer) @@ -103,7 +103,7 @@ open class SerializationOutput(internal val serializerFactory: SerializerFactory val retrievedRefCount = objectHistory[obj] if (retrievedRefCount == null) { - serializer.writeObject(obj, data, type, this, offset) + serializer.writeObject(obj, data, type, this, debugIndent) // Important to do it after serialization such that dependent object will have preceding reference numbers // assigned to them first as they will be first read from the stream on receiving end. // Skip for primitive types as they are too small and overhead of referencing them will be much higher than their content diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SingletonSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SingletonSerializer.kt index 7a25baf7cd..0c70a18935 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SingletonSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SingletonSerializer.kt @@ -22,7 +22,7 @@ class SingletonSerializer(override val type: Class<*>, val singleton: Any, facto output.writeTypeNotations(typeNotation) } - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, offset: Int) { + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) { data.withDescribed(typeNotation.descriptor) { data.putBoolean(false) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt index b2d3b2d598..1608a22e83 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt @@ -126,9 +126,9 @@ fun AMQPField.getTypeAsClass(classloader: ClassLoader) = typeStrToType[Pair(type "*" -> if (requires.isEmpty()) Any::class.java else classloader.loadClass(requires[0]) else -> { classloader.loadClass( - if (type.endsWith("")) { - type.substring(0, type.length-3) - } else type) + if (type.endsWith("?>")) { + type.substring(0, type.indexOf('<')) + } else type) } } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/GenericsTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/GenericsTests.kt index d12a5c4056..60830cda88 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/GenericsTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/GenericsTests.kt @@ -27,7 +27,7 @@ class TestAttachmentConstraint : AttachmentConstraint { class GenericsTests { companion object { - val VERBOSE = false + val VERBOSE = true @Suppress("UNUSED") var localPath = projectRootDir.toUri().resolve( @@ -365,7 +365,7 @@ class GenericsTests { } @Test - fun anotherTry() { + fun nestedGenericsWithBound() { open class BaseState(val a : Int) class DState(a: Int) : BaseState(a) data class LTransactionState constructor(val data: T) @@ -385,4 +385,120 @@ class GenericsTests { assertEquals(state.data.a, des1.obj.state.data.a) } + @Test + fun nestedMultiGenericsWithBound() { + open class BaseState(val a : Int) + class DState(a: Int) : BaseState(a) + class EState(a: Int, val msg: String) : BaseState(a) + + data class LTransactionState (val data: T1, val context: T2) + data class StateWrapper(val state: LTransactionState) + + val factory1 = testDefaultFactoryNoEvolution() + + val state = LTransactionState(DState(1020304), EState(5060708, msg = "thigns")) + val stateAndString = StateWrapper(state) + + val ser1 = TestSerializationOutput(VERBOSE, factory1).serializeAndReturnSchema(stateAndString) + + //val factory2 = testDefaultFactoryNoEvolution() + val factory2 = testDefaultFactory() + val des1 = DeserializationInput(factory2).deserializeAndReturnEnvelope(ser1.obj) + + assertEquals(state.data.a, des1.obj.state.data.a) + assertEquals(state.context.a, des1.obj.state.context.a) + } + + @Test + fun nestedMultiGenericsNoBound() { + open class BaseState(val a : Int) + class DState(a: Int) : BaseState(a) + class EState(a: Int, val msg: String) : BaseState(a) + + data class LTransactionState (val data: T1, val context: T2) + data class StateWrapper(val state: LTransactionState) + + val factory1 = testDefaultFactoryNoEvolution() + + val state = LTransactionState(DState(1020304), EState(5060708, msg = "things")) + val stateAndString = StateWrapper(state) + + val ser1 = TestSerializationOutput(VERBOSE, factory1).serializeAndReturnSchema(stateAndString) + + //val factory2 = testDefaultFactoryNoEvolution() + val factory2 = testDefaultFactory() + val des1 = DeserializationInput(factory2).deserializeAndReturnEnvelope(ser1.obj) + + assertEquals(state.data.a, des1.obj.state.data.a) + assertEquals(state.context.a, des1.obj.state.context.a) + assertEquals(state.context.msg, des1.obj.state.context.msg) + } + + @Test + fun baseClassInheritedButNotOverriden() { + val factory1 = testDefaultFactoryNoEvolution() + val factory2 = testDefaultFactory() + + open class BaseState(open val a : T1, open val b: T2) + class DState(a: T1, b: T2) : BaseState(a, b) + + val state = DState(100, "hello") + val ser1 = TestSerializationOutput(VERBOSE, factory1).serializeAndReturnSchema(state) + val des1 = DeserializationInput(factory2).deserializeAndReturnEnvelope(ser1.obj) + + assertEquals(state.a, des1.obj.a) + assertEquals(state.b, des1.obj.b) + + class DState2(a: T1, b: T2, val c: T3) : BaseState(a, b) + + val state2 = DState2(100, "hello", 100L) + val ser2 = TestSerializationOutput(VERBOSE, factory1).serializeAndReturnSchema(state2) + val des2 = DeserializationInput(factory2).deserializeAndReturnEnvelope(ser2.obj) + + assertEquals(state2.a, des2.obj.a) + assertEquals(state2.b, des2.obj.b) + assertEquals(state2.c, des2.obj.c) + } + + @Test + fun baseClassInheritedButNotOverridenBounded() { + val factory1 = testDefaultFactoryNoEvolution() + val factory2 = testDefaultFactory() + + open class Bound(val a: Int) + + open class BaseState(open val a: T1) + class DState(a: T1) : BaseState(a) + + val state = DState(Bound(100)) + val ser1 = TestSerializationOutput(VERBOSE, factory1).serializeAndReturnSchema(state) + val des1 = DeserializationInput(factory2).deserializeAndReturnEnvelope(ser1.obj) + + assertEquals(state.a.a, des1.obj.a.a) + } + + @Test + fun nestedMultiGenericsAtBottomWithBound() { + open class BaseState(val a : T1, val b: T2) + class DState(a: T1, b: T2) : BaseState(a, b) + class EState(a: T1, b: T2, val c: Long) : BaseState(a, b) + + data class LTransactionState, out T4: BaseState> (val data: T3, val context: T4) + data class StateWrapper, out T4: BaseState>(val state: LTransactionState) + + val factory1 = testDefaultFactoryNoEvolution() + + val state = LTransactionState(DState(1020304, "Hello"), EState(5060708, "thins", 100L)) + val stateAndString = StateWrapper(state) + + val ser1 = TestSerializationOutput(VERBOSE, factory1).serializeAndReturnSchema(stateAndString) + + //val factory2 = testDefaultFactoryNoEvolution() + val factory2 = testDefaultFactory() + val des1 = DeserializationInput(factory2).deserializeAndReturnEnvelope(ser1.obj) + + assertEquals(state.data.a, des1.obj.state.data.a) + assertEquals(state.context.a, des1.obj.state.context.a) + } + }