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 667b75e818..615cea26af 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 @@ -61,13 +61,13 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "list", Descriptor(typeDescriptor), emptyList()) - override fun writeClassInfo(output: SerializationOutput) { + override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({declaredType.typeName}) { if (output.writeTypeNotations(typeNotation)) { output.requireSerializer(declaredType.actualTypeArguments[0]) } } - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) { + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({declaredType.typeName}) { // Write described data.withDescribed(typeNotation.descriptor) { withList { @@ -78,8 +78,8 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali } } - override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any { + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({declaredType.typeName}) { // TODO: Can we verify the entries in the list? - return concreteBuilder((obj as List<*>).map { input.readObjectOrNull(it, schema, declaredType.actualTypeArguments[0]) }) + concreteBuilder((obj as List<*>).map { input.readObjectOrNull(it, schema, declaredType.actualTypeArguments[0]) }) } } \ No newline at end of file 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 0694d4abfa..b343e96469 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 @@ -68,14 +68,14 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "map", Descriptor(typeDescriptor), emptyList()) - override fun writeClassInfo(output: SerializationOutput) { + override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({declaredType.typeName}) { if (output.writeTypeNotations(typeNotation)) { output.requireSerializer(declaredType.actualTypeArguments[0]) output.requireSerializer(declaredType.actualTypeArguments[1]) } } - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) { + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({declaredType.typeName}) { obj.javaClass.checkSupportedMapType() // Write described data.withDescribed(typeNotation.descriptor) { @@ -90,10 +90,10 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial } } - override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any { + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({declaredType.typeName}) { // TODO: General generics question. Do we need to validate that entries in Maps and Collections match the generic type? Is it a security hole? val entries: Iterable> = (obj as Map<*, *>).map { readEntry(schema, input, it) } - return concreteBuilder(entries.toMap()) + concreteBuilder(entries.toMap()) } private fun readEntry(schema: Schema, input: DeserializationInput, entry: Map.Entry) = 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 7e277952c4..c483e8e126 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 @@ -41,7 +41,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS } } - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) { + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({clazz.typeName}) { // Write described data.withDescribed(typeNotation.descriptor) { // Write list @@ -53,11 +53,11 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS } } - override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any { + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({clazz.typeName}) { if (obj is List<*>) { if (obj.size > propertySerializers.size) throw NotSerializableException("Too many properties in described type $typeName") val params = obj.zip(propertySerializers).map { it.second.readProperty(it.first, schema, input) } - return construct(params) + construct(params) } else throw NotSerializableException("Body of described type is unexpected $obj") } @@ -74,5 +74,4 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS return javaConstructor?.newInstance(*properties.toTypedArray()) ?: throw NotSerializableException("Attempt to deserialize an interface: $clazz. Serialized form is invalid.") } - } \ No newline at end of file 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 cc5d07f095..2decd00d37 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 @@ -76,19 +76,21 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r // This is lazy so we don't get an infinite loop when a method returns an instance of the class. private val typeSerializer: AMQPSerializer<*> by lazy { lazyTypeSerializer() } - override fun writeClassInfo(output: SerializationOutput) { + override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({nameForDebug}) { if (resolvedType != Any::class.java) { typeSerializer.writeClassInfo(output) } } - override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? { - return input.readObjectOrNull(obj, schema, resolvedType) + override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? = ifThrowsAppend({nameForDebug}) { + input.readObjectOrNull(obj, schema, resolvedType) } - override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) { + override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) = ifThrowsAppend({nameForDebug}) { output.writeObjectOrNull(readMethod!!.invoke(obj), data, resolvedType) } + + private val nameForDebug = "$name(${resolvedType.typeName})" } /** 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 20259a1eed..2cc91ecb88 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 @@ -237,4 +237,27 @@ internal fun suitableForObjectReference(type: Type): Boolean { */ internal enum class CommonPropertyNames { IncludeInternalInfo, -} \ No newline at end of file +} + +/** + * Utility function which helps tracking the path in the object graph when exceptions are thrown. + * Since there might be a chain of nested calls it is useful to record which part of the graph caused an issue. + * Path information is added to the message of the exception being thrown. + */ +internal inline fun ifThrowsAppend(strToAppendFn: () -> String, block: () -> T): T { + try { + return block() + } catch (th: Throwable) { + th.setMessage("${strToAppendFn()} -> ${th.message}") + throw th + } +} + +/** + * Not a public property so will have to use reflection + */ +private fun Throwable.setMessage(newMsg: String) { + val detailMessageField = Throwable::class.java.getDeclaredField("detailMessage") + detailMessageField.isAccessible = true + detailMessageField.set(this, newMsg) +} diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt index 24bf16dc8a..6f97a39892 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt @@ -7,12 +7,12 @@ import net.corda.core.serialization.serialize import net.corda.node.services.statemachine.SessionData import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.amqpSpecific +import net.corda.testing.kryoSpecific import org.assertj.core.api.Assertions import org.junit.Assert.assertArrayEquals import org.junit.Test import org.bouncycastle.asn1.x500.X500Name import java.io.ByteArrayOutputStream -import java.io.NotSerializableException import java.util.* class MapsSerializationTest : TestDependencyInjectionBase() { @@ -45,7 +45,8 @@ class MapsSerializationTest : TestDependencyInjectionBase() { val payload = HashMap(smallMap) val wrongPayloadType = WrongPayloadType(payload) Assertions.assertThatThrownBy { wrongPayloadType.serialize() } - .isInstanceOf(NotSerializableException::class.java).hasMessageContaining("Cannot derive map type for declaredType") + .isInstanceOf(IllegalArgumentException::class.java).hasMessageContaining( + "Map type class java.util.HashMap is unstable under iteration. Suggested fix: use java.util.LinkedHashMap instead.") } @CordaSerializable @@ -63,7 +64,7 @@ class MapsSerializationTest : TestDependencyInjectionBase() { } @Test - fun `check empty map serialises as Java emptytMap`() { + fun `check empty map serialises as Java emptyMap`() = kryoSpecific("Specifically checks Kryo serialization") { val nameID = 0 val serializedForm = emptyMap().serialize() val output = ByteArrayOutputStream().apply { @@ -75,4 +76,4 @@ class MapsSerializationTest : TestDependencyInjectionBase() { } assertArrayEquals(output.toByteArray(), serializedForm.bytes) } -} +} \ No newline at end of file