mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
CORDA-1672 - Enable better user-helpful error messages (#3445)
* CORDA-1672 - Enable better user-helpful error messages The issue is that the error reporting framework in the serializer is targeted at developers in the node. However, because we pass exceptions to users over RPC those error messages aren't always helpful. Keep an internal exception that tracks debug useful information and log that just before any exception escapes the framework and allow for specific user "problem mitigation" issues to be set. * wip * update remaining excepions
This commit is contained in:
parent
c8de5ce08d
commit
e871b83464
@ -1,7 +1,82 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import org.slf4j.Logger
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class SyntheticParameterException(val type: Type) : NotSerializableException("Type '${type.typeName} has synthetic "
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 <T> ifThrowsAppend(strToAppendFn: () -> String, block: () -> T): T {
|
||||
try {
|
||||
return block()
|
||||
} catch (th: Throwable) {
|
||||
if (th is AMQPNotSerializableException) {
|
||||
th.classHierarchy.add(strToAppendFn())
|
||||
} else {
|
||||
th.setMessage("${strToAppendFn()} -> ${th.message}")
|
||||
}
|
||||
throw th
|
||||
}
|
||||
}
|
||||
|
||||
class AMQPNoTypeNotSerializableException(
|
||||
msg: String,
|
||||
mitigation: String = msg
|
||||
) : AMQPNotSerializableException(Class::class.java, msg, mitigation, mutableListOf("Unknown"))
|
||||
|
||||
/**
|
||||
* The purpose of the [AMQPNotSerializableException] is to encapsulate internal serialization errors
|
||||
* within the serialization framework and to capture errors when building serializer objects
|
||||
* that will aid in debugging whilst also allowing "user helpful" information to be communicated
|
||||
* outward to end users.
|
||||
*
|
||||
* @property type the class that failed to serialize
|
||||
* @property msg the specific error
|
||||
* @property mitigation information useful to an end user.
|
||||
* @property classHierarchy represents the call hierarchy to the point of error. This is useful
|
||||
* when debugging a deeply nested class that fails to serialize as that classes position in
|
||||
* the class hierarchy is preserved, otherwise that information is lost. This list is automatically
|
||||
* updated by the [ifThrowsAppend] function used within this library.
|
||||
*/
|
||||
open class AMQPNotSerializableException(
|
||||
val type: Type,
|
||||
val msg: String,
|
||||
val mitigation: String = msg,
|
||||
val classHierarchy : MutableList<String> = mutableListOf(type.typeName)
|
||||
) : NotSerializableException(msg) {
|
||||
@Suppress("Unused")
|
||||
constructor(type: Type) : this (type, "type=${type.typeName} is not serializable")
|
||||
|
||||
@VisibleForTesting
|
||||
fun errorMessage(direction: String) : String {
|
||||
return "Serialization failed direction=\"$direction\", type=\"${type.typeName}\", " +
|
||||
"msg=\"$msg\", " +
|
||||
"ClassChain=\"${classHierarchy.asReversed().joinToString(" -> ")}\""
|
||||
}
|
||||
|
||||
fun log(direction: String, logger: Logger) {
|
||||
logger.error(errorMessage(direction))
|
||||
|
||||
// if debug is enabled print the stack, the exception we allow to escape
|
||||
// will be printed into the log anyway by the handling thread
|
||||
logger.debug("", cause)
|
||||
}
|
||||
}
|
||||
|
||||
class SyntheticParameterException(type: Type) : AMQPNotSerializableException(
|
||||
type,
|
||||
"Type '${type.typeName} has synthetic "
|
||||
+ "fields and is likely a nested inner class. This is not support by the Corda AMQP serialization framework")
|
@ -8,7 +8,6 @@ import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.trace
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
@ -90,11 +89,11 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory)
|
||||
): Any {
|
||||
if (obj is List<*>) {
|
||||
return obj.map { input.readObjectOrNull(it, schemas, elementType, context) }.toArrayOfType(elementType)
|
||||
} else throw NotSerializableException("Expected a List but found $obj")
|
||||
} else throw AMQPNotSerializableException(type, "Expected a List but found $obj")
|
||||
}
|
||||
|
||||
open fun <T> List<T>.toArrayOfType(type: Type): Any {
|
||||
val elementType = type.asClass() ?: throw NotSerializableException("Unexpected array element type $type")
|
||||
val elementType = type.asClass() ?: throw AMQPNotSerializableException(type, "Unexpected array element type $type")
|
||||
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]) }
|
||||
@ -106,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<Char>::class.java, factory) {
|
||||
override fun <T> List<T>.toArrayOfType(type: Type): Any {
|
||||
val elementType = type.asClass() ?: throw NotSerializableException("Unexpected array element type $type")
|
||||
val elementType = type.asClass() ?: throw AMQPNotSerializableException(type, "Unexpected array element type $type")
|
||||
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()) }
|
||||
@ -160,7 +159,11 @@ class PrimCharArraySerializer(factory: SerializerFactory) : PrimArraySerializer(
|
||||
}
|
||||
|
||||
override fun <T> List<T>.toArrayOfType(type: Type): Any {
|
||||
val elementType = type.asClass() ?: throw NotSerializableException("Unexpected array element type $type")
|
||||
val elementType = type.asClass() ?: throw AMQPNotSerializableException(
|
||||
type,
|
||||
"Unexpected array element type $type",
|
||||
"blob is corrupt")
|
||||
|
||||
val list = this
|
||||
return java.lang.reflect.Array.newInstance(elementType, this.size).apply {
|
||||
val array = this
|
||||
|
@ -6,7 +6,6 @@ import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
import java.util.*
|
||||
@ -35,7 +34,10 @@ class CollectionSerializer(private val declaredType: ParameterizedType, factory:
|
||||
))
|
||||
|
||||
private fun findConcreteType(clazz: Class<*>): (List<*>) -> Collection<*> {
|
||||
return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported collection type $clazz.")
|
||||
return supportedTypes[clazz] ?: throw AMQPNotSerializableException(
|
||||
clazz,
|
||||
"Unsupported collection type $clazz.",
|
||||
"Supported Collections are ${supportedTypes.keys.joinToString(",")}")
|
||||
}
|
||||
|
||||
fun deriveParameterizedType(declaredType: Type, declaredClass: Class<*>, actualClass: Class<*>?): ParameterizedType {
|
||||
@ -48,7 +50,10 @@ class CollectionSerializer(private val declaredType: ParameterizedType, factory:
|
||||
return deriveParametrizedType(declaredType, collectionClass)
|
||||
}
|
||||
|
||||
throw NotSerializableException("Cannot derive collection type for declaredType: '$declaredType', declaredClass: '$declaredClass', actualClass: '$actualClass'")
|
||||
throw AMQPNotSerializableException(
|
||||
declaredType,
|
||||
"Cannot derive collection type for declaredType: '$declaredType', " +
|
||||
"declaredClass: '$declaredClass', actualClass: '$actualClass'")
|
||||
}
|
||||
|
||||
private fun deriveParametrizedType(declaredType: Type, collectionClass: Class<out Collection<*>>): ParameterizedType =
|
||||
|
@ -7,7 +7,6 @@ import net.corda.core.serialization.SerializationCustomSerializer
|
||||
import net.corda.serialization.internal.amqp.SerializerFactory.Companion.nameForType
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.Type
|
||||
import kotlin.reflect.jvm.javaType
|
||||
import kotlin.reflect.jvm.jvmErasure
|
||||
@ -56,7 +55,9 @@ class CorDappCustomSerializer(
|
||||
|
||||
init {
|
||||
if (types.size != 2) {
|
||||
throw NotSerializableException("Unable to determine serializer parent types")
|
||||
throw AMQPNotSerializableException(
|
||||
CorDappCustomSerializer::class.java,
|
||||
"Unable to determine serializer parent types")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,25 +29,32 @@ data class ObjectAndEnvelope<out T>(val obj: T, val envelope: Envelope)
|
||||
* instances and threads.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
class DeserializationInput @JvmOverloads constructor(private val serializerFactory: SerializerFactory,
|
||||
private val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist) {
|
||||
class DeserializationInput @JvmOverloads constructor(
|
||||
private val serializerFactory: SerializerFactory,
|
||||
private val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist
|
||||
) {
|
||||
private val objectHistory: MutableList<Any> = mutableListOf()
|
||||
private val logger = loggerFor<DeserializationInput>()
|
||||
|
||||
companion object {
|
||||
@VisibleForTesting
|
||||
@Throws(NotSerializableException::class)
|
||||
fun <T> withDataBytes(byteSequence: ByteSequence, encodingWhitelist: EncodingWhitelist, task: (ByteBuffer) -> T): T {
|
||||
@Throws(AMQPNoTypeNotSerializableException::class)
|
||||
fun <T> withDataBytes(
|
||||
byteSequence: ByteSequence,
|
||||
encodingWhitelist: EncodingWhitelist,
|
||||
task: (ByteBuffer) -> T
|
||||
) : T {
|
||||
// Check that the lead bytes match expected header
|
||||
val amqpSequence = amqpMagic.consume(byteSequence)
|
||||
?: throw NotSerializableException("Serialization header does not match.")
|
||||
?: throw AMQPNoTypeNotSerializableException("Serialization header does not match.")
|
||||
var stream: InputStream = ByteBufferInputStream(amqpSequence)
|
||||
try {
|
||||
while (true) {
|
||||
when (SectionId.reader.readFrom(stream)) {
|
||||
SectionId.ENCODING -> {
|
||||
val encoding = CordaSerializationEncoding.reader.readFrom(stream)
|
||||
encodingWhitelist.acceptEncoding(encoding) || throw NotSerializableException(encodingNotPermittedFormat.format(encoding))
|
||||
encodingWhitelist.acceptEncoding(encoding) ||
|
||||
throw AMQPNoTypeNotSerializableException(encodingNotPermittedFormat.format(encoding))
|
||||
stream = encoding.wrap(stream)
|
||||
}
|
||||
SectionId.DATA_AND_STOP, SectionId.ALT_DATA_AND_STOP -> return task(stream.asByteBuffer())
|
||||
@ -58,29 +65,40 @@ class DeserializationInput @JvmOverloads constructor(private val serializerFacto
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
@Throws(AMQPNoTypeNotSerializableException::class)
|
||||
fun getEnvelope(byteSequence: ByteSequence, encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist): Envelope {
|
||||
return withDataBytes(byteSequence, encodingWhitelist) { dataBytes ->
|
||||
val data = Data.Factory.create()
|
||||
val expectedSize = dataBytes.remaining()
|
||||
if (data.decode(dataBytes) != expectedSize.toLong()) throw NotSerializableException("Unexpected size of data")
|
||||
if (data.decode(dataBytes) != expectedSize.toLong()) {
|
||||
throw AMQPNoTypeNotSerializableException(
|
||||
"Unexpected size of data",
|
||||
"Blob is corrupted!.")
|
||||
}
|
||||
Envelope.get(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
@Throws(AMQPNoTypeNotSerializableException::class)
|
||||
fun getEnvelope(byteSequence: ByteSequence) = getEnvelope(byteSequence, encodingWhitelist)
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
@Throws(
|
||||
AMQPNotSerializableException::class,
|
||||
AMQPNoTypeNotSerializableException::class)
|
||||
inline fun <reified T : Any> deserialize(bytes: SerializedBytes<T>, context: SerializationContext): T =
|
||||
deserialize(bytes, T::class.java, context)
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
@Throws(
|
||||
AMQPNotSerializableException::class,
|
||||
AMQPNoTypeNotSerializableException::class)
|
||||
private fun <R> des(generator: () -> R): R {
|
||||
try {
|
||||
return generator()
|
||||
} catch (amqp : AMQPNotSerializableException) {
|
||||
amqp.log("Deserialize", logger)
|
||||
throw NotSerializableException(amqp.mitigation)
|
||||
} catch (nse: NotSerializableException) {
|
||||
throw nse
|
||||
} catch (t: Throwable) {
|
||||
@ -133,12 +151,15 @@ class DeserializationInput @JvmOverloads constructor(private val serializerFacto
|
||||
// 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 " +
|
||||
throw AMQPNotSerializableException(
|
||||
type,
|
||||
"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(
|
||||
throw AMQPNotSerializableException(
|
||||
type,
|
||||
"Existing reference type mismatch. Expected: '$type', found: '${objectRetrieved::class.java}' " +
|
||||
"@ $objectIndex")
|
||||
}
|
||||
@ -150,8 +171,11 @@ class DeserializationInput @JvmOverloads constructor(private val serializerFacto
|
||||
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 " +
|
||||
}
|
||||
) {
|
||||
throw AMQPNotSerializableException(
|
||||
type,
|
||||
"Described type with descriptor ${obj.descriptor} was " +
|
||||
"expected to be of type $type but was ${serializer.type}")
|
||||
}
|
||||
serializer.readObject(obj.described, schemas, this, context)
|
||||
|
@ -13,13 +13,19 @@ import java.util.*
|
||||
* of the JDK implementation which we use as the textual format in the AMQP schema.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
class DeserializedParameterizedType(private val rawType: Class<*>, private val params: Array<out Type>, private val ownerType: Type? = null) : ParameterizedType {
|
||||
class DeserializedParameterizedType(
|
||||
private val rawType: Class<*>,
|
||||
private val params: Array<out Type>,
|
||||
private val ownerType: Type? = null
|
||||
) : ParameterizedType {
|
||||
init {
|
||||
if (params.isEmpty()) {
|
||||
throw NotSerializableException("Must be at least one parameter type in a ParameterizedType")
|
||||
throw AMQPNotSerializableException(rawType, "Must be at least one parameter type in a ParameterizedType")
|
||||
}
|
||||
if (params.size != rawType.typeParameters.size) {
|
||||
throw NotSerializableException("Expected ${rawType.typeParameters.size} for ${rawType.name} but found ${params.size}")
|
||||
throw AMQPNotSerializableException(
|
||||
rawType,
|
||||
"Expected ${rawType.typeParameters.size} for ${rawType.name} but found ${params.size}")
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,10 +48,11 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p
|
||||
val paramTypes = ArrayList<Type>()
|
||||
val pos = parseTypeList("$name>", paramTypes, cl)
|
||||
if (pos <= name.length) {
|
||||
throw NotSerializableException("Malformed string form of ParameterizedType. Unexpected '>' at character position $pos of $name.")
|
||||
throw AMQPNoTypeNotSerializableException(
|
||||
"Malformed string form of ParameterizedType. Unexpected '>' at character position $pos of $name.")
|
||||
}
|
||||
if (paramTypes.size != 1) {
|
||||
throw NotSerializableException("Expected only one type, but got $paramTypes")
|
||||
throw AMQPNoTypeNotSerializableException("Expected only one type, but got $paramTypes")
|
||||
}
|
||||
return paramTypes[0]
|
||||
}
|
||||
@ -70,7 +77,7 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p
|
||||
if (!typeName.isEmpty()) {
|
||||
types += makeType(typeName, cl)
|
||||
} else if (needAType) {
|
||||
throw NotSerializableException("Expected a type, not ','")
|
||||
throw AMQPNoTypeNotSerializableException("Expected a type, not ','")
|
||||
}
|
||||
typeStart = pos
|
||||
needAType = true
|
||||
@ -80,7 +87,7 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p
|
||||
if (!typeName.isEmpty()) {
|
||||
types += makeType(typeName, cl)
|
||||
} else if (needAType) {
|
||||
throw NotSerializableException("Expected a type, not '>'")
|
||||
throw AMQPNoTypeNotSerializableException("Expected a type, not '>'")
|
||||
}
|
||||
return pos
|
||||
} else {
|
||||
@ -90,11 +97,11 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p
|
||||
if (params[pos].isWhitespace()) {
|
||||
typeStart = ++pos
|
||||
} else if (!needAType) {
|
||||
throw NotSerializableException("Not expecting a type")
|
||||
throw AMQPNoTypeNotSerializableException("Not expecting a type")
|
||||
} else if (params[pos] == '?') {
|
||||
pos++
|
||||
} else if (!params[pos].isJavaIdentifierStart()) {
|
||||
throw NotSerializableException("Invalid character at start of type: ${params[pos]}")
|
||||
throw AMQPNoTypeNotSerializableException("Invalid character at start of type: ${params[pos]}")
|
||||
} else {
|
||||
pos++
|
||||
}
|
||||
@ -105,12 +112,13 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p
|
||||
} else if (!skippingWhitespace && (params[pos] == '.' || params[pos].isJavaIdentifierPart())) {
|
||||
pos++
|
||||
} else {
|
||||
throw NotSerializableException("Invalid character ${params[pos]} in middle of type $params at idx $pos")
|
||||
throw AMQPNoTypeNotSerializableException(
|
||||
"Invalid character ${params[pos]} in middle of type $params at idx $pos")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw NotSerializableException("Missing close generics '>'")
|
||||
throw AMQPNoTypeNotSerializableException("Missing close generics '>'")
|
||||
}
|
||||
|
||||
private fun makeType(typeName: String, cl: ClassLoader): Type {
|
||||
@ -124,9 +132,15 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p
|
||||
return DeserializedParameterizedType(makeType(rawTypeName, cl) as Class<*>, args.toTypedArray(), null)
|
||||
}
|
||||
|
||||
private fun parseTypeParams(params: String, startPos: Int, paramTypes: MutableList<Type>, cl: ClassLoader, depth: Int): Int {
|
||||
private fun parseTypeParams(
|
||||
params: String,
|
||||
startPos: Int,
|
||||
paramTypes: MutableList<Type>,
|
||||
cl: ClassLoader,
|
||||
depth: Int
|
||||
): Int {
|
||||
if (depth == MAX_DEPTH) {
|
||||
throw NotSerializableException("Maximum depth of nested generics reached: $depth")
|
||||
throw AMQPNoTypeNotSerializableException("Maximum depth of nested generics reached: $depth")
|
||||
}
|
||||
return startPos + parseTypeList(params.substring(startPos), paramTypes, cl, depth)
|
||||
}
|
||||
|
@ -112,7 +112,9 @@ class EnumEvolutionSerializer(
|
||||
.associateBy({ it.value.toInt() }, { conversions[it.name] }))
|
||||
|
||||
if (ordinals.filterNot { serialisedOrds[it.value] == it.key }.isNotEmpty()) {
|
||||
throw NotSerializableException("Constants have been reordered, additions must be appended to the end")
|
||||
throw AMQPNotSerializableException(
|
||||
new.type,
|
||||
"Constants have been reordered, additions must be appended to the end")
|
||||
}
|
||||
|
||||
return EnumEvolutionSerializer(new.type, factory, conversions, ordinals)
|
||||
@ -125,7 +127,7 @@ class EnumEvolutionSerializer(
|
||||
val enumName = (obj as List<*>)[0] as String
|
||||
|
||||
if (enumName !in conversions) {
|
||||
throw NotSerializableException("No rule to evolve enum constant $type::$enumName")
|
||||
throw AMQPNotSerializableException(type, "No rule to evolve enum constant $type::$enumName")
|
||||
}
|
||||
|
||||
return type.asClass()!!.enumConstants[ordinals[conversions[enumName]]!!]
|
||||
|
@ -37,8 +37,9 @@ class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: Seria
|
||||
val fromOrd = type.asClass()!!.enumConstants[enumOrd] as Enum<*>?
|
||||
|
||||
if (enumName != fromOrd?.name) {
|
||||
throw NotSerializableException("Deserializing obj as enum $type with value $enumName.$enumOrd but "
|
||||
+ "ordinality has changed")
|
||||
throw AMQPNotSerializableException(
|
||||
type,
|
||||
"Deserializing obj as enum $type with value $enumName.$enumOrd but ordinality has changed")
|
||||
}
|
||||
return fromOrd
|
||||
}
|
||||
@ -46,7 +47,7 @@ class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: Seria
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
if (obj !is Enum<*>) throw NotSerializableException("Serializing $obj as enum when it isn't")
|
||||
if (obj !is Enum<*>) throw AMQPNotSerializableException(type, "Serializing $obj as enum when it isn't")
|
||||
|
||||
data.withDescribed(typeNotation.descriptor) {
|
||||
withList {
|
||||
|
@ -31,7 +31,8 @@ data class Envelope(val obj: Any?, val schema: Schema, val transformsSchema: Tra
|
||||
fun get(data: Data): Envelope {
|
||||
val describedType = data.`object` as DescribedType
|
||||
if (describedType.descriptor != DESCRIPTOR) {
|
||||
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}, should be $DESCRIPTOR.")
|
||||
throw AMQPNoTypeNotSerializableException(
|
||||
"Unexpected descriptor ${describedType.descriptor}, should be $DESCRIPTOR.")
|
||||
}
|
||||
val list = describedType.described as List<*>
|
||||
|
||||
@ -40,7 +41,8 @@ data class Envelope(val obj: Any?, val schema: Schema, val transformsSchema: Tra
|
||||
val transformSchema: Any? = when (list.size) {
|
||||
ENVELOPE_WITHOUT_TRANSFORMS -> null
|
||||
ENVELOPE_WITH_TRANSFORMS -> list[TRANSFORMS_SCHEMA_IDX]
|
||||
else -> throw NotSerializableException("Malformed list, bad length of ${list.size} (should be 2 or 3)")
|
||||
else -> throw AMQPNoTypeNotSerializableException(
|
||||
"Malformed list, bad length of ${list.size} (should be 2 or 3)")
|
||||
}
|
||||
|
||||
return newInstance(listOf(list[BLOB_IDX], Schema.get(list[SCHEMA_IDX]!!),
|
||||
@ -57,7 +59,8 @@ data class Envelope(val obj: Any?, val schema: Schema, val transformsSchema: Tra
|
||||
val transformSchema = when (list.size) {
|
||||
ENVELOPE_WITHOUT_TRANSFORMS -> TransformsSchema.newInstance(null)
|
||||
ENVELOPE_WITH_TRANSFORMS -> list[TRANSFORMS_SCHEMA_IDX] as TransformsSchema
|
||||
else -> throw NotSerializableException("Malformed list, bad length of ${list.size} (should be 2 or 3)")
|
||||
else -> throw AMQPNoTypeNotSerializableException(
|
||||
"Malformed list, bad length of ${list.size} (should be 2 or 3)")
|
||||
}
|
||||
|
||||
return Envelope(list[BLOB_IDX], list[SCHEMA_IDX] as Schema, transformSchema)
|
||||
|
@ -32,8 +32,8 @@ abstract class EvolutionSerializer(
|
||||
clazz: Type,
|
||||
factory: SerializerFactory,
|
||||
protected val oldReaders: Map<String, OldParam>,
|
||||
override val kotlinConstructor: KFunction<Any>?) : ObjectSerializer(clazz, factory) {
|
||||
|
||||
override val kotlinConstructor: KFunction<Any>?
|
||||
) : ObjectSerializer(clazz, factory) {
|
||||
// explicitly set as empty to indicate it's unused by this type of serializer
|
||||
override val propertySerializers = PropertySerializersEvolution()
|
||||
|
||||
@ -137,7 +137,8 @@ abstract class EvolutionSerializer(
|
||||
if ((isKotlin && !it.value.type.isMarkedNullable)
|
||||
|| (!isKotlin && isJavaPrimitive(it.value.type.jvmErasure.java))
|
||||
) {
|
||||
throw NotSerializableException(
|
||||
throw AMQPNotSerializableException(
|
||||
new.type,
|
||||
"New parameter \"${it.value.name}\" is mandatory, should be nullable for evolution " +
|
||||
"to work, isKotlinClass=$isKotlin type=${it.value.type}")
|
||||
}
|
||||
@ -180,7 +181,7 @@ abstract class EvolutionSerializer(
|
||||
OldParam(-1, PropertySerializer.make(it.name, EvolutionPropertyReader(),
|
||||
it.getTypeAsClass(factory.classloader), factory))
|
||||
} catch (e: ClassNotFoundException) {
|
||||
throw NotSerializableException(e.message)
|
||||
throw AMQPNotSerializableException(new.type, e.message ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,11 +100,12 @@ class SerializerFingerPrinter : FingerPrinter {
|
||||
return if ((type in alreadySeen)
|
||||
&& (type !== SerializerFactory.AnyType)
|
||||
&& (type !is TypeVariable<*>)
|
||||
&& (type !is WildcardType)) {
|
||||
&& (type !is WildcardType)
|
||||
) {
|
||||
hasher.putUnencodedChars(ALREADY_SEEN_HASH)
|
||||
} else {
|
||||
alreadySeen += type
|
||||
try {
|
||||
ifThrowsAppend<Hasher>({ type.typeName }) {
|
||||
when (type) {
|
||||
is ParameterizedType -> {
|
||||
// Hash the rawType + params
|
||||
@ -158,8 +159,8 @@ class SerializerFingerPrinter : FingerPrinter {
|
||||
} 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.
|
||||
// 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)
|
||||
@ -172,12 +173,8 @@ class SerializerFingerPrinter : FingerPrinter {
|
||||
fingerprintForType(type.genericComponentType, contextType, alreadySeen,
|
||||
hasher, debugIndent + 1).putUnencodedChars(ARRAY_HASH)
|
||||
}
|
||||
else -> throw NotSerializableException("Don't know how to hash")
|
||||
else -> throw AMQPNotSerializableException(type, "Don't know how to hash")
|
||||
}
|
||||
} catch (e: NotSerializableException) {
|
||||
val msg = "${e.message} -> $type"
|
||||
logger.error(msg, e)
|
||||
throw NotSerializableException(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -191,7 +188,7 @@ class SerializerFingerPrinter : FingerPrinter {
|
||||
debugIndent: Int = 0): Hasher {
|
||||
// Hash the class + properties + interfaces
|
||||
val name = type.asClass()?.name
|
||||
?: throw NotSerializableException("Expected only Class or ParameterizedType but found $type")
|
||||
?: throw AMQPNotSerializableException(type, "Expected only Class or ParameterizedType but found $type")
|
||||
|
||||
propertiesForSerialization(constructorForDeserialization(type), contextType ?: type, factory)
|
||||
.serializationOrder
|
||||
|
@ -40,7 +40,7 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial
|
||||
))
|
||||
|
||||
private fun findConcreteType(clazz: Class<*>): MapCreationFunction {
|
||||
return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported map type $clazz.")
|
||||
return supportedTypes[clazz] ?: throw AMQPNotSerializableException(clazz, "Unsupported map type $clazz.")
|
||||
}
|
||||
|
||||
fun deriveParameterizedType(declaredType: Type, declaredClass: Class<*>, actualClass: Class<*>?): ParameterizedType {
|
||||
@ -54,7 +54,8 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial
|
||||
return deriveParametrizedType(declaredType, mapClass)
|
||||
}
|
||||
|
||||
throw NotSerializableException("Cannot derive map type for declaredType: '$declaredType', declaredClass: '$declaredClass', actualClass: '$actualClass'")
|
||||
throw AMQPNotSerializableException(declaredType,
|
||||
"Cannot derive map type for declaredType=\"$declaredType\", declaredClass=\"$declaredClass\", actualClass=\"$actualClass\"")
|
||||
}
|
||||
|
||||
private fun deriveParametrizedType(declaredType: Type, collectionClass: Class<out Map<*, *>>): ParameterizedType =
|
||||
@ -88,7 +89,8 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial
|
||||
type: Type,
|
||||
output: SerializationOutput,
|
||||
context: SerializationContext,
|
||||
debugIndent: Int) = ifThrowsAppend({ declaredType.typeName }) {
|
||||
debugIndent: Int
|
||||
) = ifThrowsAppend({ declaredType.typeName }) {
|
||||
obj.javaClass.checkSupportedMapType()
|
||||
// Write described
|
||||
data.withDescribed(typeNotation.descriptor) {
|
||||
|
@ -63,7 +63,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
|
||||
if (propertySerializers.size != javaConstructor?.parameterCount &&
|
||||
javaConstructor?.parameterCount ?: 0 > 0
|
||||
) {
|
||||
throw NotSerializableException("Serialization constructor for class $type expects "
|
||||
throw AMQPNotSerializableException(type, "Serialization constructor for class $type expects "
|
||||
+ "${javaConstructor?.parameterCount} parameters but we have ${propertySerializers.size} "
|
||||
+ "properties to serialize.")
|
||||
}
|
||||
@ -86,7 +86,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
|
||||
context: SerializationContext): Any = ifThrowsAppend({ clazz.typeName }) {
|
||||
if (obj is List<*>) {
|
||||
if (obj.size > propertySerializers.size) {
|
||||
throw NotSerializableException("Too many properties in described type $typeName")
|
||||
throw AMQPNotSerializableException(type, "Too many properties in described type $typeName")
|
||||
}
|
||||
|
||||
return if (propertySerializers.byConstructor) {
|
||||
@ -95,7 +95,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
|
||||
readObjectBuildViaSetters(obj, schemas, input, context)
|
||||
}
|
||||
} else {
|
||||
throw NotSerializableException("Body of described type is unexpected $obj")
|
||||
throw AMQPNotSerializableException(type, "Body of described type is unexpected $obj")
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,7 +120,8 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
|
||||
context: SerializationContext): Any = ifThrowsAppend({ clazz.typeName }) {
|
||||
logger.trace { "Calling setter based construction for ${clazz.typeName}" }
|
||||
|
||||
val instance: Any = javaConstructor?.newInstanceUnwrapped() ?: throw NotSerializableException(
|
||||
val instance: Any = javaConstructor?.newInstanceUnwrapped() ?: throw AMQPNotSerializableException(
|
||||
type,
|
||||
"Failed to instantiate instance of object $clazz")
|
||||
|
||||
// read the properties out of the serialised form, since we're invoking the setters the order we
|
||||
@ -149,13 +150,15 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
|
||||
logger.trace { "Calling constructor: '$javaConstructor' with properties '$properties'" }
|
||||
|
||||
if (properties.size != javaConstructor?.parameterCount) {
|
||||
throw NotSerializableException("Serialization constructor for class $type expects "
|
||||
throw AMQPNotSerializableException(type, "Serialization constructor for class $type expects "
|
||||
+ "${javaConstructor?.parameterCount} parameters but we have ${properties.size} "
|
||||
+ "serialized properties.")
|
||||
}
|
||||
|
||||
return javaConstructor?.newInstanceUnwrapped(*properties.toTypedArray())
|
||||
?: throw NotSerializableException("Attempt to deserialize an interface: $clazz. Serialized form is invalid.")
|
||||
?: throw AMQPNotSerializableException(
|
||||
type,
|
||||
"Attempt to deserialize an interface: $clazz. Serialized form is invalid.")
|
||||
}
|
||||
|
||||
private fun <T> Constructor<T>.newInstanceUnwrapped(vararg args: Any?): T {
|
||||
|
@ -191,7 +191,7 @@ abstract class PropertySerializers(
|
||||
is PropertyAccessorGetterSetter -> PropertySerializersSetter(serializationOrder)
|
||||
null -> PropertySerializersNoProperties()
|
||||
else -> {
|
||||
throw NotSerializableException("Unknown Property Accessor type, cannot create set")
|
||||
throw AMQPNoTypeNotSerializableException("Unknown Property Accessor type, cannot create set")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -220,7 +220,7 @@ data class RestrictedType(override val name: String,
|
||||
|
||||
fun get(describedType: DescribedType): RestrictedType {
|
||||
if (describedType.descriptor != DESCRIPTOR) {
|
||||
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
|
||||
throw AMQPNoTypeNotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
|
||||
}
|
||||
val list = describedType.described as List<*>
|
||||
return newInstance(listOf(list[0], list[1], list[2], list[3], Descriptor.get(list[4]!!), (list[5] as List<*>).map { Choice.get(it!!) }))
|
||||
@ -297,7 +297,7 @@ data class ReferencedObject(private val refCounter: Int) : DescribedType {
|
||||
fun get(obj: Any): ReferencedObject {
|
||||
val describedType = obj as DescribedType
|
||||
if (describedType.descriptor != DESCRIPTOR) {
|
||||
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
|
||||
throw AMQPNoTypeNotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
|
||||
}
|
||||
return newInstance(describedType.described)
|
||||
}
|
||||
|
@ -41,18 +41,24 @@ fun constructorForDeserialization(type: Type): KFunction<Any>? {
|
||||
for (kotlinConstructor in kotlinConstructors) {
|
||||
if (preferredCandidate == null && kotlinConstructors.size == 1) {
|
||||
preferredCandidate = kotlinConstructor
|
||||
} else if (preferredCandidate == null && kotlinConstructors.size == 2 && hasDefault && kotlinConstructor.parameters.isNotEmpty()) {
|
||||
} else if (preferredCandidate == null &&
|
||||
kotlinConstructors.size == 2 &&
|
||||
hasDefault &&
|
||||
kotlinConstructor.parameters.isNotEmpty()
|
||||
) {
|
||||
preferredCandidate = kotlinConstructor
|
||||
} else if (kotlinConstructor.findAnnotation<ConstructorForDeserialization>() != null) {
|
||||
if (annotatedCount++ > 0) {
|
||||
throw NotSerializableException("More than one constructor for $clazz is annotated with @ConstructorForDeserialization.")
|
||||
throw AMQPNotSerializableException(
|
||||
type,
|
||||
"More than one constructor for $clazz is annotated with @ConstructorForDeserialization.")
|
||||
}
|
||||
preferredCandidate = kotlinConstructor
|
||||
}
|
||||
}
|
||||
|
||||
return preferredCandidate?.apply { isAccessible = true }
|
||||
?: throw NotSerializableException("No constructor for deserialization found for $clazz.")
|
||||
?: throw AMQPNotSerializableException(type, "No constructor for deserialization found for $clazz.")
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
@ -249,13 +255,14 @@ internal fun <T : Any> propertiesForSerializationFromConstructor(
|
||||
// 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 NotSerializableException(
|
||||
?: 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 NotSerializableException(
|
||||
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 "
|
||||
@ -263,7 +270,8 @@ internal fun <T : Any> propertiesForSerializationFromConstructor(
|
||||
|
||||
val returnType = resolveTypeVariables(getter.genericReturnType, type)
|
||||
if (!constructorParamTakesReturnTypeOfGetter(returnType, getter.genericReturnType, param.value)) {
|
||||
throw NotSerializableException(
|
||||
throw AMQPNotSerializableException(
|
||||
type,
|
||||
"Property - \"$name\" - has type \"$returnType\" on \"$clazz\" but differs from constructor " +
|
||||
"parameter type \"${param.value.type.javaType}\"")
|
||||
}
|
||||
@ -271,7 +279,8 @@ internal fun <T : Any> propertiesForSerializationFromConstructor(
|
||||
Pair(PublicPropertyReader(getter), returnType)
|
||||
} else {
|
||||
val field = classProperties[name]!!.field
|
||||
?: throw NotSerializableException("No property matching constructor parameter named - \"$name\" - " +
|
||||
?: 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")
|
||||
@ -304,22 +313,28 @@ fun propertiesForSerializationFromSetters(
|
||||
if (getter == null || setter == null) return@forEach
|
||||
|
||||
if (setter.parameterCount != 1) {
|
||||
throw NotSerializableException("Defined setter for parameter ${property.value.field?.name} " +
|
||||
"takes too many arguments")
|
||||
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 NotSerializableException("Defined setter for parameter ${property.value.field?.name} " +
|
||||
(!(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 NotSerializableException("Defined setter for parameter ${property.value.field?.name} " +
|
||||
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}]")
|
||||
}
|
||||
@ -436,7 +451,9 @@ fun resolveTypeVariables(actualType: Type, contextType: Type?): Type {
|
||||
SerializerFactory.AnyType
|
||||
} else if (bounds.size == 1) {
|
||||
resolveTypeVariables(bounds[0], contextType)
|
||||
} else throw NotSerializableException("Got bounded type $actualType but only support single bound.")
|
||||
} else throw AMQPNotSerializableException(
|
||||
actualType,
|
||||
"Got bounded type $actualType but only support single bound.")
|
||||
} else {
|
||||
resolvedType
|
||||
}
|
||||
@ -478,7 +495,7 @@ internal fun Type.asParameterizedType(): ParameterizedType {
|
||||
return when (this) {
|
||||
is Class<*> -> this.asParameterizedType()
|
||||
is ParameterizedType -> this
|
||||
else -> throw NotSerializableException("Don't know how to convert to ParameterizedType")
|
||||
else -> throw AMQPNotSerializableException(this, "Don't know how to convert to ParameterizedType")
|
||||
}
|
||||
}
|
||||
|
||||
@ -499,32 +516,13 @@ internal enum class CommonPropertyNames {
|
||||
IncludeInternalInfo,
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 <T> 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)
|
||||
}
|
||||
|
||||
fun ClassWhitelist.requireWhitelisted(type: Type) {
|
||||
if (!this.isWhitelisted(type.asClass()!!)) {
|
||||
throw NotSerializableException("Class \"$type\" is not on the whitelist or annotated with @CordaSerializable.")
|
||||
throw AMQPNotSerializableException(
|
||||
type,
|
||||
"Class \"$type\" is not on the whitelist or annotated with @CordaSerializable.")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationEncoding
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.serialization.internal.CordaSerializationEncoding
|
||||
import net.corda.serialization.internal.SectionId
|
||||
import net.corda.serialization.internal.byteArrayOutput
|
||||
@ -31,6 +32,10 @@ open class SerializationOutput @JvmOverloads constructor(
|
||||
internal val serializerFactory: SerializerFactory,
|
||||
private val encoding: SerializationEncoding? = null
|
||||
) {
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
private val objectHistory: MutableMap<Any, Int> = IdentityHashMap()
|
||||
private val serializerHistory: MutableSet<AMQPSerializer<*>> = LinkedHashSet()
|
||||
internal val schemaHistory: MutableSet<TypeNotation> = LinkedHashSet()
|
||||
@ -44,11 +49,16 @@ open class SerializationOutput @JvmOverloads constructor(
|
||||
fun <T : Any> serialize(obj: T, context: SerializationContext): SerializedBytes<T> {
|
||||
try {
|
||||
return _serialize(obj, context)
|
||||
} catch (amqp: AMQPNotSerializableException) {
|
||||
amqp.log("Serialize", logger)
|
||||
throw NotSerializableException(amqp.mitigation)
|
||||
} finally {
|
||||
andFinally()
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: No need to handle AMQPNotSerializableExceptions here as this is an internal
|
||||
// only / testing function and it doesn't matter if they escape
|
||||
@Throws(NotSerializableException::class)
|
||||
fun <T : Any> serializeAndReturnSchema(obj: T, context: SerializationContext): BytesAndSchemas<T> {
|
||||
try {
|
||||
|
@ -118,7 +118,8 @@ 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 NotSerializableException(
|
||||
val declaredClass = declaredType.asClass() ?: throw AMQPNotSerializableException(
|
||||
declaredType,
|
||||
"Declared types of $declaredType are not supported.")
|
||||
|
||||
val actualType: Type = inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType
|
||||
@ -205,9 +206,12 @@ open class SerializerFactory(
|
||||
val endType = DeserializedParameterizedType(actualClass, actualClass.typeParameters)
|
||||
val resolvedType = resolver.resolveType(endType)
|
||||
resolvedType
|
||||
} else throw NotSerializableException("No inheritance path between actual $actualClass and declared $declaredType.")
|
||||
} else throw AMQPNotSerializableException(declaredType,
|
||||
"No inheritance path between actual $actualClass and declared $declaredType.")
|
||||
} else actualClass
|
||||
} else throw NotSerializableException("Found object of type $actualClass in a property expecting $declaredType")
|
||||
} 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.
|
||||
@ -317,7 +321,7 @@ open class SerializerFactory(
|
||||
|
||||
// prevent carpenter exceptions escaping into the world, convert things into a nice
|
||||
// NotSerializableException for when this escapes over the wire
|
||||
throw NotSerializableException(e.name)
|
||||
NotSerializableException(e.name)
|
||||
}
|
||||
processSchema(schemaAndDescriptor, true)
|
||||
}
|
||||
@ -342,7 +346,9 @@ open class SerializerFactory(
|
||||
private fun processCompositeType(typeNotation: CompositeType): AMQPSerializer<Any> {
|
||||
// TODO: class loader logic, and compare the schema.
|
||||
val type = typeForName(typeNotation.name, classloader)
|
||||
return get(type.asClass() ?: throw NotSerializableException("Unable to build composite type for $type"), type)
|
||||
return get(
|
||||
type.asClass() ?: throw AMQPNotSerializableException(type, "Unable to build composite type for $type"),
|
||||
type)
|
||||
}
|
||||
|
||||
private fun makeClassSerializer(
|
||||
@ -354,13 +360,15 @@ open class SerializerFactory(
|
||||
if (clazz.isSynthetic) {
|
||||
// Explicitly ban synthetic classes, we have no way of recreating them when deserializing. This also
|
||||
// captures Lambda expressions and other anonymous functions
|
||||
throw NotSerializableException(type.typeName)
|
||||
throw AMQPNotSerializableException(
|
||||
type,
|
||||
"Serializer does not support synthetic classes")
|
||||
} else if (isPrimitive(clazz)) {
|
||||
AMQPPrimitiveSerializer(clazz)
|
||||
} else {
|
||||
findCustomSerializer(clazz, declaredType) ?: run {
|
||||
if (onlyCustomSerializers) {
|
||||
throw NotSerializableException("Only allowing custom serializers")
|
||||
throw AMQPNotSerializableException(type, "Only allowing custom serializers")
|
||||
}
|
||||
if (type.isArray()) {
|
||||
// Don't need to check the whitelist since each element will come back through the whitelisting process.
|
||||
@ -385,7 +393,6 @@ open class SerializerFactory(
|
||||
}
|
||||
|
||||
private fun doFindCustomSerializer(key: CustomSerializersCacheKey): AMQPSerializer<Any>? {
|
||||
|
||||
val (clazz, declaredType) = key
|
||||
|
||||
// e.g. Imagine if we provided a Map serializer this way, then it won't work if the declared type is
|
||||
@ -471,7 +478,7 @@ open class SerializerFactory(
|
||||
is GenericArrayType -> "${nameForType(type.genericComponentType)}[]"
|
||||
is WildcardType -> "?"
|
||||
is TypeVariable<*> -> "?"
|
||||
else -> throw NotSerializableException("Unable to render type $type to a string.")
|
||||
else -> throw AMQPNotSerializableException(type, "Unable to render type $type to a string.")
|
||||
}
|
||||
|
||||
private fun typeForName(name: String, classloader: ClassLoader): Type {
|
||||
@ -482,7 +489,7 @@ open class SerializerFactory(
|
||||
} else if (elementType is Class<*>) {
|
||||
java.lang.reflect.Array.newInstance(elementType, 0).javaClass
|
||||
} else {
|
||||
throw NotSerializableException("Not able to deserialize array type: $name")
|
||||
throw AMQPNoTypeNotSerializableException("Not able to deserialize array type: $name")
|
||||
}
|
||||
} else if (name.endsWith("[p]")) {
|
||||
// There is no need to handle the ByteArray case as that type is coercible automatically
|
||||
@ -496,7 +503,7 @@ open class SerializerFactory(
|
||||
"double[p]" -> DoubleArray::class.java
|
||||
"short[p]" -> ShortArray::class.java
|
||||
"long[p]" -> LongArray::class.java
|
||||
else -> throw NotSerializableException("Not able to deserialize array type: $name")
|
||||
else -> throw AMQPNoTypeNotSerializableException("Not able to deserialize array type: $name")
|
||||
}
|
||||
} else {
|
||||
DeserializedParameterizedType.make(name, classloader)
|
||||
|
@ -31,7 +31,7 @@ abstract class Transform : DescribedType {
|
||||
val describedType = obj as DescribedType
|
||||
|
||||
if (describedType.descriptor != DESCRIPTOR) {
|
||||
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
|
||||
throw AMQPNoTypeNotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
|
||||
}
|
||||
|
||||
return describedType.described
|
||||
@ -221,7 +221,8 @@ data class TransformsSchema(val types: Map<String, EnumMap<TransformTypes, Mutab
|
||||
// ignore them it feels like a good thing to alert the user to since this is
|
||||
// more than likely a typo in their code so best make it an actual error
|
||||
if (transforms.computeIfAbsent(transform.enum) { mutableListOf() }.any { t == it }) {
|
||||
throw NotSerializableException(
|
||||
throw AMQPNotSerializableException(
|
||||
clazz,
|
||||
"Repeated unique transformation annotation of type ${t.name}")
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,9 @@
|
||||
package net.corda.serialization.internal.amqp.custom
|
||||
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.serialization.internal.amqp.AMQPNotSerializableException
|
||||
import net.corda.serialization.internal.amqp.CustomSerializer
|
||||
import net.corda.serialization.internal.amqp.SerializerFactory
|
||||
import net.corda.serialization.internal.amqp.custom.ClassSerializer.ClassProxy
|
||||
@ -8,10 +11,36 @@ import net.corda.serialization.internal.amqp.custom.ClassSerializer.ClassProxy
|
||||
/**
|
||||
* A serializer for [Class] that uses [ClassProxy] proxy object to write out
|
||||
*/
|
||||
class ClassSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<Class<*>, ClassSerializer.ClassProxy>(Class::class.java, ClassProxy::class.java, factory) {
|
||||
override fun toProxy(obj: Class<*>): ClassProxy = ClassProxy(obj.name)
|
||||
class ClassSerializer(
|
||||
factory: SerializerFactory
|
||||
) : CustomSerializer.Proxy<Class<*>, ClassSerializer.ClassProxy>(
|
||||
Class::class.java,
|
||||
ClassProxy::class.java,
|
||||
factory
|
||||
) {
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
override fun fromProxy(proxy: ClassProxy): Class<*> = Class.forName(proxy.className, true, factory.classloader)
|
||||
override fun toProxy(obj: Class<*>): ClassProxy {
|
||||
logger.trace { "serializer=custom, type=ClassSerializer, name=\"${obj.name}\", action=toProxy" }
|
||||
return ClassProxy(obj.name)
|
||||
}
|
||||
|
||||
override fun fromProxy(proxy: ClassProxy): Class<*> {
|
||||
logger.trace { "serializer=custom, type=ClassSerializer, name=\"${proxy.className}\", action=fromProxy" }
|
||||
|
||||
return try {
|
||||
Class.forName(proxy.className, true, factory.classloader)
|
||||
} catch (e: ClassNotFoundException) {
|
||||
throw AMQPNotSerializableException(
|
||||
type,
|
||||
"Could not instantiate ${proxy.className} - not on the classpath",
|
||||
"${proxy.className} was not found by the node, check the Node containing the CorDapp that " +
|
||||
"implements ${proxy.className} is loaded and on the Classpath",
|
||||
mutableListOf(proxy.className))
|
||||
}
|
||||
}
|
||||
|
||||
@KeepForDJVM
|
||||
data class ClassProxy(val className: String)
|
||||
|
@ -4,6 +4,7 @@ import com.google.common.collect.Maps;
|
||||
import net.corda.core.serialization.SerializationContext;
|
||||
import net.corda.core.serialization.SerializationFactory;
|
||||
import net.corda.core.serialization.SerializedBytes;
|
||||
import net.corda.serialization.internal.amqp.AMQPNotSerializableException;
|
||||
import net.corda.serialization.internal.amqp.SchemaKt;
|
||||
import net.corda.testing.core.SerializationEnvironmentRule;
|
||||
import org.junit.Before;
|
||||
@ -43,7 +44,7 @@ public final class ForbiddenLambdaSerializationTests {
|
||||
assertThat(throwable)
|
||||
.isNotNull()
|
||||
.isInstanceOf(NotSerializableException.class)
|
||||
.hasMessageContaining(getClass().getName());
|
||||
.hasMessageContaining("Serializer does not support synthetic classes");
|
||||
});
|
||||
}
|
||||
|
||||
@ -61,7 +62,7 @@ public final class ForbiddenLambdaSerializationTests {
|
||||
assertThat(throwable)
|
||||
.isNotNull()
|
||||
.isInstanceOf(NotSerializableException.class)
|
||||
.hasMessageContaining(getClass().getName());
|
||||
.hasMessageContaining("Serializer does not support synthetic classes");
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,130 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.Test
|
||||
import java.io.NotSerializableException
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class AMQPExceptionsTests {
|
||||
|
||||
interface Runner {
|
||||
fun run()
|
||||
}
|
||||
|
||||
data class A<T : Runner>(val a: T, val throws: Boolean = false) : Runner {
|
||||
override fun run() = ifThrowsAppend({ javaClass.name }) {
|
||||
if (throws) {
|
||||
throw AMQPNotSerializableException(A::class.java, "it went bang!")
|
||||
} else {
|
||||
a.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class B<T : Runner>(val b: T, val throws: Boolean = false) : Runner {
|
||||
override fun run() = ifThrowsAppend({ javaClass.name }) {
|
||||
if (throws) {
|
||||
throw NotSerializableException(javaClass.name)
|
||||
} else {
|
||||
b.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class C<T : Runner>(val c: T, val throws: Boolean = false) : Runner {
|
||||
override fun run() = ifThrowsAppend({ javaClass.name }) {
|
||||
if (throws) {
|
||||
throw NotSerializableException(javaClass.name)
|
||||
} else {
|
||||
c.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class END(val throws: Boolean = false) : Runner {
|
||||
override fun run() = ifThrowsAppend({ "END" }) {
|
||||
if (throws) {
|
||||
throw NotSerializableException(javaClass.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ENDAMQP(val throws: Boolean = false) : Runner {
|
||||
override fun run() {
|
||||
if (throws) {
|
||||
throw AMQPNotSerializableException(javaClass, "End it all")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val aName get() = A::class.java.name
|
||||
private val bName get() = B::class.java.name
|
||||
private val cName get() = C::class.java.name
|
||||
private val eName get() = END::class.java.name
|
||||
private val eaName get() = ENDAMQP::class.java.name
|
||||
|
||||
// if the exception is a normal not serializable exception we'll have manipulated the
|
||||
// message
|
||||
@Test
|
||||
fun catchNotSerializable() {
|
||||
fun catchAssert(msg: String, f: () -> Unit) {
|
||||
Assertions.assertThatThrownBy { f() }
|
||||
.isInstanceOf(NotSerializableException::class.java)
|
||||
.hasMessageContaining(msg)
|
||||
}
|
||||
|
||||
catchAssert("$aName -> END") {
|
||||
A(END(true)).run()
|
||||
}
|
||||
|
||||
catchAssert("$aName -> $aName -> END") {
|
||||
A(A(END(true))).run()
|
||||
}
|
||||
|
||||
catchAssert("$aName -> $bName -> END") {
|
||||
A(B(END(true))).run()
|
||||
}
|
||||
|
||||
catchAssert("$aName -> $bName -> $cName -> END") {
|
||||
A(B(C(END(true)))).run()
|
||||
}
|
||||
}
|
||||
|
||||
// However, if its a shiny new AMQPNotSerializable one, we have cool new toys, so
|
||||
// lets make sure those are set
|
||||
@Test
|
||||
fun catchAMQPNotSerializable() {
|
||||
fun catchAssert(stack: List<String>, f: () -> Unit): AMQPNotSerializableException {
|
||||
try {
|
||||
f()
|
||||
} catch (e: AMQPNotSerializableException) {
|
||||
assertEquals(stack, e.classHierarchy)
|
||||
return e
|
||||
}
|
||||
|
||||
throw Exception("FAILED")
|
||||
}
|
||||
|
||||
|
||||
catchAssert(listOf(ENDAMQP::class.java.name, aName)) {
|
||||
A(ENDAMQP(true)).run()
|
||||
}.apply {
|
||||
assertEquals(
|
||||
"Serialization failed direction=\"up\", type=\"$eaName\", msg=\"End it all\", " +
|
||||
"ClassChain=\"$aName -> $eaName\"",
|
||||
errorMessage("up"))
|
||||
}
|
||||
|
||||
catchAssert(listOf(ENDAMQP::class.java.name, aName, aName)) {
|
||||
A(A(ENDAMQP(true))).run()
|
||||
}
|
||||
|
||||
catchAssert(listOf(ENDAMQP::class.java.name, bName, aName)) {
|
||||
A(B(ENDAMQP(true))).run()
|
||||
}
|
||||
|
||||
catchAssert(listOf(ENDAMQP::class.java.name, cName, bName, aName)) {
|
||||
A(B(C(ENDAMQP(true)))).run()
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,6 @@ import net.corda.serialization.internal.amqp.testutils.*
|
||||
import net.corda.testing.common.internal.ProjectStructure.projectRootDir
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.assertj.core.api.Condition
|
||||
import org.junit.Test
|
||||
import java.io.NotSerializableException
|
||||
import java.net.URI
|
||||
|
@ -6,7 +6,6 @@ import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.serialization.internal.amqp.testutils.TestSerializationOutput
|
||||
import net.corda.serialization.internal.amqp.testutils.deserialize
|
||||
import net.corda.serialization.internal.amqp.testutils.deserializeAndReturnEnvelope
|
||||
import net.corda.serialization.internal.amqp.testutils.serialize
|
||||
import net.corda.serialization.internal.amqp.testutils.serializeAndReturnSchema
|
||||
import net.corda.serialization.internal.amqp.testutils.testDefaultFactoryNoEvolution
|
||||
import net.corda.serialization.internal.amqp.testutils.testName
|
||||
|
@ -123,7 +123,7 @@ class PrivatePropertyTests {
|
||||
SerializationOutput(factory).serialize(c1)
|
||||
}.isInstanceOf(NotSerializableException::class.java).hasMessageContaining(
|
||||
"Defined setter for parameter a takes parameter of type class java.lang.String " +
|
||||
"yet underlying type is int ")
|
||||
"yet underlying type is int")
|
||||
}
|
||||
|
||||
@Test
|
||||
|
Loading…
Reference in New Issue
Block a user