mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
CORDA-2099 serialisation rewrite (#4257)
* Type model first draft * Introduce TypeIdentifier * Attempting to retrofit fingerprinter with type model * Complete retrofitting typemodel to fingerprinter * Ensure component types are resolved correctly * Fixes and tests * Move resolveAgainst to TypeIdentifier * Remote type modelling and reflection * Convert TypeIdentifiers back into types * Translate AMQP type strings to type identifiers * Start replacing DeserializedParameterizedType * Start roundtripping types through AMQP serialization * Comments on type modelling fingerprinter * kdocs and interface reorganisation * Lots and lots of kdocs, bugfix for cyclic references * Separate SerializerFactory construction from concrete implementation * Fewer build methods * Method naming that doesn't fatally confuse determinisation * Extract SerializerFactory interface * Reset to master's version of compiler.xml * Un-ignore flickering test * Enums don't have superclasses * Break out custom serializer registry * Refactor to separate remote and local serializer factories * Shrink interfaces * Further interface narrowing * Fingerprinting local type model * LocalSerializerFactory uses LocalTypeInformation * Resolve wildcards to their upper bounds * Actually cache custom serializers * Fix various bugs * Remove print statements * There are no cycles in type identifiers * Drive class carpentry from RemoteTypeInformation * Refactor and comment * Comments * Comments and pretty-printer extraction * Format long methods with braces * Serialise composable types using LocalTypeInformation * Warnings if a type is non-composable * Rename lookup -> findOrBuild * Evolution serialisation (no enums yet) * Eliminate old ObjectSerializer and evolver * Enum evolution * Opacity claims types less greedily * Fix type notation and type erasure bug * Clean up unused code paths * Delete unused codepaths * Move whitelist based type model configuration to own file * Move opaque type list * Make all evolution serialisers in one go when schema received * Minor tweaks * Commenting and tidying * Comments * Rebase against master * Make flag for controlling evolution behaviour visible * propertiesOrEmptyMap * Restore error messages * Test for CORDA-4107 * PR fixes * Patch cycles in remote type information after creation * Fix line breaks in unit test on Windows * This time for sure * EvolutionSerializerFactoryTests * Fix some pretty-printing issues, and a carpenter bug * PR fixes * Clarify evolution constructor ordering * Remote TODO comment, which has been moved to a JIRA story
This commit is contained in:
parent
98a495fa84
commit
2c6bce3e5d
@ -3,7 +3,7 @@
|
||||
package net.corda.client.jackson.internal
|
||||
|
||||
import com.fasterxml.jackson.annotation.*
|
||||
import com.fasterxml.jackson.annotation.JsonCreator.Mode.DISABLED
|
||||
import com.fasterxml.jackson.annotation.JsonCreator.Mode.*
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
@ -38,10 +38,8 @@ import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.parseAsHex
|
||||
import net.corda.core.utilities.toHexString
|
||||
import net.corda.serialization.internal.AllWhitelist
|
||||
import net.corda.serialization.internal.amqp.SerializerFactoryBuilder
|
||||
import net.corda.serialization.internal.amqp.constructorForDeserialization
|
||||
import net.corda.serialization.internal.amqp.hasCordaSerializable
|
||||
import net.corda.serialization.internal.amqp.propertiesForSerialization
|
||||
import net.corda.serialization.internal.amqp.*
|
||||
import net.corda.serialization.internal.model.LocalTypeInformation
|
||||
import java.math.BigDecimal
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
@ -95,10 +93,11 @@ private class CordaSerializableBeanSerializerModifier : BeanSerializerModifier()
|
||||
beanProperties: MutableList<BeanPropertyWriter>): MutableList<BeanPropertyWriter> {
|
||||
val beanClass = beanDesc.beanClass
|
||||
if (hasCordaSerializable(beanClass) && beanClass.kotlinObjectInstance == null) {
|
||||
val ctor = constructorForDeserialization(beanClass)
|
||||
val amqpProperties = propertiesForSerialization(ctor, beanClass, serializerFactory)
|
||||
.serializationOrder
|
||||
.mapNotNull { if (it.isCalculated) null else it.serializer.name }
|
||||
val typeInformation = serializerFactory.getTypeInformation(beanClass)
|
||||
val properties = typeInformation.propertiesOrEmptyMap
|
||||
val amqpProperties = properties.mapNotNull { (name, property) ->
|
||||
if (property.isCalculated) null else name
|
||||
}
|
||||
val propertyRenames = beanDesc.findProperties().associateBy({ it.name }, { it.internalName })
|
||||
(amqpProperties - propertyRenames.values).let {
|
||||
check(it.isEmpty()) { "Jackson didn't provide serialisers for $it" }
|
||||
|
@ -1,16 +1,22 @@
|
||||
package net.corda.finance.compat
|
||||
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.serialization.internal.AllWhitelist
|
||||
import net.corda.serialization.internal.amqp.DeserializationInput
|
||||
import net.corda.serialization.internal.amqp.Schema
|
||||
import net.corda.serialization.internal.amqp.SerializationOutput
|
||||
import net.corda.serialization.internal.amqp.SerializerFactoryBuilder
|
||||
import net.corda.serialization.internal.amqp.custom.PublicKeySerializer
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
|
||||
// TODO: If this type of testing gets momentum, we can create a mini-framework that rides through list of files
|
||||
// and performs necessary validation on all of them.
|
||||
@ -20,19 +26,63 @@ class CompatibilityTest {
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
|
||||
val serializerFactory = SerializerFactoryBuilder.build(AllWhitelist, ClassLoader.getSystemClassLoader()).apply {
|
||||
register(PublicKeySerializer)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun issueCashTansactionReadTest() {
|
||||
val inputStream = javaClass.classLoader.getResourceAsStream("compatibilityData/v3/node_transaction.dat")
|
||||
assertNotNull(inputStream)
|
||||
|
||||
val inByteArray: ByteArray = inputStream.readBytes()
|
||||
val transaction = inByteArray.deserialize<SignedTransaction>(context = SerializationDefaults.STORAGE_CONTEXT)
|
||||
val input = DeserializationInput(serializerFactory)
|
||||
|
||||
val (transaction, envelope) = input.deserializeAndReturnEnvelope(
|
||||
SerializedBytes(inByteArray),
|
||||
SignedTransaction::class.java,
|
||||
SerializationDefaults.STORAGE_CONTEXT)
|
||||
assertNotNull(transaction)
|
||||
|
||||
val commands = transaction.tx.commands
|
||||
assertEquals(1, commands.size)
|
||||
assertTrue(commands.first().value is Cash.Commands.Issue)
|
||||
|
||||
// Serialize back and check that representation is byte-to-byte identical to what it was originally.
|
||||
val serializedForm = transaction.serialize(context = SerializationDefaults.STORAGE_CONTEXT)
|
||||
assertTrue(inByteArray.contentEquals(serializedForm.bytes))
|
||||
val output = SerializationOutput(serializerFactory)
|
||||
val (serializedBytes, schema) = output.serializeAndReturnSchema(transaction, SerializationDefaults.STORAGE_CONTEXT)
|
||||
|
||||
assertSchemasMatch(envelope.schema, schema)
|
||||
|
||||
assertTrue(inByteArray.contentEquals(serializedBytes.bytes))
|
||||
}
|
||||
|
||||
private fun assertSchemasMatch(original: Schema, reserialized: Schema) {
|
||||
if (original.toString() == reserialized.toString()) return
|
||||
original.types.forEach { originalType ->
|
||||
val reserializedType = reserialized.types.firstOrNull { it.name == originalType.name } ?:
|
||||
fail("""Schema mismatch between original and re-serialized data. Could not find reserialized schema matching:
|
||||
|
||||
$originalType
|
||||
""")
|
||||
|
||||
if (originalType.toString() != reserializedType.toString())
|
||||
fail("""Schema mismatch between original and re-serialized data. Expected:
|
||||
|
||||
$originalType
|
||||
|
||||
but was:
|
||||
|
||||
$reserializedType
|
||||
""")
|
||||
}
|
||||
|
||||
reserialized.types.forEach { reserializedType ->
|
||||
if (original.types.none { it.name == reserializedType.name })
|
||||
fail("""Schema mismatch between original and re-serialized data. Could not find original schema matching:
|
||||
|
||||
$reserializedType
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
@ -16,8 +16,8 @@ fun createSerializerFactoryFactory(): SerializerFactoryFactory = DeterministicSe
|
||||
private class DeterministicSerializerFactoryFactory : SerializerFactoryFactory {
|
||||
override fun make(context: SerializationContext) =
|
||||
SerializerFactoryBuilder.build(
|
||||
whitelist = context.whitelist,
|
||||
classCarpenter = DummyClassCarpenter(context.whitelist, context.deserializationClassLoader))
|
||||
whitelist = context.whitelist,
|
||||
classCarpenter = DummyClassCarpenter(context.whitelist, context.deserializationClassLoader))
|
||||
}
|
||||
|
||||
private class DummyClassCarpenter(
|
||||
|
@ -12,7 +12,7 @@ import java.lang.reflect.Type
|
||||
* [ByteArray] is automatically marshalled to/from the Proton-J wrapper, [Binary].
|
||||
*/
|
||||
class AMQPPrimitiveSerializer(clazz: Class<*>) : AMQPSerializer<Any> {
|
||||
override val typeDescriptor = Symbol.valueOf(SerializerFactory.primitiveTypeName(clazz)!!)!!
|
||||
override val typeDescriptor = Symbol.valueOf(AMQPTypeIdentifiers.primitiveTypeName(clazz))!!
|
||||
override val type: Type = clazz
|
||||
|
||||
// NOOP since this is a primitive type.
|
||||
|
@ -3,6 +3,7 @@ package net.corda.serialization.internal.amqp
|
||||
import net.corda.serialization.internal.model.*
|
||||
import java.io.NotSerializableException
|
||||
import java.util.*
|
||||
import kotlin.collections.LinkedHashMap
|
||||
|
||||
/**
|
||||
* Interprets AMQP [Schema] information to obtain [RemoteTypeInformation], caching by [TypeDescriptor].
|
||||
@ -35,9 +36,17 @@ class AMQPRemoteTypeModel {
|
||||
|
||||
val interpretationState = InterpretationState(notationLookup, enumTransformsLookup, cache, emptySet())
|
||||
|
||||
return byTypeDescriptor.mapValues { (typeDescriptor, typeNotation) ->
|
||||
val result = byTypeDescriptor.mapValues { (typeDescriptor, typeNotation) ->
|
||||
cache.getOrPut(typeDescriptor) { interpretationState.run { typeNotation.name.typeIdentifier.interpretIdentifier() } }
|
||||
}
|
||||
val typesByIdentifier = result.values.associateBy { it.typeIdentifier }
|
||||
result.values.forEach { typeInformation ->
|
||||
if (typeInformation is RemoteTypeInformation.Cycle) {
|
||||
typeInformation.follow = typesByIdentifier[typeInformation.typeIdentifier] ?:
|
||||
throw NotSerializableException("Cannot resolve cyclic reference to ${typeInformation.typeIdentifier}")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
data class InterpretationState(val notationLookup: Map<TypeIdentifier, TypeNotation>,
|
||||
@ -45,9 +54,6 @@ class AMQPRemoteTypeModel {
|
||||
val cache: MutableMap<TypeDescriptor, RemoteTypeInformation>,
|
||||
val seen: Set<TypeIdentifier>) {
|
||||
|
||||
private inline fun <T> forgetSeen(block: InterpretationState.() -> T): T =
|
||||
withSeen(emptySet(), block)
|
||||
|
||||
private inline fun <T> withSeen(typeIdentifier: TypeIdentifier, block: InterpretationState.() -> T): T =
|
||||
withSeen(seen + typeIdentifier, block)
|
||||
|
||||
@ -62,7 +68,7 @@ class AMQPRemoteTypeModel {
|
||||
* know we have hit a cycle and respond accordingly.
|
||||
*/
|
||||
fun TypeIdentifier.interpretIdentifier(): RemoteTypeInformation =
|
||||
if (this in seen) RemoteTypeInformation.Cycle(this) { forgetSeen { interpretIdentifier() } }
|
||||
if (this in seen) RemoteTypeInformation.Cycle(this)
|
||||
else withSeen(this) {
|
||||
val identifier = this@interpretIdentifier
|
||||
notationLookup[identifier]?.interpretNotation(identifier) ?: interpretNoNotation()
|
||||
@ -85,7 +91,7 @@ class AMQPRemoteTypeModel {
|
||||
* [RemoteTypeInformation].
|
||||
*/
|
||||
private fun CompositeType.interpretComposite(identifier: TypeIdentifier): RemoteTypeInformation {
|
||||
val properties = fields.asSequence().map { it.interpret() }.toMap()
|
||||
val properties = fields.asSequence().sortedBy { it.name }.map { it.interpret() }.toMap(LinkedHashMap())
|
||||
val typeParameters = identifier.interpretTypeParameters()
|
||||
val interfaceIdentifiers = provides.map { name -> name.typeIdentifier }
|
||||
val isInterface = identifier in interfaceIdentifiers
|
||||
@ -175,6 +181,11 @@ class AMQPRemoteTypeModel {
|
||||
}
|
||||
}
|
||||
|
||||
fun LocalTypeInformation.getEnumTransforms(factory: LocalSerializerFactory): EnumTransforms {
|
||||
val transformsSchema = TransformsSchema.get(typeIdentifier.name, factory)
|
||||
return interpretTransformSet(transformsSchema)
|
||||
}
|
||||
|
||||
private fun interpretTransformSet(transformSet: EnumMap<TransformTypes, MutableList<Transform>>): EnumTransforms {
|
||||
val defaultTransforms = transformSet[TransformTypes.EnumDefault]?.toList() ?: emptyList()
|
||||
val defaults = defaultTransforms.associate { transform -> (transform as EnumDefaultSchemaTransform).new to transform.old }
|
||||
@ -185,7 +196,7 @@ private fun interpretTransformSet(transformSet: EnumMap<TransformTypes, MutableL
|
||||
}
|
||||
|
||||
private val TypeNotation.typeDescriptor: String get() = descriptor.name?.toString() ?:
|
||||
throw NotSerializableException("Type notation has no type descriptor: $this")
|
||||
throw NotSerializableException("Type notation has no type descriptor: $this")
|
||||
|
||||
private val String.typeIdentifier get(): TypeIdentifier = AMQPTypeIdentifierParser.parse(this)
|
||||
|
||||
|
@ -2,6 +2,7 @@ package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.serialization.internal.model.TypeIdentifier
|
||||
import org.apache.qpid.proton.amqp.*
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.Type
|
||||
import java.util.*
|
||||
|
||||
@ -9,8 +10,9 @@ object AMQPTypeIdentifiers {
|
||||
fun isPrimitive(type: Type): Boolean = isPrimitive(TypeIdentifier.forGenericType(type))
|
||||
fun isPrimitive(typeIdentifier: TypeIdentifier) = typeIdentifier in primitiveTypeNamesByName
|
||||
|
||||
fun primitiveTypeName(type: Type): String? =
|
||||
primitiveTypeNamesByName[TypeIdentifier.forGenericType(type)]
|
||||
fun primitiveTypeName(type: Type): String =
|
||||
primitiveTypeNamesByName[TypeIdentifier.forGenericType(type)] ?:
|
||||
throw NotSerializableException("Primitive type name requested for non-primitive type $type")
|
||||
|
||||
private val primitiveTypeNamesByName = sequenceOf(
|
||||
Character::class to "char",
|
||||
|
@ -14,9 +14,9 @@ import java.lang.reflect.Type
|
||||
* Serialization / deserialization of arrays.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
open class ArraySerializer(override val type: Type, factory: SerializerFactory) : AMQPSerializer<Any> {
|
||||
open class ArraySerializer(override val type: Type, factory: LocalSerializerFactory) : AMQPSerializer<Any> {
|
||||
companion object {
|
||||
fun make(type: Type, factory: SerializerFactory) : AMQPSerializer<Any> {
|
||||
fun make(type: Type, factory: LocalSerializerFactory) : AMQPSerializer<Any> {
|
||||
contextLogger().debug { "Making array serializer, typename=${type.typeName}" }
|
||||
return when (type) {
|
||||
Array<Char>::class.java -> CharArraySerializer(factory)
|
||||
@ -41,8 +41,8 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory)
|
||||
// Special case handler for primitive byte arrays. This is needed because we can silently
|
||||
// coerce a byte[] to our own binary type. Normally, if the component type was itself an
|
||||
// array we'd keep walking down the chain but for byte[] stop here and use binary instead
|
||||
val typeName = if (SerializerFactory.isPrimitive(type.componentType())) {
|
||||
SerializerFactory.nameForType(type.componentType())
|
||||
val typeName = if (AMQPTypeIdentifiers.isPrimitive(type.componentType())) {
|
||||
AMQPTypeIdentifiers.nameForType(type.componentType())
|
||||
} else {
|
||||
calcTypeName(type.componentType(), debugOffset + 4)
|
||||
}
|
||||
@ -55,7 +55,7 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory)
|
||||
}
|
||||
|
||||
override val typeDescriptor: Symbol by lazy {
|
||||
Symbol.valueOf("$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")
|
||||
factory.createDescriptor(type)
|
||||
}
|
||||
|
||||
internal val elementType: Type by lazy { type.componentType() }
|
||||
@ -103,7 +103,7 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory)
|
||||
|
||||
// Boxed Character arrays required a specialisation to handle the type conversion properly when populating
|
||||
// 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) {
|
||||
class CharArraySerializer(factory: LocalSerializerFactory) : ArraySerializer(Array<Char>::class.java, factory) {
|
||||
override fun <T> List<T>.toArrayOfType(type: Type): Any {
|
||||
val elementType = type.asClass()
|
||||
val list = this
|
||||
@ -114,11 +114,11 @@ class CharArraySerializer(factory: SerializerFactory) : ArraySerializer(Array<Ch
|
||||
}
|
||||
|
||||
// Specialisation of [ArraySerializer] that handles arrays of unboxed java primitive types
|
||||
abstract class PrimArraySerializer(type: Type, factory: SerializerFactory) : ArraySerializer(type, factory) {
|
||||
abstract class PrimArraySerializer(type: Type, factory: LocalSerializerFactory) : ArraySerializer(type, factory) {
|
||||
companion object {
|
||||
// We don't need to handle the unboxed byte type as that is coercible to a byte array, but
|
||||
// the other 7 primitive types we do
|
||||
private val primTypes: Map<Type, (SerializerFactory) -> PrimArraySerializer> = mapOf(
|
||||
private val primTypes: Map<Type, (LocalSerializerFactory) -> PrimArraySerializer> = mapOf(
|
||||
IntArray::class.java to { f -> PrimIntArraySerializer(f) },
|
||||
CharArray::class.java to { f -> PrimCharArraySerializer(f) },
|
||||
BooleanArray::class.java to { f -> PrimBooleanArraySerializer(f) },
|
||||
@ -129,7 +129,7 @@ abstract class PrimArraySerializer(type: Type, factory: SerializerFactory) : Arr
|
||||
// ByteArray::class.java <-> NOT NEEDED HERE (see comment above)
|
||||
)
|
||||
|
||||
fun make(type: Type, factory: SerializerFactory) = primTypes[type]!!(factory)
|
||||
fun make(type: Type, factory: LocalSerializerFactory) = primTypes[type]!!(factory)
|
||||
}
|
||||
|
||||
fun localWriteObject(data: Data, func: () -> Unit) {
|
||||
@ -137,7 +137,7 @@ abstract class PrimArraySerializer(type: Type, factory: SerializerFactory) : Arr
|
||||
}
|
||||
}
|
||||
|
||||
class PrimIntArraySerializer(factory: SerializerFactory) : PrimArraySerializer(IntArray::class.java, factory) {
|
||||
class PrimIntArraySerializer(factory: LocalSerializerFactory) : PrimArraySerializer(IntArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
@ -147,7 +147,7 @@ class PrimIntArraySerializer(factory: SerializerFactory) : PrimArraySerializer(I
|
||||
}
|
||||
}
|
||||
|
||||
class PrimCharArraySerializer(factory: SerializerFactory) : PrimArraySerializer(CharArray::class.java, factory) {
|
||||
class PrimCharArraySerializer(factory: LocalSerializerFactory) : PrimArraySerializer(CharArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
@ -168,7 +168,7 @@ class PrimCharArraySerializer(factory: SerializerFactory) : PrimArraySerializer(
|
||||
}
|
||||
}
|
||||
|
||||
class PrimBooleanArraySerializer(factory: SerializerFactory) : PrimArraySerializer(BooleanArray::class.java, factory) {
|
||||
class PrimBooleanArraySerializer(factory: LocalSerializerFactory) : PrimArraySerializer(BooleanArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
@ -178,7 +178,7 @@ class PrimBooleanArraySerializer(factory: SerializerFactory) : PrimArraySerializ
|
||||
}
|
||||
}
|
||||
|
||||
class PrimDoubleArraySerializer(factory: SerializerFactory) :
|
||||
class PrimDoubleArraySerializer(factory: LocalSerializerFactory) :
|
||||
PrimArraySerializer(DoubleArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
@ -189,7 +189,7 @@ class PrimDoubleArraySerializer(factory: SerializerFactory) :
|
||||
}
|
||||
}
|
||||
|
||||
class PrimFloatArraySerializer(factory: SerializerFactory) :
|
||||
class PrimFloatArraySerializer(factory: LocalSerializerFactory) :
|
||||
PrimArraySerializer(FloatArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int) {
|
||||
@ -199,7 +199,7 @@ class PrimFloatArraySerializer(factory: SerializerFactory) :
|
||||
}
|
||||
}
|
||||
|
||||
class PrimShortArraySerializer(factory: SerializerFactory) :
|
||||
class PrimShortArraySerializer(factory: LocalSerializerFactory) :
|
||||
PrimArraySerializer(ShortArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
@ -210,7 +210,7 @@ class PrimShortArraySerializer(factory: SerializerFactory) :
|
||||
}
|
||||
}
|
||||
|
||||
class PrimLongArraySerializer(factory: SerializerFactory) :
|
||||
class PrimLongArraySerializer(factory: LocalSerializerFactory) :
|
||||
PrimArraySerializer(LongArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
|
@ -1,11 +1,13 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
import net.corda.serialization.internal.model.LocalTypeInformation
|
||||
import net.corda.serialization.internal.model.TypeIdentifier
|
||||
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.*
|
||||
@ -15,11 +17,11 @@ import kotlin.collections.LinkedHashSet
|
||||
* Serialization / deserialization of predefined set of supported [Collection] types covering mostly [List]s and [Set]s.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
class CollectionSerializer(private val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer<Any> {
|
||||
override val type: Type = declaredType as? DeserializedParameterizedType
|
||||
?: DeserializedParameterizedType.make(SerializerFactory.nameForType(declaredType))
|
||||
class CollectionSerializer(private val declaredType: ParameterizedType, factory: LocalSerializerFactory) : AMQPSerializer<Any> {
|
||||
override val type: Type = declaredType
|
||||
|
||||
override val typeDescriptor: Symbol by lazy {
|
||||
Symbol.valueOf("$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")
|
||||
factory.createDescriptor(type)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -33,40 +35,60 @@ class CollectionSerializer(private val declaredType: ParameterizedType, factory:
|
||||
NonEmptySet::class.java to { list -> NonEmptySet.copyOf(list) }
|
||||
))
|
||||
|
||||
private val supportedTypeIdentifiers = supportedTypes.keys.asSequence().map { TypeIdentifier.forClass(it) }.toSet()
|
||||
|
||||
/**
|
||||
* Replace erased collection types with parameterised types with wildcard type parameters, so that they are represented
|
||||
* appropriately in the AMQP schema.
|
||||
*/
|
||||
fun resolveDeclared(declaredTypeInformation: LocalTypeInformation.ACollection): LocalTypeInformation.ACollection {
|
||||
if (declaredTypeInformation.typeIdentifier.erased in supportedTypeIdentifiers)
|
||||
return reparameterise(declaredTypeInformation)
|
||||
|
||||
throw NotSerializableException(
|
||||
"Cannot derive collection type for declared type: " +
|
||||
declaredTypeInformation.prettyPrint(false))
|
||||
}
|
||||
|
||||
fun resolveActual(actualClass: Class<*>, declaredTypeInformation: LocalTypeInformation.ACollection): LocalTypeInformation.ACollection {
|
||||
if (declaredTypeInformation.typeIdentifier.erased in supportedTypeIdentifiers)
|
||||
return reparameterise(declaredTypeInformation)
|
||||
|
||||
val collectionClass = findMostSuitableCollectionType(actualClass)
|
||||
val erasedInformation = LocalTypeInformation.ACollection(
|
||||
collectionClass,
|
||||
TypeIdentifier.forClass(collectionClass),
|
||||
LocalTypeInformation.Unknown)
|
||||
|
||||
return when(declaredTypeInformation.typeIdentifier) {
|
||||
is TypeIdentifier.Parameterised -> erasedInformation.withElementType(declaredTypeInformation.elementType)
|
||||
else -> erasedInformation.withElementType(LocalTypeInformation.Unknown)
|
||||
}
|
||||
}
|
||||
|
||||
private fun reparameterise(typeInformation: LocalTypeInformation.ACollection): LocalTypeInformation.ACollection =
|
||||
when(typeInformation.typeIdentifier) {
|
||||
is TypeIdentifier.Parameterised -> typeInformation
|
||||
is TypeIdentifier.Erased -> typeInformation.withElementType(LocalTypeInformation.Unknown)
|
||||
else -> throw NotSerializableException(
|
||||
"Unexpected type identifier ${typeInformation.typeIdentifier.prettyPrint(false)} " +
|
||||
"for collection type ${typeInformation.prettyPrint(false)}")
|
||||
}
|
||||
|
||||
private fun findMostSuitableCollectionType(actualClass: Class<*>): Class<out Collection<*>> =
|
||||
supportedTypes.keys.findLast { it.isAssignableFrom(actualClass) }!!
|
||||
|
||||
private fun findConcreteType(clazz: Class<*>): (List<*>) -> Collection<*> {
|
||||
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 {
|
||||
if (supportedTypes.containsKey(declaredClass)) {
|
||||
// Simple case - it is already known to be a collection.
|
||||
return deriveParametrizedType(declaredType, uncheckedCast(declaredClass))
|
||||
} else if (actualClass != null && Collection::class.java.isAssignableFrom(actualClass)) {
|
||||
// Declared class is not collection, but [actualClass] is - represent it accordingly.
|
||||
val collectionClass = findMostSuitableCollectionType(actualClass)
|
||||
return deriveParametrizedType(declaredType, collectionClass)
|
||||
}
|
||||
|
||||
throw AMQPNotSerializableException(
|
||||
declaredType,
|
||||
"Cannot derive collection type for declaredType: '$declaredType', " +
|
||||
"declaredClass: '$declaredClass', actualClass: '$actualClass'")
|
||||
}
|
||||
|
||||
private fun deriveParametrizedType(declaredType: Type, collectionClass: Class<out Collection<*>>): ParameterizedType =
|
||||
(declaredType as? ParameterizedType)
|
||||
?: DeserializedParameterizedType(collectionClass, arrayOf(SerializerFactory.AnyType))
|
||||
|
||||
private fun findMostSuitableCollectionType(actualClass: Class<*>): Class<out Collection<*>> =
|
||||
supportedTypes.keys.findLast { it.isAssignableFrom(actualClass) }!!
|
||||
}
|
||||
|
||||
private val concreteBuilder: (List<*>) -> Collection<*> = findConcreteType(declaredType.rawType as Class<*>)
|
||||
|
||||
private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "list", Descriptor(typeDescriptor), emptyList())
|
||||
private val typeNotation: TypeNotation = RestrictedType(AMQPTypeIdentifiers.nameForType(declaredType), null, emptyList(), "list", Descriptor(typeDescriptor), emptyList())
|
||||
|
||||
private val outboundType = resolveTypeVariables(declaredType.actualTypeArguments[0], null)
|
||||
private val inboundType = declaredType.actualTypeArguments[0]
|
||||
|
@ -0,0 +1,270 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.serialization.internal.model.*
|
||||
import org.apache.qpid.proton.amqp.Binary
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* A strategy for reading a property value during deserialization.
|
||||
*/
|
||||
interface PropertyReadStrategy {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Select the correct strategy for reading properties, based on the property type.
|
||||
*/
|
||||
fun make(name: String, typeIdentifier: TypeIdentifier, type: Type): PropertyReadStrategy =
|
||||
if (AMQPTypeIdentifiers.isPrimitive(typeIdentifier)) {
|
||||
when (typeIdentifier) {
|
||||
in characterTypes -> AMQPCharPropertyReadStrategy
|
||||
else -> AMQPPropertyReadStrategy
|
||||
}
|
||||
} else {
|
||||
DescribedTypeReadStrategy(name, typeIdentifier, type)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this strategy to read the value of a property during deserialization.
|
||||
*/
|
||||
fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any?
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A strategy for writing a property value during serialisation.
|
||||
*/
|
||||
interface PropertyWriteStrategy {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Select the correct strategy for writing properties, based on the property information.
|
||||
*/
|
||||
fun make(name: String, propertyInformation: LocalPropertyInformation, factory: LocalSerializerFactory): PropertyWriteStrategy {
|
||||
val reader = PropertyReader.make(propertyInformation)
|
||||
val type = propertyInformation.type
|
||||
return if (AMQPTypeIdentifiers.isPrimitive(type.typeIdentifier)) {
|
||||
when (type.typeIdentifier) {
|
||||
in characterTypes -> AMQPCharPropertyWriteStategy(reader)
|
||||
else -> AMQPPropertyWriteStrategy(reader)
|
||||
}
|
||||
} else {
|
||||
DescribedTypeWriteStrategy(name, propertyInformation, reader) { factory.get(propertyInformation.type) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write any [TypeNotation] needed to the [SerializationOutput].
|
||||
*/
|
||||
fun writeClassInfo(output: SerializationOutput)
|
||||
|
||||
/**
|
||||
* Write the property's value to the [SerializationOutput].
|
||||
*/
|
||||
fun writeProperty(obj: Any?, data: Data, output: SerializationOutput, context: SerializationContext, debugIndent: Int)
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines strategies for reading and writing a given property's value during serialisation/deserialisation.
|
||||
*/
|
||||
interface PropertySerializer : PropertyReadStrategy, PropertyWriteStrategy {
|
||||
/**
|
||||
* The name of the property.
|
||||
*/
|
||||
val name: String
|
||||
/**
|
||||
* Whether the property is calculated.
|
||||
*/
|
||||
val isCalculated: Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A [PropertySerializer] for a property of a [LocalTypeInformation.Composable] type.
|
||||
*/
|
||||
class ComposableTypePropertySerializer(
|
||||
override val name: String,
|
||||
override val isCalculated: Boolean,
|
||||
private val readStrategy: PropertyReadStrategy,
|
||||
private val writeStrategy: PropertyWriteStrategy) :
|
||||
PropertySerializer,
|
||||
PropertyReadStrategy by readStrategy,
|
||||
PropertyWriteStrategy by writeStrategy {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Make a [PropertySerializer] for the given [LocalPropertyInformation].
|
||||
*
|
||||
* @param name The name of the property.
|
||||
* @param propertyInformation [LocalPropertyInformation] for the property.
|
||||
* @param factory The [LocalSerializerFactory] to use when writing values for this property.
|
||||
*/
|
||||
fun make(name: String, propertyInformation: LocalPropertyInformation, factory: LocalSerializerFactory): PropertySerializer =
|
||||
ComposableTypePropertySerializer(
|
||||
name,
|
||||
propertyInformation.isCalculated,
|
||||
PropertyReadStrategy.make(name, propertyInformation.type.typeIdentifier, propertyInformation.type.observedType),
|
||||
PropertyWriteStrategy.make(name, propertyInformation, factory))
|
||||
|
||||
/**
|
||||
* Make a [PropertySerializer] for use in deserialization only, when deserializing a type that requires evolution.
|
||||
*
|
||||
* @param name The name of the property.
|
||||
* @param isCalculated Whether the property is calculated.
|
||||
* @param typeIdentifier The [TypeIdentifier] for the property type.
|
||||
* @param type The local [Type] for the property type.
|
||||
*/
|
||||
fun makeForEvolution(name: String, isCalculated: Boolean, typeIdentifier: TypeIdentifier, type: Type): PropertySerializer =
|
||||
ComposableTypePropertySerializer(
|
||||
name,
|
||||
isCalculated,
|
||||
PropertyReadStrategy.make(name, typeIdentifier, type),
|
||||
EvolutionPropertyWriteStrategy)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the value of a property from an instance of the type to which that property belongs, either by calling a getter method
|
||||
* or by reading the value of a private backing field.
|
||||
*/
|
||||
sealed class PropertyReader {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Make a [PropertyReader] based on the provided [LocalPropertyInformation].
|
||||
*/
|
||||
fun make(propertyInformation: LocalPropertyInformation) = when(propertyInformation) {
|
||||
is LocalPropertyInformation.GetterSetterProperty -> GetterReader(propertyInformation.observedGetter)
|
||||
is LocalPropertyInformation.ConstructorPairedProperty -> GetterReader(propertyInformation.observedGetter)
|
||||
is LocalPropertyInformation.ReadOnlyProperty -> GetterReader(propertyInformation.observedGetter)
|
||||
is LocalPropertyInformation.CalculatedProperty -> GetterReader(propertyInformation.observedGetter)
|
||||
is LocalPropertyInformation.PrivateConstructorPairedProperty -> FieldReader(propertyInformation.observedField)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the property from the supplied instance, or null if the instance is itself null.
|
||||
*/
|
||||
abstract fun read(obj: Any?): Any?
|
||||
|
||||
/**
|
||||
* Reads a property using a getter [Method].
|
||||
*/
|
||||
class GetterReader(private val getter: Method): PropertyReader() {
|
||||
init {
|
||||
getter.isAccessible = true
|
||||
}
|
||||
|
||||
override fun read(obj: Any?): Any? = if (obj == null) null else getter.invoke(obj)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a property using a backing [Field].
|
||||
*/
|
||||
class FieldReader(private val field: Field): PropertyReader() {
|
||||
init {
|
||||
field.isAccessible = true
|
||||
}
|
||||
|
||||
override fun read(obj: Any?): Any? = if (obj == null) null else field.get(obj)
|
||||
}
|
||||
}
|
||||
|
||||
private val characterTypes = setOf(
|
||||
TypeIdentifier.forClass(Char::class.javaObjectType),
|
||||
TypeIdentifier.forClass(Char::class.javaPrimitiveType!!)
|
||||
)
|
||||
|
||||
object EvolutionPropertyWriteStrategy : PropertyWriteStrategy {
|
||||
override fun writeClassInfo(output: SerializationOutput) =
|
||||
throw UnsupportedOperationException("Evolution serializers cannot write values")
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput, context: SerializationContext, debugIndent: Int) =
|
||||
throw UnsupportedOperationException("Evolution serializers cannot write values")
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a type that comes with its own [TypeDescriptor], by calling back into [RemoteSerializerFactory] to obtain a suitable
|
||||
* serializer for that descriptor.
|
||||
*/
|
||||
class DescribedTypeReadStrategy(name: String,
|
||||
typeIdentifier: TypeIdentifier,
|
||||
private val type: Type): PropertyReadStrategy {
|
||||
|
||||
private val nameForDebug = "$name(${typeIdentifier.prettyPrint(false)})"
|
||||
|
||||
override fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any? =
|
||||
ifThrowsAppend({ nameForDebug }) {
|
||||
input.readObjectOrNull(obj, schemas, type, context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a property value into [SerializationOutput], together with a schema information describing it.
|
||||
*/
|
||||
class DescribedTypeWriteStrategy(private val name: String,
|
||||
private val propertyInformation: LocalPropertyInformation,
|
||||
private val reader: PropertyReader,
|
||||
private val serializerProvider: () -> AMQPSerializer<Any>) : PropertyWriteStrategy {
|
||||
|
||||
// Lazy to avoid getting into infinite loops when there are cycles.
|
||||
private val serializer by lazy { serializerProvider() }
|
||||
|
||||
private val nameForDebug get() = "$name(${propertyInformation.type.typeIdentifier.prettyPrint(false)})"
|
||||
|
||||
override fun writeClassInfo(output: SerializationOutput) {
|
||||
if (propertyInformation.type !is LocalTypeInformation.Top) {
|
||||
serializer.writeClassInfo(output)
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput, context: SerializationContext,
|
||||
debugIndent: Int) = ifThrowsAppend({ nameForDebug }) {
|
||||
val propertyValue = reader.read(obj)
|
||||
output.writeObjectOrNull(propertyValue, data, propertyInformation.type.observedType, context, debugIndent)
|
||||
}
|
||||
}
|
||||
|
||||
object AMQPPropertyReadStrategy : PropertyReadStrategy {
|
||||
override fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any? =
|
||||
if (obj is Binary) obj.array else obj
|
||||
}
|
||||
|
||||
class AMQPPropertyWriteStrategy(private val reader: PropertyReader) : PropertyWriteStrategy {
|
||||
override fun writeClassInfo(output: SerializationOutput) {}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
val value = reader.read(obj)
|
||||
// ByteArrays have to be wrapped in an AMQP Binary wrapper.
|
||||
if (value is ByteArray) {
|
||||
data.putObject(Binary(value))
|
||||
} else {
|
||||
data.putObject(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object AMQPCharPropertyReadStrategy : PropertyReadStrategy {
|
||||
override fun readProperty(obj: Any?, schemas: SerializationSchemas,
|
||||
input: DeserializationInput, context: SerializationContext
|
||||
): Any? {
|
||||
return if (obj == null) null else (obj as Short).toChar()
|
||||
}
|
||||
}
|
||||
|
||||
class AMQPCharPropertyWriteStategy(private val reader: PropertyReader) : PropertyWriteStrategy {
|
||||
override fun writeClassInfo(output: SerializationOutput) {}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
val input = reader.read(obj)
|
||||
if (input != null) data.putShort((input as Char).toShort()) else data.putNull()
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@ import com.google.common.reflect.TypeToken
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
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.lang.reflect.Type
|
||||
@ -63,9 +62,11 @@ class CorDappCustomSerializer(
|
||||
|
||||
override val type = types[CORDAPP_TYPE]
|
||||
val proxyType = types[PROXY_TYPE]
|
||||
override val typeDescriptor: Symbol = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${nameForType(type)}")
|
||||
override val typeDescriptor: Symbol = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${AMQPTypeIdentifiers.nameForType(type)}")
|
||||
val descriptor: Descriptor = Descriptor(typeDescriptor)
|
||||
private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer(proxyType, factory) }
|
||||
private val proxySerializer: ObjectSerializer by lazy {
|
||||
ObjectSerializer.make(factory.getTypeInformation(proxyType), factory)
|
||||
}
|
||||
|
||||
override fun writeClassInfo(output: SerializationOutput) {}
|
||||
|
||||
@ -77,8 +78,8 @@ class CorDappCustomSerializer(
|
||||
|
||||
data.withDescribed(descriptor) {
|
||||
data.withList {
|
||||
proxySerializer.propertySerializers.serializationOrder.forEach {
|
||||
it.serializer.writeProperty(proxy, this, output, context)
|
||||
(proxySerializer as ObjectSerializer).propertySerializers.forEach { (_, serializer) ->
|
||||
serializer.writeProperty(proxy, this, output, context, debugIndent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package net.corda.serialization.internal.amqp
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.serialization.internal.amqp.SerializerFactory.Companion.nameForType
|
||||
import net.corda.serialization.internal.model.FingerprintWriter
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.lang.reflect.Type
|
||||
@ -67,13 +67,13 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
||||
override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.clazz
|
||||
override val type: Type get() = clazz
|
||||
override val typeDescriptor: Symbol by lazy {
|
||||
Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForDescriptors(superClassSerializer.typeDescriptor.toString(), nameForType(clazz))}")
|
||||
Symbol.valueOf("$DESCRIPTOR_DOMAIN:${FingerprintWriter(false).write(arrayOf(superClassSerializer.typeDescriptor.toString(), AMQPTypeIdentifiers.nameForType(clazz)).joinToString()).fingerprint}")
|
||||
}
|
||||
private val typeNotation: TypeNotation = RestrictedType(
|
||||
SerializerFactory.nameForType(clazz),
|
||||
AMQPTypeIdentifiers.nameForType(clazz),
|
||||
null,
|
||||
emptyList(),
|
||||
SerializerFactory.nameForType(superClassSerializer.type),
|
||||
AMQPTypeIdentifiers.nameForType(superClassSerializer.type),
|
||||
Descriptor(typeDescriptor),
|
||||
emptyList())
|
||||
|
||||
@ -102,7 +102,7 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
||||
*/
|
||||
abstract class CustomSerializerImp<T : Any>(protected val clazz: Class<T>, protected val withInheritance: Boolean) : CustomSerializer<T>() {
|
||||
override val type: Type get() = clazz
|
||||
override val typeDescriptor: Symbol = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${nameForType(clazz)}")
|
||||
override val typeDescriptor: Symbol = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${AMQPTypeIdentifiers.nameForType(clazz)}")
|
||||
override fun writeClassInfo(output: SerializationOutput) {}
|
||||
override val descriptor: Descriptor = Descriptor(typeDescriptor)
|
||||
override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz
|
||||
@ -127,19 +127,19 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
||||
*/
|
||||
abstract class Proxy<T : Any, P : Any>(clazz: Class<T>,
|
||||
protected val proxyClass: Class<P>,
|
||||
protected val factory: SerializerFactory,
|
||||
protected val factory: LocalSerializerFactory,
|
||||
withInheritance: Boolean = true) : CustomSerializerImp<T>(clazz, withInheritance) {
|
||||
override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz
|
||||
|
||||
private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer(proxyClass, factory) }
|
||||
private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer.make(factory.getTypeInformation(proxyClass), factory) }
|
||||
|
||||
override val schemaForDocumentation: Schema by lazy {
|
||||
val typeNotations = mutableSetOf<TypeNotation>(
|
||||
CompositeType(
|
||||
nameForType(type),
|
||||
AMQPTypeIdentifiers.nameForType(type),
|
||||
null,
|
||||
emptyList(),
|
||||
descriptor, (proxySerializer.typeNotation as CompositeType).fields))
|
||||
descriptor, proxySerializer.fields))
|
||||
for (additional in additionalSerializers) {
|
||||
typeNotations.addAll(additional.schemaForDocumentation.types)
|
||||
}
|
||||
@ -158,8 +158,8 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
||||
) {
|
||||
val proxy = toProxy(obj)
|
||||
data.withList {
|
||||
proxySerializer.propertySerializers.serializationOrder.forEach {
|
||||
it.serializer.writeProperty(proxy, this, output, context)
|
||||
proxySerializer.propertySerializers.forEach { (_, serializer) ->
|
||||
serializer.writeProperty(proxy, this, output, context, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -191,8 +191,8 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
||||
: CustomSerializerImp<T>(clazz, withInheritance) {
|
||||
|
||||
override val schemaForDocumentation = Schema(
|
||||
listOf(RestrictedType(nameForType(type), "", listOf(nameForType(type)),
|
||||
SerializerFactory.primitiveTypeName(String::class.java)!!,
|
||||
listOf(RestrictedType(AMQPTypeIdentifiers.nameForType(type), "", listOf(AMQPTypeIdentifiers.nameForType(type)),
|
||||
AMQPTypeIdentifiers.primitiveTypeName(String::class.java),
|
||||
descriptor, emptyList())))
|
||||
|
||||
override fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput,
|
||||
|
@ -25,5 +25,5 @@ class DefaultDescriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistr
|
||||
}
|
||||
|
||||
override fun getOrBuild(descriptor: String, builder: () -> AMQPSerializer<Any>) =
|
||||
get(descriptor) ?: builder().also { newSerializer -> this[descriptor] = newSerializer }
|
||||
registry.getOrPut(descriptor) { builder() }
|
||||
}
|
@ -8,6 +8,7 @@ import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.serialization.internal.*
|
||||
import net.corda.serialization.internal.model.TypeIdentifier
|
||||
import org.apache.qpid.proton.amqp.Binary
|
||||
import org.apache.qpid.proton.amqp.DescribedType
|
||||
import org.apache.qpid.proton.amqp.UnsignedInteger
|
||||
@ -168,8 +169,8 @@ class DeserializationInput constructor(
|
||||
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) {
|
||||
val serializer = serializerFactory.get(obj.descriptor.toString(), schemas)
|
||||
if (type != TypeIdentifier.UnknownType.getLocalType() && serializer.type != type && with(serializer.type) {
|
||||
!isSubClassOf(type) && !materiallyEquivalentTo(type)
|
||||
}
|
||||
) {
|
||||
|
@ -1,18 +0,0 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import java.lang.reflect.GenericArrayType
|
||||
import java.lang.reflect.Type
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Implementation of [GenericArrayType] that we can actually construct.
|
||||
*/
|
||||
class DeserializedGenericArrayType(private val componentType: Type) : GenericArrayType {
|
||||
override fun getGenericComponentType(): Type = componentType
|
||||
override fun getTypeName(): String = "${componentType.typeName}[]"
|
||||
override fun toString(): String = typeName
|
||||
override fun hashCode(): Int = Objects.hashCode(componentType)
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other is GenericArrayType && (componentType == other.genericComponentType)
|
||||
}
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import com.google.common.primitives.Primitives
|
||||
import net.corda.core.KeepForDJVM
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
import java.lang.reflect.TypeVariable
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Implementation of [ParameterizedType] that we can actually construct, and a parser from the string representation
|
||||
* 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 {
|
||||
init {
|
||||
if (params.isEmpty()) {
|
||||
throw AMQPNotSerializableException(rawType, "Must be at least one parameter type in a ParameterizedType")
|
||||
}
|
||||
if (params.size != rawType.typeParameters.size) {
|
||||
throw AMQPNotSerializableException(
|
||||
rawType,
|
||||
"Expected ${rawType.typeParameters.size} for ${rawType.name} but found ${params.size}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun boundedType(type: TypeVariable<out Class<out Any>>): Boolean {
|
||||
return !(type.bounds.size == 1 && type.bounds[0] == Object::class.java)
|
||||
}
|
||||
|
||||
private val _typeName: String = makeTypeName()
|
||||
|
||||
private fun makeTypeName(): String {
|
||||
val paramsJoined = params.joinToString(", ") { it.typeName }
|
||||
return "${rawType.name}<$paramsJoined>"
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Maximum depth/nesting of generics before we suspect some DoS attempt.
|
||||
const val MAX_DEPTH: Int = 32
|
||||
|
||||
fun make(name: String, cl: ClassLoader = DeserializedParameterizedType::class.java.classLoader): Type {
|
||||
val paramTypes = ArrayList<Type>()
|
||||
val pos = parseTypeList("$name>", paramTypes, cl)
|
||||
if (pos <= name.length) {
|
||||
throw AMQPNoTypeNotSerializableException(
|
||||
"Malformed string form of ParameterizedType. Unexpected '>' at character position $pos of $name.")
|
||||
}
|
||||
if (paramTypes.size != 1) {
|
||||
throw AMQPNoTypeNotSerializableException("Expected only one type, but got $paramTypes")
|
||||
}
|
||||
return paramTypes[0]
|
||||
}
|
||||
|
||||
private fun parseTypeList(params: String, types: MutableList<Type>, cl: ClassLoader, depth: Int = 0): Int {
|
||||
var pos = 0
|
||||
var typeStart = 0
|
||||
var needAType = true
|
||||
var skippingWhitespace = false
|
||||
|
||||
while (pos < params.length) {
|
||||
if (params[pos] == '<') {
|
||||
val typeEnd = pos++
|
||||
val paramTypes = ArrayList<Type>()
|
||||
pos = parseTypeParams(params, pos, paramTypes, cl, depth + 1)
|
||||
types += makeParameterizedType(params.substring(typeStart, typeEnd).trim(), paramTypes, cl)
|
||||
typeStart = pos
|
||||
needAType = false
|
||||
} else if (params[pos] == ',') {
|
||||
val typeEnd = pos++
|
||||
val typeName = params.substring(typeStart, typeEnd).trim()
|
||||
if (!typeName.isEmpty()) {
|
||||
types += makeType(typeName, cl)
|
||||
} else if (needAType) {
|
||||
throw AMQPNoTypeNotSerializableException("Expected a type, not ','")
|
||||
}
|
||||
typeStart = pos
|
||||
needAType = true
|
||||
} else if (params[pos] == '>') {
|
||||
val typeEnd = pos++
|
||||
val typeName = params.substring(typeStart, typeEnd).trim()
|
||||
if (!typeName.isEmpty()) {
|
||||
types += makeType(typeName, cl)
|
||||
} else if (needAType) {
|
||||
throw AMQPNoTypeNotSerializableException("Expected a type, not '>'")
|
||||
}
|
||||
return pos
|
||||
} else {
|
||||
// Skip forwards, checking character types
|
||||
if (pos == typeStart) {
|
||||
skippingWhitespace = false
|
||||
if (params[pos].isWhitespace()) {
|
||||
typeStart = ++pos
|
||||
} else if (!needAType) {
|
||||
throw AMQPNoTypeNotSerializableException("Not expecting a type")
|
||||
} else if (params[pos] == '?') {
|
||||
pos++
|
||||
} else if (!params[pos].isJavaIdentifierStart()) {
|
||||
throw AMQPNoTypeNotSerializableException("Invalid character at start of type: ${params[pos]}")
|
||||
} else {
|
||||
pos++
|
||||
}
|
||||
} else {
|
||||
if (params[pos].isWhitespace()) {
|
||||
pos++
|
||||
skippingWhitespace = true
|
||||
} else if (!skippingWhitespace && (params[pos] == '.' || params[pos].isJavaIdentifierPart())) {
|
||||
pos++
|
||||
} else {
|
||||
throw AMQPNoTypeNotSerializableException(
|
||||
"Invalid character ${params[pos]} in middle of type $params at idx $pos")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw AMQPNoTypeNotSerializableException("Missing close generics '>'")
|
||||
}
|
||||
|
||||
private fun makeType(typeName: String, cl: ClassLoader): Type {
|
||||
// Not generic
|
||||
return if (typeName == "?") SerializerFactory.AnyType else {
|
||||
Primitives.wrap(SerializerFactory.primitiveType(typeName) ?: Class.forName(typeName, false, cl))
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeParameterizedType(rawTypeName: String, args: MutableList<Type>, cl: ClassLoader): Type {
|
||||
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 {
|
||||
if (depth == MAX_DEPTH) {
|
||||
throw AMQPNoTypeNotSerializableException("Maximum depth of nested generics reached: $depth")
|
||||
}
|
||||
return startPos + parseTypeList(params.substring(startPos), paramTypes, cl, depth)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRawType(): Type = rawType
|
||||
|
||||
override fun getOwnerType(): Type? = ownerType
|
||||
|
||||
override fun getActualTypeArguments(): Array<out Type> = params
|
||||
|
||||
override fun getTypeName(): String = _typeName
|
||||
|
||||
override fun toString(): String = _typeName
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return Arrays.hashCode(this.actualTypeArguments) xor Objects.hashCode(this.ownerType) xor Objects.hashCode(this.rawType)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return if (other is ParameterizedType) {
|
||||
if (this === other) {
|
||||
true
|
||||
} else {
|
||||
this.ownerType == other.ownerType && this.rawType == other.rawType && Arrays.equals(this.actualTypeArguments, other.actualTypeArguments)
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.serialization.internal.model.LocalTypeInformation
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.io.NotSerializableException
|
||||
@ -37,100 +38,20 @@ import java.util.*
|
||||
*/
|
||||
class EnumEvolutionSerializer(
|
||||
override val type: Type,
|
||||
factory: SerializerFactory,
|
||||
factory: LocalSerializerFactory,
|
||||
private val conversions: Map<String, String>,
|
||||
private val ordinals: Map<String, Int>) : AMQPSerializer<Any> {
|
||||
override val typeDescriptor = Symbol.valueOf(
|
||||
"$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")!!
|
||||
|
||||
companion object {
|
||||
private fun MutableMap<String, String>.mapInPlace(f: (String) -> String) {
|
||||
val i = iterator()
|
||||
while (i.hasNext()) {
|
||||
val curr = i.next()
|
||||
curr.setValue(f(curr.value))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an Enum Evolver serializer.
|
||||
*
|
||||
* @param old The description of the enum as it existed at the time of serialisation taken from the
|
||||
* received AMQP header
|
||||
* @param new The Serializer object we built based on the current state of the enum class on our classpath
|
||||
* @param factory the [SerializerFactory] that is building this serialization object.
|
||||
* @param schemas the transforms attached to the class in the AMQP header, i.e. the transforms
|
||||
* known at serialization time
|
||||
*/
|
||||
fun make(old: RestrictedType,
|
||||
new: AMQPSerializer<Any>,
|
||||
factory: SerializerFactory,
|
||||
schemas: SerializationSchemas): AMQPSerializer<Any> {
|
||||
val wireTransforms = schemas.transforms.types[old.name]
|
||||
?: EnumMap<TransformTypes, MutableList<Transform>>(TransformTypes::class.java)
|
||||
val localTransforms = TransformsSchema.get(old.name, factory)
|
||||
|
||||
// remember, the longer the list the newer we're assuming the transform set it as we assume
|
||||
// evolution annotations are never removed, only added to
|
||||
val transforms = if (wireTransforms.size > localTransforms.size) wireTransforms else localTransforms
|
||||
|
||||
// if either of these isn't of the cast type then something has gone terribly wrong
|
||||
// elsewhere in the code
|
||||
val defaultRules: List<EnumDefaultSchemaTransform>? = uncheckedCast(transforms[TransformTypes.EnumDefault])
|
||||
val renameRules: List<RenameSchemaTransform>? = uncheckedCast(transforms[TransformTypes.Rename])
|
||||
|
||||
// What values exist on the enum as it exists on the class path
|
||||
val localValues = new.type.asClass().enumConstants.map { it.toString() }
|
||||
|
||||
val conversions: MutableMap<String, String> = localValues
|
||||
.union(defaultRules?.map { it.new }?.toSet() ?: emptySet())
|
||||
.union(renameRules?.map { it.to } ?: emptySet())
|
||||
.associateBy({ it }, { it })
|
||||
.toMutableMap()
|
||||
|
||||
val rules: MutableMap<String, String> = mutableMapOf()
|
||||
rules.putAll(defaultRules?.associateBy({ it.new }, { it.old }) ?: emptyMap())
|
||||
val renameRulesMap = renameRules?.associateBy({ it.to }, { it.from }) ?: emptyMap()
|
||||
rules.putAll(renameRulesMap)
|
||||
|
||||
// take out set of all possible constants and build a map from those to the
|
||||
// existing constants applying the rename and defaulting rules as defined
|
||||
// in the schema
|
||||
while (conversions.filterNot { it.value in localValues }.isNotEmpty()) {
|
||||
conversions.mapInPlace { rules[it] ?: it }
|
||||
}
|
||||
|
||||
// you'd think this was overkill to get access to the ordinal values for each constant but it's actually
|
||||
// rather tricky when you don't have access to the actual type, so this is a nice way to be able
|
||||
// to precompute and pass to the actual object
|
||||
val ordinals = localValues.mapIndexed { i, s -> Pair(s, i) }.toMap()
|
||||
|
||||
// create a mapping between the ordinal value and the name as it was serialised converted
|
||||
// to the name as it exists. We want to test any new constants have been added to the end
|
||||
// of the enum class
|
||||
val serialisedOrds = ((schemas.schema.types.find { it.name == old.name } as RestrictedType).choices
|
||||
.associateBy({ it.value.toInt() }, { conversions[it.name] }))
|
||||
|
||||
if (ordinals.filterNot { serialisedOrds[it.value] == it.key }.isNotEmpty()) {
|
||||
throw AMQPNotSerializableException(
|
||||
new.type,
|
||||
"Constants have been reordered, additions must be appended to the end")
|
||||
}
|
||||
|
||||
return EnumEvolutionSerializer(new.type, factory, conversions, ordinals)
|
||||
}
|
||||
}
|
||||
override val typeDescriptor = factory.createDescriptor(type)
|
||||
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
context: SerializationContext
|
||||
): Any {
|
||||
val enumName = (obj as List<*>)[0] as String
|
||||
|
||||
if (enumName !in conversions) {
|
||||
throw AMQPNotSerializableException(type, "No rule to evolve enum constant $type::$enumName")
|
||||
}
|
||||
val converted = conversions[enumName] ?: throw AMQPNotSerializableException(type, "No rule to evolve enum constant $type::$enumName")
|
||||
val ordinal = ordinals[converted] ?: throw AMQPNotSerializableException(type, "Ordinal not found for enum value $type::$converted")
|
||||
|
||||
return type.asClass().enumConstants[ordinals[conversions[enumName]]!!]
|
||||
return type.asClass().enumConstants[ordinal]
|
||||
}
|
||||
|
||||
override fun writeClassInfo(output: SerializationOutput) {
|
||||
|
@ -1,24 +1,21 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* Our definition of an enum with the AMQP spec is a list (of two items, a string and an int) that is
|
||||
* a restricted type with a number of choices associated with it
|
||||
*/
|
||||
class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: SerializerFactory) : AMQPSerializer<Any> {
|
||||
class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: LocalSerializerFactory) : AMQPSerializer<Any> {
|
||||
override val type: Type = declaredType
|
||||
private val typeNotation: TypeNotation
|
||||
override val typeDescriptor = Symbol.valueOf(
|
||||
"$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")!!
|
||||
override val typeDescriptor = factory.createDescriptor(type)
|
||||
|
||||
init {
|
||||
typeNotation = RestrictedType(
|
||||
SerializerFactory.nameForType(declaredType),
|
||||
AMQPTypeIdentifiers.nameForType(declaredType),
|
||||
null, emptyList(), "list", Descriptor(typeDescriptor),
|
||||
declaredClass.enumConstants.zip(IntRange(0, declaredClass.enumConstants.size)).map {
|
||||
Choice(it.first.toString(), it.second.toString())
|
||||
|
@ -1,312 +0,0 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.internal.isConcreteClass
|
||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.serialization.internal.carpenter.getTypeAsClass
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.Type
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.jvm.javaType
|
||||
import kotlin.reflect.jvm.jvmErasure
|
||||
|
||||
|
||||
/**
|
||||
* Serializer for deserializing objects whose definition has changed since they
|
||||
* were serialised.
|
||||
*
|
||||
* @property oldReaders A linked map representing the properties of the object as they were serialized. Note
|
||||
* this may contain properties that are no longer needed by the class. These *must* be read however to ensure
|
||||
* any refferenced objects in the object stream are captured properly
|
||||
* @property kotlinConstructor
|
||||
* @property constructorArgs used to hold the properties as sent to the object's constructor. Passed in as a
|
||||
* pre populated array as properties not present on the old constructor must be initialised in the factory
|
||||
*/
|
||||
abstract class EvolutionSerializer(
|
||||
clazz: Type,
|
||||
factory: SerializerFactory,
|
||||
protected val oldReaders: Map<String, OldParam>,
|
||||
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()
|
||||
|
||||
/**
|
||||
* Represents a parameter as would be passed to the constructor of the class as it was
|
||||
* when it was serialised and NOT how that class appears now
|
||||
*
|
||||
* @param resultsIndex index into the constructor argument list where the read property
|
||||
* should be placed
|
||||
* @param property object to read the actual property value
|
||||
*/
|
||||
@KeepForDJVM
|
||||
data class OldParam(var resultsIndex: Int, val property: PropertySerializer) {
|
||||
fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
new: Array<Any?>, context: SerializationContext
|
||||
) = property.readProperty(obj, schemas, input, context).apply {
|
||||
if (resultsIndex >= 0) {
|
||||
new[resultsIndex] = this
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "resultsIndex = $resultsIndex property = ${property.name}"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val logger = contextLogger()
|
||||
|
||||
/**
|
||||
* Unlike the generic deserialization case where we need to locate the primary constructor
|
||||
* for the object (or our best guess) in the case of an object whose structure has changed
|
||||
* since serialisation we need to attempt to locate a constructor that we can use. For example,
|
||||
* its parameters match the serialised members and it will initialise any newly added
|
||||
* elements.
|
||||
*
|
||||
* TODO: Type evolution
|
||||
* TODO: rename annotation
|
||||
*/
|
||||
private fun getEvolverConstructor(type: Type, oldArgs: Map<String, OldParam>): KFunction<Any>? {
|
||||
val clazz: Class<*> = type.asClass()
|
||||
|
||||
if (!clazz.isConcreteClass) return null
|
||||
|
||||
val oldArgumentSet = oldArgs.map { Pair(it.key as String?, it.value.property.resolvedType.asClass()) }
|
||||
var maxConstructorVersion = Integer.MIN_VALUE
|
||||
var constructor: KFunction<Any>? = null
|
||||
|
||||
clazz.kotlin.constructors.forEach {
|
||||
val version = it.findAnnotation<DeprecatedConstructorForDeserialization>()?.version ?: Integer.MIN_VALUE
|
||||
|
||||
if (version > maxConstructorVersion &&
|
||||
oldArgumentSet.containsAll(it.parameters.map { v -> Pair(v.name, v.type.javaType.asClass()) })
|
||||
) {
|
||||
constructor = it
|
||||
maxConstructorVersion = version
|
||||
|
||||
with(logger) {
|
||||
info("Select annotated constructor version=$version nparams=${it.parameters.size}")
|
||||
debug{" params=${it.parameters}"}
|
||||
}
|
||||
} else if (version != Integer.MIN_VALUE){
|
||||
with(logger) {
|
||||
info("Ignore annotated constructor version=$version nparams=${it.parameters.size}")
|
||||
debug{" params=${it.parameters}"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we didn't get an exact match revert to existing behaviour, if the new parameters
|
||||
// are not mandatory (i.e. nullable) things are fine
|
||||
return constructor ?: run {
|
||||
logger.info("Failed to find annotated historic constructor")
|
||||
constructorForDeserialization(type)
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeWithConstructor(
|
||||
new: ObjectSerializer,
|
||||
factory: SerializerFactory,
|
||||
constructor: KFunction<Any>,
|
||||
readersAsSerialized: Map<String, OldParam>): AMQPSerializer<Any> {
|
||||
|
||||
// Java doesn't care about nullability unless it's a primitive in which
|
||||
// case it can't be referenced. Unfortunately whilst Kotlin does apply
|
||||
// Nullability annotations we cannot use them here as they aren't
|
||||
// retained at runtime so we cannot rely on the absence of
|
||||
// any particular NonNullable annotation type to indicate cross
|
||||
// compiler nullability
|
||||
val isKotlin = (new.type.javaClass.declaredAnnotations.any {
|
||||
it.annotationClass.qualifiedName == "kotlin.Metadata"
|
||||
})
|
||||
|
||||
constructor.parameters.withIndex().forEach {
|
||||
if ((readersAsSerialized[it.value.name!!] ?.apply { this.resultsIndex = it.index }) == null) {
|
||||
// If there is no value in the byte stream to map to the parameter of the constructor
|
||||
// this is ok IFF it's a Kotlin class and the parameter is non nullable OR
|
||||
// its a Java class and the parameter is anything but an unboxed primitive.
|
||||
// Otherwise we throw the error and leave
|
||||
if ((isKotlin && !it.value.type.isMarkedNullable)
|
||||
|| (!isKotlin && isJavaPrimitive(it.value.type.jvmErasure.java))
|
||||
) {
|
||||
throw AMQPNotSerializableException(
|
||||
new.type,
|
||||
"New parameter \"${it.value.name}\" is mandatory, should be nullable for evolution " +
|
||||
"to work, isKotlinClass=$isKotlin type=${it.value.type}")
|
||||
}
|
||||
}
|
||||
}
|
||||
return EvolutionSerializerViaConstructor(new.type, factory, readersAsSerialized, constructor)
|
||||
}
|
||||
|
||||
private fun makeWithSetters(
|
||||
new: ObjectSerializer,
|
||||
factory: SerializerFactory,
|
||||
constructor: KFunction<Any>,
|
||||
readersAsSerialized: Map<String, OldParam>,
|
||||
classProperties: Map<String, PropertyDescriptor>): AMQPSerializer<Any> {
|
||||
val setters = propertiesForSerializationFromSetters(classProperties,
|
||||
new.type,
|
||||
factory).associateBy({ it.serializer.name }, { it })
|
||||
return EvolutionSerializerViaSetters(new.type, factory, readersAsSerialized, constructor, setters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a serialization object for deserialization only of objects serialised
|
||||
* as different versions of a class.
|
||||
*
|
||||
* @param old is an object holding the schema that represents the object
|
||||
* as it was serialised and the type descriptor of that type
|
||||
* @param new is the Serializer built for the Class as it exists now, not
|
||||
* how it was serialised and persisted.
|
||||
* @param factory the [SerializerFactory] associated with the serialization
|
||||
* context this serializer is being built for
|
||||
*/
|
||||
fun make(old: CompositeType,
|
||||
new: ObjectSerializer,
|
||||
factory: SerializerFactory
|
||||
): AMQPSerializer<Any> {
|
||||
// The order in which the properties were serialised is important and must be preserved
|
||||
val readersAsSerialized = LinkedHashMap<String, OldParam>()
|
||||
old.fields.forEach {
|
||||
readersAsSerialized[it.name] = try {
|
||||
OldParam(-1, PropertySerializer.make(it.name, EvolutionPropertyReader(),
|
||||
it.getTypeAsClass(factory.classloader), factory))
|
||||
} catch (e: ClassNotFoundException) {
|
||||
throw AMQPNotSerializableException(new.type, e.message ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
// cope with the situation where a generic interface was serialised as a type, in such cases
|
||||
// return the synthesised object which is, given the absence of a constructor, a no op
|
||||
val constructor = getEvolverConstructor(new.type, readersAsSerialized) ?: return new
|
||||
|
||||
val classProperties = new.type.asClass().propertyDescriptors()
|
||||
|
||||
return if (classProperties.isNotEmpty() && constructor.parameters.isEmpty()) {
|
||||
makeWithSetters(new, factory, constructor, readersAsSerialized, classProperties)
|
||||
} else {
|
||||
makeWithConstructor(new, factory, constructor, readersAsSerialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
throw UnsupportedOperationException("It should be impossible to write an evolution serializer")
|
||||
}
|
||||
}
|
||||
|
||||
class EvolutionSerializerViaConstructor(
|
||||
clazz: Type,
|
||||
factory: SerializerFactory,
|
||||
oldReaders: Map<String, EvolutionSerializer.OldParam>,
|
||||
kotlinConstructor: KFunction<Any>) : EvolutionSerializer(clazz, factory, oldReaders, kotlinConstructor) {
|
||||
/**
|
||||
* Unlike a normal [readObject] call where we simply apply the parameter deserialisers
|
||||
* to the object list of values we need to map that list, which is ordered per the
|
||||
* constructor of the original state of the object, we need to map the new parameter order
|
||||
* of the current constructor onto that list inserting nulls where new parameters are
|
||||
* encountered.
|
||||
*
|
||||
* TODO: Object references
|
||||
*/
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
context: SerializationContext
|
||||
): Any {
|
||||
if (obj !is List<*>) throw NotSerializableException("Body of described type is unexpected $obj")
|
||||
|
||||
val constructorArgs : Array<Any?> = arrayOfNulls<Any?>(kotlinConstructor.parameters.size)
|
||||
// *must* read all the parameters in the order they were serialized
|
||||
oldReaders.values.zip(obj).map { it.first.readProperty(it.second, schemas, input, constructorArgs, context) }
|
||||
|
||||
return javaConstructor?.newInstance(*(constructorArgs)) ?: throw NotSerializableException(
|
||||
"Attempt to deserialize an interface: $clazz. Serialized form is invalid.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specific instance of an [EvolutionSerializer] where the properties of the object are set via calling
|
||||
* named setter functions on the instantiated object.
|
||||
*/
|
||||
class EvolutionSerializerViaSetters(
|
||||
clazz: Type,
|
||||
factory: SerializerFactory,
|
||||
oldReaders: Map<String, EvolutionSerializer.OldParam>,
|
||||
kotlinConstructor: KFunction<Any>,
|
||||
private val setters: Map<String, PropertyAccessor>) : EvolutionSerializer(clazz, factory, oldReaders, kotlinConstructor) {
|
||||
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
context: SerializationContext
|
||||
): Any {
|
||||
if (obj !is List<*>) throw NotSerializableException("Body of described type is unexpected $obj")
|
||||
|
||||
val instance: Any = javaConstructor?.newInstance() ?: throw NotSerializableException(
|
||||
"Failed to instantiate instance of object $clazz")
|
||||
|
||||
// *must* read all the parameters in the order they were serialized
|
||||
oldReaders.values.zip(obj).forEach {
|
||||
// if that property still exists on the new object then set it
|
||||
it.first.property.readProperty(it.second, schemas, input, context).apply {
|
||||
setters[it.first.property.name]?.set(instance, this)
|
||||
}
|
||||
}
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instances of this type are injected into a [SerializerFactory] at creation time to dictate the
|
||||
* behaviour of evolution within that factory. Under normal circumstances this will simply
|
||||
* be an object that returns an [EvolutionSerializer]. Of course, any implementation that
|
||||
* extends this class can be written to invoke whatever behaviour is desired.
|
||||
*/
|
||||
interface EvolutionSerializerProvider {
|
||||
fun getEvolutionSerializer(
|
||||
factory: SerializerFactory,
|
||||
typeNotation: TypeNotation,
|
||||
newSerializer: AMQPSerializer<Any>,
|
||||
schemas: SerializationSchemas): AMQPSerializer<Any>
|
||||
}
|
||||
|
||||
/**
|
||||
* The normal use case for generating an [EvolutionSerializer]'s based on the differences
|
||||
* between the received schema and the class as it exists now on the class path,
|
||||
*/
|
||||
@KeepForDJVM
|
||||
object DefaultEvolutionSerializerProvider : EvolutionSerializerProvider {
|
||||
override fun getEvolutionSerializer(factory: SerializerFactory,
|
||||
typeNotation: TypeNotation,
|
||||
newSerializer: AMQPSerializer<Any>,
|
||||
schemas: SerializationSchemas): AMQPSerializer<Any> {
|
||||
return factory.registerByDescriptor(typeNotation.descriptor.name!!) {
|
||||
when (typeNotation) {
|
||||
is CompositeType -> EvolutionSerializer.make(typeNotation, newSerializer as ObjectSerializer, factory)
|
||||
is RestrictedType -> {
|
||||
// The fingerprint of a generic collection can be changed through bug fixes to the
|
||||
// fingerprinting function making it appear as if the class has altered whereas it hasn't.
|
||||
// Given we don't support the evolution of these generic containers, if it appears
|
||||
// one has been changed, simply return the original serializer and associate it with
|
||||
// both the new and old fingerprint
|
||||
if (newSerializer is CollectionSerializer || newSerializer is MapSerializer) {
|
||||
newSerializer
|
||||
} else if (newSerializer is EnumSerializer){
|
||||
EnumEvolutionSerializer.make(typeNotation, newSerializer, factory, schemas)
|
||||
}
|
||||
else {
|
||||
loggerFor<SerializerFactory>().error("typeNotation=${typeNotation.name} Need to evolve unsupported type")
|
||||
throw NotSerializableException ("${typeNotation.name} cannot be evolved")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,170 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.serialization.internal.model.*
|
||||
import java.io.NotSerializableException
|
||||
|
||||
/**
|
||||
* A factory that knows how to create serialisers when there is a mismatch between the remote and local type schemas.
|
||||
*/
|
||||
interface EvolutionSerializerFactory {
|
||||
|
||||
/**
|
||||
* Compare the given [RemoteTypeInformation] and [LocalTypeInformation], and construct (if needed) an evolution
|
||||
* serialiser that can take properties serialised in the remote schema and construct an object conformant to the local schema.
|
||||
*
|
||||
* Will return null if no evolution is necessary, because the schemas are compatible.
|
||||
*/
|
||||
fun getEvolutionSerializer(
|
||||
remote: RemoteTypeInformation,
|
||||
local: LocalTypeInformation): AMQPSerializer<Any>?
|
||||
}
|
||||
|
||||
class EvolutionSerializationException(remoteTypeInformation: RemoteTypeInformation, reason: String)
|
||||
: NotSerializableException(
|
||||
"""
|
||||
Cannot construct evolution serializer for remote type ${remoteTypeInformation.prettyPrint(false)}
|
||||
|
||||
$reason
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
class DefaultEvolutionSerializerFactory(
|
||||
private val localSerializerFactory: LocalSerializerFactory,
|
||||
private val classLoader: ClassLoader,
|
||||
private val mustPreserveDataWhenEvolving: Boolean): EvolutionSerializerFactory {
|
||||
|
||||
override fun getEvolutionSerializer(remote: RemoteTypeInformation,
|
||||
local: LocalTypeInformation): AMQPSerializer<Any>? =
|
||||
when(remote) {
|
||||
is RemoteTypeInformation.Composable ->
|
||||
if (local is LocalTypeInformation.Composable) remote.getEvolutionSerializer(local)
|
||||
else null
|
||||
is RemoteTypeInformation.AnEnum ->
|
||||
if (local is LocalTypeInformation.AnEnum) remote.getEvolutionSerializer(local)
|
||||
else null
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun RemoteTypeInformation.Composable.getEvolutionSerializer(
|
||||
localTypeInformation: LocalTypeInformation.Composable): AMQPSerializer<Any>? {
|
||||
// The no-op case: although the fingerprints don't match for some reason, we have compatible signatures.
|
||||
// This might happen because of inconsistent type erasure, changes to the behaviour of the fingerprinter,
|
||||
// or changes to the type itself - such as adding an interface - that do not change its serialisation/deserialisation
|
||||
// signature.
|
||||
if (propertyNamesMatch(localTypeInformation)) {
|
||||
// Make sure types are assignment-compatible, and return the local serializer for the type.
|
||||
validateCompatibility(localTypeInformation)
|
||||
return null
|
||||
}
|
||||
|
||||
// Failing that, we have to create an evolution serializer.
|
||||
val bestMatchEvolutionConstructor = findEvolverConstructor(localTypeInformation.evolutionConstructors, properties)
|
||||
val constructorForEvolution = bestMatchEvolutionConstructor?.constructor ?: localTypeInformation.constructor
|
||||
val evolverProperties = bestMatchEvolutionConstructor?.properties ?: localTypeInformation.properties
|
||||
|
||||
validateEvolvability(evolverProperties)
|
||||
|
||||
return buildComposableEvolutionSerializer(localTypeInformation, constructorForEvolution, evolverProperties)
|
||||
}
|
||||
|
||||
private fun RemoteTypeInformation.Composable.propertyNamesMatch(localTypeInformation: LocalTypeInformation.Composable): Boolean =
|
||||
properties.keys == localTypeInformation.properties.keys
|
||||
|
||||
private fun RemoteTypeInformation.Composable.validateCompatibility(localTypeInformation: LocalTypeInformation.Composable) {
|
||||
properties.asSequence().zip(localTypeInformation.properties.values.asSequence()).forEach { (remote, localProperty) ->
|
||||
val (name, remoteProperty) = remote
|
||||
val localClass = localProperty.type.observedType.asClass()
|
||||
val remoteClass = remoteProperty.type.typeIdentifier.getLocalType(classLoader).asClass()
|
||||
|
||||
if (!localClass.isAssignableFrom(remoteClass)) {
|
||||
throw EvolutionSerializationException(this,
|
||||
"Local type $localClass of property $name is not assignable from remote type $remoteClass")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find the evolution constructor with the highest version number whose parameters are all assignable from the
|
||||
// provided property types.
|
||||
private fun findEvolverConstructor(constructors: List<EvolutionConstructorInformation>,
|
||||
properties: Map<String, RemotePropertyInformation>): EvolutionConstructorInformation? {
|
||||
val propertyTypes = properties.mapValues { (_, info) -> info.type.typeIdentifier.getLocalType(classLoader).asClass() }
|
||||
|
||||
// Evolver constructors are listed in ascending version order, so we just want the last that matches.
|
||||
return constructors.lastOrNull { (_, evolverProperties) ->
|
||||
// We have a match if all mandatory evolver properties have a type-compatible property in the remote type.
|
||||
evolverProperties.all { (name, evolverProperty) ->
|
||||
val propertyType = propertyTypes[name]
|
||||
if (propertyType == null) !evolverProperty.isMandatory
|
||||
else evolverProperty.type.observedType.asClass().isAssignableFrom(propertyType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun RemoteTypeInformation.Composable.validateEvolvability(localProperties: Map<PropertyName, LocalPropertyInformation>) {
|
||||
val remotePropertyNames = properties.keys
|
||||
val localPropertyNames = localProperties.keys
|
||||
val deletedProperties = remotePropertyNames - localPropertyNames
|
||||
val newProperties = localPropertyNames - remotePropertyNames
|
||||
|
||||
// Here is where we can exercise a veto on evolutions that remove properties.
|
||||
if (deletedProperties.isNotEmpty() && mustPreserveDataWhenEvolving)
|
||||
throw EvolutionSerializationException(this,
|
||||
"Property ${deletedProperties.first()} of remote ContractState type is not present in local type, " +
|
||||
"and context is configured to prevent forwards-compatible deserialization.")
|
||||
|
||||
// Check mandatory-ness of constructor-set properties.
|
||||
newProperties.forEach { propertyName ->
|
||||
if (localProperties[propertyName]!!.mustBeProvided) throw EvolutionSerializationException(
|
||||
this,
|
||||
"Mandatory property $propertyName of local type is not present in remote type - " +
|
||||
"did someone remove a property from the schema without considering old clients?")
|
||||
}
|
||||
}
|
||||
|
||||
private val LocalPropertyInformation.mustBeProvided: Boolean get() = when(this) {
|
||||
is LocalPropertyInformation.ConstructorPairedProperty -> isMandatory
|
||||
is LocalPropertyInformation.PrivateConstructorPairedProperty -> isMandatory
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun RemoteTypeInformation.AnEnum.getEvolutionSerializer(
|
||||
localTypeInformation: LocalTypeInformation.AnEnum): AMQPSerializer<Any>? {
|
||||
if (members == localTypeInformation.members) return null
|
||||
|
||||
val remoteTransforms = transforms
|
||||
val localTransforms = localTypeInformation.getEnumTransforms(localSerializerFactory)
|
||||
val transforms = if (remoteTransforms.size > localTransforms.size) remoteTransforms else localTransforms
|
||||
|
||||
val localOrdinals = localTypeInformation.members.asSequence().mapIndexed { ord, member -> member to ord }.toMap()
|
||||
val remoteOrdinals = members.asSequence().mapIndexed { ord, member -> member to ord }.toMap()
|
||||
val rules = transforms.defaults + transforms.renames
|
||||
|
||||
// We just trust our transformation rules not to contain cycles here.
|
||||
tailrec fun findLocal(remote: String): String =
|
||||
if (remote in localOrdinals) remote
|
||||
else findLocal(rules[remote] ?: throw EvolutionSerializationException(
|
||||
this,
|
||||
"Cannot resolve local enum member $remote to a member of ${localOrdinals.keys} using rules $rules"
|
||||
))
|
||||
|
||||
val conversions = members.associate { it to findLocal(it) }
|
||||
val convertedOrdinals = remoteOrdinals.asSequence().map { (member, ord) -> ord to conversions[member]!! }.toMap()
|
||||
if (localOrdinals.any { (name, ordinal) -> convertedOrdinals[ordinal] != name })
|
||||
throw EvolutionSerializationException(
|
||||
this,
|
||||
"Constants have been reordered, additions must be appended to the end")
|
||||
|
||||
return EnumEvolutionSerializer(localTypeInformation.observedType, localSerializerFactory, conversions, localOrdinals)
|
||||
}
|
||||
|
||||
private fun RemoteTypeInformation.Composable.buildComposableEvolutionSerializer(
|
||||
localTypeInformation: LocalTypeInformation.Composable,
|
||||
constructor: LocalConstructorInformation,
|
||||
properties: Map<String, LocalPropertyInformation>): AMQPSerializer<Any> =
|
||||
EvolutionObjectSerializer.make(
|
||||
localTypeInformation,
|
||||
this,
|
||||
constructor,
|
||||
properties,
|
||||
classLoader)
|
||||
}
|
@ -1,202 +0,0 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import com.google.common.hash.Hasher
|
||||
import com.google.common.hash.Hashing
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.internal.isConcreteClass
|
||||
import net.corda.core.internal.kotlinObjectInstance
|
||||
import net.corda.core.utilities.toBase64
|
||||
import net.corda.serialization.internal.amqp.SerializerFactory.Companion.isPrimitive
|
||||
import java.lang.reflect.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Should be implemented by classes which wish to provide pluggable fingerprinting on types for a [SerializerFactory]
|
||||
*/
|
||||
@KeepForDJVM
|
||||
interface FingerPrinter {
|
||||
/**
|
||||
* Return a unique identifier for a type, usually this will take into account the constituent elements
|
||||
* of said type such that any modification to any sub element wll generate a different fingerprint
|
||||
*/
|
||||
fun fingerprint(type: Type): String
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the finger printing mechanism used by default
|
||||
*/
|
||||
@KeepForDJVM
|
||||
class SerializerFingerPrinter(val factory: SerializerFactory) : FingerPrinter {
|
||||
|
||||
/**
|
||||
* The method generates a fingerprint for a given JVM [Type] that should be unique to the schema representation.
|
||||
* Thus it only takes into account properties and types and only supports the same object graph subset as the overall
|
||||
* serialization code.
|
||||
*
|
||||
* The idea being that even for two classes that share the same name but differ in a minor way, the fingerprint will be
|
||||
* different.
|
||||
*/
|
||||
override fun fingerprint(type: Type): String = FingerPrintingState(factory).fingerprint(type)
|
||||
}
|
||||
|
||||
// Representation of the current state of fingerprinting
|
||||
internal class FingerPrintingState(private val factory: SerializerFactory) {
|
||||
|
||||
companion object {
|
||||
private const val ARRAY_HASH: String = "Array = true"
|
||||
private const val ENUM_HASH: String = "Enum = true"
|
||||
private const val ALREADY_SEEN_HASH: String = "Already seen = true"
|
||||
private const val NULLABLE_HASH: String = "Nullable = true"
|
||||
private const val NOT_NULLABLE_HASH: String = "Nullable = false"
|
||||
private const val ANY_TYPE_HASH: String = "Any type = true"
|
||||
}
|
||||
|
||||
private val typesSeen: MutableSet<Type> = mutableSetOf()
|
||||
private var currentContext: Type? = null
|
||||
private var hasher: Hasher = newDefaultHasher()
|
||||
|
||||
// Fingerprint the type recursively, and return the encoded fingerprint written into the hasher.
|
||||
fun fingerprint(type: Type) = fingerprintType(type).hasher.fingerprint
|
||||
|
||||
// This method concatenates various elements of the types recursively as unencoded strings into the hasher,
|
||||
// effectively creating a unique string for a type which we then hash in the calling function above.
|
||||
private fun fingerprintType(type: Type): FingerPrintingState = apply {
|
||||
// Don't go round in circles.
|
||||
if (hasSeen(type)) append(ALREADY_SEEN_HASH)
|
||||
else ifThrowsAppend(
|
||||
{ type.typeName },
|
||||
{
|
||||
typesSeen.add(type)
|
||||
currentContext = type
|
||||
fingerprintNewType(type)
|
||||
})
|
||||
}
|
||||
|
||||
// For a type we haven't seen before, determine the correct path depending on the type of type it is.
|
||||
private fun fingerprintNewType(type: Type) = when (type) {
|
||||
is ParameterizedType -> fingerprintParameterizedType(type)
|
||||
// Previously, we drew a distinction between TypeVariable, WildcardType, and AnyType, changing
|
||||
// the signature of the fingerprinted object. This, however, doesn't work as it breaks bi-
|
||||
// directional fingerprints. That is, fingerprinting a concrete instance of a generic
|
||||
// type (Example<Int>), creates a different fingerprint from the generic type itself (Example<T>)
|
||||
//
|
||||
// On serialization Example<Int> is treated as Example<T>, a TypeVariable
|
||||
// On deserialisation it is seen as Example<?>, A WildcardType *and* a TypeVariable
|
||||
// Note: AnyType is a special case of WildcardType used in other parts of the
|
||||
// serializer so both cases need to be dealt with here
|
||||
//
|
||||
// If we treat these types as fundamentally different and alter the fingerprint we will
|
||||
// end up breaking into the evolver when we shouldn't or, worse, evoking the carpenter.
|
||||
is SerializerFactory.AnyType,
|
||||
is WildcardType,
|
||||
is TypeVariable<*> -> append("?$ANY_TYPE_HASH")
|
||||
is Class<*> -> fingerprintClass(type)
|
||||
is GenericArrayType -> fingerprintType(type.genericComponentType).append(ARRAY_HASH)
|
||||
else -> throw AMQPNotSerializableException(type, "Don't know how to hash")
|
||||
}
|
||||
|
||||
private fun fingerprintClass(type: Class<*>) = when {
|
||||
type.isArray -> fingerprintType(type.componentType).append(ARRAY_HASH)
|
||||
type.isPrimitiveOrCollection -> append(type.name)
|
||||
type.isEnum -> fingerprintEnum(type)
|
||||
else -> fingerprintWithCustomSerializerOrElse(type, type) {
|
||||
if (type.kotlinObjectInstance != null) append(type.name)
|
||||
else fingerprintObject(type)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fingerprintParameterizedType(type: ParameterizedType) {
|
||||
// Hash the rawType + params
|
||||
type.asClass().let { clazz ->
|
||||
if (clazz.isCollectionOrMap) append(clazz.name)
|
||||
else fingerprintWithCustomSerializerOrElse(clazz, type) {
|
||||
fingerprintObject(type)
|
||||
}
|
||||
}
|
||||
|
||||
// ...and concatenate the type data for each parameter type.
|
||||
type.actualTypeArguments.forEach { paramType ->
|
||||
fingerprintType(paramType)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fingerprintObject(type: Type) {
|
||||
// Hash the class + properties + interfaces
|
||||
append(type.asClass().name)
|
||||
|
||||
orderedPropertiesForSerialization(type).forEach { prop ->
|
||||
fingerprintType(prop.serializer.resolvedType)
|
||||
fingerprintPropSerialiser(prop)
|
||||
}
|
||||
|
||||
interfacesForSerialization(type, factory).forEach { iface ->
|
||||
fingerprintType(iface)
|
||||
}
|
||||
}
|
||||
|
||||
// ensures any change to the enum (adding constants) will trigger the need for evolution
|
||||
private fun fingerprintEnum(type: Class<*>) {
|
||||
append(type.enumConstants.joinToString())
|
||||
append(type.name)
|
||||
append(ENUM_HASH)
|
||||
}
|
||||
|
||||
private fun fingerprintPropSerialiser(prop: PropertyAccessor) {
|
||||
append(prop.serializer.name)
|
||||
append(if (prop.serializer.mandatory) NOT_NULLABLE_HASH
|
||||
else NULLABLE_HASH)
|
||||
}
|
||||
|
||||
// Write the given character sequence into the hasher.
|
||||
private fun append(chars: CharSequence) {
|
||||
hasher = hasher.putUnencodedChars(chars)
|
||||
}
|
||||
|
||||
// Give any custom serializers loaded into the factory the chance to supply their own type-descriptors
|
||||
private fun fingerprintWithCustomSerializerOrElse(
|
||||
clazz: Class<*>,
|
||||
declaredType: Type,
|
||||
defaultAction: () -> Unit)
|
||||
: Unit = factory.findCustomSerializer(clazz, declaredType)?.let {
|
||||
append(it.typeDescriptor)
|
||||
} ?: defaultAction()
|
||||
|
||||
// Test whether we are in a state in which we have already seen the given type.
|
||||
//
|
||||
// We don't include Example<?> and Example<T> where type is ? or T in this otherwise we
|
||||
// generate different fingerprints for class Outer<T>(val a: Inner<T>) when serialising
|
||||
// and deserializing (assuming deserialization is occurring in a factory that didn't
|
||||
// serialise the object in the first place (and thus the cache lookup fails). This is also
|
||||
// true of Any, where we need Example<A, B> and Example<?, ?> to have the same fingerprint
|
||||
private fun hasSeen(type: Type) = (type in typesSeen)
|
||||
&& (type !== SerializerFactory.AnyType)
|
||||
&& (type !is TypeVariable<*>)
|
||||
&& (type !is WildcardType)
|
||||
|
||||
private fun orderedPropertiesForSerialization(type: Type): List<PropertyAccessor> {
|
||||
return propertiesForSerialization(
|
||||
if (type.asClass().isConcreteClass) constructorForDeserialization(type) else null,
|
||||
currentContext ?: type,
|
||||
factory).serializationOrder
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// region Utility functions
|
||||
|
||||
// Create a new instance of the [Hasher] used for fingerprinting by the default [SerializerFingerPrinter]
|
||||
private fun newDefaultHasher() = Hashing.murmur3_128().newHasher()
|
||||
|
||||
// We obtain a fingerprint from a [Hasher] by taking the Base 64 encoding of its hash bytes
|
||||
private val Hasher.fingerprint get() = hash().asBytes().toBase64()
|
||||
|
||||
internal fun fingerprintForDescriptors(vararg typeDescriptors: String): String =
|
||||
newDefaultHasher().putUnencodedChars(typeDescriptors.joinToString()).fingerprint
|
||||
|
||||
private val Class<*>.isCollectionOrMap get() =
|
||||
(Collection::class.java.isAssignableFrom(this) || Map::class.java.isAssignableFrom(this))
|
||||
&& !EnumSet::class.java.isAssignableFrom(this)
|
||||
|
||||
private val Class<*>.isPrimitiveOrCollection get() =
|
||||
isPrimitive(this) || isCollectionOrMap
|
||||
// endregion
|
@ -0,0 +1,226 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.core.internal.kotlinObjectInstance
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.serialization.internal.model.*
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
import java.lang.reflect.WildcardType
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* A factory that handles the serialisation and deserialisation of [Type]s visible from a given [ClassLoader].
|
||||
*
|
||||
* Unlike the [RemoteSerializerFactory], which deals with types for which we have [Schema] information and serialised data,
|
||||
* the [LocalSerializerFactory] deals with types for which we have a Java [Type] (and perhaps some in-memory data, from which
|
||||
* we can discover the actual [Class] we are working with.
|
||||
*/
|
||||
interface LocalSerializerFactory {
|
||||
/**
|
||||
* The [ClassWhitelist] used by this factory. Classes must be whitelisted for serialization, because they are expected
|
||||
* to be written in a secure manner.
|
||||
*/
|
||||
val whitelist: ClassWhitelist
|
||||
|
||||
/**
|
||||
* The [ClassLoader] used by this factory.
|
||||
*/
|
||||
val classloader: ClassLoader
|
||||
|
||||
/**
|
||||
* Obtain an [AMQPSerializer] for an object of actual type [actualClass], and declared type [declaredType].
|
||||
*/
|
||||
fun get(actualClass: Class<*>, declaredType: Type): AMQPSerializer<Any>
|
||||
|
||||
/**
|
||||
* Obtain an [AMQPSerializer] for the [declaredType].
|
||||
*/
|
||||
fun get(declaredType: Type): AMQPSerializer<Any> = get(getTypeInformation(declaredType))
|
||||
|
||||
/**
|
||||
* Obtain an [AMQPSerializer] for the type having the given [typeInformation].
|
||||
*/
|
||||
fun get(typeInformation: LocalTypeInformation): AMQPSerializer<Any>
|
||||
|
||||
/**
|
||||
* Obtain [LocalTypeInformation] for the given [Type].
|
||||
*/
|
||||
fun getTypeInformation(type: Type): LocalTypeInformation
|
||||
|
||||
/**
|
||||
* Use the [FingerPrinter] to create a type descriptor for the given [type].
|
||||
*/
|
||||
fun createDescriptor(type: Type): Symbol = createDescriptor(getTypeInformation(type))
|
||||
|
||||
/**
|
||||
* Use the [FingerPrinter] to create a type descriptor for the given [typeInformation].
|
||||
*/
|
||||
fun createDescriptor(typeInformation: LocalTypeInformation): Symbol
|
||||
|
||||
/**
|
||||
* Obtain or register [Transform]s for the given class [name].
|
||||
*
|
||||
* Eventually this information should be moved into the [LocalTypeInformation] for the type.
|
||||
*/
|
||||
fun getOrBuildTransform(name: String, builder: () -> EnumMap<TransformTypes, MutableList<Transform>>):
|
||||
EnumMap<TransformTypes, MutableList<Transform>>
|
||||
}
|
||||
|
||||
/**
|
||||
* A [LocalSerializerFactory] equipped with a [LocalTypeModel] and a [FingerPrinter] to help it build fingerprint-based descriptors
|
||||
* and serializers for local types.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class DefaultLocalSerializerFactory(
|
||||
override val whitelist: ClassWhitelist,
|
||||
private val typeModel: LocalTypeModel,
|
||||
private val fingerPrinter: FingerPrinter,
|
||||
override val classloader: ClassLoader,
|
||||
private val descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry,
|
||||
private val customSerializerRegistry: CustomSerializerRegistry,
|
||||
private val onlyCustomSerializers: Boolean)
|
||||
: LocalSerializerFactory {
|
||||
|
||||
companion object {
|
||||
val logger = contextLogger()
|
||||
}
|
||||
|
||||
private val transformsCache: MutableMap<String, EnumMap<TransformTypes, MutableList<Transform>>> = DefaultCacheProvider.createCache()
|
||||
private val serializersByType: MutableMap<TypeIdentifier, AMQPSerializer<Any>> = DefaultCacheProvider.createCache()
|
||||
|
||||
override fun createDescriptor(typeInformation: LocalTypeInformation): Symbol =
|
||||
Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerPrinter.fingerprint(typeInformation)}")
|
||||
|
||||
override fun getTypeInformation(type: Type): LocalTypeInformation = typeModel.inspect(type)
|
||||
|
||||
override fun getOrBuildTransform(name: String, builder: () -> EnumMap<TransformTypes, MutableList<Transform>>):
|
||||
EnumMap<TransformTypes, MutableList<Transform>> =
|
||||
transformsCache.computeIfAbsent(name) { _ -> builder() }
|
||||
|
||||
override fun get(typeInformation: LocalTypeInformation): AMQPSerializer<Any> =
|
||||
get(typeInformation.observedType, typeInformation)
|
||||
|
||||
private fun make(typeInformation: LocalTypeInformation, build: () -> AMQPSerializer<Any>) =
|
||||
make(typeInformation.typeIdentifier, build)
|
||||
|
||||
private fun make(typeIdentifier: TypeIdentifier, build: () -> AMQPSerializer<Any>) =
|
||||
serializersByType.computeIfAbsent(typeIdentifier) { _ -> build() }
|
||||
|
||||
private fun get(declaredType: Type, localTypeInformation: LocalTypeInformation): AMQPSerializer<Any> {
|
||||
val declaredClass = declaredType.asClass()
|
||||
|
||||
// can be useful to enable but will be *extremely* chatty if you do
|
||||
logger.trace { "Get Serializer for $declaredClass ${declaredType.typeName}" }
|
||||
|
||||
return when(localTypeInformation) {
|
||||
is LocalTypeInformation.ACollection -> makeDeclaredCollection(localTypeInformation)
|
||||
is LocalTypeInformation.AMap -> makeDeclaredMap(localTypeInformation)
|
||||
is LocalTypeInformation.AnEnum -> makeDeclaredEnum(localTypeInformation, declaredType, declaredClass)
|
||||
else -> makeClassSerializer(declaredClass, declaredType, declaredType, localTypeInformation)
|
||||
}.also { serializer -> descriptorBasedSerializerRegistry[serializer.typeDescriptor.toString()] = serializer }
|
||||
}
|
||||
|
||||
private fun makeDeclaredEnum(localTypeInformation: LocalTypeInformation, declaredType: Type, declaredClass: Class<*>): AMQPSerializer<Any> =
|
||||
make(localTypeInformation) {
|
||||
whitelist.requireWhitelisted(declaredType)
|
||||
EnumSerializer(declaredType, declaredClass, this)
|
||||
}
|
||||
|
||||
private fun makeActualEnum(localTypeInformation: LocalTypeInformation, declaredType: Type, declaredClass: Class<*>): AMQPSerializer<Any> =
|
||||
make(localTypeInformation) {
|
||||
whitelist.requireWhitelisted(declaredType)
|
||||
EnumSerializer(declaredType, declaredClass, this)
|
||||
}
|
||||
|
||||
private fun makeDeclaredCollection(localTypeInformation: LocalTypeInformation.ACollection): AMQPSerializer<Any> {
|
||||
val resolved = CollectionSerializer.resolveDeclared(localTypeInformation)
|
||||
return make(resolved) {
|
||||
CollectionSerializer(resolved.typeIdentifier.getLocalType(classloader) as ParameterizedType, this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeDeclaredMap(localTypeInformation: LocalTypeInformation.AMap): AMQPSerializer<Any> {
|
||||
val resolved = MapSerializer.resolveDeclared(localTypeInformation)
|
||||
return make(resolved) {
|
||||
MapSerializer(resolved.typeIdentifier.getLocalType(classloader) as ParameterizedType, this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun get(actualClass: Class<*>, declaredType: Type): AMQPSerializer<Any> {
|
||||
// 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()
|
||||
val actualType: Type = inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType
|
||||
val declaredTypeInformation = typeModel.inspect(declaredType)
|
||||
val actualTypeInformation = typeModel.inspect(actualType)
|
||||
|
||||
return when(actualTypeInformation) {
|
||||
is LocalTypeInformation.ACollection -> makeActualCollection(actualClass,declaredTypeInformation as? LocalTypeInformation.ACollection ?: actualTypeInformation)
|
||||
is LocalTypeInformation.AMap -> makeActualMap(declaredType, actualClass,declaredTypeInformation as? LocalTypeInformation.AMap ?: actualTypeInformation)
|
||||
is LocalTypeInformation.AnEnum -> makeActualEnum(actualTypeInformation, actualType, actualClass)
|
||||
else -> makeClassSerializer(actualClass, actualType, declaredType, actualTypeInformation)
|
||||
}.also { serializer -> descriptorBasedSerializerRegistry[serializer.typeDescriptor.toString()] = serializer }
|
||||
}
|
||||
|
||||
private fun makeActualMap(declaredType: Type, actualClass: Class<*>, typeInformation: LocalTypeInformation.AMap): AMQPSerializer<Any> {
|
||||
declaredType.asClass().checkSupportedMapType()
|
||||
val resolved = MapSerializer.resolveActual(actualClass, typeInformation)
|
||||
return make(resolved) {
|
||||
MapSerializer(resolved.typeIdentifier.getLocalType(classloader) as ParameterizedType, this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeActualCollection(actualClass: Class<*>, typeInformation: LocalTypeInformation.ACollection): AMQPSerializer<Any> {
|
||||
val resolved = CollectionSerializer.resolveActual(actualClass, typeInformation)
|
||||
|
||||
return serializersByType.computeIfAbsent(resolved.typeIdentifier) {
|
||||
CollectionSerializer(resolved.typeIdentifier.getLocalType(classloader) as ParameterizedType, this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeClassSerializer(
|
||||
clazz: Class<*>,
|
||||
type: Type,
|
||||
declaredType: Type,
|
||||
typeInformation: LocalTypeInformation
|
||||
): AMQPSerializer<Any> = make(typeInformation) {
|
||||
logger.debug { "class=${clazz.simpleName}, type=$type is a composite type" }
|
||||
when {
|
||||
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 AMQPNotSerializableException(
|
||||
type,
|
||||
"Serializer does not support synthetic classes")
|
||||
AMQPTypeIdentifiers.isPrimitive(typeInformation.typeIdentifier) -> AMQPPrimitiveSerializer(clazz)
|
||||
else -> customSerializerRegistry.findCustomSerializer(clazz, declaredType) ?:
|
||||
makeNonCustomSerializer(type, typeInformation, clazz)
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeNonCustomSerializer(type: Type, typeInformation: LocalTypeInformation, clazz: Class<*>): AMQPSerializer<Any> = when {
|
||||
onlyCustomSerializers -> throw AMQPNotSerializableException(type, "Only allowing custom serializers")
|
||||
type.isArray() ->
|
||||
if (clazz.componentType.isPrimitive) PrimArraySerializer.make(type, this)
|
||||
else {
|
||||
ArraySerializer.make(type, this)
|
||||
}
|
||||
else -> {
|
||||
val singleton = clazz.kotlinObjectInstance
|
||||
if (singleton != null) {
|
||||
whitelist.requireWhitelisted(clazz)
|
||||
SingletonSerializer(clazz, singleton, this)
|
||||
} else {
|
||||
whitelist.requireWhitelisted(type)
|
||||
ObjectSerializer.make(typeInformation, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -4,6 +4,8 @@ import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.StubOutForDJVM
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.serialization.internal.model.LocalTypeInformation
|
||||
import net.corda.serialization.internal.model.TypeIdentifier
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.io.NotSerializableException
|
||||
@ -18,11 +20,10 @@ private typealias MapCreationFunction = (Map<*, *>) -> Map<*, *>
|
||||
* Serialization / deserialization of certain supported [Map] types.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
class MapSerializer(private val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer<Any> {
|
||||
override val type: Type = (declaredType as? DeserializedParameterizedType)
|
||||
?: DeserializedParameterizedType.make(SerializerFactory.nameForType(declaredType), factory.classloader)
|
||||
override val typeDescriptor: Symbol = Symbol.valueOf(
|
||||
"$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")
|
||||
class MapSerializer(private val declaredType: ParameterizedType, factory: LocalSerializerFactory) : AMQPSerializer<Any> {
|
||||
override val type: Type = declaredType
|
||||
|
||||
override val typeDescriptor: Symbol = factory.createDescriptor(type)
|
||||
|
||||
companion object {
|
||||
// NB: Order matters in this map, the most specific classes should be listed at the end
|
||||
@ -39,29 +40,43 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial
|
||||
}
|
||||
))
|
||||
|
||||
private val supportedTypeIdentifiers = supportedTypes.keys.asSequence()
|
||||
.map { TypeIdentifier.forGenericType(it) }.toSet()
|
||||
|
||||
private fun findConcreteType(clazz: Class<*>): MapCreationFunction {
|
||||
return supportedTypes[clazz] ?: throw AMQPNotSerializableException(clazz, "Unsupported map type $clazz.")
|
||||
}
|
||||
|
||||
fun deriveParameterizedType(declaredType: Type, declaredClass: Class<*>, actualClass: Class<*>?): ParameterizedType {
|
||||
declaredClass.checkSupportedMapType()
|
||||
if (supportedTypes.containsKey(declaredClass)) {
|
||||
// Simple case - it is already known to be a map.
|
||||
return deriveParametrizedType(declaredType, uncheckedCast(declaredClass))
|
||||
} else if (actualClass != null && Map::class.java.isAssignableFrom(actualClass)) {
|
||||
// Declared class is not map, but [actualClass] is - represent it accordingly.
|
||||
val mapClass = findMostSuitableMapType(actualClass)
|
||||
return deriveParametrizedType(declaredType, mapClass)
|
||||
}
|
||||
fun resolveDeclared(declaredTypeInformation: LocalTypeInformation.AMap): LocalTypeInformation.AMap {
|
||||
declaredTypeInformation.observedType.asClass().checkSupportedMapType()
|
||||
if (supportedTypeIdentifiers.contains(declaredTypeInformation.typeIdentifier.erased))
|
||||
return if (!declaredTypeInformation.isErased) declaredTypeInformation
|
||||
else declaredTypeInformation.withParameters(LocalTypeInformation.Unknown, LocalTypeInformation.Unknown)
|
||||
|
||||
throw AMQPNotSerializableException(declaredType,
|
||||
"Cannot derive map type for declaredType=\"$declaredType\", declaredClass=\"$declaredClass\", actualClass=\"$actualClass\"")
|
||||
throw NotSerializableException("Cannot derive map type for declared type " +
|
||||
declaredTypeInformation.prettyPrint(false))
|
||||
}
|
||||
|
||||
private fun deriveParametrizedType(declaredType: Type, collectionClass: Class<out Map<*, *>>): ParameterizedType =
|
||||
(declaredType as? ParameterizedType)
|
||||
?: DeserializedParameterizedType(collectionClass, arrayOf(SerializerFactory.AnyType, SerializerFactory.AnyType))
|
||||
fun resolveActual(actualClass: Class<*>, declaredTypeInformation: LocalTypeInformation.AMap): LocalTypeInformation.AMap {
|
||||
declaredTypeInformation.observedType.asClass().checkSupportedMapType()
|
||||
if (supportedTypeIdentifiers.contains(declaredTypeInformation.typeIdentifier.erased)) {
|
||||
return if (!declaredTypeInformation.isErased) declaredTypeInformation
|
||||
else declaredTypeInformation.withParameters(LocalTypeInformation.Unknown, LocalTypeInformation.Unknown)
|
||||
}
|
||||
|
||||
val mapClass = findMostSuitableMapType(actualClass)
|
||||
val erasedInformation = LocalTypeInformation.AMap(
|
||||
mapClass,
|
||||
TypeIdentifier.forClass(mapClass),
|
||||
LocalTypeInformation.Unknown, LocalTypeInformation.Unknown)
|
||||
|
||||
return when(declaredTypeInformation.typeIdentifier) {
|
||||
is TypeIdentifier.Parameterised -> erasedInformation.withParameters(
|
||||
declaredTypeInformation.keyType,
|
||||
declaredTypeInformation.valueType)
|
||||
else -> erasedInformation.withParameters(LocalTypeInformation.Unknown, LocalTypeInformation.Unknown)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findMostSuitableMapType(actualClass: Class<*>): Class<out Map<*, *>> =
|
||||
MapSerializer.supportedTypes.keys.findLast { it.isAssignableFrom(actualClass) }!!
|
||||
@ -69,7 +84,7 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial
|
||||
|
||||
private val concreteBuilder: MapCreationFunction = findConcreteType(declaredType.rawType as Class<*>)
|
||||
|
||||
private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "map", Descriptor(typeDescriptor), emptyList())
|
||||
private val typeNotation: TypeNotation = RestrictedType(AMQPTypeIdentifiers.nameForType(declaredType), null, emptyList(), "map", Descriptor(typeDescriptor), emptyList())
|
||||
|
||||
private val inboundKeyType = declaredType.actualTypeArguments[0]
|
||||
private val outboundKeyType = resolveTypeVariables(inboundKeyType, null)
|
||||
@ -108,7 +123,6 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
context: SerializationContext
|
||||
): 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<Pair<Any?, Any?>> = (obj as Map<*, *>).map { readEntry(schemas, input, it, context) }
|
||||
concreteBuilder(entries.toMap())
|
||||
}
|
||||
|
@ -0,0 +1,112 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.serialization.internal.model.*
|
||||
import java.io.NotSerializableException
|
||||
|
||||
interface ObjectBuilder {
|
||||
|
||||
companion object {
|
||||
fun makeProvider(typeInformation: LocalTypeInformation.Composable): () -> ObjectBuilder =
|
||||
makeProvider(typeInformation.typeIdentifier, typeInformation.constructor, typeInformation.properties)
|
||||
|
||||
fun makeProvider(typeIdentifier: TypeIdentifier, constructor: LocalConstructorInformation, properties: Map<String, LocalPropertyInformation>): () -> ObjectBuilder {
|
||||
val nonCalculatedProperties = properties.asSequence()
|
||||
.filterNot { (name, property) -> property.isCalculated }
|
||||
.sortedBy { (name, _) -> name }
|
||||
.map { (_, property) -> property }
|
||||
.toList()
|
||||
|
||||
val propertyIndices = nonCalculatedProperties.mapNotNull {
|
||||
when(it) {
|
||||
is LocalPropertyInformation.ConstructorPairedProperty -> it.constructorSlot.parameterIndex
|
||||
is LocalPropertyInformation.PrivateConstructorPairedProperty -> it.constructorSlot.parameterIndex
|
||||
else -> null
|
||||
}
|
||||
}.toIntArray()
|
||||
|
||||
if (propertyIndices.isNotEmpty()) {
|
||||
if (propertyIndices.size != nonCalculatedProperties.size) {
|
||||
throw NotSerializableException(
|
||||
"Some but not all properties of ${typeIdentifier.prettyPrint(false)} " +
|
||||
"are constructor-based")
|
||||
}
|
||||
return { ConstructorBasedObjectBuilder(constructor, propertyIndices) }
|
||||
}
|
||||
|
||||
val getterSetter = nonCalculatedProperties.filterIsInstance<LocalPropertyInformation.GetterSetterProperty>()
|
||||
return { SetterBasedObjectBuilder(constructor, getterSetter) }
|
||||
}
|
||||
}
|
||||
|
||||
fun initialize()
|
||||
fun populate(slot: Int, value: Any?)
|
||||
fun build(): Any
|
||||
}
|
||||
|
||||
class SetterBasedObjectBuilder(
|
||||
val constructor: LocalConstructorInformation,
|
||||
val properties: List<LocalPropertyInformation.GetterSetterProperty>): ObjectBuilder {
|
||||
|
||||
private lateinit var target: Any
|
||||
|
||||
override fun initialize() {
|
||||
target = constructor.observedMethod.call()
|
||||
}
|
||||
|
||||
override fun populate(slot: Int, value: Any?) {
|
||||
properties[slot].observedSetter.invoke(target, value)
|
||||
}
|
||||
|
||||
override fun build(): Any = target
|
||||
}
|
||||
|
||||
class ConstructorBasedObjectBuilder(
|
||||
val constructor: LocalConstructorInformation,
|
||||
val parameterIndices: IntArray): ObjectBuilder {
|
||||
|
||||
private val params = arrayOfNulls<Any>(parameterIndices.size)
|
||||
|
||||
override fun initialize() {}
|
||||
|
||||
override fun populate(slot: Int, value: Any?) {
|
||||
if (slot >= parameterIndices.size) {
|
||||
assert(false)
|
||||
}
|
||||
val parameterIndex = parameterIndices[slot]
|
||||
if (parameterIndex >= params.size) {
|
||||
assert(false)
|
||||
}
|
||||
params[parameterIndex] = value
|
||||
}
|
||||
|
||||
override fun build(): Any = constructor.observedMethod.call(*params)
|
||||
}
|
||||
|
||||
class EvolutionObjectBuilder(private val localBuilder: ObjectBuilder, val slotAssignments: IntArray): ObjectBuilder {
|
||||
|
||||
companion object {
|
||||
fun makeProvider(typeIdentifier: TypeIdentifier, constructor: LocalConstructorInformation, localProperties: Map<String, LocalPropertyInformation>, providedProperties: List<String>): () -> ObjectBuilder {
|
||||
val localBuilderProvider = ObjectBuilder.makeProvider(typeIdentifier, constructor, localProperties)
|
||||
val localPropertyIndices = localProperties.asSequence()
|
||||
.filter { (_, property) -> !property.isCalculated }
|
||||
.mapIndexed { slot, (name, _) -> name to slot }
|
||||
.toMap()
|
||||
|
||||
val reroutedIndices = providedProperties.map { propertyName -> localPropertyIndices[propertyName] ?: -1 }
|
||||
.toIntArray()
|
||||
|
||||
return { EvolutionObjectBuilder(localBuilderProvider(), reroutedIndices) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun initialize() {
|
||||
localBuilder.initialize()
|
||||
}
|
||||
|
||||
override fun populate(slot: Int, value: Any?) {
|
||||
val slotAssignment = slotAssignments[slot]
|
||||
if (slotAssignment != -1) localBuilder.populate(slotAssignment, value)
|
||||
}
|
||||
|
||||
override fun build(): Any = localBuilder.build()
|
||||
}
|
@ -1,185 +1,204 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.core.internal.isConcreteClass
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.serialization.internal.amqp.SerializerFactory.Companion.nameForType
|
||||
import net.corda.serialization.internal.model.*
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.Constructor
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.lang.reflect.Type
|
||||
import kotlin.reflect.jvm.javaConstructor
|
||||
|
||||
/**
|
||||
* Responsible for serializing and deserializing a regular object instance via a series of properties
|
||||
* (matched with a constructor).
|
||||
*/
|
||||
open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerializer<Any> {
|
||||
override val type: Type get() = clazz
|
||||
open val kotlinConstructor = if (clazz.asClass().isConcreteClass) constructorForDeserialization(clazz) else null
|
||||
val javaConstructor by lazy { kotlinConstructor?.javaConstructor }
|
||||
interface ObjectSerializer : AMQPSerializer<Any> {
|
||||
|
||||
val propertySerializers: Map<String, PropertySerializer>
|
||||
val fields: List<Field>
|
||||
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
fun make(typeInformation: LocalTypeInformation, factory: LocalSerializerFactory): ObjectSerializer {
|
||||
val typeDescriptor = factory.createDescriptor(typeInformation)
|
||||
val typeNotation = TypeNotationGenerator.getTypeNotation(typeInformation, typeDescriptor)
|
||||
|
||||
return when (typeInformation) {
|
||||
is LocalTypeInformation.Composable ->
|
||||
makeForComposable(typeInformation, typeNotation, typeDescriptor, factory)
|
||||
is LocalTypeInformation.AnInterface,
|
||||
is LocalTypeInformation.Abstract ->
|
||||
makeForAbstract(typeNotation, typeInformation, typeDescriptor, factory)
|
||||
else -> throw NotSerializableException("Cannot build object serializer for $typeInformation")
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeForAbstract(typeNotation: CompositeType,
|
||||
typeInformation: LocalTypeInformation,
|
||||
typeDescriptor: Symbol,
|
||||
factory: LocalSerializerFactory): AbstractObjectSerializer {
|
||||
val propertySerializers = makePropertySerializers(typeInformation.propertiesOrEmptyMap, factory)
|
||||
val writer = ComposableObjectWriter(typeNotation, typeInformation.interfacesOrEmptyList, propertySerializers)
|
||||
return AbstractObjectSerializer(typeInformation.observedType, typeDescriptor, propertySerializers,
|
||||
typeNotation.fields, writer)
|
||||
}
|
||||
|
||||
private fun makeForComposable(typeInformation: LocalTypeInformation.Composable,
|
||||
typeNotation: CompositeType,
|
||||
typeDescriptor: Symbol,
|
||||
factory: LocalSerializerFactory): ComposableObjectSerializer {
|
||||
val propertySerializers = makePropertySerializers(typeInformation.properties, factory)
|
||||
val reader = ComposableObjectReader(
|
||||
typeInformation.typeIdentifier,
|
||||
propertySerializers,
|
||||
ObjectBuilder.makeProvider(typeInformation))
|
||||
|
||||
val writer = ComposableObjectWriter(
|
||||
typeNotation,
|
||||
typeInformation.interfaces,
|
||||
propertySerializers)
|
||||
|
||||
return ComposableObjectSerializer(
|
||||
typeInformation.observedType,
|
||||
typeDescriptor,
|
||||
propertySerializers,
|
||||
typeNotation.fields,
|
||||
reader,
|
||||
writer)
|
||||
}
|
||||
|
||||
private fun makePropertySerializers(properties: Map<PropertyName, LocalPropertyInformation>,
|
||||
factory: LocalSerializerFactory): Map<String, PropertySerializer> =
|
||||
properties.mapValues { (name, property) ->
|
||||
ComposableTypePropertySerializer.make(name, property, factory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open val propertySerializers: PropertySerializers by lazy {
|
||||
propertiesForSerialization(kotlinConstructor, clazz, factory)
|
||||
}
|
||||
class ComposableObjectSerializer(
|
||||
override val type: Type,
|
||||
override val typeDescriptor: Symbol,
|
||||
override val propertySerializers: Map<PropertyName, PropertySerializer>,
|
||||
override val fields: List<Field>,
|
||||
private val reader: ComposableObjectReader,
|
||||
private val writer: ComposableObjectWriter): ObjectSerializer {
|
||||
|
||||
private val typeName = nameForType(clazz)
|
||||
override fun writeClassInfo(output: SerializationOutput) = writer.writeClassInfo(output)
|
||||
|
||||
override val typeDescriptor: Symbol = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext, debugIndent: Int) =
|
||||
writer.writeObject(obj, data, type, output, context, debugIndent)
|
||||
|
||||
// We restrict to only those annotated or whitelisted
|
||||
private val interfaces = interfacesForSerialization(clazz, factory)
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any =
|
||||
reader.readObject(obj, schemas, input, context)
|
||||
}
|
||||
|
||||
internal open val typeNotation: TypeNotation by lazy {
|
||||
CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor), generateFields())
|
||||
}
|
||||
|
||||
override fun writeClassInfo(output: SerializationOutput) {
|
||||
class ComposableObjectWriter(
|
||||
private val typeNotation: TypeNotation,
|
||||
private val interfaces: List<LocalTypeInformation>,
|
||||
private val propertySerializers: Map<PropertyName, PropertySerializer>
|
||||
) {
|
||||
fun writeClassInfo(output: SerializationOutput) {
|
||||
if (output.writeTypeNotations(typeNotation)) {
|
||||
for (iface in interfaces) {
|
||||
output.requireSerializer(iface)
|
||||
output.requireSerializer(iface.observedType)
|
||||
}
|
||||
|
||||
propertySerializers.serializationOrder.forEach { property ->
|
||||
property.serializer.writeClassInfo(output)
|
||||
propertySerializers.values.forEach { serializer ->
|
||||
serializer.writeClassInfo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeObject(
|
||||
obj: Any,
|
||||
data: Data,
|
||||
type: Type,
|
||||
output: SerializationOutput,
|
||||
context: SerializationContext,
|
||||
debugIndent: Int) = ifThrowsAppend({ clazz.typeName }
|
||||
) {
|
||||
if (propertySerializers.deserializableSize != javaConstructor?.parameterCount &&
|
||||
javaConstructor?.parameterCount ?: 0 > 0
|
||||
) {
|
||||
throw AMQPNotSerializableException(type, "Serialization constructor for class $type expects "
|
||||
+ "${javaConstructor?.parameterCount} parameters but we have ${propertySerializers.size} "
|
||||
+ "properties to serialize.")
|
||||
}
|
||||
|
||||
// Write described
|
||||
fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext, debugIndent: Int) {
|
||||
data.withDescribed(typeNotation.descriptor) {
|
||||
// Write list
|
||||
withList {
|
||||
propertySerializers.serializationOrder.forEach { property ->
|
||||
property.serializer.writeProperty(obj, this, output, context, debugIndent + 1)
|
||||
propertySerializers.values.forEach { propertySerializer ->
|
||||
propertySerializer.writeProperty(obj, this, output, context, debugIndent + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun readObject(
|
||||
obj: Any,
|
||||
schemas: SerializationSchemas,
|
||||
input: DeserializationInput,
|
||||
context: SerializationContext): Any = ifThrowsAppend({ clazz.typeName }) {
|
||||
if (obj is List<*>) {
|
||||
if (obj.size != propertySerializers.size) {
|
||||
throw AMQPNotSerializableException(type, "${obj.size} objects to deserialize, but " +
|
||||
"${propertySerializers.size} properties in described type $typeName")
|
||||
}
|
||||
class ComposableObjectReader(
|
||||
val typeIdentifier: TypeIdentifier,
|
||||
private val propertySerializers: Map<PropertyName, PropertySerializer>,
|
||||
private val objectBuilderProvider: () -> ObjectBuilder
|
||||
) {
|
||||
|
||||
return if (propertySerializers.byConstructor) {
|
||||
readObjectBuildViaConstructor(obj, schemas, input, context)
|
||||
} else {
|
||||
readObjectBuildViaSetters(obj, schemas, input, context)
|
||||
}
|
||||
} else {
|
||||
throw AMQPNotSerializableException(type, "Body of described type is unexpected $obj")
|
||||
}
|
||||
}
|
||||
|
||||
private fun readObjectBuildViaConstructor(
|
||||
obj: List<*>,
|
||||
schemas: SerializationSchemas,
|
||||
input: DeserializationInput,
|
||||
context: SerializationContext): Any = ifThrowsAppend({ clazz.typeName }) {
|
||||
logger.trace { "Calling construction based construction for ${clazz.typeName}" }
|
||||
|
||||
return construct(propertySerializers.serializationOrder
|
||||
.zip(obj)
|
||||
.mapNotNull { (accessor, obj) ->
|
||||
// Ensure values get read out of input no matter what
|
||||
val value = accessor.serializer.readProperty(obj, schemas, input, context)
|
||||
|
||||
when(accessor) {
|
||||
is PropertyAccessorConstructor -> accessor.initialPosition to value
|
||||
is CalculatedPropertyAccessor -> null
|
||||
else -> throw UnsupportedOperationException(
|
||||
"${accessor::class.simpleName} accessor not supported " +
|
||||
"for constructor-based object building")
|
||||
}
|
||||
fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any =
|
||||
ifThrowsAppend({ typeIdentifier.prettyPrint(false) }) {
|
||||
if (obj !is List<*>) throw NotSerializableException("Body of described type is unexpected $obj")
|
||||
if (obj.size < propertySerializers.size) {
|
||||
throw NotSerializableException("${obj.size} objects to deserialize, but " +
|
||||
"${propertySerializers.size} properties in described type ${typeIdentifier.prettyPrint(false)}")
|
||||
}
|
||||
.sortedWith(compareBy { it.first })
|
||||
.map { it.second })
|
||||
}
|
||||
|
||||
private fun readObjectBuildViaSetters(
|
||||
obj: List<*>,
|
||||
schemas: SerializationSchemas,
|
||||
input: DeserializationInput,
|
||||
context: SerializationContext): Any = ifThrowsAppend({ clazz.typeName }) {
|
||||
logger.trace { "Calling setter based construction for ${clazz.typeName}" }
|
||||
val builder = objectBuilderProvider()
|
||||
builder.initialize()
|
||||
obj.asSequence().zip(propertySerializers.values.asSequence())
|
||||
// Read _all_ properties from the stream
|
||||
.map { (item, property) -> property to property.readProperty(item, schemas, input, context) }
|
||||
// Throw away any calculated properties
|
||||
.filter { (property, _) -> !property.isCalculated }
|
||||
// Write the rest into the builder
|
||||
.forEachIndexed { slot, (_, propertyValue) -> builder.populate(slot, propertyValue) }
|
||||
return builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
val instance: Any = javaConstructor?.newInstanceUnwrapped() ?: throw AMQPNotSerializableException(
|
||||
type,
|
||||
"Failed to instantiate instance of object $clazz")
|
||||
class AbstractObjectSerializer(
|
||||
override val type: Type,
|
||||
override val typeDescriptor: Symbol,
|
||||
override val propertySerializers: Map<PropertyName, PropertySerializer>,
|
||||
override val fields: List<Field>,
|
||||
private val writer: ComposableObjectWriter): ObjectSerializer {
|
||||
override fun writeClassInfo(output: SerializationOutput) =
|
||||
writer.writeClassInfo(output)
|
||||
|
||||
// read the properties out of the serialised form, since we're invoking the setters the order we
|
||||
// do it in doesn't matter
|
||||
val propertiesFromBlob = obj
|
||||
.zip(propertySerializers.serializationOrder)
|
||||
.map { it.second.serializer.readProperty(it.first, schemas, input, context) }
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext, debugIndent: Int) =
|
||||
writer.writeObject(obj, data, type, output, context, debugIndent)
|
||||
|
||||
// one by one take a property and invoke the setter on the class
|
||||
propertySerializers.serializationOrder.zip(propertiesFromBlob).forEach {
|
||||
it.first.set(instance, it.second)
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any =
|
||||
throw UnsupportedOperationException("Cannot deserialize abstract type ${type.typeName}")
|
||||
}
|
||||
|
||||
class EvolutionObjectSerializer(
|
||||
override val type: Type,
|
||||
override val typeDescriptor: Symbol,
|
||||
override val propertySerializers: Map<PropertyName, PropertySerializer>,
|
||||
private val reader: ComposableObjectReader): ObjectSerializer {
|
||||
|
||||
companion object {
|
||||
fun make(localTypeInformation: LocalTypeInformation.Composable, remoteTypeInformation: RemoteTypeInformation.Composable, constructor: LocalConstructorInformation,
|
||||
properties: Map<String, LocalPropertyInformation>, classLoader: ClassLoader): EvolutionObjectSerializer {
|
||||
val propertySerializers = makePropertySerializers(properties, remoteTypeInformation.properties, classLoader)
|
||||
val reader = ComposableObjectReader(
|
||||
localTypeInformation.typeIdentifier,
|
||||
propertySerializers,
|
||||
EvolutionObjectBuilder.makeProvider(localTypeInformation.typeIdentifier, constructor, properties, remoteTypeInformation.properties.keys.sorted()))
|
||||
|
||||
return EvolutionObjectSerializer(
|
||||
localTypeInformation.observedType,
|
||||
Symbol.valueOf(remoteTypeInformation.typeDescriptor),
|
||||
propertySerializers,
|
||||
reader)
|
||||
}
|
||||
|
||||
return instance
|
||||
private fun makePropertySerializers(localProperties: Map<String, LocalPropertyInformation>,
|
||||
remoteProperties: Map<String, RemotePropertyInformation>,
|
||||
classLoader: ClassLoader): Map<String, PropertySerializer> =
|
||||
remoteProperties.mapValues { (name, property) ->
|
||||
val localProperty = localProperties[name]
|
||||
val isCalculated = localProperty?.isCalculated ?: false
|
||||
val type = localProperty?.type?.observedType ?: property.type.typeIdentifier.getLocalType(classLoader)
|
||||
ComposableTypePropertySerializer.makeForEvolution(name, isCalculated, property.type.typeIdentifier, type)
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateFields(): List<Field> {
|
||||
return propertySerializers.serializationOrder.map {
|
||||
Field(it.serializer.name, it.serializer.type, it.serializer.requires, it.serializer.default, null, it.serializer.mandatory, false)
|
||||
}
|
||||
}
|
||||
override val fields: List<Field> get() = emptyList()
|
||||
|
||||
private fun generateProvides(): List<String> = interfaces.map { nameForType(it) }
|
||||
override fun writeClassInfo(output: SerializationOutput) =
|
||||
throw UnsupportedOperationException("Evolved types cannot be written")
|
||||
|
||||
fun construct(properties: List<Any?>): Any {
|
||||
logger.trace { "Calling constructor: '$javaConstructor' with properties '$properties'" }
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext, debugIndent: Int) =
|
||||
throw UnsupportedOperationException("Evolved types cannot be written")
|
||||
|
||||
if (properties.size != javaConstructor?.parameterCount) {
|
||||
throw AMQPNotSerializableException(type, "Serialization constructor for class $type expects "
|
||||
+ "${javaConstructor?.parameterCount} parameters but we have ${properties.size} "
|
||||
+ "serialized properties.")
|
||||
}
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any =
|
||||
reader.readObject(obj, schemas, input, context)
|
||||
|
||||
return javaConstructor?.newInstanceUnwrapped(*properties.toTypedArray())
|
||||
?: throw AMQPNotSerializableException(
|
||||
type,
|
||||
"Attempt to deserialize an interface: $clazz. Serialized form is invalid.")
|
||||
}
|
||||
|
||||
private fun <T> Constructor<T>.newInstanceUnwrapped(vararg args: Any?): T {
|
||||
try {
|
||||
return newInstance(*args)
|
||||
} catch (e: InvocationTargetException) {
|
||||
throw e.cause!!
|
||||
}
|
||||
}
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import org.apache.qpid.proton.amqp.Binary
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* Base class for serialization of a property of an object.
|
||||
*/
|
||||
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, context: SerializationContext, debugIndent: Int = 0)
|
||||
abstract fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any?
|
||||
|
||||
val type: String = generateType()
|
||||
val requires: List<String> = generateRequires()
|
||||
val default: String? = generateDefault()
|
||||
val mandatory: Boolean = generateMandatory()
|
||||
|
||||
private val isInterface: Boolean get() = resolvedType.asClass().isInterface
|
||||
private val isJVMPrimitive: Boolean get() = resolvedType.asClass().isPrimitive
|
||||
|
||||
private fun generateType(): String {
|
||||
return if (isInterface || resolvedType == Any::class.java) "*" else SerializerFactory.nameForType(resolvedType)
|
||||
}
|
||||
|
||||
private fun generateRequires(): List<String> {
|
||||
return if (isInterface) listOf(SerializerFactory.nameForType(resolvedType)) else emptyList()
|
||||
}
|
||||
|
||||
private fun generateDefault(): String? =
|
||||
if (isJVMPrimitive) {
|
||||
when (resolvedType) {
|
||||
java.lang.Boolean.TYPE -> "false"
|
||||
java.lang.Character.TYPE -> "�"
|
||||
else -> "0"
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
private fun generateMandatory(): Boolean {
|
||||
return isJVMPrimitive || !(propertyReader.isNullable())
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun make(name: String, readMethod: PropertyReader, resolvedType: Type, factory: SerializerFactory): PropertySerializer {
|
||||
return if (SerializerFactory.isPrimitive(resolvedType)) {
|
||||
when (resolvedType) {
|
||||
Char::class.java, Character::class.java -> AMQPCharPropertySerializer(name, readMethod)
|
||||
else -> AMQPPrimitivePropertySerializer(name, readMethod, resolvedType)
|
||||
}
|
||||
} else {
|
||||
DescribedTypePropertySerializer(name, readMethod, resolvedType) { factory.get(null, resolvedType) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A property serializer for a complex type (another object).
|
||||
*/
|
||||
@KeepForDJVM
|
||||
class DescribedTypePropertySerializer(
|
||||
name: String,
|
||||
readMethod: PropertyReader,
|
||||
resolvedType: Type,
|
||||
private val lazyTypeSerializer: () -> AMQPSerializer<*>) : PropertySerializer(name, readMethod, resolvedType) {
|
||||
// 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) = ifThrowsAppend({ nameForDebug }) {
|
||||
if (resolvedType != Any::class.java) {
|
||||
typeSerializer.writeClassInfo(output)
|
||||
}
|
||||
}
|
||||
|
||||
override fun readProperty(
|
||||
obj: Any?,
|
||||
schemas: SerializationSchemas,
|
||||
input: DeserializationInput,
|
||||
context: SerializationContext): Any? = ifThrowsAppend({ nameForDebug }) {
|
||||
input.readObjectOrNull(obj, schemas, resolvedType, context)
|
||||
}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int) = ifThrowsAppend({ nameForDebug }
|
||||
) {
|
||||
output.writeObjectOrNull(propertyReader.read(obj), data, resolvedType, context, debugIndent)
|
||||
}
|
||||
|
||||
private val nameForDebug = "$name(${resolvedType.typeName})"
|
||||
}
|
||||
|
||||
/**
|
||||
* A property serializer for most AMQP primitive type (Int, String, etc).
|
||||
*/
|
||||
class AMQPPrimitivePropertySerializer(
|
||||
name: String,
|
||||
readMethod: PropertyReader,
|
||||
resolvedType: Type) : PropertySerializer(name, readMethod, resolvedType) {
|
||||
override fun writeClassInfo(output: SerializationOutput) {}
|
||||
|
||||
override fun readProperty(obj: Any?, schemas: SerializationSchemas,
|
||||
input: DeserializationInput, context: SerializationContext
|
||||
): Any? {
|
||||
return if (obj is Binary) obj.array else obj
|
||||
}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
val value = propertyReader.read(obj)
|
||||
if (value is ByteArray) {
|
||||
data.putObject(Binary(value))
|
||||
} else {
|
||||
data.putObject(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A property serializer for the AMQP char type, needed as a specialisation as the underlying
|
||||
* value of the character is stored in numeric UTF-16 form and on deserialization requires explicit
|
||||
* casting back to a char otherwise it's treated as an Integer and a TypeMismatch occurs
|
||||
*/
|
||||
class AMQPCharPropertySerializer(name: String, readMethod: PropertyReader) :
|
||||
PropertySerializer(name, readMethod, Character::class.java) {
|
||||
override fun writeClassInfo(output: SerializationOutput) {}
|
||||
|
||||
override fun readProperty(obj: Any?, schemas: SerializationSchemas,
|
||||
input: DeserializationInput, context: SerializationContext
|
||||
): Any? {
|
||||
return if (obj == null) null else (obj as Short).toChar()
|
||||
}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
val input = propertyReader.read(obj)
|
||||
if (input != null) data.putShort((input as Char).toShort()) else data.putNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,243 +0,0 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.serialization.SerializableCalculatedProperty
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Type
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.jvm.javaGetter
|
||||
import kotlin.reflect.jvm.kotlinProperty
|
||||
|
||||
abstract class PropertyReader {
|
||||
abstract fun read(obj: Any?): Any?
|
||||
abstract fun isNullable(): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor for those properties of a class that have defined getter functions.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
class PublicPropertyReader(private val readMethod: Method) : PropertyReader() {
|
||||
init {
|
||||
readMethod.isAccessible = true
|
||||
}
|
||||
|
||||
private fun Method.returnsNullable(): Boolean {
|
||||
try {
|
||||
val returnTypeString = this.declaringClass.kotlin.memberProperties.firstOrNull {
|
||||
it.javaGetter == this
|
||||
}?.returnType?.toString() ?: "?"
|
||||
|
||||
return returnTypeString.endsWith('?') || returnTypeString.endsWith('!')
|
||||
} catch (e: kotlin.reflect.jvm.internal.KotlinReflectionInternalError) {
|
||||
// This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue
|
||||
// is: https://youtrack.jetbrains.com/issue/KT-13077
|
||||
// TODO: Revisit this when Kotlin issue is fixed.
|
||||
|
||||
// So this used to report as an error, but given we serialise exceptions all the time it
|
||||
// provides for very scary log files so move this to trace level
|
||||
loggerFor<PropertySerializer>().let { logger ->
|
||||
logger.trace("Using kotlin introspection on internal type ${this.declaringClass}")
|
||||
logger.trace("Unexpected internal Kotlin error", e)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override fun read(obj: Any?): Any? {
|
||||
return readMethod.invoke(obj)
|
||||
}
|
||||
|
||||
override fun isNullable(): Boolean = readMethod.returnsNullable()
|
||||
|
||||
val genericReturnType get() = readMethod.genericReturnType
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor for those properties of a class that do not have defined getter functions. In which case
|
||||
* we used reflection to remove the unreadable status from that property whilst it's accessed.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
class PrivatePropertyReader(val field: Field, parentType: Type) : PropertyReader() {
|
||||
init {
|
||||
loggerFor<PropertySerializer>().warn("Create property Serializer for private property '${field.name}' not "
|
||||
+ "exposed by a getter on class '$parentType'\n"
|
||||
+ "\tNOTE: This behaviour will be deprecated at some point in the future and a getter required")
|
||||
}
|
||||
|
||||
override fun read(obj: Any?): Any? {
|
||||
field.isAccessible = true
|
||||
val rtn = field.get(obj)
|
||||
field.isAccessible = false
|
||||
return rtn
|
||||
}
|
||||
|
||||
override fun isNullable() = try {
|
||||
field.kotlinProperty?.returnType?.isMarkedNullable ?: false
|
||||
} catch (e: kotlin.reflect.jvm.internal.KotlinReflectionInternalError) {
|
||||
// This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue
|
||||
// is: https://youtrack.jetbrains.com/issue/KT-13077
|
||||
// TODO: Revisit this when Kotlin issue is fixed.
|
||||
|
||||
// So this used to report as an error, but given we serialise exceptions all the time it
|
||||
// provides for very scary log files so move this to trace level
|
||||
loggerFor<PropertySerializer>().let { logger ->
|
||||
logger.trace("Using kotlin introspection on internal type $field")
|
||||
logger.trace("Unexpected internal Kotlin error", e)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Special instance of a [PropertyReader] for use only by [EvolutionSerializer]s to make
|
||||
* it explicit that no properties are ever actually read from an object as the evolution
|
||||
* serializer should only be accessing the already serialized form.
|
||||
*/
|
||||
class EvolutionPropertyReader : PropertyReader() {
|
||||
override fun read(obj: Any?): Any? {
|
||||
throw UnsupportedOperationException("It should be impossible for an evolution serializer to "
|
||||
+ "be reading from an object")
|
||||
}
|
||||
|
||||
override fun isNullable() = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a generic interface to a serializable property of an object.
|
||||
*
|
||||
* @property initialPosition where in the constructor used for serialization the property occurs.
|
||||
* @property serializer a [PropertySerializer] wrapping access to the property. This will either be a
|
||||
* method invocation on the getter or, if not publicly accessible, reflection based by temporally
|
||||
* making the property accessible.
|
||||
*/
|
||||
abstract class PropertyAccessor(
|
||||
open val serializer: PropertySerializer) {
|
||||
companion object : Comparator<PropertyAccessor> {
|
||||
override fun compare(p0: PropertyAccessor?, p1: PropertyAccessor?): Int {
|
||||
return p0?.serializer?.name?.compareTo(p1?.serializer?.name ?: "") ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
open val isCalculated get() = false
|
||||
|
||||
/**
|
||||
* Override to control how the property is set on the object.
|
||||
*/
|
||||
abstract fun set(instance: Any, obj: Any?)
|
||||
|
||||
override fun toString(): String {
|
||||
return serializer.name
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of [PropertyAccessor] representing a property of an object that
|
||||
* is serialized and deserialized via JavaBean getter and setter style methods.
|
||||
*/
|
||||
class PropertyAccessorGetterSetter(
|
||||
getter: PropertySerializer,
|
||||
private val setter: Method) : PropertyAccessor(getter) {
|
||||
init {
|
||||
/**
|
||||
* Play nicely with Java interop, public methods aren't marked as accessible
|
||||
*/
|
||||
setter.isAccessible = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the setter on the underlying object passing in the serialized value.
|
||||
*/
|
||||
override fun set(instance: Any, obj: Any?) {
|
||||
setter.invoke(instance, *listOf(obj).toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of [PropertyAccessor] representing a property of an object that
|
||||
* is serialized via a JavaBean getter but deserialized using the constructor
|
||||
* of the object the property belongs to.
|
||||
*/
|
||||
class PropertyAccessorConstructor(
|
||||
val initialPosition: Int,
|
||||
override val serializer: PropertySerializer) : PropertyAccessor(serializer) {
|
||||
/**
|
||||
* Because the property should be being set on the object through the constructor any
|
||||
* calls to the explicit setter should be an error.
|
||||
*/
|
||||
override fun set(instance: Any, obj: Any?) {
|
||||
NotSerializableException("Attempting to access a setter on an object being instantiated " +
|
||||
"via its constructor.")
|
||||
}
|
||||
|
||||
override fun toString(): String =
|
||||
"${serializer.name}($initialPosition)"
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of [PropertyAccessor] representing a calculated property of an object that is serialized
|
||||
* so that it can be used by the class carpenter, but ignored on deserialisation as there is no setter or
|
||||
* constructor parameter to receive its value.
|
||||
*
|
||||
* This will only be created for calculated properties that are accessible via no-argument methods annotated
|
||||
* with [SerializableCalculatedProperty].
|
||||
*/
|
||||
class CalculatedPropertyAccessor(override val serializer: PropertySerializer): PropertyAccessor(serializer) {
|
||||
override val isCalculated: Boolean
|
||||
get() = true
|
||||
|
||||
override fun set(instance: Any, obj: Any?) = Unit // do nothing, as it's a calculated value
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a collection of [PropertyAccessor]s that represent the serialized form
|
||||
* of an object.
|
||||
*
|
||||
* @property serializationOrder a list of [PropertyAccessor]. For deterministic serialization
|
||||
* should be sorted.
|
||||
* @property size how many properties are being serialized.
|
||||
* @property byConstructor are the properties of the class represented by this set of properties populated
|
||||
* on deserialization via the object's constructor or the corresponding setter functions. Should be
|
||||
* overridden and set appropriately by child types.
|
||||
*/
|
||||
abstract class PropertySerializers(
|
||||
val serializationOrder: List<PropertyAccessor>) {
|
||||
companion object {
|
||||
fun make(serializationOrder: List<PropertyAccessor>) =
|
||||
when (serializationOrder.find { !it.isCalculated }) {
|
||||
is PropertyAccessorConstructor -> PropertySerializersConstructor(serializationOrder)
|
||||
is PropertyAccessorGetterSetter -> PropertySerializersSetter(serializationOrder)
|
||||
null -> PropertySerializersNoProperties()
|
||||
else -> {
|
||||
throw AMQPNoTypeNotSerializableException("Unknown Property Accessor type, cannot create set")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val size get() = serializationOrder.size
|
||||
abstract val byConstructor: Boolean
|
||||
val deserializableSize = serializationOrder.count { !it.isCalculated }
|
||||
}
|
||||
|
||||
class PropertySerializersNoProperties : PropertySerializers(emptyList()) {
|
||||
override val byConstructor get() = true
|
||||
}
|
||||
|
||||
class PropertySerializersConstructor(
|
||||
serializationOrder: List<PropertyAccessor>) : PropertySerializers(serializationOrder) {
|
||||
override val byConstructor get() = true
|
||||
}
|
||||
|
||||
class PropertySerializersSetter(
|
||||
serializationOrder: List<PropertyAccessor>) : PropertySerializers(serializationOrder) {
|
||||
override val byConstructor get() = false
|
||||
}
|
||||
|
||||
class PropertySerializersEvolution : PropertySerializers(emptyList()) {
|
||||
override val byConstructor get() = false
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,141 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.serialization.internal.model.*
|
||||
import org.hibernate.type.descriptor.java.ByteTypeDescriptor
|
||||
import java.io.NotSerializableException
|
||||
|
||||
/**
|
||||
* A factory that knows how to create serializers to deserialize values sent to us by remote parties.
|
||||
*/
|
||||
interface RemoteSerializerFactory {
|
||||
/**
|
||||
* Lookup and manufacture a serializer for the given AMQP type descriptor, assuming we also have the necessary types
|
||||
* contained in the provided [Schema].
|
||||
*
|
||||
* @param typeDescriptor The type descriptor for the type to obtain a serializer for.
|
||||
* @param schema The schemas sent along with the serialized data.
|
||||
*/
|
||||
@Throws(NotSerializableException::class)
|
||||
fun get(typeDescriptor: TypeDescriptor, schema: SerializationSchemas): AMQPSerializer<Any>
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the reflection of some [RemoteTypeInformation] by some [LocalTypeInformation], which we use to make
|
||||
* decisions about evolution.
|
||||
*/
|
||||
data class RemoteAndLocalTypeInformation(
|
||||
val remoteTypeInformation: RemoteTypeInformation,
|
||||
val localTypeInformation: LocalTypeInformation)
|
||||
|
||||
/**
|
||||
* A [RemoteSerializerFactory] which uses an [AMQPRemoteTypeModel] to interpret AMQP [Schema]s into [RemoteTypeInformation],
|
||||
* reflects this into [LocalTypeInformation] using a [LocalTypeModel] and a [TypeLoader], and compares the two in order to
|
||||
* decide whether to return the serializer provided by the [LocalSerializerFactory] or to construct a special evolution serializer
|
||||
* using the [EvolutionSerializerFactory].
|
||||
*
|
||||
* Its decisions are recorded by registering the chosen serialisers against their type descriptors
|
||||
* in the [DescriptorBasedSerializerRegistry].
|
||||
*
|
||||
* @param evolutionSerializerFactory The [EvolutionSerializerFactory] to use to create evolution serializers, when necessary.
|
||||
* @param descriptorBasedSerializerRegistry The registry to use to store serializers by [TypeDescriptor].
|
||||
* @param remoteTypeModel The [AMQPRemoteTypeModel] to use to interpret AMPQ [Schema] information into [RemoteTypeInformation].
|
||||
* @param localTypeModel The [LocalTypeModel] to use to obtain [LocalTypeInformation] for reflected [Type]s.
|
||||
* @param typeLoader The [TypeLoader] to use to load local [Type]s reflecting [RemoteTypeInformation].
|
||||
* @param localSerializerFactory The [LocalSerializerFactory] to use to obtain serializers for non-evolved types.
|
||||
*/
|
||||
class DefaultRemoteSerializerFactory(
|
||||
private val evolutionSerializerFactory: EvolutionSerializerFactory,
|
||||
private val descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry,
|
||||
private val remoteTypeModel: AMQPRemoteTypeModel,
|
||||
private val localTypeModel: LocalTypeModel,
|
||||
private val typeLoader: TypeLoader,
|
||||
private val localSerializerFactory: LocalSerializerFactory)
|
||||
: RemoteSerializerFactory {
|
||||
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
override fun get(typeDescriptor: TypeDescriptor, schema: SerializationSchemas): AMQPSerializer<Any> =
|
||||
// If we have seen this descriptor before, we assume we have seen everything in this schema before.
|
||||
descriptorBasedSerializerRegistry.getOrBuild(typeDescriptor) {
|
||||
logger.trace("get Serializer descriptor=$typeDescriptor")
|
||||
|
||||
// Interpret all of the types in the schema into RemoteTypeInformation, and reflect that into LocalTypeInformation.
|
||||
val remoteTypeInformationMap = remoteTypeModel.interpret(schema)
|
||||
val reflected = reflect(remoteTypeInformationMap)
|
||||
|
||||
// Get, and record in the registry, serializers for all of the types contained in the schema.
|
||||
// This will save us having to re-interpret the entire schema on re-entry when deserialising individual property values.
|
||||
val serializers = reflected.mapValues { (descriptor, remoteLocalPair) ->
|
||||
descriptorBasedSerializerRegistry.getOrBuild(descriptor) {
|
||||
getUncached(remoteLocalPair.remoteTypeInformation, remoteLocalPair.localTypeInformation)
|
||||
}
|
||||
}
|
||||
|
||||
// Return the specific serializer the caller asked for.
|
||||
serializers[typeDescriptor] ?: throw NotSerializableException(
|
||||
"Could not find type matching descriptor $typeDescriptor.")
|
||||
}
|
||||
|
||||
private fun getUncached(remoteTypeInformation: RemoteTypeInformation, localTypeInformation: LocalTypeInformation): AMQPSerializer<Any> {
|
||||
val remoteDescriptor = remoteTypeInformation.typeDescriptor
|
||||
|
||||
// Obtain a serializer and descriptor for the local type.
|
||||
val localSerializer = localSerializerFactory.get(localTypeInformation)
|
||||
val localDescriptor = localSerializer.typeDescriptor.toString()
|
||||
|
||||
return when {
|
||||
// If descriptors match, we can return the local serializer straight away.
|
||||
localDescriptor == remoteDescriptor -> localSerializer
|
||||
|
||||
// Can we deserialise without evolution, e.g. going from List<Foo> to List<*>?
|
||||
remoteTypeInformation.isDeserialisableWithoutEvolutionTo(localTypeInformation) -> localSerializer
|
||||
|
||||
// Are the remote/local types evolvable? If so, ask the evolution serializer factory for a serializer, returning
|
||||
// the local serializer if it returns null (i.e. no evolution required).
|
||||
remoteTypeInformation.isEvolvableTo(localTypeInformation) ->
|
||||
evolutionSerializerFactory.getEvolutionSerializer(remoteTypeInformation, localTypeInformation)
|
||||
?: localSerializer
|
||||
|
||||
// Descriptors don't match, and something is probably broken, but we let the framework do what it can with the local
|
||||
// serialiser (BlobInspectorTest uniquely breaks if we throw an exception here, and passes if we just warn and continue).
|
||||
else -> {
|
||||
logger.warn("""
|
||||
Mismatch between type descriptors, but remote type is not evolvable to local type.
|
||||
|
||||
Remote type (descriptor: $remoteDescriptor)
|
||||
${remoteTypeInformation.prettyPrint(false)}
|
||||
|
||||
Local type (descriptor $localDescriptor):
|
||||
${localTypeInformation.prettyPrint(false)}
|
||||
""")
|
||||
|
||||
localSerializer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun reflect(remoteInformation: Map<TypeDescriptor, RemoteTypeInformation>):
|
||||
Map<TypeDescriptor, RemoteAndLocalTypeInformation> {
|
||||
val localInformationByIdentifier = typeLoader.load(remoteInformation.values).mapValues { (_, type) ->
|
||||
localTypeModel.inspect(type)
|
||||
}
|
||||
|
||||
return remoteInformation.mapValues { (_, remoteInformation) ->
|
||||
RemoteAndLocalTypeInformation(remoteInformation, localInformationByIdentifier[remoteInformation.typeIdentifier]!!)
|
||||
}
|
||||
}
|
||||
|
||||
private fun RemoteTypeInformation.isEvolvableTo(localTypeInformation: LocalTypeInformation): Boolean = when(this) {
|
||||
is RemoteTypeInformation.Composable -> localTypeInformation is LocalTypeInformation.Composable
|
||||
is RemoteTypeInformation.AnEnum -> localTypeInformation is LocalTypeInformation.AnEnum
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun RemoteTypeInformation.isDeserialisableWithoutEvolutionTo(localTypeInformation: LocalTypeInformation) =
|
||||
this is RemoteTypeInformation.Parameterised &&
|
||||
(localTypeInformation is LocalTypeInformation.ACollection ||
|
||||
localTypeInformation is LocalTypeInformation.AMap)
|
||||
}
|
@ -2,243 +2,10 @@ package net.corda.serialization.internal.amqp
|
||||
|
||||
import com.google.common.primitives.Primitives
|
||||
import com.google.common.reflect.TypeToken
|
||||
import net.corda.core.internal.isConcreteClass
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.serialization.internal.model.TypeIdentifier
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.lang.reflect.*
|
||||
import java.lang.reflect.Field
|
||||
import java.util.*
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.KParameter
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
import kotlin.reflect.jvm.isAccessible
|
||||
import kotlin.reflect.jvm.javaConstructor
|
||||
import kotlin.reflect.jvm.javaType
|
||||
|
||||
/**
|
||||
* Code for finding the constructor we will use for deserialization.
|
||||
*
|
||||
* If any constructor is uniquely annotated with [@ConstructorForDeserialization], then that constructor is chosen.
|
||||
* An error is reported if more than one constructor is annotated.
|
||||
*
|
||||
* Otherwise, if there is a Kotlin primary constructor, it selects that, and if not it selects either the unique
|
||||
* constructor or, if there are two and one is the default no-argument constructor, the non-default constructor.
|
||||
*/
|
||||
fun constructorForDeserialization(type: Type): KFunction<Any> {
|
||||
val clazz = type.asClass().apply {
|
||||
if (!isConcreteClass) throw AMQPNotSerializableException(type,
|
||||
"Cannot find deserialisation constructor for non-concrete class $this")
|
||||
}
|
||||
|
||||
val kotlinCtors = clazz.kotlin.constructors
|
||||
|
||||
val annotatedCtors = kotlinCtors.filter { it.findAnnotation<ConstructorForDeserialization>() != null }
|
||||
if (annotatedCtors.size > 1) throw AMQPNotSerializableException(
|
||||
type,
|
||||
"More than one constructor for $clazz is annotated with @ConstructorForDeserialization.")
|
||||
|
||||
val defaultCtor = kotlinCtors.firstOrNull { it.parameters.isEmpty() }
|
||||
val nonDefaultCtors = kotlinCtors.filter { it != defaultCtor }
|
||||
|
||||
val preferredCandidate = annotatedCtors.firstOrNull() ?:
|
||||
clazz.kotlin.primaryConstructor ?:
|
||||
when(nonDefaultCtors.size) {
|
||||
1 -> nonDefaultCtors.first()
|
||||
0 -> defaultCtor ?: throw AMQPNotSerializableException(type, "No constructor found for $clazz.")
|
||||
else -> throw AMQPNotSerializableException(type, "No unique non-default constructor found for $clazz.")
|
||||
}
|
||||
|
||||
return preferredCandidate.apply { isAccessible = true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the properties to be used during serialization by attempting to find those that match the parameters
|
||||
* to the deserialization constructor, if the class is concrete. If it is abstract, or an interface, then use all
|
||||
* the properties.
|
||||
*
|
||||
* Note, you will need any Java classes to be compiled with the `-parameters` option to ensure constructor parameters
|
||||
* have names accessible via reflection.
|
||||
*/
|
||||
fun <T : Any> propertiesForSerialization(
|
||||
kotlinConstructor: KFunction<T>?,
|
||||
type: Type,
|
||||
factory: SerializerFactory): PropertySerializers = PropertySerializers.make(
|
||||
getValueProperties(kotlinConstructor, type, factory)
|
||||
.addCalculatedProperties(factory, type)
|
||||
.sortedWith(PropertyAccessor))
|
||||
|
||||
fun <T : Any> getValueProperties(kotlinConstructor: KFunction<T>?, type: Type, factory: SerializerFactory)
|
||||
: List<PropertyAccessor> =
|
||||
if (kotlinConstructor != null) {
|
||||
propertiesForSerializationFromConstructor(kotlinConstructor, type, factory)
|
||||
} else {
|
||||
propertiesForSerializationFromAbstract(type.asClass(), type, factory)
|
||||
}
|
||||
|
||||
private fun List<PropertyAccessor>.addCalculatedProperties(factory: SerializerFactory, type: Type)
|
||||
: List<PropertyAccessor> {
|
||||
val nonCalculated = map { it.serializer.name }.toSet()
|
||||
return this + type.asClass().calculatedPropertyDescriptors().mapNotNull { (name, descriptor) ->
|
||||
if (name in nonCalculated) null else {
|
||||
val calculatedPropertyMethod = descriptor.getter
|
||||
?: throw IllegalStateException("Property $name is not a calculated property")
|
||||
CalculatedPropertyAccessor(PropertySerializer.make(
|
||||
name,
|
||||
PublicPropertyReader(calculatedPropertyMethod),
|
||||
calculatedPropertyMethod.genericReturnType,
|
||||
factory))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* From a constructor, determine which properties of a class are to be serialized.
|
||||
*
|
||||
* @param kotlinConstructor The constructor to be used to instantiate instances of the class
|
||||
* @param type The class's [Type]
|
||||
* @param factory The factory generating the serializer wrapping this function.
|
||||
*/
|
||||
internal fun <T : Any> propertiesForSerializationFromConstructor(
|
||||
kotlinConstructor: KFunction<T>,
|
||||
type: Type,
|
||||
factory: SerializerFactory): List<PropertyAccessor> {
|
||||
val clazz = (kotlinConstructor.returnType.classifier as KClass<*>).javaObjectType
|
||||
|
||||
val classProperties = clazz.propertyDescriptors()
|
||||
|
||||
// Annoyingly there isn't a better way to ascertain that the constructor for the class
|
||||
// has a synthetic parameter inserted to capture the reference to the outer class. You'd
|
||||
// think you could inspect the parameter and check the isSynthetic flag but that is always
|
||||
// false so given the naming convention is specified by the standard we can just check for
|
||||
// this
|
||||
kotlinConstructor.javaConstructor?.apply {
|
||||
if (parameterCount > 0 && parameters[0].name == "this$0") throw SyntheticParameterException(type)
|
||||
}
|
||||
|
||||
if (classProperties.isNotEmpty() && kotlinConstructor.parameters.isEmpty()) {
|
||||
return propertiesForSerializationFromSetters(classProperties, type, factory)
|
||||
}
|
||||
|
||||
return kotlinConstructor.parameters.withIndex().map { param ->
|
||||
toPropertyAccessorConstructor(param.index, param.value, classProperties, type, clazz, factory)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toPropertyAccessorConstructor(index: Int, param: KParameter, classProperties: Map<String, PropertyDescriptor>, type: Type, clazz: Class<out Any>, factory: SerializerFactory): PropertyAccessorConstructor {
|
||||
// name cannot be null, if it is then this is a synthetic field and we will have bailed
|
||||
// out prior to this
|
||||
val name = param.name!!
|
||||
|
||||
// We will already have disambiguated getA for property A or a but we still need to cope
|
||||
// with the case we don't know the case of A when the parameter doesn't match a property
|
||||
// but has a getter
|
||||
val matchingProperty = classProperties[name] ?: classProperties[name.capitalize()]
|
||||
?: throw AMQPNotSerializableException(type,
|
||||
"Constructor parameter - \"$name\" - doesn't refer to a property of \"$clazz\"")
|
||||
|
||||
// If the property has a getter we'll use that to retrieve it's value from the instance, if it doesn't
|
||||
// *for *now* we switch to a reflection based method
|
||||
val propertyReader = matchingProperty.getter?.let { getter ->
|
||||
getPublicPropertyReader(getter, type, param, name, clazz)
|
||||
} ?: matchingProperty.field?.let { field ->
|
||||
getPrivatePropertyReader(field, type)
|
||||
} ?: throw AMQPNotSerializableException(type,
|
||||
"No property matching constructor parameter named - \"$name\" - " +
|
||||
"of \"${param}\". If using Java, check that you have the -parameters option specified " +
|
||||
"in the Java compiler. Alternately, provide a proxy serializer " +
|
||||
"(SerializationCustomSerializer) if recompiling isn't an option")
|
||||
|
||||
return PropertyAccessorConstructor(
|
||||
index,
|
||||
PropertySerializer.make(name, propertyReader.first, propertyReader.second, factory))
|
||||
}
|
||||
|
||||
/**
|
||||
* If we determine a class has a constructor that takes no parameters then check for pairs of getters / setters
|
||||
* and use those
|
||||
*/
|
||||
fun propertiesForSerializationFromSetters(
|
||||
properties: Map<String, PropertyDescriptor>,
|
||||
type: Type,
|
||||
factory: SerializerFactory): List<PropertyAccessor> =
|
||||
properties.asSequence().map { entry ->
|
||||
val (name, property) = entry
|
||||
|
||||
val getter = property.getter
|
||||
val setter = property.setter
|
||||
|
||||
if (getter == null || setter == null) return@map null
|
||||
|
||||
PropertyAccessorGetterSetter(
|
||||
PropertySerializer.make(
|
||||
name,
|
||||
PublicPropertyReader(getter),
|
||||
resolveTypeVariables(getter.genericReturnType, type),
|
||||
factory),
|
||||
setter)
|
||||
}.filterNotNull().toList()
|
||||
|
||||
private fun getPrivatePropertyReader(field: Field, type: Type) =
|
||||
PrivatePropertyReader(field, type) to resolveTypeVariables(field.genericType, type)
|
||||
|
||||
private fun getPublicPropertyReader(getter: Method, type: Type, param: KParameter, name: String, clazz: Class<out Any>): Pair<PublicPropertyReader, Type> {
|
||||
val returnType = resolveTypeVariables(getter.genericReturnType, type)
|
||||
val paramToken = TypeToken.of(param.type.javaType)
|
||||
val rawParamType = TypeToken.of(paramToken.rawType)
|
||||
|
||||
if (!(paramToken.isSupertypeOf(returnType)
|
||||
|| paramToken.isSupertypeOf(getter.genericReturnType)
|
||||
// cope with the case where the constructor parameter is a generic type (T etc) but we
|
||||
// can discover it's raw type. When bounded this wil be the bounding type, unbounded
|
||||
// generics this will be object
|
||||
|| rawParamType.isSupertypeOf(returnType)
|
||||
|| rawParamType.isSupertypeOf(getter.genericReturnType))) {
|
||||
throw AMQPNotSerializableException(
|
||||
type,
|
||||
"Property - \"$name\" - has type \"$returnType\" on \"$clazz\" " +
|
||||
"but differs from constructor parameter type \"${param.type.javaType}\"")
|
||||
}
|
||||
|
||||
return PublicPropertyReader(getter) to returnType
|
||||
}
|
||||
|
||||
private fun propertiesForSerializationFromAbstract(
|
||||
clazz: Class<*>,
|
||||
type: Type,
|
||||
factory: SerializerFactory): List<PropertyAccessor> =
|
||||
clazz.propertyDescriptors().asSequence().withIndex().mapNotNull { (index, entry) ->
|
||||
val (name, property) = entry
|
||||
if (property.getter == null || property.field == null) return@mapNotNull null
|
||||
|
||||
val getter = property.getter
|
||||
val returnType = resolveTypeVariables(getter.genericReturnType, type)
|
||||
|
||||
PropertyAccessorConstructor(
|
||||
index,
|
||||
PropertySerializer.make(name, PublicPropertyReader(getter), returnType, factory))
|
||||
}.toList()
|
||||
|
||||
internal fun interfacesForSerialization(type: Type, serializerFactory: SerializerFactory): List<Type> =
|
||||
exploreType(type, serializerFactory).toList()
|
||||
|
||||
private fun exploreType(type: Type, serializerFactory: SerializerFactory, interfaces: MutableSet<Type> = LinkedHashSet()): MutableSet<Type> {
|
||||
val clazz = type.asClass()
|
||||
|
||||
if (clazz.isInterface) {
|
||||
// Ignore classes we've already seen, and stop exploring once we reach a branch that has no `CordaSerializable`
|
||||
// annotation or whitelisting.
|
||||
if (clazz in interfaces || serializerFactory.whitelist.isNotWhitelisted(clazz)) return interfaces
|
||||
else interfaces += type
|
||||
}
|
||||
|
||||
(clazz.genericInterfaces.asSequence() + clazz.genericSuperclass)
|
||||
.filterNotNull()
|
||||
.forEach { exploreType(resolveTypeVariables(it, type), serializerFactory, interfaces) }
|
||||
|
||||
return interfaces
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension helper for writing described objects.
|
||||
@ -283,7 +50,7 @@ fun resolveTypeVariables(actualType: Type, contextType: Type?): Type {
|
||||
return if (resolvedType is TypeVariable<*>) {
|
||||
val bounds = resolvedType.bounds
|
||||
return if (bounds.isEmpty()) {
|
||||
SerializerFactory.AnyType
|
||||
TypeIdentifier.UnknownType.getLocalType()
|
||||
} else if (bounds.size == 1) {
|
||||
resolveTypeVariables(bounds[0], contextType)
|
||||
} else throw AMQPNotSerializableException(
|
||||
@ -309,8 +76,9 @@ internal fun Type.asClass(): Class<*> {
|
||||
|
||||
internal fun Type.asArray(): Type? {
|
||||
return when(this) {
|
||||
is Class<*> -> this.arrayClass()
|
||||
is ParameterizedType -> DeserializedGenericArrayType(this)
|
||||
is Class<*>,
|
||||
is ParameterizedType -> TypeIdentifier.ArrayOf(TypeIdentifier.forGenericType(this))
|
||||
.getLocalType(this::class.java.classLoader ?: TypeIdentifier::class.java.classLoader)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@ -324,9 +92,10 @@ internal fun Type.componentType(): Type {
|
||||
return (this as? Class<*>)?.componentType ?: (this as GenericArrayType).genericComponentType
|
||||
}
|
||||
|
||||
internal fun Class<*>.asParameterizedType(): ParameterizedType {
|
||||
return DeserializedParameterizedType(this, this.typeParameters)
|
||||
}
|
||||
internal fun Class<*>.asParameterizedType(): ParameterizedType =
|
||||
TypeIdentifier.Erased(this.name, this.typeParameters.size)
|
||||
.toParameterized(this.typeParameters.map { TypeIdentifier.forGenericType(it) })
|
||||
.getLocalType(classLoader ?: TypeIdentifier::class.java.classLoader) as ParameterizedType
|
||||
|
||||
internal fun Type.asParameterizedType(): ParameterizedType {
|
||||
return when (this) {
|
||||
@ -374,19 +143,4 @@ fun hasCordaSerializable(type: Class<*>): Boolean {
|
||||
return type.isAnnotationPresent(CordaSerializable::class.java)
|
||||
|| type.interfaces.any(::hasCordaSerializable)
|
||||
|| (type.superclass != null && hasCordaSerializable(type.superclass))
|
||||
}
|
||||
|
||||
fun isJavaPrimitive(type: Class<*>) = type in JavaPrimitiveTypes.primativeTypes
|
||||
|
||||
private object JavaPrimitiveTypes {
|
||||
val primativeTypes = hashSetOf<Class<*>>(
|
||||
Boolean::class.java,
|
||||
Char::class.java,
|
||||
Byte::class.java,
|
||||
Short::class.java,
|
||||
Int::class.java,
|
||||
Long::class.java,
|
||||
Float::class.java,
|
||||
Double::class.java,
|
||||
Void::class.java)
|
||||
}
|
||||
}
|
@ -7,10 +7,12 @@ import net.corda.core.utilities.contextLogger
|
||||
import net.corda.serialization.internal.CordaSerializationEncoding
|
||||
import net.corda.serialization.internal.SectionId
|
||||
import net.corda.serialization.internal.byteArrayOutput
|
||||
import net.corda.serialization.internal.model.TypeIdentifier
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.io.NotSerializableException
|
||||
import java.io.OutputStream
|
||||
import java.lang.reflect.Type
|
||||
import java.lang.reflect.WildcardType
|
||||
import java.util.*
|
||||
import kotlin.collections.LinkedHashSet
|
||||
|
||||
@ -28,7 +30,7 @@ data class BytesAndSchemas<T : Any>(
|
||||
*/
|
||||
@KeepForDJVM
|
||||
open class SerializationOutput constructor(
|
||||
internal val serializerFactory: SerializerFactory
|
||||
internal val serializerFactory: LocalSerializerFactory
|
||||
) {
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
@ -118,7 +120,7 @@ open class SerializationOutput constructor(
|
||||
if (obj == null) {
|
||||
data.putNull()
|
||||
} else {
|
||||
writeObject(obj, data, if (type == SerializerFactory.AnyType) obj.javaClass else type, context, debugIndent)
|
||||
writeObject(obj, data, if (type == TypeIdentifier.UnknownType.getLocalType()) obj.javaClass else type, context, debugIndent)
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,8 +150,15 @@ open class SerializationOutput constructor(
|
||||
}
|
||||
|
||||
internal open fun requireSerializer(type: Type) {
|
||||
if (type != SerializerFactory.AnyType && type != Object::class.java) {
|
||||
val serializer = serializerFactory.get(null, type)
|
||||
if (type != Object::class.java && type.typeName != "?") {
|
||||
val resolvedType = when(type) {
|
||||
is WildcardType ->
|
||||
if (type.upperBounds.size == 1) type.upperBounds[0]
|
||||
else throw NotSerializableException("Cannot obtain upper bound for type $type")
|
||||
else -> type
|
||||
}
|
||||
|
||||
val serializer = serializerFactory.get(resolvedType)
|
||||
if (serializer !in serializerHistory) {
|
||||
serializerHistory.add(serializer)
|
||||
serializer.writeClassInfo(this)
|
||||
|
@ -1,29 +1,11 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import com.google.common.primitives.Primitives
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.StubOutForDJVM
|
||||
import net.corda.core.internal.kotlinObjectInstance
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.serialization.internal.carpenter.*
|
||||
import net.corda.serialization.internal.model.DefaultCacheProvider
|
||||
import org.apache.qpid.proton.amqp.*
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.*
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
@KeepForDJVM
|
||||
data class SerializationSchemas(val schema: Schema, val transforms: TransformsSchema)
|
||||
@KeepForDJVM
|
||||
data class FactorySchemaAndDescriptor(val schemas: SerializationSchemas, val typeDescriptor: Any)
|
||||
@KeepForDJVM
|
||||
data class CustomSerializersCacheKey(val clazz: Class<*>, val declaredType: Type)
|
||||
|
||||
/**
|
||||
* Factory of serializers designed to be shared across threads and invocations.
|
||||
@ -34,426 +16,15 @@ data class CustomSerializersCacheKey(val clazz: Class<*>, val declaredType: Type
|
||||
* @property onlyCustomSerializers used for testing, when set will cause the factory to throw a
|
||||
* [NotSerializableException] if it cannot find a registered custom serializer for a given type
|
||||
*/
|
||||
// TODO: support for intern-ing of deserialized objects for some core types (e.g. PublicKey) for memory efficiency
|
||||
// TODO: maybe support for caching of serialized form of some core types for performance
|
||||
// TODO: profile for performance in general
|
||||
// TODO: use guava caches etc so not unbounded
|
||||
// TODO: allow definition of well known types that are left out of the schema.
|
||||
// TODO: migrate some core types to unsigned integer descriptor
|
||||
// TODO: document and alert to the fact that classes cannot default superclass/interface properties otherwise they are "erased" due to matching with constructor.
|
||||
// TODO: type name prefixes for interfaces and abstract classes? Or use label?
|
||||
// TODO: generic types should define restricted type alias with source of the wildcarded version, I think, if we're to generate classes from schema
|
||||
// TODO: need to rethink matching of constructor to properties in relation to implementing interfaces and needing those properties etc.
|
||||
// TODO: need to support super classes as well as interfaces with our current code base... what's involved? If we continue to ban, what is the impact?
|
||||
@KeepForDJVM
|
||||
@ThreadSafe
|
||||
interface SerializerFactory {
|
||||
val whitelist: ClassWhitelist
|
||||
val classCarpenter: ClassCarpenter
|
||||
val fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter
|
||||
// Caches
|
||||
val serializersByType: MutableMap<Type, AMQPSerializer<Any>>
|
||||
val serializersByDescriptor: MutableMap<Any, AMQPSerializer<Any>>
|
||||
val transformsCache: MutableMap<String, EnumMap<TransformTypes, MutableList<Transform>>>
|
||||
val fingerPrinter: FingerPrinter
|
||||
val classloader: ClassLoader
|
||||
/**
|
||||
* Look up, and manufacture if necessary, a serializer for the given type.
|
||||
*
|
||||
* @param actualClass Will be null if there isn't an actual object instance available (e.g. for
|
||||
* restricted type processing).
|
||||
*/
|
||||
@Throws(NotSerializableException::class)
|
||||
fun get(actualClass: Class<*>?, declaredType: Type): AMQPSerializer<Any>
|
||||
interface SerializerFactory : LocalSerializerFactory, RemoteSerializerFactory, CustomSerializerRegistry
|
||||
|
||||
/**
|
||||
* Lookup and manufacture a serializer for the given AMQP type descriptor, assuming we also have the necessary types
|
||||
* contained in the [Schema].
|
||||
*/
|
||||
@Throws(NotSerializableException::class)
|
||||
fun get(typeDescriptor: Any, schema: SerializationSchemas): AMQPSerializer<Any>
|
||||
|
||||
/**
|
||||
* Register a custom serializer for any type that cannot be serialized or deserialized by the default serializer
|
||||
* that expects to find getters and a constructor with a parameter for each property.
|
||||
*/
|
||||
fun register(customSerializer: CustomSerializer<out Any>)
|
||||
|
||||
fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer<Any>?
|
||||
fun registerExternal(customSerializer: CorDappCustomSerializer)
|
||||
fun registerByDescriptor(name: Symbol, serializerCreator: () -> AMQPSerializer<Any>): AMQPSerializer<Any>
|
||||
|
||||
object AnyType : WildcardType {
|
||||
override fun getUpperBounds(): Array<Type> = arrayOf(Object::class.java)
|
||||
|
||||
override fun getLowerBounds(): Array<Type> = emptyArray()
|
||||
|
||||
override fun toString(): String = "?"
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun isPrimitive(type: Type): Boolean = primitiveTypeName(type) != null
|
||||
|
||||
fun primitiveTypeName(type: Type): String? {
|
||||
val clazz = type as? Class<*> ?: return null
|
||||
return primitiveTypeNames[Primitives.unwrap(clazz)]
|
||||
}
|
||||
|
||||
fun primitiveType(type: String): Class<*>? {
|
||||
return namesOfPrimitiveTypes[type]
|
||||
}
|
||||
|
||||
private val primitiveTypeNames: Map<Class<*>, String> = mapOf(
|
||||
Character::class.java to "char",
|
||||
Char::class.java to "char",
|
||||
Boolean::class.java to "boolean",
|
||||
Byte::class.java to "byte",
|
||||
UnsignedByte::class.java to "ubyte",
|
||||
Short::class.java to "short",
|
||||
UnsignedShort::class.java to "ushort",
|
||||
Int::class.java to "int",
|
||||
UnsignedInteger::class.java to "uint",
|
||||
Long::class.java to "long",
|
||||
UnsignedLong::class.java to "ulong",
|
||||
Float::class.java to "float",
|
||||
Double::class.java to "double",
|
||||
Decimal32::class.java to "decimal32",
|
||||
Decimal64::class.java to "decimal64",
|
||||
Decimal128::class.java to "decimal128",
|
||||
Date::class.java to "timestamp",
|
||||
UUID::class.java to "uuid",
|
||||
ByteArray::class.java to "binary",
|
||||
String::class.java to "string",
|
||||
Symbol::class.java to "symbol")
|
||||
|
||||
private val namesOfPrimitiveTypes: Map<String, Class<*>> = primitiveTypeNames.map { it.value to it.key }.toMap()
|
||||
|
||||
fun nameForType(type: Type): String = when (type) {
|
||||
is Class<*> -> {
|
||||
primitiveTypeName(type) ?: if (type.isArray) {
|
||||
"${nameForType(type.componentType)}${if (type.componentType.isPrimitive) "[p]" else "[]"}"
|
||||
} else type.name
|
||||
}
|
||||
is ParameterizedType -> {
|
||||
"${nameForType(type.rawType)}<${type.actualTypeArguments.joinToString { nameForType(it) }}>"
|
||||
}
|
||||
is GenericArrayType -> "${nameForType(type.genericComponentType)}[]"
|
||||
is WildcardType -> "?"
|
||||
is TypeVariable<*> -> "?"
|
||||
else -> throw AMQPNotSerializableException(type, "Unable to render type $type to a string.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class DefaultSerializerFactory(
|
||||
override val whitelist: ClassWhitelist,
|
||||
override val classCarpenter: ClassCarpenter,
|
||||
private val evolutionSerializerProvider: EvolutionSerializerProvider,
|
||||
override val fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter,
|
||||
private val onlyCustomSerializers: Boolean = false
|
||||
) : SerializerFactory {
|
||||
|
||||
// Caches
|
||||
override val serializersByType: MutableMap<Type, AMQPSerializer<Any>> = DefaultCacheProvider.createCache()
|
||||
override val serializersByDescriptor: MutableMap<Any, AMQPSerializer<Any>> = DefaultCacheProvider.createCache()
|
||||
private var customSerializers: List<SerializerFor> = emptyList()
|
||||
private val customSerializersCache: MutableMap<CustomSerializersCacheKey, AMQPSerializer<Any>?> = DefaultCacheProvider.createCache()
|
||||
override val transformsCache: MutableMap<String, EnumMap<TransformTypes, MutableList<Transform>>> = DefaultCacheProvider.createCache()
|
||||
|
||||
override val fingerPrinter by lazy { fingerPrinterConstructor(this) }
|
||||
|
||||
override val classloader: ClassLoader get() = classCarpenter.classloader
|
||||
|
||||
// Used to short circuit any computation for a given input, for performance.
|
||||
private data class MemoType(val actualClass: Class<*>?, val declaredType: Type) : Type
|
||||
|
||||
/**
|
||||
* Look up, and manufacture if necessary, a serializer for the given type.
|
||||
*
|
||||
* @param actualClass Will be null if there isn't an actual object instance available (e.g. for
|
||||
* restricted type processing).
|
||||
*/
|
||||
@Throws(NotSerializableException::class)
|
||||
override fun get(actualClass: Class<*>?, declaredType: Type): AMQPSerializer<Any> {
|
||||
// can be useful to enable but will be *extremely* chatty if you do
|
||||
logger.trace { "Get Serializer for $actualClass ${declaredType.typeName}" }
|
||||
|
||||
val ourType = MemoType(actualClass, declaredType)
|
||||
// ConcurrentHashMap.get() is lock free, but computeIfAbsent is not, even if the key is in the map already.
|
||||
return serializersByType[ourType] ?: run {
|
||||
|
||||
val declaredClass = declaredType.asClass()
|
||||
val actualType: Type = if (actualClass == null) declaredType
|
||||
else inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType
|
||||
|
||||
val serializer = when {
|
||||
// Declared class may not be set to Collection, but actual class could be a collection.
|
||||
// In this case use of CollectionSerializer is perfectly appropriate.
|
||||
(Collection::class.java.isAssignableFrom(declaredClass) ||
|
||||
(actualClass != null && Collection::class.java.isAssignableFrom(actualClass))) &&
|
||||
!EnumSet::class.java.isAssignableFrom(actualClass ?: declaredClass) -> {
|
||||
val declaredTypeAmended = CollectionSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass)
|
||||
serializersByType.computeIfAbsent(declaredTypeAmended) {
|
||||
CollectionSerializer(declaredTypeAmended, this)
|
||||
}
|
||||
}
|
||||
// Declared class may not be set to Map, but actual class could be a map.
|
||||
// In this case use of MapSerializer is perfectly appropriate.
|
||||
(Map::class.java.isAssignableFrom(declaredClass) ||
|
||||
(actualClass != null && Map::class.java.isAssignableFrom(actualClass))) -> {
|
||||
val declaredTypeAmended = MapSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass)
|
||||
serializersByType.computeIfAbsent(declaredTypeAmended) {
|
||||
makeMapSerializer(declaredTypeAmended)
|
||||
}
|
||||
}
|
||||
Enum::class.java.isAssignableFrom(actualClass ?: declaredClass) -> {
|
||||
logger.trace {
|
||||
"class=[${actualClass?.simpleName} | $declaredClass] is an enumeration " +
|
||||
"declaredType=${declaredType.typeName} " +
|
||||
"isEnum=${declaredType::class.java.isEnum}"
|
||||
}
|
||||
|
||||
serializersByType.computeIfAbsent(actualClass ?: declaredClass) {
|
||||
whitelist.requireWhitelisted(actualType)
|
||||
EnumSerializer(actualType, actualClass ?: declaredClass, this)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType)
|
||||
}
|
||||
}
|
||||
|
||||
serializersByDescriptor.putIfAbsent(serializer.typeDescriptor, serializer)
|
||||
// Always store the short-circuit too, for performance.
|
||||
serializersByType.putIfAbsent(ourType, serializer)
|
||||
return serializer
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup and manufacture a serializer for the given AMQP type descriptor, assuming we also have the necessary types
|
||||
* contained in the [Schema].
|
||||
*/
|
||||
@Throws(NotSerializableException::class)
|
||||
override fun get(typeDescriptor: Any, schema: SerializationSchemas): AMQPSerializer<Any> {
|
||||
return serializersByDescriptor[typeDescriptor] ?: {
|
||||
logger.trace("get Serializer descriptor=${typeDescriptor}")
|
||||
processSchema(FactorySchemaAndDescriptor(schema, typeDescriptor))
|
||||
serializersByDescriptor[typeDescriptor] ?: throw NotSerializableException(
|
||||
"Could not find type matching descriptor $typeDescriptor.")
|
||||
}()
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a custom serializer for any type that cannot be serialized or deserialized by the default serializer
|
||||
* that expects to find getters and a constructor with a parameter for each property.
|
||||
*/
|
||||
override fun register(customSerializer: CustomSerializer<out Any>) {
|
||||
logger.trace("action=\"Registering custom serializer\", class=\"${customSerializer.type}\"")
|
||||
if (!serializersByDescriptor.containsKey(customSerializer.typeDescriptor)) {
|
||||
customSerializers += customSerializer
|
||||
serializersByDescriptor[customSerializer.typeDescriptor] = customSerializer
|
||||
for (additional in customSerializer.additionalSerializers) {
|
||||
register(additional)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerExternal(customSerializer: CorDappCustomSerializer) {
|
||||
logger.trace("action=\"Registering external serializer\", class=\"${customSerializer.type}\"")
|
||||
if (!serializersByDescriptor.containsKey(customSerializer.typeDescriptor)) {
|
||||
customSerializers += customSerializer
|
||||
serializersByDescriptor[customSerializer.typeDescriptor] = customSerializer
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over an AMQP schema, for each type ascertain whether it's on ClassPath of [classloader] and,
|
||||
* if not, use the [ClassCarpenter] to generate a class to use in its place.
|
||||
*/
|
||||
private fun processSchema(schemaAndDescriptor: FactorySchemaAndDescriptor, sentinel: Boolean = false) {
|
||||
val requiringCarpentry = schemaAndDescriptor.schemas.schema.types.mapNotNull { typeNotation ->
|
||||
try {
|
||||
getOrRegisterSerializer(schemaAndDescriptor, typeNotation)
|
||||
return@mapNotNull null
|
||||
} catch (e: ClassNotFoundException) {
|
||||
if (sentinel) {
|
||||
logger.error("typeNotation=${typeNotation.name} error=\"after Carpentry attempt failed to load\"")
|
||||
throw e
|
||||
}
|
||||
logger.trace { "typeNotation=\"${typeNotation.name}\" action=\"carpentry required\"" }
|
||||
return@mapNotNull typeNotation
|
||||
}
|
||||
}.toList()
|
||||
|
||||
if (requiringCarpentry.isEmpty()) return
|
||||
|
||||
runCarpentry(schemaAndDescriptor, CarpenterMetaSchema.buildWith(classloader, requiringCarpentry))
|
||||
}
|
||||
|
||||
private fun getOrRegisterSerializer(schemaAndDescriptor: FactorySchemaAndDescriptor, typeNotation: TypeNotation) {
|
||||
logger.trace { "descriptor=${schemaAndDescriptor.typeDescriptor}, typeNotation=${typeNotation.name}" }
|
||||
val serialiser = processSchemaEntry(typeNotation)
|
||||
|
||||
// if we just successfully built a serializer for the type but the type fingerprint
|
||||
// doesn't match that of the serialised object then we may be dealing with different
|
||||
// instance of the class, and such we need to build an EvolutionSerializer
|
||||
if (serialiser.typeDescriptor == typeNotation.descriptor.name) return
|
||||
|
||||
logger.trace { "typeNotation=${typeNotation.name} action=\"requires Evolution\"" }
|
||||
evolutionSerializerProvider.getEvolutionSerializer(this, typeNotation, serialiser, schemaAndDescriptor.schemas)
|
||||
}
|
||||
|
||||
private fun processSchemaEntry(typeNotation: TypeNotation) = when (typeNotation) {
|
||||
// java.lang.Class (whether a class or interface)
|
||||
is CompositeType -> {
|
||||
logger.trace("typeNotation=${typeNotation.name} amqpType=CompositeType")
|
||||
processCompositeType(typeNotation)
|
||||
}
|
||||
// Collection / Map, possibly with generics
|
||||
is RestrictedType -> {
|
||||
logger.trace("typeNotation=${typeNotation.name} amqpType=RestrictedType")
|
||||
processRestrictedType(typeNotation)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: class loader logic, and compare the schema.
|
||||
private fun processRestrictedType(typeNotation: RestrictedType) =
|
||||
get(null, typeForName(typeNotation.name, classloader))
|
||||
|
||||
private fun processCompositeType(typeNotation: CompositeType): AMQPSerializer<Any> {
|
||||
// TODO: class loader logic, and compare the schema.
|
||||
val type = typeForName(typeNotation.name, classloader)
|
||||
return get(type.asClass(), type)
|
||||
}
|
||||
|
||||
private fun typeForName(name: String, classloader: ClassLoader): Type = when {
|
||||
name.endsWith("[]") -> {
|
||||
val elementType = typeForName(name.substring(0, name.lastIndex - 1), classloader)
|
||||
if (elementType is ParameterizedType || elementType is GenericArrayType) {
|
||||
DeserializedGenericArrayType(elementType)
|
||||
} else if (elementType is Class<*>) {
|
||||
java.lang.reflect.Array.newInstance(elementType, 0).javaClass
|
||||
} else {
|
||||
throw AMQPNoTypeNotSerializableException("Not able to deserialize array type: $name")
|
||||
}
|
||||
}
|
||||
name.endsWith("[p]") -> // There is no need to handle the ByteArray case as that type is coercible automatically
|
||||
// to the binary type and is thus handled by the main serializer and doesn't need a
|
||||
// special case for a primitive array of bytes
|
||||
when (name) {
|
||||
"int[p]" -> IntArray::class.java
|
||||
"char[p]" -> CharArray::class.java
|
||||
"boolean[p]" -> BooleanArray::class.java
|
||||
"float[p]" -> FloatArray::class.java
|
||||
"double[p]" -> DoubleArray::class.java
|
||||
"short[p]" -> ShortArray::class.java
|
||||
"long[p]" -> LongArray::class.java
|
||||
else -> throw AMQPNoTypeNotSerializableException("Not able to deserialize array type: $name")
|
||||
}
|
||||
else -> DeserializedParameterizedType.make(name, classloader)
|
||||
}
|
||||
|
||||
@StubOutForDJVM
|
||||
private fun runCarpentry(schemaAndDescriptor: FactorySchemaAndDescriptor, metaSchema: CarpenterMetaSchema) {
|
||||
val mc = MetaCarpenter(metaSchema, classCarpenter)
|
||||
try {
|
||||
mc.build()
|
||||
} catch (e: MetaCarpenterException) {
|
||||
// preserve the actual message locally
|
||||
loggerFor<SerializerFactory>().apply {
|
||||
error("${e.message} [hint: enable trace debugging for the stack trace]")
|
||||
trace("", e)
|
||||
}
|
||||
|
||||
// prevent carpenter exceptions escaping into the world, convert things into a nice
|
||||
// NotSerializableException for when this escapes over the wire
|
||||
NotSerializableException(e.name)
|
||||
}
|
||||
processSchema(schemaAndDescriptor, true)
|
||||
}
|
||||
|
||||
private fun makeClassSerializer(
|
||||
clazz: Class<*>,
|
||||
type: Type,
|
||||
declaredType: Type
|
||||
): AMQPSerializer<Any> = serializersByType.computeIfAbsent(type) {
|
||||
logger.debug { "class=${clazz.simpleName}, type=$type is a composite type" }
|
||||
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 AMQPNotSerializableException(
|
||||
type,
|
||||
"Serializer does not support synthetic classes")
|
||||
} else if (SerializerFactory.isPrimitive(clazz)) {
|
||||
AMQPPrimitiveSerializer(clazz)
|
||||
} else {
|
||||
findCustomSerializer(clazz, declaredType) ?: run {
|
||||
if (onlyCustomSerializers) {
|
||||
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.
|
||||
if (clazz.componentType.isPrimitive) PrimArraySerializer.make(type, this)
|
||||
else ArraySerializer.make(type, this)
|
||||
} else {
|
||||
val singleton = clazz.kotlinObjectInstance
|
||||
if (singleton != null) {
|
||||
whitelist.requireWhitelisted(clazz)
|
||||
SingletonSerializer(clazz, singleton, this)
|
||||
} else {
|
||||
whitelist.requireWhitelisted(type)
|
||||
ObjectSerializer(type, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer<Any>? {
|
||||
return customSerializersCache.computeIfAbsent(CustomSerializersCacheKey(clazz, declaredType), ::doFindCustomSerializer)
|
||||
}
|
||||
|
||||
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
|
||||
// AbstractMap, only Map. Otherwise it needs to inject additional schema for a RestrictedType source of the
|
||||
// super type. Could be done, but do we need it?
|
||||
for (customSerializer in customSerializers) {
|
||||
if (customSerializer.isSerializerFor(clazz)) {
|
||||
val declaredSuperClass = declaredType.asClass().superclass
|
||||
|
||||
|
||||
return if (declaredSuperClass == null
|
||||
|| !customSerializer.isSerializerFor(declaredSuperClass)
|
||||
|| !customSerializer.revealSubclassesInSchema
|
||||
) {
|
||||
logger.debug("action=\"Using custom serializer\", class=${clazz.typeName}, " +
|
||||
"declaredType=${declaredType.typeName}")
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
customSerializer as? AMQPSerializer<Any>
|
||||
} else {
|
||||
// Make a subclass serializer for the subclass and return that...
|
||||
CustomSerializer.SubClass(clazz, uncheckedCast(customSerializer))
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun makeMapSerializer(declaredType: ParameterizedType): AMQPSerializer<Any> {
|
||||
val rawType = declaredType.rawType as Class<*>
|
||||
rawType.checkSupportedMapType()
|
||||
return MapSerializer(declaredType, this)
|
||||
}
|
||||
|
||||
override fun registerByDescriptor(name: Symbol, serializerCreator: () -> AMQPSerializer<Any>): AMQPSerializer<Any> =
|
||||
serializersByDescriptor.computeIfAbsent(name) { _ -> serializerCreator() }
|
||||
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
}
|
||||
class ComposedSerializerFactory(
|
||||
private val localSerializerFactory: LocalSerializerFactory,
|
||||
private val remoteSerializerFactory: RemoteSerializerFactory,
|
||||
private val customSerializerRegistry: CachingCustomSerializerRegistry
|
||||
) : SerializerFactory,
|
||||
LocalSerializerFactory by localSerializerFactory,
|
||||
RemoteSerializerFactory by remoteSerializerFactory,
|
||||
CustomSerializerRegistry by customSerializerRegistry
|
@ -5,49 +5,126 @@ import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.serialization.internal.carpenter.ClassCarpenter
|
||||
import net.corda.serialization.internal.carpenter.ClassCarpenterImpl
|
||||
import net.corda.serialization.internal.model.*
|
||||
import java.io.NotSerializableException
|
||||
|
||||
@KeepForDJVM
|
||||
object SerializerFactoryBuilder {
|
||||
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun build(
|
||||
whitelist: ClassWhitelist,
|
||||
classCarpenter: ClassCarpenter,
|
||||
evolutionSerializerProvider: EvolutionSerializerProvider = DefaultEvolutionSerializerProvider,
|
||||
fingerPrinterProvider: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter,
|
||||
onlyCustomSerializers: Boolean = false): SerializerFactory {
|
||||
fun build(whitelist: ClassWhitelist, classCarpenter: ClassCarpenter): SerializerFactory {
|
||||
return makeFactory(
|
||||
whitelist,
|
||||
classCarpenter,
|
||||
evolutionSerializerProvider,
|
||||
fingerPrinterProvider,
|
||||
onlyCustomSerializers)
|
||||
DefaultDescriptorBasedSerializerRegistry(),
|
||||
true,
|
||||
null,
|
||||
false,
|
||||
false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@DeleteForDJVM
|
||||
fun build(
|
||||
whitelist: ClassWhitelist,
|
||||
classCarpenter: ClassCarpenter,
|
||||
descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry =
|
||||
DefaultDescriptorBasedSerializerRegistry(),
|
||||
allowEvolution: Boolean = true,
|
||||
overrideFingerPrinter: FingerPrinter? = null,
|
||||
onlyCustomSerializers: Boolean = false,
|
||||
mustPreserveDataWhenEvolving: Boolean = false): SerializerFactory {
|
||||
return makeFactory(
|
||||
whitelist,
|
||||
classCarpenter,
|
||||
descriptorBasedSerializerRegistry,
|
||||
allowEvolution,
|
||||
overrideFingerPrinter,
|
||||
onlyCustomSerializers,
|
||||
mustPreserveDataWhenEvolving)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
@DeleteForDJVM
|
||||
fun build(
|
||||
whitelist: ClassWhitelist,
|
||||
carpenterClassLoader: ClassLoader,
|
||||
lenientCarpenterEnabled: Boolean = false,
|
||||
evolutionSerializerProvider: EvolutionSerializerProvider = DefaultEvolutionSerializerProvider,
|
||||
fingerPrinterProvider: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter,
|
||||
onlyCustomSerializers: Boolean = false): SerializerFactory {
|
||||
descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry =
|
||||
DefaultDescriptorBasedSerializerRegistry(),
|
||||
allowEvolution: Boolean = true,
|
||||
overrideFingerPrinter: FingerPrinter? = null,
|
||||
onlyCustomSerializers: Boolean = false,
|
||||
mustPreserveDataWhenEvolving: Boolean = false): SerializerFactory {
|
||||
return makeFactory(
|
||||
whitelist,
|
||||
ClassCarpenterImpl(whitelist, carpenterClassLoader, lenientCarpenterEnabled),
|
||||
evolutionSerializerProvider,
|
||||
fingerPrinterProvider,
|
||||
onlyCustomSerializers)
|
||||
descriptorBasedSerializerRegistry,
|
||||
allowEvolution,
|
||||
overrideFingerPrinter,
|
||||
onlyCustomSerializers,
|
||||
mustPreserveDataWhenEvolving)
|
||||
}
|
||||
|
||||
private fun makeFactory(whitelist: ClassWhitelist,
|
||||
classCarpenter: ClassCarpenter,
|
||||
evolutionSerializerProvider: EvolutionSerializerProvider,
|
||||
fingerPrinterProvider: (SerializerFactory) -> FingerPrinter,
|
||||
onlyCustomSerializers: Boolean) =
|
||||
DefaultSerializerFactory(whitelist, classCarpenter, evolutionSerializerProvider, fingerPrinterProvider,
|
||||
onlyCustomSerializers)
|
||||
descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry,
|
||||
allowEvolution: Boolean,
|
||||
overrideFingerPrinter: FingerPrinter?,
|
||||
onlyCustomSerializers: Boolean,
|
||||
mustPreserveDataWhenEvolving: Boolean): SerializerFactory {
|
||||
val customSerializerRegistry = CachingCustomSerializerRegistry(descriptorBasedSerializerRegistry)
|
||||
|
||||
val localTypeModel = ConfigurableLocalTypeModel(
|
||||
WhitelistBasedTypeModelConfiguration(
|
||||
whitelist,
|
||||
customSerializerRegistry))
|
||||
|
||||
val fingerPrinter = overrideFingerPrinter ?:
|
||||
TypeModellingFingerPrinter(customSerializerRegistry)
|
||||
|
||||
val localSerializerFactory = DefaultLocalSerializerFactory(
|
||||
whitelist,
|
||||
localTypeModel,
|
||||
fingerPrinter,
|
||||
classCarpenter.classloader,
|
||||
descriptorBasedSerializerRegistry,
|
||||
customSerializerRegistry,
|
||||
onlyCustomSerializers)
|
||||
|
||||
val typeLoader = ClassCarpentingTypeLoader(
|
||||
SchemaBuildingRemoteTypeCarpenter(classCarpenter),
|
||||
classCarpenter.classloader)
|
||||
|
||||
val evolutionSerializerFactory = if (allowEvolution) DefaultEvolutionSerializerFactory(
|
||||
localSerializerFactory,
|
||||
classCarpenter.classloader,
|
||||
mustPreserveDataWhenEvolving
|
||||
) else NoEvolutionSerializerFactory
|
||||
|
||||
val remoteSerializerFactory = DefaultRemoteSerializerFactory(
|
||||
evolutionSerializerFactory,
|
||||
descriptorBasedSerializerRegistry,
|
||||
AMQPRemoteTypeModel(),
|
||||
localTypeModel,
|
||||
typeLoader,
|
||||
localSerializerFactory)
|
||||
|
||||
return ComposedSerializerFactory(localSerializerFactory, remoteSerializerFactory, customSerializerRegistry)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object NoEvolutionSerializerFactory : EvolutionSerializerFactory {
|
||||
override fun getEvolutionSerializer(remoteTypeInformation: RemoteTypeInformation, localTypeInformation: LocalTypeInformation): AMQPSerializer<Any> {
|
||||
throw NotSerializableException("""
|
||||
Evolution not permitted.
|
||||
|
||||
Remote:
|
||||
${remoteTypeInformation.prettyPrint(false)}
|
||||
|
||||
Local:
|
||||
${localTypeInformation.prettyPrint(false)}
|
||||
""")
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.serialization.internal.model.LocalTypeInformation
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.lang.reflect.Type
|
||||
@ -10,13 +11,12 @@ import java.lang.reflect.Type
|
||||
* absolutely nothing, or null as a described type) when we have a singleton within the node that we just
|
||||
* want converting back to that singleton instance on the receiving JVM.
|
||||
*/
|
||||
class SingletonSerializer(override val type: Class<*>, val singleton: Any, factory: SerializerFactory) : AMQPSerializer<Any> {
|
||||
override val typeDescriptor = Symbol.valueOf(
|
||||
"$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")!!
|
||||
class SingletonSerializer(override val type: Class<*>, val singleton: Any, factory: LocalSerializerFactory) : AMQPSerializer<Any> {
|
||||
override val typeDescriptor = factory.createDescriptor(type)
|
||||
|
||||
private val interfaces = interfacesForSerialization(type, factory)
|
||||
private val interfaces = (factory.getTypeInformation(type) as LocalTypeInformation.Singleton).interfaces
|
||||
|
||||
private fun generateProvides(): List<String> = interfaces.map { it.typeName }
|
||||
private fun generateProvides(): List<String> = interfaces.map { it.typeIdentifier.name }
|
||||
|
||||
internal val typeNotation: TypeNotation = RestrictedType(type.typeName, "Singleton", generateProvides(), "boolean", Descriptor(typeDescriptor), emptyList())
|
||||
|
||||
|
@ -7,6 +7,7 @@ import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.serialization.internal.NotSerializableDetailedException
|
||||
import net.corda.serialization.internal.NotSerializableWithReasonException
|
||||
import net.corda.serialization.internal.model.DefaultCacheProvider
|
||||
import org.apache.qpid.proton.amqp.DescribedType
|
||||
import org.apache.qpid.proton.codec.DescribedTypeConstructor
|
||||
import java.io.NotSerializableException
|
||||
@ -207,7 +208,8 @@ data class TransformsSchema(val types: Map<String, EnumMap<TransformTypes, Mutab
|
||||
* @param sf the [SerializerFactory] building this transform set. Needed as each can define it's own
|
||||
* class loader and this dictates which classes we can and cannot see
|
||||
*/
|
||||
fun get(name: String, sf: SerializerFactory) = sf.transformsCache.computeIfAbsent(name) {
|
||||
fun get(name: String, sf: LocalSerializerFactory) =
|
||||
sf.getOrBuildTransform(name) {
|
||||
val transforms = EnumMap<TransformTypes, MutableList<Transform>>(TransformTypes::class.java)
|
||||
try {
|
||||
val clazz = sf.classloader.loadClass(name)
|
||||
@ -244,7 +246,7 @@ data class TransformsSchema(val types: Map<String, EnumMap<TransformTypes, Mutab
|
||||
|
||||
private fun getAndAdd(
|
||||
type: String,
|
||||
sf: SerializerFactory,
|
||||
sf: LocalSerializerFactory,
|
||||
map: MutableMap<String, EnumMap<TransformTypes, MutableList<Transform>>>) {
|
||||
try {
|
||||
get(type, sf).apply {
|
||||
@ -268,7 +270,7 @@ data class TransformsSchema(val types: Map<String, EnumMap<TransformTypes, Mutab
|
||||
* @param schema should be a [Schema] generated for a serialised data structure
|
||||
* @param sf should be provided by the same serialization context that generated the schema
|
||||
*/
|
||||
fun build(schema: Schema, sf: SerializerFactory) = TransformsSchema(
|
||||
fun build(schema: Schema, sf: LocalSerializerFactory) = TransformsSchema(
|
||||
mutableMapOf<String, EnumMap<TransformTypes, MutableList<Transform>>>().apply {
|
||||
schema.types.forEach { type -> getAndAdd(type.name, sf, this) }
|
||||
})
|
||||
|
@ -0,0 +1,73 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.serialization.internal.model.LocalPropertyInformation
|
||||
import net.corda.serialization.internal.model.LocalTypeInformation
|
||||
import net.corda.serialization.internal.model.TypeIdentifier
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import java.io.NotSerializableException
|
||||
|
||||
object TypeNotationGenerator {
|
||||
|
||||
fun getTypeNotation(typeInformation: LocalTypeInformation, typeDescriptor: Symbol) = when(typeInformation) {
|
||||
is LocalTypeInformation.AnInterface -> typeInformation.getTypeNotation(typeDescriptor)
|
||||
is LocalTypeInformation.Composable -> typeInformation.getTypeNotation(typeDescriptor)
|
||||
is LocalTypeInformation.Abstract -> typeInformation.getTypeNotation(typeDescriptor)
|
||||
else -> throw NotSerializableException("Cannot generate type notation for $typeInformation")
|
||||
}
|
||||
|
||||
private val LocalTypeInformation.amqpTypeName get() = AMQPTypeIdentifiers.nameForType(typeIdentifier)
|
||||
|
||||
private fun LocalTypeInformation.AnInterface.getTypeNotation(typeDescriptor: Symbol): CompositeType =
|
||||
makeCompositeType(
|
||||
(sequenceOf(this) + interfaces.asSequence()).toList(),
|
||||
properties,
|
||||
typeDescriptor)
|
||||
|
||||
private fun LocalTypeInformation.Composable.getTypeNotation(typeDescriptor: Symbol): CompositeType =
|
||||
makeCompositeType(interfaces, properties, typeDescriptor)
|
||||
|
||||
private fun LocalTypeInformation.Abstract.getTypeNotation(typeDescriptor: Symbol): CompositeType =
|
||||
makeCompositeType(interfaces, properties, typeDescriptor)
|
||||
|
||||
private fun LocalTypeInformation.makeCompositeType(
|
||||
interfaces: List<LocalTypeInformation>,
|
||||
properties: Map<String, LocalPropertyInformation>,
|
||||
typeDescriptor: Symbol): CompositeType {
|
||||
val provides = interfaces.map { it.amqpTypeName }
|
||||
val fields = properties.map { (name, property) ->
|
||||
property.getField(name)
|
||||
}
|
||||
|
||||
return CompositeType(
|
||||
amqpTypeName,
|
||||
null,
|
||||
provides,
|
||||
Descriptor(typeDescriptor),
|
||||
fields)
|
||||
}
|
||||
|
||||
private fun LocalPropertyInformation.getField(name: String): Field {
|
||||
val (typeName, requires) = when(type) {
|
||||
is LocalTypeInformation.AnInterface,
|
||||
is LocalTypeInformation.ACollection,
|
||||
is LocalTypeInformation.AMap -> "*" to listOf(type.amqpTypeName)
|
||||
else -> type.amqpTypeName to emptyList()
|
||||
}
|
||||
|
||||
val defaultValue: String? = defaultValues[type.typeIdentifier]
|
||||
|
||||
return Field(name, typeName, requires, defaultValue, null, isMandatory, false)
|
||||
}
|
||||
|
||||
private val defaultValues = sequenceOf(
|
||||
Boolean::class to "false",
|
||||
Byte::class to "0",
|
||||
Int::class to "0",
|
||||
Char::class to "�",
|
||||
Short::class to "0",
|
||||
Long::class to "0",
|
||||
Float::class to "0",
|
||||
Double::class to "0").associate { (type, value) ->
|
||||
TypeIdentifier.forClass(type.javaPrimitiveType!!) to value
|
||||
}
|
||||
}
|
@ -7,7 +7,6 @@ import java.lang.reflect.*
|
||||
* Try and infer concrete types for any generics type variables for the actual class encountered,
|
||||
* based on the declared type.
|
||||
*/
|
||||
// TODO: test GenericArrayType
|
||||
fun inferTypeVariables(actualClass: Class<*>,
|
||||
declaredClass: Class<*>,
|
||||
declaredType: Type): Type? = when (declaredType) {
|
||||
@ -17,10 +16,7 @@ fun inferTypeVariables(actualClass: Class<*>,
|
||||
inferTypeVariables(actualClass.componentType, declaredComponent.asClass(), declaredComponent)?.asArray()
|
||||
}
|
||||
// Nothing to infer, otherwise we'd have ParameterizedType
|
||||
is Class<*> -> actualClass
|
||||
is TypeVariable<*> -> actualClass
|
||||
is WildcardType -> actualClass
|
||||
else -> throw UnsupportedOperationException("Cannot infer type variables for type $declaredType")
|
||||
else -> actualClass
|
||||
}
|
||||
|
||||
/**
|
||||
@ -32,12 +28,6 @@ private fun inferTypeVariables(actualClass: Class<*>, declaredClass: Class<*>, d
|
||||
return null
|
||||
}
|
||||
|
||||
if (!declaredClass.isAssignableFrom(actualClass)) {
|
||||
throw AMQPNotSerializableException(
|
||||
declaredType,
|
||||
"Found object of type $actualClass in a property expecting $declaredType")
|
||||
}
|
||||
|
||||
if (actualClass.typeParameters.isEmpty()) {
|
||||
return actualClass
|
||||
}
|
||||
@ -55,7 +45,7 @@ private fun inferTypeVariables(actualClass: Class<*>, declaredClass: Class<*>, d
|
||||
TypeResolver().where(chainEntry, newResolved)
|
||||
}
|
||||
// The end type is a special case as it is a Class, so we need to fake up a ParameterizedType for it to get the TypeResolver to do anything.
|
||||
val endType = DeserializedParameterizedType(actualClass, actualClass.typeParameters)
|
||||
val endType = actualClass.asParameterizedType()
|
||||
return resolver.resolveType(endType)
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ 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.LocalSerializerFactory
|
||||
import net.corda.serialization.internal.amqp.SerializerFactory
|
||||
import net.corda.serialization.internal.amqp.custom.ClassSerializer.ClassProxy
|
||||
|
||||
@ -12,7 +13,7 @@ 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
|
||||
factory: LocalSerializerFactory
|
||||
) : CustomSerializer.Proxy<Class<*>, ClassSerializer.ClassProxy>(
|
||||
Class::class.java,
|
||||
ClassProxy::class.java,
|
||||
|
@ -20,7 +20,7 @@ object InputStreamSerializer : CustomSerializer.Implements<InputStream>(InputStr
|
||||
type.toString(),
|
||||
"",
|
||||
listOf(type.toString()),
|
||||
SerializerFactory.primitiveTypeName(ByteArray::class.java)!!,
|
||||
AMQPTypeIdentifiers.primitiveTypeName(ByteArray::class.java),
|
||||
descriptor,
|
||||
emptyList())))
|
||||
|
||||
|
@ -8,11 +8,10 @@ import net.corda.serialization.internal.checkUseCase
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.lang.reflect.Type
|
||||
import java.security.PrivateKey
|
||||
import java.util.*
|
||||
|
||||
object PrivateKeySerializer : CustomSerializer.Implements<PrivateKey>(PrivateKey::class.java) {
|
||||
|
||||
override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), SerializerFactory.primitiveTypeName(ByteArray::class.java)!!, descriptor, emptyList())))
|
||||
override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), AMQPTypeIdentifiers.primitiveTypeName(ByteArray::class.java), descriptor, emptyList())))
|
||||
|
||||
override fun writeDescribedObject(obj: PrivateKey, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext
|
||||
|
@ -11,7 +11,7 @@ import java.security.PublicKey
|
||||
* A serializer that writes out a public key in X.509 format.
|
||||
*/
|
||||
object PublicKeySerializer : CustomSerializer.Implements<PublicKey>(PublicKey::class.java) {
|
||||
override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), SerializerFactory.primitiveTypeName(ByteArray::class.java)!!, descriptor, emptyList())))
|
||||
override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), AMQPTypeIdentifiers.primitiveTypeName(ByteArray::class.java), descriptor, emptyList())))
|
||||
|
||||
override fun writeDescribedObject(obj: PublicKey, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext
|
||||
|
@ -6,10 +6,13 @@ import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.serialization.internal.amqp.*
|
||||
import net.corda.serialization.internal.model.LocalConstructorInformation
|
||||
import net.corda.serialization.internal.model.LocalPropertyInformation
|
||||
import net.corda.serialization.internal.model.LocalTypeInformation
|
||||
import java.io.NotSerializableException
|
||||
|
||||
@KeepForDJVM
|
||||
class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<Throwable, ThrowableSerializer.ThrowableProxy>(Throwable::class.java, ThrowableProxy::class.java, factory) {
|
||||
class ThrowableSerializer(factory: LocalSerializerFactory) : CustomSerializer.Proxy<Throwable, ThrowableSerializer.ThrowableProxy>(Throwable::class.java, ThrowableProxy::class.java, factory) {
|
||||
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
@ -19,15 +22,23 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<T
|
||||
|
||||
override val additionalSerializers: Iterable<CustomSerializer<out Any>> = listOf(StackTraceElementSerializer(factory))
|
||||
|
||||
private val LocalTypeInformation.constructor: LocalConstructorInformation get() = when(this) {
|
||||
is LocalTypeInformation.NonComposable -> constructor ?:
|
||||
throw NotSerializableException("$this has no deserialization constructor")
|
||||
is LocalTypeInformation.Composable -> constructor
|
||||
is LocalTypeInformation.Opaque -> expand.constructor
|
||||
else -> throw NotSerializableException("$this has no deserialization constructor")
|
||||
}
|
||||
|
||||
override fun toProxy(obj: Throwable): ThrowableProxy {
|
||||
val extraProperties: MutableMap<String, Any?> = LinkedHashMap()
|
||||
val message = if (obj is CordaThrowable) {
|
||||
// Try and find a constructor
|
||||
try {
|
||||
val constructor = constructorForDeserialization(obj.javaClass)
|
||||
propertiesForSerializationFromConstructor(constructor, obj.javaClass, factory).forEach { property ->
|
||||
extraProperties[property.serializer.name] = property.serializer.propertyReader.read(obj)
|
||||
}
|
||||
val typeInformation = factory.getTypeInformation(obj.javaClass)
|
||||
extraProperties.putAll(typeInformation.propertiesOrEmptyMap.mapValues { (_, property) ->
|
||||
PropertyReader.make(property).read(obj)
|
||||
})
|
||||
} catch (e: NotSerializableException) {
|
||||
logger.warn("Unexpected exception", e)
|
||||
}
|
||||
@ -52,8 +63,13 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<T
|
||||
// If it is CordaException or CordaRuntimeException, we can seek any constructor and then set the properties
|
||||
// Otherwise we just make a CordaRuntimeException
|
||||
if (CordaThrowable::class.java.isAssignableFrom(clazz) && Throwable::class.java.isAssignableFrom(clazz)) {
|
||||
val constructor = constructorForDeserialization(clazz)
|
||||
val throwable = constructor.callBy(constructor.parameters.map { it to proxy.additionalProperties[it.name] }.toMap())
|
||||
val typeInformation = factory.getTypeInformation(clazz)
|
||||
val constructor = typeInformation.constructor
|
||||
val params = constructor.parameters.map { parameter ->
|
||||
proxy.additionalProperties[parameter.name] ?:
|
||||
proxy.additionalProperties[parameter.name.capitalize()]
|
||||
}
|
||||
val throwable = constructor.observedMethod.call(*params.toTypedArray())
|
||||
(throwable as CordaThrowable).apply {
|
||||
if (this.javaClass.name != proxy.exceptionClass) this.originalExceptionClassName = proxy.exceptionClass
|
||||
this.setMessage(proxy.message)
|
||||
@ -85,7 +101,7 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<T
|
||||
val additionalProperties: Map<String, Any?>)
|
||||
}
|
||||
|
||||
class StackTraceElementSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<StackTraceElement, StackTraceElementSerializer.StackTraceElementProxy>(StackTraceElement::class.java, StackTraceElementProxy::class.java, factory) {
|
||||
class StackTraceElementSerializer(factory: LocalSerializerFactory) : CustomSerializer.Proxy<StackTraceElement, StackTraceElementSerializer.StackTraceElementProxy>(StackTraceElement::class.java, StackTraceElementProxy::class.java, factory) {
|
||||
override fun toProxy(obj: StackTraceElement): StackTraceElementProxy = StackTraceElementProxy(obj.className, obj.methodName, obj.fileName, obj.lineNumber)
|
||||
|
||||
override fun fromProxy(proxy: StackTraceElementProxy): StackTraceElement = StackTraceElement(proxy.declaringClass, proxy.methodName, proxy.fileName, proxy.lineNumber)
|
||||
|
@ -12,7 +12,7 @@ object X509CRLSerializer : CustomSerializer.Implements<X509CRL>(X509CRL::class.j
|
||||
type.toString(),
|
||||
"",
|
||||
listOf(type.toString()),
|
||||
SerializerFactory.primitiveTypeName(ByteArray::class.java)!!,
|
||||
AMQPTypeIdentifiers.primitiveTypeName(ByteArray::class.java),
|
||||
descriptor,
|
||||
emptyList()
|
||||
)))
|
||||
|
@ -12,7 +12,7 @@ object X509CertificateSerializer : CustomSerializer.Implements<X509Certificate>(
|
||||
type.toString(),
|
||||
"",
|
||||
listOf(type.toString()),
|
||||
SerializerFactory.primitiveTypeName(ByteArray::class.java)!!,
|
||||
AMQPTypeIdentifiers.primitiveTypeName(ByteArray::class.java),
|
||||
descriptor,
|
||||
emptyList()
|
||||
)))
|
||||
|
@ -1,154 +0,0 @@
|
||||
@file:JvmName("AMQPSchemaExtensions")
|
||||
|
||||
package net.corda.serialization.internal.carpenter
|
||||
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.serialization.internal.amqp.CompositeType
|
||||
import net.corda.serialization.internal.amqp.RestrictedType
|
||||
import net.corda.serialization.internal.amqp.Field as AMQPField
|
||||
import net.corda.serialization.internal.amqp.Schema as AMQPSchema
|
||||
|
||||
@DeleteForDJVM
|
||||
fun AMQPSchema.carpenterSchema(classloader: ClassLoader): CarpenterMetaSchema {
|
||||
val rtn = CarpenterMetaSchema.newInstance()
|
||||
|
||||
types.filterIsInstance<CompositeType>().forEach {
|
||||
it.carpenterSchema(classloader, carpenterSchemas = rtn)
|
||||
}
|
||||
|
||||
return rtn
|
||||
}
|
||||
|
||||
/**
|
||||
* if we can load the class then we MUST know about all of it's composite elements
|
||||
*/
|
||||
private fun CompositeType.validatePropertyTypes(classloader: ClassLoader) {
|
||||
fields.forEach {
|
||||
if (!it.validateType(classloader)) throw UncarpentableException(name, it.name, it.type)
|
||||
}
|
||||
}
|
||||
|
||||
fun AMQPField.typeAsString() = if (type == "*") requires[0] else type
|
||||
|
||||
/**
|
||||
* based upon this AMQP schema either
|
||||
* a) add the corresponding carpenter schema to the [carpenterSchemas] param
|
||||
* b) add the class to the dependency tree in [carpenterSchemas] if it cannot be instantiated
|
||||
* at this time
|
||||
*
|
||||
* @param classloader the class loader provided by the [SerializationContext]
|
||||
* @param carpenterSchemas structure that holds the dependency tree and list of classes that
|
||||
* need constructing
|
||||
* @param force by default a schema is not added to [carpenterSchemas] if it already exists
|
||||
* on the class path. For testing purposes schema generation can be forced
|
||||
*/
|
||||
@DeleteForDJVM
|
||||
fun CompositeType.carpenterSchema(classloader: ClassLoader,
|
||||
carpenterSchemas: CarpenterMetaSchema,
|
||||
force: Boolean = false) {
|
||||
if (classloader.exists(name)) {
|
||||
validatePropertyTypes(classloader)
|
||||
if (!force) return
|
||||
}
|
||||
|
||||
val providesList = mutableListOf<Class<*>>()
|
||||
var isInterface = false
|
||||
var isCreatable = true
|
||||
|
||||
provides.forEach {
|
||||
if (name == it) {
|
||||
isInterface = true
|
||||
return@forEach
|
||||
}
|
||||
|
||||
try {
|
||||
providesList.add(classloader.loadClass(it.stripGenerics()))
|
||||
} catch (e: ClassNotFoundException) {
|
||||
carpenterSchemas.addDepPair(this, name, it)
|
||||
isCreatable = false
|
||||
}
|
||||
}
|
||||
|
||||
val m: MutableMap<String, Field> = mutableMapOf()
|
||||
|
||||
fields.forEach {
|
||||
try {
|
||||
m[it.name] = FieldFactory.newInstance(it.mandatory, it.name, it.getTypeAsClass(classloader))
|
||||
} catch (e: ClassNotFoundException) {
|
||||
carpenterSchemas.addDepPair(this, name, it.typeAsString())
|
||||
isCreatable = false
|
||||
}
|
||||
}
|
||||
|
||||
if (isCreatable) {
|
||||
carpenterSchemas.carpenterSchemas.add(CarpenterSchemaFactory.newInstance(
|
||||
name = name,
|
||||
fields = m,
|
||||
interfaces = providesList,
|
||||
isInterface = isInterface))
|
||||
}
|
||||
}
|
||||
|
||||
// This is potentially problematic as we're assuming the only type of restriction we will be
|
||||
// carpenting for, an enum, but actually trying to split out RestrictedType into something
|
||||
// more polymorphic is hard. Additionally, to conform to AMQP we're really serialising
|
||||
// this as a list so...
|
||||
@DeleteForDJVM
|
||||
fun RestrictedType.carpenterSchema(carpenterSchemas: CarpenterMetaSchema) {
|
||||
val m: MutableMap<String, Field> = mutableMapOf()
|
||||
|
||||
choices.forEach { m[it.name] = EnumField() }
|
||||
|
||||
carpenterSchemas.carpenterSchemas.add(EnumSchema(name = name, fields = m))
|
||||
}
|
||||
|
||||
// map a pair of (typename, mandatory) to the corresponding class type
|
||||
// where the mandatory AMQP flag maps to the types nullability
|
||||
val typeStrToType: Map<Pair<String, Boolean>, Class<out Any?>> = mapOf(
|
||||
Pair("int", true) to Int::class.javaPrimitiveType!!,
|
||||
Pair("int", false) to Integer::class.javaObjectType,
|
||||
Pair("short", true) to Short::class.javaPrimitiveType!!,
|
||||
Pair("short", false) to Short::class.javaObjectType,
|
||||
Pair("long", true) to Long::class.javaPrimitiveType!!,
|
||||
Pair("long", false) to Long::class.javaObjectType,
|
||||
Pair("char", true) to Char::class.javaPrimitiveType!!,
|
||||
Pair("char", false) to java.lang.Character::class.java,
|
||||
Pair("boolean", true) to Boolean::class.javaPrimitiveType!!,
|
||||
Pair("boolean", false) to Boolean::class.javaObjectType,
|
||||
Pair("double", true) to Double::class.javaPrimitiveType!!,
|
||||
Pair("double", false) to Double::class.javaObjectType,
|
||||
Pair("float", true) to Float::class.javaPrimitiveType!!,
|
||||
Pair("float", false) to Float::class.javaObjectType,
|
||||
Pair("byte", true) to Byte::class.javaPrimitiveType!!,
|
||||
Pair("byte", false) to Byte::class.javaObjectType
|
||||
)
|
||||
|
||||
fun String.stripGenerics(): String = if (this.endsWith('>')) {
|
||||
this.substring(0, this.indexOf('<'))
|
||||
} else this
|
||||
|
||||
fun AMQPField.getTypeAsClass(classloader: ClassLoader) = (typeStrToType[Pair(type, mandatory)] ?: when (type) {
|
||||
"string" -> String::class.java
|
||||
"binary" -> ByteArray::class.java
|
||||
"*" -> if (requires.isEmpty()) Any::class.java else {
|
||||
classloader.loadClass(requires[0].stripGenerics())
|
||||
}
|
||||
else -> classloader.loadClass(type.stripGenerics())
|
||||
})!!
|
||||
|
||||
fun AMQPField.validateType(classloader: ClassLoader) =
|
||||
when (type) {
|
||||
"byte", "int", "string", "short", "long", "char", "boolean", "double", "float" -> true
|
||||
"*" -> classloader.exists(requires[0])
|
||||
else -> classloader.exists(type)
|
||||
}
|
||||
|
||||
private fun ClassLoader.exists(clazz: String) = run {
|
||||
try {
|
||||
this.loadClass(clazz); true
|
||||
} catch (e: ClassNotFoundException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
@ -23,14 +23,3 @@ class NullablePrimitiveException(val name: String, val field: Class<out Any>) :
|
||||
|
||||
class UncarpentableException(name: String, field: String, type: String) :
|
||||
ClassCarpenterException("Class $name is loadable yet contains field $field of unknown type $type")
|
||||
|
||||
/**
|
||||
* A meta exception used by the [MetaCarpenter] to wrap any exceptions generated during the build
|
||||
* process and associate those with the current schema being processed. This makes for cleaner external
|
||||
* error hand
|
||||
*
|
||||
* @property name The name of the schema, and thus the class being created, when the error was occured
|
||||
* @property e The [ClassCarpenterException] this is wrapping
|
||||
*/
|
||||
class MetaCarpenterException(val name: String, val e: ClassCarpenterException) : CordaRuntimeException(
|
||||
"Whilst processing class '$name' - ${e.message}")
|
||||
|
@ -1,127 +0,0 @@
|
||||
package net.corda.serialization.internal.carpenter
|
||||
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.StubOutForDJVM
|
||||
import net.corda.serialization.internal.amqp.CompositeType
|
||||
import net.corda.serialization.internal.amqp.RestrictedType
|
||||
import net.corda.serialization.internal.amqp.TypeNotation
|
||||
|
||||
/**
|
||||
* Generated from an AMQP schema this class represents the classes unknown to the deserializer and that thusly
|
||||
* require carpenting up in bytecode form. This is a multi step process as carpenting one object may be dependent
|
||||
* upon the creation of others, this information is tracked in the dependency tree represented by
|
||||
* [dependencies] and [dependsOn]. Creatable classes are stored in [carpenterSchemas].
|
||||
*
|
||||
* The state of this class after initial generation is expected to mutate as classes are built by the carpenter
|
||||
* enabling the resolution of dependencies and thus new carpenter schemas added whilst those already
|
||||
* carpented schemas are removed.
|
||||
*
|
||||
* @property carpenterSchemas The list of carpentable classes
|
||||
* @property dependencies Maps a class to a list of classes that depend on it being built first
|
||||
* @property dependsOn Maps a class to a list of classes it depends on being built before it
|
||||
*
|
||||
* Once a class is constructed we can quickly check for resolution by first looking at all of its dependents in the
|
||||
* [dependencies] map. This will give us a list of classes that depended on that class being carpented. We can then
|
||||
* in turn look up all of those classes in the [dependsOn] list, remove their dependency on the newly created class,
|
||||
* and if that list is reduced to zero know we can now generate a [Schema] for them and carpent them up
|
||||
*/
|
||||
@KeepForDJVM
|
||||
data class CarpenterMetaSchema(
|
||||
val carpenterSchemas: MutableList<Schema>,
|
||||
val dependencies: MutableMap<String, Pair<TypeNotation, MutableList<String>>>,
|
||||
val dependsOn: MutableMap<String, MutableList<String>>) {
|
||||
companion object CarpenterSchemaConstructor {
|
||||
fun buildWith(classLoader: ClassLoader, types: List<TypeNotation>) =
|
||||
newInstance().apply {
|
||||
types.forEach { buildFor(it, classLoader) }
|
||||
}
|
||||
|
||||
fun newInstance(): CarpenterMetaSchema {
|
||||
return CarpenterMetaSchema(mutableListOf(), mutableMapOf(), mutableMapOf())
|
||||
}
|
||||
}
|
||||
|
||||
fun addDepPair(type: TypeNotation, dependant: String, dependee: String) {
|
||||
dependsOn.computeIfAbsent(dependee, { mutableListOf() }).add(dependant)
|
||||
dependencies.computeIfAbsent(dependant, { Pair(type, mutableListOf()) }).second.add(dependee)
|
||||
}
|
||||
|
||||
val size
|
||||
get() = carpenterSchemas.size
|
||||
|
||||
fun isEmpty() = carpenterSchemas.isEmpty()
|
||||
fun isNotEmpty() = carpenterSchemas.isNotEmpty()
|
||||
|
||||
// We could make this an abstract method on TypeNotation but that
|
||||
// would mean the amqp package being "more" infected with carpenter
|
||||
// specific bits.
|
||||
@StubOutForDJVM
|
||||
fun buildFor(target: TypeNotation, cl: ClassLoader): Unit = when (target) {
|
||||
is RestrictedType -> target.carpenterSchema(this)
|
||||
is CompositeType -> target.carpenterSchema(cl, this, false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a dependency tree of [CarpenterMetaSchema] and reduce it to zero by carpenting those classes that
|
||||
* require it. As classes are carpented check for dependency resolution, if now free generate a [Schema] for
|
||||
* that class and add it to the list of classes ([CarpenterMetaSchema.carpenterSchemas]) that require
|
||||
* carpenting
|
||||
*
|
||||
* @property cc a reference to the actual class carpenter we're using to constuct classes
|
||||
* @property objects a list of carpented classes loaded into the carpenters class loader
|
||||
*/
|
||||
@DeleteForDJVM
|
||||
abstract class MetaCarpenterBase(val schemas: CarpenterMetaSchema, val cc: ClassCarpenter) {
|
||||
val objects = mutableMapOf<String, Class<*>>()
|
||||
|
||||
fun step(newObject: Schema) {
|
||||
objects[newObject.name] = cc.build(newObject)
|
||||
|
||||
// go over the list of everything that had a dependency on the newly
|
||||
// carpented class existing and remove it from their dependency list, If that
|
||||
// list is now empty we have no impediment to carpenting that class up
|
||||
schemas.dependsOn.remove(newObject.name)?.forEach { dependent ->
|
||||
|
||||
require(newObject.name in schemas.dependencies[dependent]!!.second)
|
||||
|
||||
schemas.dependencies[dependent]?.second?.remove(newObject.name)
|
||||
|
||||
// we're out of blockers so we can now create the type
|
||||
if (schemas.dependencies[dependent]?.second?.isEmpty() == true) {
|
||||
(schemas.dependencies.remove(dependent)?.first as CompositeType).carpenterSchema(
|
||||
classloader = cc.classloader,
|
||||
carpenterSchemas = schemas)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun build()
|
||||
|
||||
val classloader: ClassLoader
|
||||
get() = cc.classloader
|
||||
}
|
||||
|
||||
@DeleteForDJVM
|
||||
class MetaCarpenter(schemas: CarpenterMetaSchema, cc: ClassCarpenter) : MetaCarpenterBase(schemas, cc) {
|
||||
override fun build() {
|
||||
while (schemas.carpenterSchemas.isNotEmpty()) {
|
||||
val newObject = schemas.carpenterSchemas.removeAt(0)
|
||||
try {
|
||||
step(newObject)
|
||||
} catch (e: ClassCarpenterException) {
|
||||
throw MetaCarpenterException(newObject.name, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteForDJVM
|
||||
class TestMetaCarpenter(schemas: CarpenterMetaSchema, cc: ClassCarpenter) : MetaCarpenterBase(schemas, cc) {
|
||||
override fun build() {
|
||||
if (schemas.carpenterSchemas.isEmpty()) return
|
||||
step(schemas.carpenterSchemas.removeAt(0))
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ fun EnumMap<SchemaFlags, Boolean>.simpleFieldAccess(): Boolean {
|
||||
class ClassSchema(
|
||||
name: String,
|
||||
fields: Map<String, Field>,
|
||||
superclass: Schema? = null,
|
||||
superclass: Schema? = null, // always null for now, but retained because non-null superclass is supported by carpenter.
|
||||
interfaces: List<Class<*>> = emptyList()
|
||||
) : Schema(name, fields, superclass, interfaces, { newName, field -> field.name = newName }) {
|
||||
override fun generateFields(cw: ClassWriter) {
|
||||
@ -128,11 +128,10 @@ object CarpenterSchemaFactory {
|
||||
fun newInstance(
|
||||
name: String,
|
||||
fields: Map<String, Field>,
|
||||
superclass: Schema? = null,
|
||||
interfaces: List<Class<*>> = emptyList(),
|
||||
isInterface: Boolean = false
|
||||
): Schema =
|
||||
if (isInterface) InterfaceSchema(name, fields, superclass, interfaces)
|
||||
else ClassSchema(name, fields, superclass, interfaces)
|
||||
if (isInterface) InterfaceSchema(name, fields, null, interfaces)
|
||||
else ClassSchema(name, fields, null, interfaces)
|
||||
}
|
||||
|
||||
|
@ -6,8 +6,7 @@ import java.lang.reflect.Type
|
||||
/**
|
||||
* Once we have the complete graph of types requiring carpentry to hand, we can use it to sort those types in reverse-
|
||||
* dependency order, i.e. beginning with those types that have no dependencies on other types, then the types that
|
||||
* depended on those types, and so on. This means we can feed types directly to the [RemoteTypeCarpenter], and don't
|
||||
* have to use the [CarpenterMetaSchema].
|
||||
* depended on those types, and so on. This means we can feed types in this order directly to the [RemoteTypeCarpenter].
|
||||
*
|
||||
* @param typesRequiringCarpentry The set of [RemoteTypeInformation] for types that are not reachable by the current
|
||||
* classloader.
|
||||
|
@ -56,8 +56,19 @@ sealed class LocalTypeInformation {
|
||||
* @param type The [Type] to obtain [LocalTypeInformation] for.
|
||||
* @param lookup The [LocalTypeLookup] to use to find previously-constructed [LocalTypeInformation].
|
||||
*/
|
||||
fun forType(type: Type, lookup: LocalTypeLookup): LocalTypeInformation =
|
||||
LocalTypeInformationBuilder(lookup).build(type, TypeIdentifier.forGenericType(type))
|
||||
fun forType(type: Type, lookup: LocalTypeLookup): LocalTypeInformation {
|
||||
val builder = LocalTypeInformationBuilder(lookup)
|
||||
val result = builder.build(type, TypeIdentifier.forGenericType(type))
|
||||
|
||||
// Patch every cyclic reference with a `follow` property pointing to the type information it refers to.
|
||||
builder.cycles.forEach { cycle ->
|
||||
cycle.follow = lookup.findOrBuild(cycle.observedType, cycle.typeIdentifier) {
|
||||
throw IllegalStateException("Should not be attempting to build new type information when populating a cycle")
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -71,6 +82,29 @@ sealed class LocalTypeInformation {
|
||||
*/
|
||||
abstract val typeIdentifier: TypeIdentifier
|
||||
|
||||
/**
|
||||
* Get the map of [LocalPropertyInformation], for all types that have it, or an empty map otherwise.
|
||||
*/
|
||||
val propertiesOrEmptyMap: Map<PropertyName, LocalPropertyInformation> get() = when(this) {
|
||||
is LocalTypeInformation.Composable -> properties
|
||||
is LocalTypeInformation.Abstract -> properties
|
||||
is LocalTypeInformation.AnInterface -> properties
|
||||
is LocalTypeInformation.NonComposable -> properties
|
||||
is LocalTypeInformation.Opaque -> expand.propertiesOrEmptyMap
|
||||
else -> emptyMap()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of interfaces, for all types that have them, or an empty list otherwise.
|
||||
*/
|
||||
val interfacesOrEmptyList: List<LocalTypeInformation> get() = when(this) {
|
||||
is LocalTypeInformation.Composable -> interfaces
|
||||
is LocalTypeInformation.Abstract -> interfaces
|
||||
is LocalTypeInformation.AnInterface -> interfaces
|
||||
is LocalTypeInformation.NonComposable -> interfaces
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a multi-line, recursively-indented representation of this type information.
|
||||
*
|
||||
@ -101,11 +135,10 @@ sealed class LocalTypeInformation {
|
||||
*/
|
||||
data class Cycle(
|
||||
override val observedType: Type,
|
||||
override val typeIdentifier: TypeIdentifier,
|
||||
private val _follow: () -> LocalTypeInformation) : LocalTypeInformation() {
|
||||
val follow: LocalTypeInformation get() = _follow()
|
||||
override val typeIdentifier: TypeIdentifier) : LocalTypeInformation() {
|
||||
lateinit var follow: LocalTypeInformation
|
||||
|
||||
// Custom equals / hashcode because otherwise the "follow" lambda makes equality harder to reason about.
|
||||
// Custom equals / hashcode omitting "follow"
|
||||
override fun equals(other: Any?): Boolean =
|
||||
other is Cycle &&
|
||||
other.observedType == observedType &&
|
||||
@ -121,7 +154,10 @@ sealed class LocalTypeInformation {
|
||||
*/
|
||||
data class Opaque(override val observedType: Class<*>, override val typeIdentifier: TypeIdentifier,
|
||||
private val _expand: () -> LocalTypeInformation) : LocalTypeInformation() {
|
||||
val expand: LocalTypeInformation get() = _expand()
|
||||
/**
|
||||
* In some rare cases, e.g. during Exception serialisation, we may want to "look inside" an opaque type.
|
||||
*/
|
||||
val expand: LocalTypeInformation by lazy { _expand() }
|
||||
|
||||
// Custom equals / hashcode because otherwise the "expand" lambda makes equality harder to reason about.
|
||||
override fun equals(other: Any?): Boolean =
|
||||
@ -202,6 +238,7 @@ sealed class LocalTypeInformation {
|
||||
*
|
||||
* @param constructor [LocalConstructorInformation] for the constructor used when building instances of this type
|
||||
* out of dictionaries of typed values.
|
||||
* @param evolutionConstructors Evolution constructors in ascending version order.
|
||||
* @param properties [LocalPropertyInformation] for the properties of the interface.
|
||||
* @param superclass [LocalTypeInformation] for the superclass of the underlying class of this type.
|
||||
* @param interfaces [LocalTypeInformation] for the interfaces extended by this interface.
|
||||
@ -211,7 +248,7 @@ sealed class LocalTypeInformation {
|
||||
override val observedType: Type,
|
||||
override val typeIdentifier: TypeIdentifier,
|
||||
val constructor: LocalConstructorInformation,
|
||||
val evolverConstructors: List<EvolverConstructorInformation>,
|
||||
val evolutionConstructors: List<EvolutionConstructorInformation>,
|
||||
val properties: Map<PropertyName, LocalPropertyInformation>,
|
||||
val superclass: LocalTypeInformation,
|
||||
val interfaces: List<LocalTypeInformation>,
|
||||
@ -312,7 +349,7 @@ data class LocalConstructorInformation(
|
||||
* Represents information about a constructor that is specifically to be used for evolution, and is potentially matched
|
||||
* with a different set of properties to the regular constructor.
|
||||
*/
|
||||
data class EvolverConstructorInformation(
|
||||
data class EvolutionConstructorInformation(
|
||||
val constructor: LocalConstructorInformation,
|
||||
val properties: Map<String, LocalPropertyInformation>)
|
||||
|
||||
@ -330,16 +367,16 @@ private data class LocalTypeInformationPrettyPrinter(private val simplifyClassNa
|
||||
with(typeInformation) {
|
||||
when (this) {
|
||||
is LocalTypeInformation.Abstract ->
|
||||
typeIdentifier.prettyPrint() +
|
||||
typeIdentifier.prettyPrint(simplifyClassNames) +
|
||||
printInheritsFrom(interfaces, superclass) +
|
||||
indentAnd { printProperties(properties) }
|
||||
is LocalTypeInformation.AnInterface ->
|
||||
typeIdentifier.prettyPrint() + printInheritsFrom(interfaces)
|
||||
is LocalTypeInformation.Composable -> typeIdentifier.prettyPrint() +
|
||||
typeIdentifier.prettyPrint(simplifyClassNames) + printInheritsFrom(interfaces)
|
||||
is LocalTypeInformation.Composable -> typeIdentifier.prettyPrint(simplifyClassNames) +
|
||||
printConstructor(constructor) +
|
||||
printInheritsFrom(interfaces, superclass) +
|
||||
indentAnd { printProperties(properties) }
|
||||
else -> typeIdentifier.prettyPrint()
|
||||
else -> typeIdentifier.prettyPrint(simplifyClassNames)
|
||||
}
|
||||
}
|
||||
|
||||
@ -366,7 +403,7 @@ private data class LocalTypeInformationPrettyPrinter(private val simplifyClassNa
|
||||
" ".repeat(indent) + key +
|
||||
(if(!value.isMandatory) " (optional)" else "") +
|
||||
(if (value.isCalculated) " (calculated)" else "") +
|
||||
": " + value.type.prettyPrint(simplifyClassNames)
|
||||
": " + prettyPrint(value.type)
|
||||
|
||||
private inline fun indentAnd(block: LocalTypeInformationPrettyPrinter.() -> String) =
|
||||
copy(indent = indent + 1).block()
|
||||
|
@ -32,7 +32,10 @@ import kotlin.reflect.jvm.javaType
|
||||
* this is not a [MutableSet], as we want to be able to backtrack while traversing through the graph of related types, and
|
||||
* will find it useful to revert to earlier states of knowledge about which types have been visited on a given branch.
|
||||
*/
|
||||
internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup, val resolutionContext: Type? = null, val visited: Set<TypeIdentifier> = emptySet()) {
|
||||
internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
|
||||
val resolutionContext: Type? = null,
|
||||
val visited: Set<TypeIdentifier> = emptySet(),
|
||||
val cycles: MutableList<LocalTypeInformation.Cycle> = mutableListOf()) {
|
||||
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
@ -42,9 +45,7 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup, val
|
||||
* Recursively build [LocalTypeInformation] for the given [Type] and [TypeIdentifier]
|
||||
*/
|
||||
fun build(type: Type, typeIdentifier: TypeIdentifier): LocalTypeInformation =
|
||||
if (typeIdentifier in visited) LocalTypeInformation.Cycle(type, typeIdentifier) {
|
||||
LocalTypeInformationBuilder(lookup, resolutionContext).build(type, typeIdentifier)
|
||||
}
|
||||
if (typeIdentifier in visited) LocalTypeInformation.Cycle(type, typeIdentifier).apply { cycles.add(this) }
|
||||
else lookup.findOrBuild(type, typeIdentifier) { isOpaque ->
|
||||
copy(visited = visited + typeIdentifier).buildIfNotFound(type, typeIdentifier, isOpaque)
|
||||
}
|
||||
@ -184,13 +185,13 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup, val
|
||||
interfaceInformation, typeParameterInformation)
|
||||
}
|
||||
|
||||
val evolverConstructors = evolverConstructors(type).map { ctor ->
|
||||
val evolutionConstructors = evolutionConstructors(type).map { ctor ->
|
||||
val constructorInformation = buildConstructorInformation(type, ctor)
|
||||
val evolverProperties = buildObjectProperties(rawType, constructorInformation)
|
||||
EvolverConstructorInformation(constructorInformation, evolverProperties)
|
||||
val evolutionProperties = buildObjectProperties(rawType, constructorInformation)
|
||||
EvolutionConstructorInformation(constructorInformation, evolutionProperties)
|
||||
}
|
||||
|
||||
return LocalTypeInformation.Composable(type, typeIdentifier, constructorInformation, evolverConstructors, properties,
|
||||
return LocalTypeInformation.Composable(type, typeIdentifier, constructorInformation, evolutionConstructors, properties,
|
||||
superclassInformation, interfaceInformation, typeParameterInformation)
|
||||
}
|
||||
|
||||
@ -395,7 +396,10 @@ private fun constructorForDeserialization(type: Type): KFunction<Any>? {
|
||||
}
|
||||
}
|
||||
|
||||
private fun evolverConstructors(type: Type): List<KFunction<Any>> {
|
||||
/**
|
||||
* Obtain evolution constructors in ascending version order.
|
||||
*/
|
||||
private fun evolutionConstructors(type: Type): List<KFunction<Any>> {
|
||||
val clazz = type.asClass()
|
||||
if (!clazz.isConcreteClass || clazz.isSynthetic) return emptyList()
|
||||
|
||||
|
@ -23,7 +23,10 @@ class SchemaBuildingRemoteTypeCarpenter(private val carpenter: ClassCarpenter):
|
||||
try {
|
||||
when (typeInformation) {
|
||||
is RemoteTypeInformation.AnInterface -> typeInformation.carpentInterface()
|
||||
is RemoteTypeInformation.Composable -> typeInformation.carpentComposable()
|
||||
is RemoteTypeInformation.Composable ->
|
||||
// We cannot carpent parameterised types, and if the type is parameterised assume we are really here
|
||||
// because a type parameter needed carpenting.
|
||||
if (typeInformation.typeIdentifier !is TypeIdentifier.Parameterised) typeInformation.carpentComposable()
|
||||
is RemoteTypeInformation.AnEnum -> typeInformation.carpentEnum()
|
||||
else -> {
|
||||
} // Anything else, such as arrays, will be taken care of by the above
|
||||
@ -31,7 +34,14 @@ class SchemaBuildingRemoteTypeCarpenter(private val carpenter: ClassCarpenter):
|
||||
} catch (e: ClassCarpenterException) {
|
||||
throw NotSerializableException("${typeInformation.typeIdentifier.name}: ${e.message}")
|
||||
}
|
||||
return typeInformation.typeIdentifier.getLocalType(classLoader)
|
||||
|
||||
return try {
|
||||
typeInformation.typeIdentifier.getLocalType(classLoader)
|
||||
} catch (e: ClassNotFoundException) {
|
||||
// This might happen if we've been asked to carpent up a parameterised type, and it's the rawtype itself
|
||||
// rather than any of its type parameters that were missing.
|
||||
throw NotSerializableException("Could not carpent ${typeInformation.typeIdentifier.prettyPrint(false)}")
|
||||
}
|
||||
}
|
||||
|
||||
private val RemoteTypeInformation.erasedLocalClass get() = typeIdentifier.getLocalType(classLoader).asClass()
|
||||
|
@ -1,9 +1,5 @@
|
||||
package net.corda.serialization.internal.model
|
||||
|
||||
import net.corda.serialization.internal.amqp.Transform
|
||||
import net.corda.serialization.internal.amqp.TransformTypes
|
||||
import java.util.*
|
||||
|
||||
typealias TypeDescriptor = String
|
||||
|
||||
/**
|
||||
@ -88,9 +84,9 @@ sealed class RemoteTypeInformation {
|
||||
/**
|
||||
* The [RemoteTypeInformation] emitted if we hit a cycle while traversing the graph of related types.
|
||||
*/
|
||||
data class Cycle(override val typeIdentifier: TypeIdentifier, private val _follow: () -> RemoteTypeInformation) : RemoteTypeInformation() {
|
||||
override val typeDescriptor = typeIdentifier.name
|
||||
val follow: RemoteTypeInformation get() = _follow()
|
||||
data class Cycle(override val typeIdentifier: TypeIdentifier) : RemoteTypeInformation() {
|
||||
override val typeDescriptor by lazy { follow.typeDescriptor }
|
||||
lateinit var follow: RemoteTypeInformation
|
||||
|
||||
override fun equals(other: Any?): Boolean = other is Cycle && other.typeIdentifier == typeIdentifier
|
||||
override fun hashCode(): Int = typeIdentifier.hashCode()
|
||||
@ -176,14 +172,14 @@ private data class RemoteTypeInformationPrettyPrinter(private val simplifyClassN
|
||||
}
|
||||
|
||||
private fun printProperties(properties: Map<String, RemotePropertyInformation>) =
|
||||
properties.entries.sortedBy { it.key }.joinToString("\n", "\n", "") {
|
||||
properties.entries.joinToString("\n", "\n", "") {
|
||||
it.prettyPrint()
|
||||
}
|
||||
|
||||
private fun Map.Entry<String, RemotePropertyInformation>.prettyPrint(): String =
|
||||
" ".repeat(indent) + key +
|
||||
(if(!value.isMandatory) " (optional)" else "") +
|
||||
": " + value.type.prettyPrint(simplifyClassNames)
|
||||
": " + prettyPrint(value.type)
|
||||
}
|
||||
|
||||
data class EnumTransforms(val defaults: Map<String, String>, val renames: Map<String, String>) {
|
||||
|
@ -0,0 +1,233 @@
|
||||
package net.corda.serialization.internal.model
|
||||
|
||||
import com.google.common.hash.Hashing
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.toBase64
|
||||
import net.corda.serialization.internal.amqp.*
|
||||
import java.io.NotSerializableException
|
||||
|
||||
/**
|
||||
* A fingerprinter that fingerprints [LocalTypeInformation].
|
||||
*/
|
||||
interface FingerPrinter {
|
||||
/**
|
||||
* Traverse the provided [LocalTypeInformation] graph and emit a short fingerprint string uniquely representing
|
||||
* the shape of that graph.
|
||||
*
|
||||
* @param typeInformation The [LocalTypeInformation] to fingerprint.
|
||||
*/
|
||||
fun fingerprint(typeInformation: LocalTypeInformation): String
|
||||
}
|
||||
|
||||
/**
|
||||
* A [FingerPrinter] that consults a [CustomTypeDescriptorLookup] to obtain type descriptors for
|
||||
* types that do not need to be traversed to calculate their fingerprint information. (Usually these will be the type
|
||||
* descriptors supplied by custom serializers).
|
||||
*
|
||||
* @param customTypeDescriptorLookup The [CustomTypeDescriptorLookup] to use to obtain custom type descriptors for
|
||||
* selected types.
|
||||
*/
|
||||
class TypeModellingFingerPrinter(
|
||||
private val customTypeDescriptorLookup: CustomSerializerRegistry,
|
||||
private val debugEnabled: Boolean = false) : FingerPrinter {
|
||||
|
||||
private val cache: MutableMap<TypeIdentifier, String> = DefaultCacheProvider.createCache()
|
||||
|
||||
override fun fingerprint(typeInformation: LocalTypeInformation): String =
|
||||
cache.computeIfAbsent(typeInformation.typeIdentifier) {
|
||||
FingerPrintingState(
|
||||
customTypeDescriptorLookup,
|
||||
FingerprintWriter(debugEnabled)).fingerprint(typeInformation)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for the [Hasher] we use to generate fingerprints, providing methods for writing various kinds of content
|
||||
* into the hash.
|
||||
*/
|
||||
internal class FingerprintWriter(debugEnabled: Boolean) {
|
||||
|
||||
companion object {
|
||||
private const val ARRAY_HASH: String = "Array = true"
|
||||
private const val ENUM_HASH: String = "Enum = true"
|
||||
private const val ALREADY_SEEN_HASH: String = "Already seen = true"
|
||||
private const val NULLABLE_HASH: String = "Nullable = true"
|
||||
private const val NOT_NULLABLE_HASH: String = "Nullable = false"
|
||||
private const val ANY_TYPE_HASH: String = "Any type = true"
|
||||
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
private val debugBuffer: StringBuilder? = if (debugEnabled) StringBuilder() else null
|
||||
private var hasher = Hashing.murmur3_128().newHasher() // FIXUP: remove dependency on Guava Hasher
|
||||
|
||||
fun write(chars: CharSequence) = append(chars)
|
||||
fun write(words: List<CharSequence>) = append(words.joinToString())
|
||||
fun writeAlreadySeen() = append(ALREADY_SEEN_HASH)
|
||||
fun writeEnum() = append(ENUM_HASH)
|
||||
fun writeArray() = append(ARRAY_HASH)
|
||||
fun writeNullable() = append(NULLABLE_HASH)
|
||||
fun writeNotNullable() = append(NOT_NULLABLE_HASH)
|
||||
fun writeAny() = append(ANY_TYPE_HASH)
|
||||
|
||||
private fun append(chars: CharSequence) = apply {
|
||||
debugBuffer?.append(chars)
|
||||
hasher = hasher.putUnencodedChars(chars)
|
||||
}
|
||||
|
||||
val fingerprint: String by lazy {
|
||||
val fingerprint = hasher.hash().asBytes().toBase64()
|
||||
if (debugBuffer != null) logger.info("$fingerprint from $debugBuffer")
|
||||
fingerprint
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Representation of the current state of fingerprinting, which keeps track of which types have already been visited
|
||||
* during fingerprinting.
|
||||
*/
|
||||
private class FingerPrintingState(
|
||||
private val customSerializerRegistry: CustomSerializerRegistry,
|
||||
private val writer: FingerprintWriter) {
|
||||
|
||||
companion object {
|
||||
private var CHARACTER_TYPE = LocalTypeInformation.Atomic(
|
||||
Character::class.java,
|
||||
TypeIdentifier.forClass(Character::class.java))
|
||||
}
|
||||
|
||||
private val typesSeen: MutableSet<TypeIdentifier> = mutableSetOf()
|
||||
|
||||
/**
|
||||
* Fingerprint the type recursively, and return the encoded fingerprint written into the hasher.
|
||||
*/
|
||||
fun fingerprint(type: LocalTypeInformation): String =
|
||||
fingerprintType(type).writer.fingerprint
|
||||
|
||||
// This method concatenates various elements of the types recursively as unencoded strings into the hasher,
|
||||
// effectively creating a unique string for a type which we then hash in the calling function above.
|
||||
private fun fingerprintType(type: LocalTypeInformation): FingerPrintingState = apply {
|
||||
// Don't go round in circles.
|
||||
when {
|
||||
hasSeen(type.typeIdentifier) -> writer.writeAlreadySeen()
|
||||
type is LocalTypeInformation.Cycle -> fingerprintType(type.follow)
|
||||
else -> ifThrowsAppend({ type.observedType.typeName }, {
|
||||
typesSeen.add(type.typeIdentifier)
|
||||
fingerprintNewType(type)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// For a type we haven't seen before, determine the correct path depending on the type of type it is.
|
||||
private fun fingerprintNewType(type: LocalTypeInformation) = apply {
|
||||
when (type) {
|
||||
is LocalTypeInformation.Cycle ->
|
||||
throw IllegalStateException("Cyclic references must be dereferenced before fingerprinting")
|
||||
is LocalTypeInformation.Unknown,
|
||||
is LocalTypeInformation.Top -> writer.writeAny()
|
||||
is LocalTypeInformation.AnArray -> {
|
||||
fingerprintType(type.componentType)
|
||||
writer.writeArray()
|
||||
}
|
||||
is LocalTypeInformation.ACollection -> fingerprintCollection(type)
|
||||
is LocalTypeInformation.AMap -> fingerprintMap(type)
|
||||
is LocalTypeInformation.Atomic -> fingerprintName(type)
|
||||
is LocalTypeInformation.Opaque -> fingerprintOpaque(type)
|
||||
is LocalTypeInformation.AnEnum -> fingerprintEnum(type)
|
||||
is LocalTypeInformation.AnInterface -> fingerprintInterface(type)
|
||||
is LocalTypeInformation.Abstract -> fingerprintAbstract(type)
|
||||
is LocalTypeInformation.Singleton -> fingerprintName(type)
|
||||
is LocalTypeInformation.Composable -> fingerprintComposable(type)
|
||||
is LocalTypeInformation.NonComposable -> throw NotSerializableException(
|
||||
"Attempted to fingerprint non-composable type ${type.typeIdentifier.prettyPrint(false)}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun fingerprintCollection(type: LocalTypeInformation.ACollection) {
|
||||
fingerprintName(type)
|
||||
fingerprintType(type.elementType)
|
||||
}
|
||||
|
||||
private fun fingerprintMap(type: LocalTypeInformation.AMap) {
|
||||
fingerprintName(type)
|
||||
fingerprintType(type.keyType)
|
||||
fingerprintType(type.valueType)
|
||||
}
|
||||
|
||||
private fun fingerprintOpaque(type: LocalTypeInformation) =
|
||||
fingerprintWithCustomSerializerOrElse(type) {
|
||||
fingerprintName(type)
|
||||
}
|
||||
|
||||
private fun fingerprintInterface(type: LocalTypeInformation.AnInterface) =
|
||||
fingerprintWithCustomSerializerOrElse(type) {
|
||||
fingerprintName(type)
|
||||
writer.writeAlreadySeen() // FIXUP: this replicates the behaviour of the old fingerprinter for compatibility reasons.
|
||||
fingerprintInterfaces(type.interfaces)
|
||||
fingerprintTypeParameters(type.typeParameters)
|
||||
}
|
||||
|
||||
private fun fingerprintAbstract(type: LocalTypeInformation.Abstract) =
|
||||
fingerprintWithCustomSerializerOrElse(type) {
|
||||
fingerprintName(type)
|
||||
fingerprintProperties(type.properties)
|
||||
fingerprintInterfaces(type.interfaces)
|
||||
fingerprintTypeParameters(type.typeParameters)
|
||||
}
|
||||
|
||||
private fun fingerprintComposable(type: LocalTypeInformation.Composable) =
|
||||
fingerprintWithCustomSerializerOrElse(type) {
|
||||
fingerprintName(type)
|
||||
fingerprintProperties(type.properties)
|
||||
fingerprintInterfaces(type.interfaces)
|
||||
fingerprintTypeParameters(type.typeParameters)
|
||||
}
|
||||
|
||||
private fun fingerprintName(type: LocalTypeInformation) {
|
||||
val identifier = type.typeIdentifier
|
||||
when (identifier) {
|
||||
is TypeIdentifier.ArrayOf -> writer.write(identifier.componentType.name).writeArray()
|
||||
else -> writer.write(identifier.name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fingerprintTypeParameters(typeParameters: List<LocalTypeInformation>) =
|
||||
typeParameters.forEach { fingerprintType(it) }
|
||||
|
||||
private fun fingerprintProperties(properties: Map<String, LocalPropertyInformation>) =
|
||||
properties.asSequence().sortedBy { it.key }.forEach { (propertyName, propertyType) ->
|
||||
val (neverMandatory, adjustedType) = adjustType(propertyType.type)
|
||||
fingerprintType(adjustedType)
|
||||
writer.write(propertyName)
|
||||
if (propertyType.isMandatory && !neverMandatory) writer.writeNotNullable() else writer.writeNullable()
|
||||
}
|
||||
|
||||
// Compensate for the serialisation framework's forcing of char to Character
|
||||
private fun adjustType(propertyType: LocalTypeInformation): Pair<Boolean, LocalTypeInformation> =
|
||||
if (propertyType.typeIdentifier.name == "char") true to CHARACTER_TYPE else false to propertyType
|
||||
|
||||
private fun fingerprintInterfaces(interfaces: List<LocalTypeInformation>) =
|
||||
interfaces.forEach { fingerprintType(it) }
|
||||
|
||||
// ensures any change to the enum (adding constants) will trigger the need for evolution
|
||||
private fun fingerprintEnum(type: LocalTypeInformation.AnEnum) {
|
||||
writer.write(type.members).write(type.typeIdentifier.name).writeEnum()
|
||||
}
|
||||
|
||||
// Give any custom serializers loaded into the factory the chance to supply their own type-descriptors
|
||||
private fun fingerprintWithCustomSerializerOrElse(type: LocalTypeInformation, defaultAction: () -> Unit) {
|
||||
val customTypeDescriptor = customSerializerRegistry.findCustomSerializer(type.observedType.asClass(), type.observedType)?.typeDescriptor?.toString()
|
||||
if (customTypeDescriptor != null) writer.write(customTypeDescriptor)
|
||||
else defaultAction()
|
||||
}
|
||||
|
||||
// Test whether we are in a state in which we have already seen the given type.
|
||||
//
|
||||
// We don't include Example<?> and Example<T> where type is ? or T in this otherwise we
|
||||
// generate different fingerprints for class Outer<T>(val a: Inner<T>) when serialising
|
||||
// and deserializing (assuming deserialization is occurring in a factory that didn't
|
||||
// serialise the object in the first place (and thus the cache lookup fails). This is also
|
||||
// true of Any, where we need Example<A, B> and Example<?, ?> to have the same fingerprint
|
||||
private fun hasSeen(type: TypeIdentifier) = (type in typesSeen)
|
||||
&& (type != TypeIdentifier.UnknownType)
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package net.corda.serialization.internal.amqp;
|
||||
|
||||
import net.corda.serialization.internal.amqp.testutils.TestDescriptorBasedSerializerRegistry;
|
||||
import net.corda.serialization.internal.amqp.testutils.TestSerializationContext;
|
||||
import org.junit.Test;
|
||||
|
||||
@ -133,8 +134,9 @@ public class JavaPrivatePropertyTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singlePrivateWithConstructor() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
|
||||
SerializerFactory factory = testDefaultFactory();
|
||||
public void singlePrivateWithConstructor() throws NotSerializableException {
|
||||
TestDescriptorBasedSerializerRegistry registry = new TestDescriptorBasedSerializerRegistry();
|
||||
SerializerFactory factory = testDefaultFactory(registry);
|
||||
|
||||
SerializationOutput ser = new SerializationOutput(factory);
|
||||
DeserializationInput des = new DeserializationInput(factory);
|
||||
@ -144,22 +146,14 @@ public class JavaPrivatePropertyTests {
|
||||
|
||||
assertEquals (c.a, c2.a);
|
||||
|
||||
//
|
||||
// Now ensure we actually got a private property serializer
|
||||
//
|
||||
Map<Object, AMQPSerializer<Object>> serializersByDescriptor = factory.getSerializersByDescriptor();
|
||||
|
||||
assertEquals(1, serializersByDescriptor.size());
|
||||
ObjectSerializer cSerializer = ((ObjectSerializer)serializersByDescriptor.values().toArray()[0]);
|
||||
assertEquals(1, cSerializer.getPropertySerializers().getSerializationOrder().size());
|
||||
Object[] propertyReaders = cSerializer.getPropertySerializers().getSerializationOrder().toArray();
|
||||
assertTrue (((PropertyAccessor)propertyReaders[0]).getSerializer().getPropertyReader() instanceof PrivatePropertyReader);
|
||||
assertEquals(1, registry.getContents().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singlePrivateWithConstructorAndGetter()
|
||||
throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
|
||||
SerializerFactory factory = testDefaultFactory();
|
||||
throws NotSerializableException {
|
||||
TestDescriptorBasedSerializerRegistry registry = new TestDescriptorBasedSerializerRegistry();
|
||||
SerializerFactory factory = testDefaultFactory(registry);
|
||||
|
||||
SerializationOutput ser = new SerializationOutput(factory);
|
||||
DeserializationInput des = new DeserializationInput(factory);
|
||||
@ -169,15 +163,6 @@ public class JavaPrivatePropertyTests {
|
||||
|
||||
assertEquals (c.a, c2.a);
|
||||
|
||||
//
|
||||
// Now ensure we actually got a private property serializer
|
||||
//
|
||||
Map<Object, AMQPSerializer<Object>> serializersByDescriptor = factory.getSerializersByDescriptor();
|
||||
|
||||
assertEquals(1, serializersByDescriptor.size());
|
||||
ObjectSerializer cSerializer = ((ObjectSerializer)serializersByDescriptor.values().toArray()[0]);
|
||||
assertEquals(1, cSerializer.getPropertySerializers().getSerializationOrder().size());
|
||||
Object[] propertyReaders = cSerializer.getPropertySerializers().getSerializationOrder().toArray();
|
||||
assertTrue (((PropertyAccessor)propertyReaders[0]).getSerializer().getPropertyReader() instanceof PublicPropertyReader);
|
||||
assertEquals(1, registry.getContents().size());
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ import net.corda.core.serialization.SerializedBytes;
|
||||
import net.corda.serialization.internal.AllWhitelist;
|
||||
import net.corda.serialization.internal.amqp.*;
|
||||
import net.corda.serialization.internal.amqp.Schema;
|
||||
import net.corda.serialization.internal.model.RemoteTypeInformation;
|
||||
import net.corda.serialization.internal.model.TypeIdentifier;
|
||||
import net.corda.testing.core.SerializationEnvironmentRule;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
@ -66,42 +68,23 @@ public class JavaCalculatedValuesToClassCarpenterTest extends AmqpCarpenterBase
|
||||
ObjectAndEnvelope<C> objAndEnv = new DeserializationInput(factory)
|
||||
.deserializeAndReturnEnvelope(serialized, C.class, context);
|
||||
|
||||
C amqpObj = objAndEnv.getObj();
|
||||
Schema schema = objAndEnv.getEnvelope().getSchema();
|
||||
|
||||
assertEquals(2, amqpObj.getI());
|
||||
assertEquals("4", amqpObj.getSquared());
|
||||
assertEquals(2, schema.getTypes().size());
|
||||
assertTrue(schema.getTypes().get(0) instanceof CompositeType);
|
||||
|
||||
CompositeType concrete = (CompositeType) schema.getTypes().get(0);
|
||||
assertEquals(3, concrete.getFields().size());
|
||||
assertEquals("doubled", concrete.getFields().get(0).getName());
|
||||
assertEquals("int", concrete.getFields().get(0).getType());
|
||||
assertEquals("i", concrete.getFields().get(1).getName());
|
||||
assertEquals("int", concrete.getFields().get(1).getType());
|
||||
assertEquals("squared", concrete.getFields().get(2).getName());
|
||||
assertEquals("string", concrete.getFields().get(2).getType());
|
||||
|
||||
assertEquals(0, AMQPSchemaExtensions.carpenterSchema(schema, ClassLoader.getSystemClassLoader()).getSize());
|
||||
Schema mangledSchema = ClassCarpenterTestUtilsKt.mangleNames(schema, singletonList(C.class.getTypeName()));
|
||||
CarpenterMetaSchema l2 = AMQPSchemaExtensions.carpenterSchema(mangledSchema, ClassLoader.getSystemClassLoader());
|
||||
String mangledClassName = ClassCarpenterTestUtilsKt.mangleName(C.class.getTypeName());
|
||||
|
||||
assertEquals(1, l2.getSize());
|
||||
net.corda.serialization.internal.carpenter.Schema carpenterSchema = l2.getCarpenterSchemas().stream()
|
||||
.filter(s -> s.getName().equals(mangledClassName))
|
||||
TypeIdentifier typeToMangle = TypeIdentifier.Companion.forClass(C.class);
|
||||
Envelope env = objAndEnv.getEnvelope();
|
||||
RemoteTypeInformation typeInformation = getTypeInformation(env).values().stream()
|
||||
.filter(it -> it.getTypeIdentifier().equals(typeToMangle))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("No schema found for mangled class name " + mangledClassName));
|
||||
.orElseThrow(IllegalStateException::new);
|
||||
|
||||
Class<?> pinochio = new ClassCarpenterImpl(AllWhitelist.INSTANCE).build(carpenterSchema);
|
||||
RemoteTypeInformation renamed = rename(typeInformation, typeToMangle, mangle(typeToMangle));
|
||||
|
||||
Class<?> pinochio = load(renamed);
|
||||
Object p = pinochio.getConstructors()[0].newInstance(4, 2, "4");
|
||||
|
||||
assertEquals(pinochio.getMethod("getI").invoke(p), amqpObj.getI());
|
||||
assertEquals(pinochio.getMethod("getSquared").invoke(p), amqpObj.getSquared());
|
||||
assertEquals(pinochio.getMethod("getDoubled").invoke(p), amqpObj.getDoubled());
|
||||
assertEquals(2, pinochio.getMethod("getI").invoke(p));
|
||||
assertEquals("4", pinochio.getMethod("getSquared").invoke(p));
|
||||
assertEquals(4, pinochio.getMethod("getDoubled").invoke(p));
|
||||
|
||||
Parent upcast = (Parent) p;
|
||||
assertEquals(upcast.getDoubled(), amqpObj.getDoubled());
|
||||
assertEquals(4, upcast.getDoubled());
|
||||
}
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ class ListsSerializationTest {
|
||||
payload.add(2)
|
||||
val wrongPayloadType = WrongPayloadType(payload)
|
||||
Assertions.assertThatThrownBy { wrongPayloadType.serialize() }
|
||||
.isInstanceOf(NotSerializableException::class.java).hasMessageContaining("Cannot derive collection type for declaredType")
|
||||
.isInstanceOf(NotSerializableException::class.java).hasMessageContaining("Cannot derive collection type for declared type")
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
@ -107,7 +107,9 @@ class ListsSerializationTest {
|
||||
val container = CovariantContainer(payload)
|
||||
|
||||
fun verifyEnvelopeBody(envelope: Envelope) {
|
||||
envelope.schema.types.single { typeNotation -> typeNotation.name == java.util.List::class.java.name + "<?>" }
|
||||
envelope.schema.types.single { typeNotation ->
|
||||
typeNotation.name == "java.util.List<${Parent::class.java.name}>"
|
||||
}
|
||||
}
|
||||
|
||||
assertEqualAfterRoundTripSerialization(container, { bytes -> verifyEnvelope(bytes, ::verifyEnvelopeBody) })
|
||||
|
@ -37,7 +37,7 @@ class AbstractAMQPSerializationSchemeTest {
|
||||
null)
|
||||
|
||||
|
||||
val factory = TestSerializerFactory(TESTING_CONTEXT.whitelist, TESTING_CONTEXT.deserializationClassLoader)
|
||||
val factory = SerializerFactoryBuilder.build(TESTING_CONTEXT.whitelist, TESTING_CONTEXT.deserializationClassLoader)
|
||||
val maxFactories = 512
|
||||
val backingMap = AccessOrderLinkedHashMap<Pair<ClassWhitelist, ClassLoader>, SerializerFactory>({ maxFactories })
|
||||
val scheme = object : AbstractAMQPSerializationScheme(emptySet(), backingMap, createSerializerFactoryFactory()) {
|
||||
@ -55,7 +55,6 @@ class AbstractAMQPSerializationSchemeTest {
|
||||
|
||||
}
|
||||
|
||||
|
||||
IntStream.range(0, 2048).parallel().forEach {
|
||||
val context = if (ThreadLocalRandom.current().nextBoolean()) {
|
||||
genesisContext.withClassLoader(URLClassLoader(emptyArray()))
|
||||
|
@ -13,16 +13,13 @@ import kotlin.test.assertEquals
|
||||
class CorDappSerializerTests {
|
||||
data class NeedsProxy(val a: String)
|
||||
|
||||
private fun proxyFactory(serializers: List<SerializationCustomSerializer<*, *>>): SerializerFactory {
|
||||
val factory = SerializerFactoryBuilder.build(AllWhitelist,
|
||||
ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader()),
|
||||
DefaultEvolutionSerializerProvider)
|
||||
|
||||
private fun proxyFactory(
|
||||
serializers: List<SerializationCustomSerializer<*, *>>
|
||||
) = SerializerFactoryBuilder.build(AllWhitelist,
|
||||
ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader())).apply {
|
||||
serializers.forEach {
|
||||
factory.registerExternal(CorDappCustomSerializer(it, factory))
|
||||
registerExternal(CorDappCustomSerializer(it, this))
|
||||
}
|
||||
|
||||
return factory
|
||||
}
|
||||
|
||||
class NeedsProxyProxySerializer : SerializationCustomSerializer<NeedsProxy, NeedsProxyProxySerializer.Proxy> {
|
||||
|
@ -20,15 +20,15 @@ class DeserializeNeedingCarpentryOfEnumsTest : AmqpCarpenterBase(AllWhitelist) {
|
||||
// Setup the test
|
||||
//
|
||||
val setupFactory = testDefaultFactoryNoEvolution()
|
||||
|
||||
val classCarpenter = ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader())
|
||||
val enumConstants = listOf("AAA", "BBB", "CCC", "DDD", "EEE", "FFF",
|
||||
"GGG", "HHH", "III", "JJJ").associateBy({ it }, { EnumField() })
|
||||
|
||||
// create the enum
|
||||
val testEnumType = setupFactory.classCarpenter.build(EnumSchema("test.testEnumType", enumConstants))
|
||||
val testEnumType = classCarpenter.build(EnumSchema("test.testEnumType", enumConstants))
|
||||
|
||||
// create the class that has that enum as an element
|
||||
val testClassType = setupFactory.classCarpenter.build(ClassSchema("test.testClassType",
|
||||
val testClassType = classCarpenter.build(ClassSchema("test.testClassType",
|
||||
mapOf("a" to NonNullableField(testEnumType))))
|
||||
|
||||
// create an instance of the class we can then serialise
|
||||
@ -59,16 +59,16 @@ class DeserializeNeedingCarpentryOfEnumsTest : AmqpCarpenterBase(AllWhitelist) {
|
||||
// Setup the test
|
||||
//
|
||||
val setupFactory = testDefaultFactoryNoEvolution()
|
||||
|
||||
val classCarpenter = ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader())
|
||||
val enumConstants = listOf("AAA", "BBB", "CCC", "DDD", "EEE", "FFF",
|
||||
"GGG", "HHH", "III", "JJJ").associateBy({ it }, { EnumField() })
|
||||
|
||||
// create the enum
|
||||
val testEnumType1 = setupFactory.classCarpenter.build(EnumSchema("test.testEnumType1", enumConstants))
|
||||
val testEnumType2 = setupFactory.classCarpenter.build(EnumSchema("test.testEnumType2", enumConstants))
|
||||
val testEnumType1 = classCarpenter.build(EnumSchema("test.testEnumType1", enumConstants))
|
||||
val testEnumType2 = classCarpenter.build(EnumSchema("test.testEnumType2", enumConstants))
|
||||
|
||||
// create the class that has that enum as an element
|
||||
val testClassType = setupFactory.classCarpenter.build(ClassSchema("test.testClassType",
|
||||
val testClassType = classCarpenter.build(ClassSchema("test.testClassType",
|
||||
mapOf(
|
||||
"a" to NonNullableField(testEnumType1),
|
||||
"b" to NonNullableField(testEnumType2),
|
||||
|
@ -441,7 +441,4 @@ class DeserializeNeedingCarpentrySimpleTypesTest : AmqpCarpenterBase(AllWhitelis
|
||||
assertEquals(0b1010.toByte(), deserializedObj::class.java.getMethod("getByteB").invoke(deserializedObj))
|
||||
assertEquals(null, deserializedObj::class.java.getMethod("getByteC").invoke(deserializedObj))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
@ -74,7 +74,7 @@ class DeserializeSimpleTypesTests {
|
||||
val ia = IA(arrayOf(1, 2, 3))
|
||||
|
||||
assertEquals("class [Ljava.lang.Integer;", ia.ia::class.java.toString())
|
||||
assertEquals(SerializerFactory.nameForType(ia.ia::class.java), "int[]")
|
||||
assertEquals(AMQPTypeIdentifiers.nameForType(ia.ia::class.java), "int[]")
|
||||
|
||||
val serialisedIA = TestSerializationOutput(VERBOSE, sf1).serialize(ia)
|
||||
val deserializedIA = DeserializationInput(sf1).deserialize(serialisedIA)
|
||||
@ -93,7 +93,7 @@ class DeserializeSimpleTypesTests {
|
||||
val ia = IA(arrayOf(Integer(1), Integer(2), Integer(3)))
|
||||
|
||||
assertEquals("class [Ljava.lang.Integer;", ia.ia::class.java.toString())
|
||||
assertEquals(SerializerFactory.nameForType(ia.ia::class.java), "int[]")
|
||||
assertEquals(AMQPTypeIdentifiers.nameForType(ia.ia::class.java), "int[]")
|
||||
|
||||
val serialisedIA = TestSerializationOutput(VERBOSE, sf1).serialize(ia)
|
||||
val deserializedIA = DeserializationInput(sf1).deserialize(serialisedIA)
|
||||
@ -116,7 +116,7 @@ class DeserializeSimpleTypesTests {
|
||||
val ia = IA(v)
|
||||
|
||||
assertEquals("class [I", ia.ia::class.java.toString())
|
||||
assertEquals(SerializerFactory.nameForType(ia.ia::class.java), "int[p]")
|
||||
assertEquals(AMQPTypeIdentifiers.nameForType(ia.ia::class.java), "int[p]")
|
||||
|
||||
val serialisedIA = TestSerializationOutput(VERBOSE, sf1).serialize(ia)
|
||||
val deserializedIA = DeserializationInput(sf1).deserialize(serialisedIA)
|
||||
@ -134,7 +134,7 @@ class DeserializeSimpleTypesTests {
|
||||
val c = C(arrayOf('a', 'b', 'c'))
|
||||
|
||||
assertEquals("class [Ljava.lang.Character;", c.c::class.java.toString())
|
||||
assertEquals(SerializerFactory.nameForType(c.c::class.java), "char[]")
|
||||
assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "char[]")
|
||||
|
||||
val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c)
|
||||
val deserializedC = DeserializationInput(sf1).deserialize(serialisedC)
|
||||
@ -154,7 +154,7 @@ class DeserializeSimpleTypesTests {
|
||||
val c = C(v)
|
||||
|
||||
assertEquals("class [C", c.c::class.java.toString())
|
||||
assertEquals(SerializerFactory.nameForType(c.c::class.java), "char[p]")
|
||||
assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "char[p]")
|
||||
|
||||
val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c)
|
||||
var deserializedC = DeserializationInput(sf1).deserialize(serialisedC)
|
||||
@ -183,7 +183,7 @@ class DeserializeSimpleTypesTests {
|
||||
val c = C(arrayOf(true, false, false, true))
|
||||
|
||||
assertEquals("class [Ljava.lang.Boolean;", c.c::class.java.toString())
|
||||
assertEquals(SerializerFactory.nameForType(c.c::class.java), "boolean[]")
|
||||
assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "boolean[]")
|
||||
|
||||
val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c)
|
||||
val deserializedC = DeserializationInput(sf1).deserialize(serialisedC)
|
||||
@ -203,7 +203,7 @@ class DeserializeSimpleTypesTests {
|
||||
c.c[0] = true; c.c[1] = false; c.c[2] = false; c.c[3] = true
|
||||
|
||||
assertEquals("class [Z", c.c::class.java.toString())
|
||||
assertEquals(SerializerFactory.nameForType(c.c::class.java), "boolean[p]")
|
||||
assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "boolean[p]")
|
||||
|
||||
val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c)
|
||||
val deserializedC = DeserializationInput(sf1).deserialize(serialisedC)
|
||||
@ -222,7 +222,7 @@ class DeserializeSimpleTypesTests {
|
||||
val c = C(arrayOf(0b0001, 0b0101, 0b1111))
|
||||
|
||||
assertEquals("class [Ljava.lang.Byte;", c.c::class.java.toString())
|
||||
assertEquals(SerializerFactory.nameForType(c.c::class.java), "byte[]")
|
||||
assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "byte[]")
|
||||
|
||||
val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c)
|
||||
val deserializedC = DeserializationInput(sf1).deserialize(serialisedC)
|
||||
@ -241,7 +241,7 @@ class DeserializeSimpleTypesTests {
|
||||
c.c[0] = 0b0001; c.c[1] = 0b0101; c.c[2] = 0b1111
|
||||
|
||||
assertEquals("class [B", c.c::class.java.toString())
|
||||
assertEquals(SerializerFactory.nameForType(c.c::class.java), "binary")
|
||||
assertEquals("binary", AMQPTypeIdentifiers.nameForType(c.c::class.java))
|
||||
|
||||
val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c)
|
||||
val deserializedC = DeserializationInput(sf1).deserialize(serialisedC)
|
||||
@ -267,7 +267,7 @@ class DeserializeSimpleTypesTests {
|
||||
val c = C(arrayOf(1, 2, 3))
|
||||
|
||||
assertEquals("class [Ljava.lang.Short;", c.c::class.java.toString())
|
||||
assertEquals(SerializerFactory.nameForType(c.c::class.java), "short[]")
|
||||
assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "short[]")
|
||||
|
||||
val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c)
|
||||
val deserializedC = DeserializationInput(sf1).deserialize(serialisedC)
|
||||
@ -286,7 +286,7 @@ class DeserializeSimpleTypesTests {
|
||||
c.c[0] = 1; c.c[1] = 2; c.c[2] = 5
|
||||
|
||||
assertEquals("class [S", c.c::class.java.toString())
|
||||
assertEquals(SerializerFactory.nameForType(c.c::class.java), "short[p]")
|
||||
assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "short[p]")
|
||||
|
||||
val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c)
|
||||
val deserializedC = DeserializationInput(sf1).deserialize(serialisedC)
|
||||
@ -304,7 +304,7 @@ class DeserializeSimpleTypesTests {
|
||||
val c = C(arrayOf(2147483650, -2147483800, 10))
|
||||
|
||||
assertEquals("class [Ljava.lang.Long;", c.c::class.java.toString())
|
||||
assertEquals(SerializerFactory.nameForType(c.c::class.java), "long[]")
|
||||
assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "long[]")
|
||||
|
||||
val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c)
|
||||
val deserializedC = DeserializationInput(sf1).deserialize(serialisedC)
|
||||
@ -323,7 +323,7 @@ class DeserializeSimpleTypesTests {
|
||||
c.c[0] = 2147483650; c.c[1] = -2147483800; c.c[2] = 10
|
||||
|
||||
assertEquals("class [J", c.c::class.java.toString())
|
||||
assertEquals(SerializerFactory.nameForType(c.c::class.java), "long[p]")
|
||||
assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "long[p]")
|
||||
|
||||
val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c)
|
||||
val deserializedC = DeserializationInput(sf1).deserialize(serialisedC)
|
||||
@ -341,7 +341,7 @@ class DeserializeSimpleTypesTests {
|
||||
val c = C(arrayOf(10F, 100.023232F, -1455.433400F))
|
||||
|
||||
assertEquals("class [Ljava.lang.Float;", c.c::class.java.toString())
|
||||
assertEquals(SerializerFactory.nameForType(c.c::class.java), "float[]")
|
||||
assertEquals("float[]", AMQPTypeIdentifiers.nameForType(c.c::class.java))
|
||||
|
||||
val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c)
|
||||
val deserializedC = DeserializationInput(sf1).deserialize(serialisedC)
|
||||
@ -360,7 +360,7 @@ class DeserializeSimpleTypesTests {
|
||||
c.c[0] = 10F; c.c[1] = 100.023232F; c.c[2] = -1455.433400F
|
||||
|
||||
assertEquals("class [F", c.c::class.java.toString())
|
||||
assertEquals(SerializerFactory.nameForType(c.c::class.java), "float[p]")
|
||||
assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "float[p]")
|
||||
|
||||
val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c)
|
||||
val deserializedC = DeserializationInput(sf1).deserialize(serialisedC)
|
||||
@ -378,7 +378,7 @@ class DeserializeSimpleTypesTests {
|
||||
val c = C(arrayOf(10.0, 100.2, -1455.2))
|
||||
|
||||
assertEquals("class [Ljava.lang.Double;", c.c::class.java.toString())
|
||||
assertEquals(SerializerFactory.nameForType(c.c::class.java), "double[]")
|
||||
assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "double[]")
|
||||
|
||||
val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c)
|
||||
val deserializedC = DeserializationInput(sf1).deserialize(serialisedC)
|
||||
@ -397,7 +397,7 @@ class DeserializeSimpleTypesTests {
|
||||
c.c[0] = 10.0; c.c[1] = 100.2; c.c[2] = -1455.2
|
||||
|
||||
assertEquals("class [D", c.c::class.java.toString())
|
||||
assertEquals(SerializerFactory.nameForType(c.c::class.java), "double[p]")
|
||||
assertEquals(AMQPTypeIdentifiers.nameForType(c.c::class.java), "double[p]")
|
||||
|
||||
val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c)
|
||||
val deserializedC = DeserializationInput(sf1).deserialize(serialisedC)
|
||||
|
@ -1,105 +0,0 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import org.junit.Test
|
||||
import java.io.NotSerializableException
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class DeserializedParameterizedTypeTests {
|
||||
private fun normalise(string: String): String {
|
||||
return string.replace(" ", "")
|
||||
}
|
||||
|
||||
private fun verify(typeName: String) {
|
||||
val type = DeserializedParameterizedType.make(typeName)
|
||||
assertEquals(normalise(type.typeName), normalise(typeName))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test nested`() {
|
||||
verify(" java.util.Map < java.util.Map< java.lang.String, java.lang.Integer >, java.util.Map < java.lang.Long , java.lang.String > >")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test simple`() {
|
||||
verify("java.util.List<java.lang.String>")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test multiple args`() {
|
||||
verify("java.util.Map<java.lang.String,java.lang.Integer>")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test trailing whitespace`() {
|
||||
verify("java.util.Map<java.lang.String, java.lang.Integer> ")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test list of commands`() {
|
||||
verify("java.util.List<net.corda.core.contracts.Command<?>>")
|
||||
}
|
||||
|
||||
@Test(expected = NotSerializableException::class)
|
||||
fun `test trailing text`() {
|
||||
verify("java.util.Map<java.lang.String, java.lang.Integer>foo")
|
||||
}
|
||||
|
||||
@Test(expected = NotSerializableException::class)
|
||||
fun `test trailing comma`() {
|
||||
verify("java.util.Map<java.lang.String, java.lang.Integer,>")
|
||||
}
|
||||
|
||||
@Test(expected = NotSerializableException::class)
|
||||
fun `test leading comma`() {
|
||||
verify("java.util.Map<,java.lang.String, java.lang.Integer>")
|
||||
}
|
||||
|
||||
@Test(expected = NotSerializableException::class)
|
||||
fun `test middle comma`() {
|
||||
verify("java.util.Map<,java.lang.String,, java.lang.Integer>")
|
||||
}
|
||||
|
||||
@Test(expected = NotSerializableException::class)
|
||||
fun `test trailing close`() {
|
||||
verify("java.util.Map<java.lang.String, java.lang.Integer>>")
|
||||
}
|
||||
|
||||
@Test(expected = NotSerializableException::class)
|
||||
fun `test empty params`() {
|
||||
verify("java.util.Map<>")
|
||||
}
|
||||
|
||||
@Test(expected = NotSerializableException::class)
|
||||
fun `test mid whitespace`() {
|
||||
verify("java.u til.List<java.lang.String>")
|
||||
}
|
||||
|
||||
@Test(expected = NotSerializableException::class)
|
||||
fun `test mid whitespace2`() {
|
||||
verify("java.util.List<java.l ng.String>")
|
||||
}
|
||||
|
||||
@Test(expected = NotSerializableException::class)
|
||||
fun `test wrong number of parameters`() {
|
||||
verify("java.util.List<java.lang.String, java.lang.Integer>")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test no parameters`() {
|
||||
verify("java.lang.String")
|
||||
}
|
||||
|
||||
@Test(expected = NotSerializableException::class)
|
||||
fun `test parameters on non-generic type`() {
|
||||
verify("java.lang.String<java.lang.Integer>")
|
||||
}
|
||||
|
||||
@Test(expected = NotSerializableException::class)
|
||||
fun `test excessive nesting`() {
|
||||
var nested = "java.lang.Integer"
|
||||
for (i in 1..DeserializedParameterizedType.MAX_DEPTH) {
|
||||
nested = "java.util.List<$nested>"
|
||||
}
|
||||
verify(nested)
|
||||
}
|
||||
}
|
@ -392,27 +392,10 @@ class EnumEvolvabilityTests {
|
||||
data class C1(val annotatedEnum: AnnotatedEnumOnce)
|
||||
|
||||
val sf = testDefaultFactory()
|
||||
val f = sf.javaClass.getDeclaredField("transformsCache")
|
||||
f.isAccessible = true
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val transformsCache = f.get(sf) as ConcurrentHashMap<String, EnumMap<TransformTypes, MutableList<Transform>>>
|
||||
|
||||
assertEquals(0, transformsCache.size)
|
||||
|
||||
val sb1 = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C1(AnnotatedEnumOnce.D))
|
||||
|
||||
assertEquals(2, transformsCache.size)
|
||||
assertTrue(transformsCache.containsKey(C1::class.java.name))
|
||||
assertTrue(transformsCache.containsKey(AnnotatedEnumOnce::class.java.name))
|
||||
|
||||
val sb2 = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C2(AnnotatedEnumOnce.D))
|
||||
|
||||
assertEquals(3, transformsCache.size)
|
||||
assertTrue(transformsCache.containsKey(C1::class.java.name))
|
||||
assertTrue(transformsCache.containsKey(C2::class.java.name))
|
||||
assertTrue(transformsCache.containsKey(AnnotatedEnumOnce::class.java.name))
|
||||
|
||||
assertEquals(sb1.transformsSchema.types[AnnotatedEnumOnce::class.java.name],
|
||||
sb2.transformsSchema.types[AnnotatedEnumOnce::class.java.name])
|
||||
}
|
||||
|
@ -0,0 +1,55 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.serialization.internal.amqp.testutils.deserializeAndReturnEnvelope
|
||||
import net.corda.serialization.internal.amqp.testutils.serialize
|
||||
import net.corda.serialization.internal.amqp.testutils.testDefaultFactory
|
||||
import net.corda.serialization.internal.model.RemoteTypeInformation
|
||||
import net.corda.serialization.internal.model.TypeIdentifier
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class EvolutionSerializerFactoryTests {
|
||||
|
||||
private val factory = testDefaultFactory()
|
||||
|
||||
@Test
|
||||
fun preservesDataWhenFlagSet() {
|
||||
val nonStrictEvolutionSerializerFactory = DefaultEvolutionSerializerFactory(
|
||||
factory,
|
||||
ClassLoader.getSystemClassLoader(),
|
||||
false)
|
||||
|
||||
val strictEvolutionSerializerFactory = DefaultEvolutionSerializerFactory(
|
||||
factory,
|
||||
ClassLoader.getSystemClassLoader(),
|
||||
true)
|
||||
|
||||
@Suppress("unused")
|
||||
class C(val importantFieldA: Int)
|
||||
val (_, env) = DeserializationInput(factory).deserializeAndReturnEnvelope(
|
||||
SerializationOutput(factory).serialize(C(1)))
|
||||
|
||||
val remoteTypeInformation = AMQPRemoteTypeModel().interpret(SerializationSchemas(env.schema, env.transformsSchema))
|
||||
.values.find { it.typeIdentifier == TypeIdentifier.forClass(C::class.java) }
|
||||
as RemoteTypeInformation.Composable
|
||||
|
||||
val withAddedField = remoteTypeInformation.copy(properties = remoteTypeInformation.properties.plus(
|
||||
"importantFieldB" to remoteTypeInformation.properties["importantFieldA"]!!))
|
||||
|
||||
val localTypeInformation = factory.getTypeInformation(C::class.java)
|
||||
|
||||
// No evolution required with original fields.
|
||||
assertNull(strictEvolutionSerializerFactory.getEvolutionSerializer(remoteTypeInformation, localTypeInformation))
|
||||
|
||||
// Returns an evolution serializer if the fields have changed.
|
||||
assertNotNull(nonStrictEvolutionSerializerFactory.getEvolutionSerializer(withAddedField, localTypeInformation))
|
||||
|
||||
// Fails in strict mode if the remote type information includes a field not included in the local type.
|
||||
assertFailsWith<EvolutionSerializationException> {
|
||||
strictEvolutionSerializerFactory.getEvolutionSerializer(withAddedField, localTypeInformation)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import java.io.NotSerializableException
|
||||
|
||||
/**
|
||||
* An implementation of [EvolutionSerializerProvider] that disables all evolution within a
|
||||
* [SerializerFactory]. This is most useful in testing where it is known that evolution should not be
|
||||
* occurring and where bugs may be hidden by transparent invocation of an [EvolutionSerializer]. This
|
||||
* prevents that by simply throwing an exception whenever such a serializer is requested.
|
||||
*/
|
||||
object FailIfEvolutionAttempted : EvolutionSerializerProvider {
|
||||
override fun getEvolutionSerializer(factory: SerializerFactory,
|
||||
typeNotation: TypeNotation,
|
||||
newSerializer: AMQPSerializer<Any>,
|
||||
schemas: SerializationSchemas): AMQPSerializer<Any> {
|
||||
throw NotSerializableException("No evolution should be occurring\n" +
|
||||
" ${typeNotation.name}\n" +
|
||||
" ${typeNotation.descriptor.name}\n" +
|
||||
" ${newSerializer.type.typeName}\n" +
|
||||
" ${newSerializer.typeDescriptor}\n\n${schemas.schema}")
|
||||
}
|
||||
}
|
@ -461,6 +461,17 @@ class EvolvabilityTests {
|
||||
assertEquals(oa, outer.a)
|
||||
assertEquals(ia, outer.b.a)
|
||||
assertEquals(null, outer.b.b)
|
||||
|
||||
// Repeat, but receiving a message with the newer version of Inner
|
||||
val newVersion = SerializationOutput(sf).serializeAndReturnSchema(Outer(oa, Inner(ia, "new value")))
|
||||
val model = AMQPRemoteTypeModel()
|
||||
val remoteTypeInfo = model.interpret(SerializationSchemas(newVersion.schema, newVersion.transformsSchema))
|
||||
println(remoteTypeInfo)
|
||||
|
||||
val newOuter = DeserializationInput(sf).deserialize(SerializedBytes<Outer>(newVersion.obj.bytes))
|
||||
assertEquals(oa, newOuter.a)
|
||||
assertEquals(ia, newOuter.b.a)
|
||||
assertEquals("new value", newOuter.b.b)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1,23 +1,25 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import org.junit.Test
|
||||
import java.lang.reflect.Type
|
||||
import kotlin.test.assertEquals
|
||||
import net.corda.serialization.internal.AllWhitelist
|
||||
import net.corda.serialization.internal.amqp.testutils.TestSerializationOutput
|
||||
import net.corda.serialization.internal.amqp.testutils.serializeAndReturnSchema
|
||||
import net.corda.serialization.internal.carpenter.ClassCarpenterImpl
|
||||
import net.corda.serialization.internal.model.ConfigurableLocalTypeModel
|
||||
import net.corda.serialization.internal.model.LocalTypeInformation
|
||||
import net.corda.serialization.internal.model.FingerPrinter
|
||||
|
||||
class FingerPrinterTesting : FingerPrinter {
|
||||
private var index = 0
|
||||
private val cache = mutableMapOf<Type, String>()
|
||||
private val cache = mutableMapOf<LocalTypeInformation, String>()
|
||||
|
||||
override fun fingerprint(type: Type): String {
|
||||
return cache.computeIfAbsent(type) { index++.toString() }
|
||||
override fun fingerprint(typeInformation: LocalTypeInformation): String {
|
||||
return cache.computeIfAbsent(typeInformation) { index++.toString() }
|
||||
}
|
||||
|
||||
@Suppress("UNUSED")
|
||||
fun changeFingerprint(type: Type) {
|
||||
fun changeFingerprint(type: LocalTypeInformation) {
|
||||
cache.computeIfAbsent(type) { "" }.apply { index++.toString() }
|
||||
}
|
||||
}
|
||||
@ -30,10 +32,14 @@ class FingerPrinterTestingTests {
|
||||
@Test
|
||||
fun testingTest() {
|
||||
val fpt = FingerPrinterTesting()
|
||||
assertEquals("0", fpt.fingerprint(Integer::class.java))
|
||||
assertEquals("1", fpt.fingerprint(String::class.java))
|
||||
assertEquals("0", fpt.fingerprint(Integer::class.java))
|
||||
assertEquals("1", fpt.fingerprint(String::class.java))
|
||||
val descriptorBasedSerializerRegistry = DefaultDescriptorBasedSerializerRegistry()
|
||||
val customSerializerRegistry: CustomSerializerRegistry = CachingCustomSerializerRegistry(descriptorBasedSerializerRegistry)
|
||||
val typeModel = ConfigurableLocalTypeModel(WhitelistBasedTypeModelConfiguration(AllWhitelist, customSerializerRegistry))
|
||||
|
||||
assertEquals("0", fpt.fingerprint(typeModel.inspect(Integer::class.java)))
|
||||
assertEquals("1", fpt.fingerprint(typeModel.inspect(String::class.java)))
|
||||
assertEquals("0", fpt.fingerprint(typeModel.inspect(Integer::class.java)))
|
||||
assertEquals("1", fpt.fingerprint(typeModel.inspect(String::class.java)))
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -42,7 +48,7 @@ class FingerPrinterTestingTests {
|
||||
|
||||
val factory = SerializerFactoryBuilder.build(AllWhitelist,
|
||||
ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader()),
|
||||
fingerPrinterProvider = { _ -> FingerPrinterTesting() })
|
||||
overrideFingerPrinter = FingerPrinterTesting())
|
||||
|
||||
val blob = TestSerializationOutput(VERBOSE, factory).serializeAndReturnSchema(C(1, 2L))
|
||||
|
||||
|
@ -40,15 +40,6 @@ class GenericsTests {
|
||||
|
||||
private fun <T : Any> BytesAndSchemas<T>.printSchema() = if (VERBOSE) println("${this.schema}\n") else Unit
|
||||
|
||||
private fun MutableMap<Any, AMQPSerializer<Any>>.printKeyToType() {
|
||||
if (!VERBOSE) return
|
||||
|
||||
forEach {
|
||||
println("Key = ${it.key} - ${it.value.type.typeName}")
|
||||
}
|
||||
println()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun twoDifferentTypesSameParameterizedOuter() {
|
||||
data class G<A>(val a: A)
|
||||
@ -57,12 +48,8 @@ class GenericsTests {
|
||||
|
||||
val bytes1 = SerializationOutput(factory).serializeAndReturnSchema(G("hi")).apply { printSchema() }
|
||||
|
||||
factory.serializersByDescriptor.printKeyToType()
|
||||
|
||||
val bytes2 = SerializationOutput(factory).serializeAndReturnSchema(G(121)).apply { printSchema() }
|
||||
|
||||
factory.serializersByDescriptor.printKeyToType()
|
||||
|
||||
listOf(factory, testDefaultFactory()).forEach { f ->
|
||||
DeserializationInput(f).deserialize(bytes1.obj).apply { assertEquals("hi", this.a) }
|
||||
DeserializationInput(f).deserialize(bytes2.obj).apply { assertEquals(121, this.a) }
|
||||
@ -94,15 +81,11 @@ class GenericsTests {
|
||||
|
||||
val bytes = ser.serializeAndReturnSchema(G("hi")).apply { printSchema() }
|
||||
|
||||
factory.serializersByDescriptor.printKeyToType()
|
||||
|
||||
assertEquals("hi", DeserializationInput(factory).deserialize(bytes.obj).a)
|
||||
assertEquals("hi", DeserializationInput(altContextFactory).deserialize(bytes.obj).a)
|
||||
|
||||
val bytes2 = ser.serializeAndReturnSchema(Wrapper(1, G("hi"))).apply { printSchema() }
|
||||
|
||||
factory.serializersByDescriptor.printKeyToType()
|
||||
|
||||
printSeparator()
|
||||
|
||||
DeserializationInput(factory).deserialize(bytes2.obj).apply {
|
||||
@ -161,21 +144,18 @@ class GenericsTests {
|
||||
ser.serialize(Wrapper(Container(InnerA(1)))).apply {
|
||||
factories.forEach {
|
||||
DeserializationInput(it).deserialize(this).apply { assertEquals(1, c.b.a_a) }
|
||||
it.serializersByDescriptor.printKeyToType(); printSeparator()
|
||||
}
|
||||
}
|
||||
|
||||
ser.serialize(Wrapper(Container(InnerB(1)))).apply {
|
||||
factories.forEach {
|
||||
DeserializationInput(it).deserialize(this).apply { assertEquals(1, c.b.a_b) }
|
||||
it.serializersByDescriptor.printKeyToType(); printSeparator()
|
||||
}
|
||||
}
|
||||
|
||||
ser.serialize(Wrapper(Container(InnerC("Ho ho ho")))).apply {
|
||||
factories.forEach {
|
||||
DeserializationInput(it).deserialize(this).apply { assertEquals("Ho ho ho", c.b.a_c) }
|
||||
it.serializersByDescriptor.printKeyToType(); printSeparator()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -217,7 +197,6 @@ class GenericsTests {
|
||||
ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader())
|
||||
)): SerializedBytes<*> {
|
||||
val bytes = SerializationOutput(factory).serializeAndReturnSchema(a)
|
||||
factory.serializersByDescriptor.printKeyToType()
|
||||
bytes.printSchema()
|
||||
return bytes.obj
|
||||
}
|
||||
|
@ -3,19 +3,20 @@ package net.corda.serialization.internal.amqp
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import net.corda.core.serialization.ConstructorForDeserialization
|
||||
import net.corda.serialization.internal.amqp.testutils.deserialize
|
||||
import net.corda.serialization.internal.amqp.testutils.serializeAndReturnSchema
|
||||
import net.corda.serialization.internal.amqp.testutils.serialize
|
||||
import net.corda.serialization.internal.amqp.testutils.testDefaultFactoryNoEvolution
|
||||
import net.corda.serialization.internal.amqp.testutils.*
|
||||
import net.corda.serialization.internal.model.ConfigurableLocalTypeModel
|
||||
import net.corda.serialization.internal.model.LocalPropertyInformation
|
||||
import net.corda.serialization.internal.model.LocalTypeInformation
|
||||
import org.junit.Test
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import org.assertj.core.api.Assertions
|
||||
import java.io.NotSerializableException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.*
|
||||
|
||||
class PrivatePropertyTests {
|
||||
private val factory = testDefaultFactoryNoEvolution()
|
||||
|
||||
private val registry = TestDescriptorBasedSerializerRegistry()
|
||||
private val factory = testDefaultFactoryNoEvolution(registry)
|
||||
val typeModel = ConfigurableLocalTypeModel(WhitelistBasedTypeModelConfiguration(factory.whitelist, factory))
|
||||
|
||||
@Test
|
||||
fun testWithOnePrivateProperty() {
|
||||
@ -125,21 +126,13 @@ class PrivatePropertyTests {
|
||||
val schemaAndBlob = SerializationOutput(factory).serializeAndReturnSchema(c1)
|
||||
assertEquals(1, schemaAndBlob.schema.types.size)
|
||||
|
||||
val serializersByDescriptor = factory.serializersByDescriptor
|
||||
val typeInformation = typeModel.inspect(C::class.java)
|
||||
assertTrue(typeInformation is LocalTypeInformation.Composable)
|
||||
typeInformation as LocalTypeInformation.Composable
|
||||
|
||||
val schemaDescriptor = schemaAndBlob.schema.types.first().descriptor.name
|
||||
serializersByDescriptor.filterKeys { (it as Symbol) == schemaDescriptor }.values.apply {
|
||||
assertEquals(1, this.size)
|
||||
assertTrue(this.first() is ObjectSerializer)
|
||||
val propertySerializers = (this.first() as ObjectSerializer).propertySerializers.serializationOrder.map { it.serializer }
|
||||
assertEquals(2, propertySerializers.size)
|
||||
// a was public so should have a synthesised getter
|
||||
assertTrue(propertySerializers[0].propertyReader is PublicPropertyReader)
|
||||
|
||||
// b is private and thus won't have teh getter so we'll have reverted
|
||||
// to using reflection to remove the inaccessible property
|
||||
assertTrue(propertySerializers[1].propertyReader is PrivatePropertyReader)
|
||||
}
|
||||
assertEquals(2, typeInformation.properties.size)
|
||||
assertTrue(typeInformation.properties["a"] is LocalPropertyInformation.ConstructorPairedProperty)
|
||||
assertTrue(typeInformation.properties["b"] is LocalPropertyInformation.PrivateConstructorPairedProperty)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -153,22 +146,14 @@ class PrivatePropertyTests {
|
||||
val schemaAndBlob = SerializationOutput(factory).serializeAndReturnSchema(c1)
|
||||
assertEquals(1, schemaAndBlob.schema.types.size)
|
||||
|
||||
val serializersByDescriptor = factory.serializersByDescriptor
|
||||
|
||||
val schemaDescriptor = schemaAndBlob.schema.types.first().descriptor.name
|
||||
serializersByDescriptor.filterKeys { (it as Symbol) == schemaDescriptor }.values.apply {
|
||||
assertEquals(1, this.size)
|
||||
assertTrue(this.first() is ObjectSerializer)
|
||||
val propertySerializers = (this.first() as ObjectSerializer).propertySerializers.serializationOrder.map { it.serializer }
|
||||
assertEquals(2, propertySerializers.size)
|
||||
val typeInformation = typeModel.inspect(C::class.java)
|
||||
assertTrue(typeInformation is LocalTypeInformation.Composable)
|
||||
typeInformation as LocalTypeInformation.Composable
|
||||
|
||||
// as before, a is public so we'll use the getter method
|
||||
assertTrue(propertySerializers[0].propertyReader is PublicPropertyReader)
|
||||
|
||||
// the getB() getter explicitly added means we should use the "normal" public
|
||||
// method reader rather than the private oen
|
||||
assertTrue(propertySerializers[1].propertyReader is PublicPropertyReader)
|
||||
}
|
||||
assertEquals(2, typeInformation.properties.size)
|
||||
assertTrue(typeInformation.properties["a"] is LocalPropertyInformation.ConstructorPairedProperty)
|
||||
assertTrue(typeInformation.properties["b"] is LocalPropertyInformation.ConstructorPairedProperty)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@ -179,9 +164,8 @@ class PrivatePropertyTests {
|
||||
|
||||
val c1 = Outer(Inner(1010101))
|
||||
val output = SerializationOutput(factory).serializeAndReturnSchema(c1)
|
||||
println (output.schema)
|
||||
|
||||
val serializersByDescriptor = factory.serializersByDescriptor
|
||||
val serializersByDescriptor = registry.contents
|
||||
|
||||
// Inner and Outer
|
||||
assertEquals(2, serializersByDescriptor.size)
|
||||
@ -198,24 +182,13 @@ class PrivatePropertyTests {
|
||||
@Test
|
||||
fun allCapsProprtyNotPrivate() {
|
||||
data class C (val CCC: String)
|
||||
val typeInformation = typeModel.inspect(C::class.java)
|
||||
|
||||
val output = SerializationOutput(factory).serializeAndReturnSchema(C("this is nice"))
|
||||
assertTrue(typeInformation is LocalTypeInformation.Composable)
|
||||
typeInformation as LocalTypeInformation.Composable
|
||||
|
||||
val serializersByDescriptor = factory.serializersByDescriptor
|
||||
|
||||
val schemaDescriptor = output.schema.types.first().descriptor.name
|
||||
serializersByDescriptor.filterKeys { (it as Symbol) == schemaDescriptor }.values.apply {
|
||||
assertEquals(1, size)
|
||||
|
||||
assertTrue(this.first() is ObjectSerializer)
|
||||
val propertySerializers = (this.first() as ObjectSerializer).propertySerializers.serializationOrder.map { it.serializer }
|
||||
|
||||
// CCC is the only property to be serialised
|
||||
assertEquals(1, propertySerializers.size)
|
||||
|
||||
// and despite being all caps it should still be a public getter
|
||||
assertTrue(propertySerializers[0].propertyReader is PublicPropertyReader)
|
||||
}
|
||||
assertEquals(1, typeInformation.properties.size)
|
||||
assertTrue(typeInformation.properties["CCC"] is LocalPropertyInformation.ConstructorPairedProperty)
|
||||
}
|
||||
|
||||
}
|
@ -21,7 +21,6 @@ import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.node.serialization.amqp.AMQPServerSerializationScheme
|
||||
import net.corda.nodeapi.internal.crypto.ContentSignerBuilder
|
||||
import net.corda.serialization.internal.*
|
||||
import net.corda.serialization.internal.amqp.SerializerFactory.Companion.isPrimitive
|
||||
import net.corda.serialization.internal.amqp.testutils.*
|
||||
import net.corda.serialization.internal.carpenter.ClassCarpenterImpl
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
@ -210,7 +209,7 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi
|
||||
private fun defaultFactory(): SerializerFactory {
|
||||
return SerializerFactoryBuilder.build(AllWhitelist,
|
||||
ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader()),
|
||||
evolutionSerializerProvider = FailIfEvolutionAttempted
|
||||
allowEvolution = false
|
||||
)
|
||||
}
|
||||
|
||||
@ -258,27 +257,27 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi
|
||||
|
||||
@Test
|
||||
fun isPrimitive() {
|
||||
assertTrue(isPrimitive(Character::class.java))
|
||||
assertTrue(isPrimitive(Boolean::class.java))
|
||||
assertTrue(isPrimitive(Byte::class.java))
|
||||
assertTrue(isPrimitive(UnsignedByte::class.java))
|
||||
assertTrue(isPrimitive(Short::class.java))
|
||||
assertTrue(isPrimitive(UnsignedShort::class.java))
|
||||
assertTrue(isPrimitive(Int::class.java))
|
||||
assertTrue(isPrimitive(UnsignedInteger::class.java))
|
||||
assertTrue(isPrimitive(Long::class.java))
|
||||
assertTrue(isPrimitive(UnsignedLong::class.java))
|
||||
assertTrue(isPrimitive(Float::class.java))
|
||||
assertTrue(isPrimitive(Double::class.java))
|
||||
assertTrue(isPrimitive(Decimal32::class.java))
|
||||
assertTrue(isPrimitive(Decimal64::class.java))
|
||||
assertTrue(isPrimitive(Decimal128::class.java))
|
||||
assertTrue(isPrimitive(Char::class.java))
|
||||
assertTrue(isPrimitive(Date::class.java))
|
||||
assertTrue(isPrimitive(UUID::class.java))
|
||||
assertTrue(isPrimitive(ByteArray::class.java))
|
||||
assertTrue(isPrimitive(String::class.java))
|
||||
assertTrue(isPrimitive(Symbol::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(Character::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(Boolean::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(Byte::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(UnsignedByte::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(Short::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(UnsignedShort::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(Int::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(UnsignedInteger::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(Long::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(UnsignedLong::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(Float::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(Double::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(Decimal32::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(Decimal64::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(Decimal128::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(Char::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(Date::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(UUID::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(ByteArray::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(String::class.java))
|
||||
assertTrue(AMQPTypeIdentifiers.isPrimitive(Symbol::class.java))
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -475,10 +474,11 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi
|
||||
@Test
|
||||
fun `class constructor is invoked on deserialisation`() {
|
||||
compression == null || return // Manipulation of serialized bytes is invalid if they're compressed.
|
||||
val ser = SerializationOutput(SerializerFactoryBuilder.build(AllWhitelist,
|
||||
val serializerFactory = SerializerFactoryBuilder.build(AllWhitelist,
|
||||
ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader())
|
||||
))
|
||||
val des = DeserializationInput(ser.serializerFactory)
|
||||
)
|
||||
val ser = SerializationOutput(serializerFactory)
|
||||
val des = DeserializationInput(serializerFactory)
|
||||
val serialisedOne = ser.serialize(NonZeroByte(1), compression).bytes
|
||||
val serialisedTwo = ser.serialize(NonZeroByte(2), compression).bytes
|
||||
|
||||
|
@ -1,12 +1,8 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.core.serialization.ConstructorForDeserialization
|
||||
import net.corda.serialization.internal.amqp.testutils.TestSerializationOutput
|
||||
import net.corda.serialization.internal.amqp.testutils.deserialize
|
||||
import net.corda.serialization.internal.amqp.testutils.serializeAndReturnSchema
|
||||
import net.corda.serialization.internal.amqp.testutils.testDefaultFactoryNoEvolution
|
||||
import net.corda.serialization.internal.amqp.testutils.*
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.test.assertEquals
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import java.lang.reflect.Method
|
||||
@ -17,7 +13,8 @@ class SerializationPropertyOrdering {
|
||||
companion object {
|
||||
val VERBOSE get() = false
|
||||
|
||||
val sf = testDefaultFactoryNoEvolution()
|
||||
val registry = TestDescriptorBasedSerializerRegistry()
|
||||
val sf = testDefaultFactoryNoEvolution(registry)
|
||||
}
|
||||
|
||||
// Force object references to be ued to ensure we go through that code path
|
||||
@ -100,25 +97,6 @@ class SerializationPropertyOrdering {
|
||||
assertEquals("e", this.fields[4].name)
|
||||
}
|
||||
|
||||
// Test needs to look at a bunch of private variables, change the access semantics for them
|
||||
val fields : Map<String, java.lang.reflect.Field> = mapOf (
|
||||
"setter" to PropertyAccessorGetterSetter::class.java.getDeclaredField("setter")).apply {
|
||||
this.values.forEach {
|
||||
it.isAccessible = true
|
||||
}
|
||||
}
|
||||
|
||||
val serializersByDescriptor = sf.serializersByDescriptor
|
||||
val schemaDescriptor = output.schema.types.first().descriptor.name
|
||||
|
||||
// make sure that each property accessor has a setter to ensure we're using getter / setter instantiation
|
||||
serializersByDescriptor.filterKeys { (it as Symbol) == schemaDescriptor }.values.apply {
|
||||
assertEquals(1, this.size)
|
||||
assertTrue(this.first() is ObjectSerializer)
|
||||
val propertyAccessors = (this.first() as ObjectSerializer).propertySerializers.serializationOrder as List<PropertyAccessorGetterSetter>
|
||||
propertyAccessors.forEach { property -> assertNotNull(fields["setter"]!!.get(property) as Method?) }
|
||||
}
|
||||
|
||||
val input = DeserializationInput(sf).deserialize(output.obj)
|
||||
assertEquals(100, input.a)
|
||||
assertEquals(200, input.b)
|
||||
|
@ -17,87 +17,4 @@ val TESTING_CONTEXT = SerializationContextImpl(amqpMagic,
|
||||
emptyMap(),
|
||||
true,
|
||||
SerializationContext.UseCase.Testing,
|
||||
null)
|
||||
|
||||
// Test factory that lets us count the number of serializer registration attempts
|
||||
class TestSerializerFactory(
|
||||
wl: ClassWhitelist,
|
||||
cl: ClassLoader
|
||||
) : DefaultSerializerFactory(wl, ClassCarpenterImpl(wl, cl, false), DefaultEvolutionSerializerProvider, ::SerializerFingerPrinter) {
|
||||
var registerCount = 0
|
||||
|
||||
override fun register(customSerializer: CustomSerializer<out Any>) {
|
||||
++registerCount
|
||||
return super.register(customSerializer)
|
||||
}
|
||||
}
|
||||
|
||||
// Instance of our test factory counting registration attempts. Sucks its global, but for testing purposes this
|
||||
// is the easiest way of getting access to the object.
|
||||
val testFactory = TestSerializerFactory(TESTING_CONTEXT.whitelist, TESTING_CONTEXT.deserializationClassLoader)
|
||||
|
||||
// Serializer factory factory, plugs into the SerializationScheme and controls which factory type
|
||||
// we make for each use case. For our tests we need to make sure if its the Testing use case we return
|
||||
// the global factory object created above that counts registrations.
|
||||
class TestSerializerFactoryFactory : SerializerFactoryFactoryImpl() {
|
||||
override fun make(context: SerializationContext) =
|
||||
when (context.useCase) {
|
||||
SerializationContext.UseCase.Testing -> testFactory
|
||||
else -> super.make(context)
|
||||
}
|
||||
}
|
||||
|
||||
class AMQPTestSerializationScheme : AbstractAMQPSerializationScheme(emptySet(), AccessOrderLinkedHashMap { 128 }, TestSerializerFactoryFactory()) {
|
||||
override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase) = true
|
||||
}
|
||||
|
||||
// Test SerializationFactory that wraps a serialization scheme that just allows us to call <OBJ>.serialize.
|
||||
// Returns the testing scheme we created above that wraps the testing factory.
|
||||
class TestSerializationFactory : SerializationFactory() {
|
||||
private val scheme = AMQPTestSerializationScheme()
|
||||
|
||||
override fun <T : Any> deserialize(
|
||||
byteSequence: ByteSequence,
|
||||
clazz: Class<T>, context:
|
||||
SerializationContext
|
||||
): T {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun <T : Any> deserializeWithCompatibleContext(
|
||||
byteSequence: ByteSequence,
|
||||
clazz: Class<T>,
|
||||
context: SerializationContext
|
||||
): ObjectWithCompatibleContext<T> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun <T : Any> serialize(obj: T, context: SerializationContext) = scheme.serialize(obj, context)
|
||||
}
|
||||
|
||||
// The actual test
|
||||
class SerializationSchemaTests {
|
||||
@Test
|
||||
fun onlyRegisterCustomSerializersOnce() {
|
||||
@CordaSerializable
|
||||
data class C(val a: Int)
|
||||
|
||||
val c = C(1)
|
||||
val testSerializationFactory = TestSerializationFactory()
|
||||
val expectedCustomSerializerCount = 41
|
||||
|
||||
assertEquals(0, testFactory.registerCount)
|
||||
c.serialize(testSerializationFactory, TESTING_CONTEXT)
|
||||
assertEquals(expectedCustomSerializerCount, testFactory.registerCount)
|
||||
c.serialize(testSerializationFactory, TESTING_CONTEXT)
|
||||
assertEquals(expectedCustomSerializerCount, testFactory.registerCount)
|
||||
}
|
||||
}
|
||||
null)
|
@ -6,6 +6,7 @@ import net.corda.serialization.internal.AllWhitelist
|
||||
import net.corda.serialization.internal.amqp.testutils.deserialize
|
||||
import net.corda.serialization.internal.carpenter.ClassCarpenterImpl
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.Type
|
||||
@ -44,6 +45,7 @@ class StaticInitialisationOfSerializedObjectTest {
|
||||
C()
|
||||
}
|
||||
|
||||
@Ignore("Suppressing this, as it depends on obtaining internal access to serialiser cache")
|
||||
@Test
|
||||
fun kotlinObjectWithCompanionObject() {
|
||||
data class D(val c: C)
|
||||
@ -63,7 +65,7 @@ class StaticInitialisationOfSerializedObjectTest {
|
||||
|
||||
// build a serializer for type D without an instance of it to serialise, since
|
||||
// we can't actually construct one
|
||||
sf.get(null, D::class.java)
|
||||
sf.get(D::class.java)
|
||||
|
||||
// post creation of the serializer we should have two elements in the map, this
|
||||
// proves we didn't statically construct an instance of C when building the serializer
|
||||
|
@ -18,20 +18,45 @@ import java.io.File.separatorChar
|
||||
import java.io.NotSerializableException
|
||||
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
|
||||
|
||||
fun testDefaultFactory() = SerializerFactoryBuilder.build(AllWhitelist,
|
||||
ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader())
|
||||
)
|
||||
/**
|
||||
* For tests that want to see inside the serializer registry
|
||||
*/
|
||||
class TestDescriptorBasedSerializerRegistry : DescriptorBasedSerializerRegistry {
|
||||
val contents = mutableMapOf<String, AMQPSerializer<Any>>()
|
||||
|
||||
fun testDefaultFactoryNoEvolution(): SerializerFactory {
|
||||
return SerializerFactoryBuilder.build(
|
||||
AllWhitelist,
|
||||
ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader()),
|
||||
FailIfEvolutionAttempted)
|
||||
override fun get(descriptor: String): AMQPSerializer<Any>? = contents[descriptor]
|
||||
|
||||
override fun set(descriptor: String, serializer: AMQPSerializer<Any>) {
|
||||
contents.putIfAbsent(descriptor, serializer)
|
||||
}
|
||||
|
||||
override fun getOrBuild(descriptor: String, builder: () -> AMQPSerializer<Any>): AMQPSerializer<Any> =
|
||||
get(descriptor) ?: builder().also { set(descriptor, it) }
|
||||
}
|
||||
|
||||
fun testDefaultFactoryWithWhitelist() = SerializerFactoryBuilder.build(EmptyWhitelist,
|
||||
ClassCarpenterImpl(EmptyWhitelist, ClassLoader.getSystemClassLoader())
|
||||
)
|
||||
@JvmOverloads
|
||||
fun testDefaultFactory(descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry =
|
||||
DefaultDescriptorBasedSerializerRegistry()) =
|
||||
SerializerFactoryBuilder.build(
|
||||
AllWhitelist,
|
||||
ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader()),
|
||||
descriptorBasedSerializerRegistry = descriptorBasedSerializerRegistry)
|
||||
|
||||
@JvmOverloads
|
||||
fun testDefaultFactoryNoEvolution(descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry =
|
||||
DefaultDescriptorBasedSerializerRegistry()): SerializerFactory =
|
||||
SerializerFactoryBuilder.build(
|
||||
AllWhitelist,
|
||||
ClassCarpenterImpl(AllWhitelist, ClassLoader.getSystemClassLoader()),
|
||||
descriptorBasedSerializerRegistry = descriptorBasedSerializerRegistry,
|
||||
allowEvolution = false)
|
||||
|
||||
@JvmOverloads
|
||||
fun testDefaultFactoryWithWhitelist(descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry =
|
||||
DefaultDescriptorBasedSerializerRegistry()) =
|
||||
SerializerFactoryBuilder.build(EmptyWhitelist,
|
||||
ClassCarpenterImpl(EmptyWhitelist, ClassLoader.getSystemClassLoader()),
|
||||
descriptorBasedSerializerRegistry = descriptorBasedSerializerRegistry)
|
||||
|
||||
class TestSerializationOutput(
|
||||
private val verbose: Boolean,
|
||||
|
@ -1,101 +0,0 @@
|
||||
package net.corda.serialization.internal.carpenter
|
||||
|
||||
import net.corda.core.serialization.SerializableCalculatedProperty
|
||||
import net.corda.serialization.internal.AllWhitelist
|
||||
import net.corda.serialization.internal.amqp.CompositeType
|
||||
import net.corda.serialization.internal.amqp.DeserializationInput
|
||||
import net.corda.serialization.internal.amqp.testutils.deserializeAndReturnEnvelope
|
||||
import net.corda.serialization.internal.amqp.testutils.testDefaultFactoryNoEvolution
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class CalculatedValuesToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) {
|
||||
|
||||
interface Parent {
|
||||
@get:SerializableCalculatedProperty
|
||||
val doubled: Int
|
||||
}
|
||||
|
||||
@Test
|
||||
fun calculatedValues() {
|
||||
data class C(val i: Int): Parent {
|
||||
@get:SerializableCalculatedProperty
|
||||
val squared = (i * i).toString()
|
||||
|
||||
override val doubled get() = i * 2
|
||||
}
|
||||
|
||||
val factory = testDefaultFactoryNoEvolution()
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(C(2)))
|
||||
val amqpObj = obj.obj
|
||||
val serSchema = obj.envelope.schema
|
||||
|
||||
assertEquals(2, amqpObj.i)
|
||||
assertEquals("4", amqpObj.squared)
|
||||
assertEquals(2, serSchema.types.size)
|
||||
require(serSchema.types[0] is CompositeType)
|
||||
|
||||
val concrete = serSchema.types[0] as CompositeType
|
||||
assertEquals(3, concrete.fields.size)
|
||||
assertEquals("doubled", concrete.fields[0].name)
|
||||
assertEquals("int", concrete.fields[0].type)
|
||||
assertEquals("i", concrete.fields[1].name)
|
||||
assertEquals("int", concrete.fields[1].type)
|
||||
assertEquals("squared", concrete.fields[2].name)
|
||||
assertEquals("string", concrete.fields[2].type)
|
||||
|
||||
val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
assertEquals(0, l1.size)
|
||||
val mangleSchema = serSchema.mangleNames(listOf((classTestName("C"))))
|
||||
val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
val aName = mangleName(classTestName("C"))
|
||||
|
||||
assertEquals(1, l2.size)
|
||||
val aSchema = l2.carpenterSchemas.find { it.name == aName }!!
|
||||
|
||||
val pinochio = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema)
|
||||
val p = pinochio.constructors[0].newInstance(4, 2, "4")
|
||||
|
||||
assertEquals(pinochio.getMethod("getI").invoke(p), amqpObj.i)
|
||||
assertEquals(pinochio.getMethod("getSquared").invoke(p), amqpObj.squared)
|
||||
assertEquals(pinochio.getMethod("getDoubled").invoke(p), amqpObj.doubled)
|
||||
|
||||
val upcast = p as Parent
|
||||
assertEquals(upcast.doubled, amqpObj.doubled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun implementingClassDoesNotCalculateValue() {
|
||||
class C(override val doubled: Int): Parent
|
||||
|
||||
val factory = testDefaultFactoryNoEvolution()
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(C(5)))
|
||||
val amqpObj = obj.obj
|
||||
val serSchema = obj.envelope.schema
|
||||
|
||||
assertEquals(2, serSchema.types.size)
|
||||
require(serSchema.types[0] is CompositeType)
|
||||
|
||||
val concrete = serSchema.types[0] as CompositeType
|
||||
assertEquals(1, concrete.fields.size)
|
||||
assertEquals("doubled", concrete.fields[0].name)
|
||||
assertEquals("int", concrete.fields[0].type)
|
||||
|
||||
val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
assertEquals(0, l1.size)
|
||||
val mangleSchema = serSchema.mangleNames(listOf((classTestName("C"))))
|
||||
val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
val aName = mangleName(classTestName("C"))
|
||||
|
||||
assertEquals(1, l2.size)
|
||||
val aSchema = l2.carpenterSchemas.find { it.name == aName }!!
|
||||
|
||||
val pinochio = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema)
|
||||
val p = pinochio.constructors[0].newInstance(5)
|
||||
|
||||
assertEquals(pinochio.getMethod("getDoubled").invoke(p), amqpObj.doubled)
|
||||
|
||||
val upcast = p as Parent
|
||||
assertEquals(upcast.doubled, amqpObj.doubled)
|
||||
}
|
||||
}
|
@ -27,7 +27,7 @@ class ClassCarpenterTest {
|
||||
|
||||
@Test
|
||||
fun empty() {
|
||||
val clazz = cc.build(ClassSchema("gen.EmptyClass", emptyMap(), null))
|
||||
val clazz = cc.build(ClassSchema("gen.EmptyClass", emptyMap()))
|
||||
assertEquals(0, clazz.nonSyntheticFields.size)
|
||||
assertEquals(2, clazz.nonSyntheticMethods.size) // get, toString
|
||||
assertEquals(0, clazz.declaredConstructors[0].parameterCount)
|
||||
@ -97,8 +97,8 @@ class ClassCarpenterTest {
|
||||
|
||||
@Test(expected = DuplicateNameException::class)
|
||||
fun duplicates() {
|
||||
cc.build(ClassSchema("gen.EmptyClass", emptyMap(), null))
|
||||
cc.build(ClassSchema("gen.EmptyClass", emptyMap(), null))
|
||||
cc.build(ClassSchema("gen.EmptyClass", emptyMap()))
|
||||
cc.build(ClassSchema("gen.EmptyClass", emptyMap()))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1,41 +1,14 @@
|
||||
package net.corda.serialization.internal.carpenter
|
||||
|
||||
import com.google.common.reflect.TypeToken
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.serialization.internal.amqp.*
|
||||
import net.corda.serialization.internal.amqp.Field
|
||||
import net.corda.serialization.internal.amqp.Schema
|
||||
import net.corda.serialization.internal.amqp.testutils.deserializeAndReturnEnvelope
|
||||
import net.corda.serialization.internal.amqp.testutils.serialize
|
||||
import net.corda.serialization.internal.amqp.testutils.testName
|
||||
|
||||
fun mangleName(name: String) = "${name}__carpenter"
|
||||
|
||||
/**
|
||||
* given a list of class names work through the amqp envelope schema and alter any that
|
||||
* match in the fashion defined above
|
||||
*/
|
||||
fun Schema.mangleNames(names: List<String>): Schema {
|
||||
val newTypes: MutableList<TypeNotation> = mutableListOf()
|
||||
|
||||
for (type in types) {
|
||||
val newName = if (type.name in names) mangleName(type.name) else type.name
|
||||
val newProvides = type.provides.map { if (it in names) mangleName(it) else it }
|
||||
val newFields = mutableListOf<Field>()
|
||||
|
||||
(type as CompositeType).fields.forEach {
|
||||
val fieldType = if (it.type in names) mangleName(it.type) else it.type
|
||||
val requires =
|
||||
if (it.requires.isNotEmpty() && (it.requires[0] in names)) listOf(mangleName(it.requires[0]))
|
||||
else it.requires
|
||||
|
||||
newFields.add(it.copy(type = fieldType, requires = requires))
|
||||
}
|
||||
|
||||
newTypes.add(type.copy(name = newName, provides = newProvides, fields = newFields))
|
||||
}
|
||||
|
||||
return Schema(types = newTypes)
|
||||
}
|
||||
import net.corda.serialization.internal.model.*
|
||||
import org.junit.Assert.assertTrue
|
||||
|
||||
/**
|
||||
* Custom implementation of a [SerializerFactory] where we need to give it a class carpenter
|
||||
@ -48,7 +21,78 @@ open class AmqpCarpenterBase(whitelist: ClassWhitelist) {
|
||||
var cc = ClassCarpenterImpl(whitelist = whitelist)
|
||||
var factory = serializerFactoryExternalCarpenter(cc)
|
||||
|
||||
fun <T: Any> serialise(obj: T): SerializedBytes<T> = SerializationOutput(factory).serialize(obj)
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz"
|
||||
protected val remoteTypeModel = AMQPRemoteTypeModel()
|
||||
protected val typeLoader = ClassCarpentingTypeLoader(SchemaBuildingRemoteTypeCarpenter(cc), cc.classloader)
|
||||
|
||||
protected inline fun <reified T: Any> T.roundTrip(): ObjectAndEnvelope<T> =
|
||||
DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(this))
|
||||
|
||||
protected val Envelope.typeInformation: Map<TypeDescriptor, RemoteTypeInformation> get() =
|
||||
remoteTypeModel.interpret(SerializationSchemas(schema, transformsSchema))
|
||||
|
||||
protected inline fun <reified T: Any> Envelope.typeInformationFor(): RemoteTypeInformation {
|
||||
val interpreted = typeInformation
|
||||
val type = object : TypeToken<T>() {}.type
|
||||
return interpreted.values.find { it.typeIdentifier == TypeIdentifier.forGenericType(type) }
|
||||
as RemoteTypeInformation
|
||||
}
|
||||
|
||||
protected inline fun <reified T: Any> Envelope.getMangled(): RemoteTypeInformation =
|
||||
typeInformationFor<T>().mangle<T>()
|
||||
|
||||
protected fun <T: Any> serialise(obj: T): SerializedBytes<T> = SerializationOutput(factory).serialize(obj)
|
||||
|
||||
protected inline fun <reified T: Any> RemoteTypeInformation.mangle(): RemoteTypeInformation {
|
||||
val from = TypeIdentifier.forGenericType(object : TypeToken<T>() {}.type)
|
||||
return rename(from, from.mangle())
|
||||
}
|
||||
|
||||
protected fun TypeIdentifier.mangle(): TypeIdentifier = when(this) {
|
||||
is TypeIdentifier.Unparameterised -> copy(name = name + "_carpenter")
|
||||
is TypeIdentifier.Parameterised -> copy(name = name + "_carpenter")
|
||||
is TypeIdentifier.Erased -> copy(name = name + "_carpenter")
|
||||
is TypeIdentifier.ArrayOf -> copy(componentType = componentType.mangle())
|
||||
else -> this
|
||||
}
|
||||
|
||||
protected fun TypeIdentifier.rename(from: TypeIdentifier, to: TypeIdentifier): TypeIdentifier = when(this) {
|
||||
from -> to.rename(from, to)
|
||||
is TypeIdentifier.Parameterised -> copy(parameters = parameters.map { it.rename(from, to) })
|
||||
is TypeIdentifier.ArrayOf -> copy(componentType = componentType.rename(from, to))
|
||||
else -> this
|
||||
}
|
||||
|
||||
protected fun RemoteTypeInformation.rename(from: TypeIdentifier, to: TypeIdentifier): RemoteTypeInformation = when(this) {
|
||||
is RemoteTypeInformation.Composable -> copy(
|
||||
typeIdentifier = typeIdentifier.rename(from, to),
|
||||
properties = properties.mapValues { (_, property) -> property.copy(type = property.type.rename(from, to)) },
|
||||
interfaces = interfaces.map { it.rename(from, to) },
|
||||
typeParameters = typeParameters.map { it.rename(from, to) })
|
||||
is RemoteTypeInformation.Unparameterised -> copy(typeIdentifier = typeIdentifier.rename(from, to))
|
||||
is RemoteTypeInformation.Parameterised -> copy(
|
||||
typeIdentifier = typeIdentifier.rename(from, to),
|
||||
typeParameters = typeParameters.map { it.rename(from, to) })
|
||||
is RemoteTypeInformation.AnInterface -> copy(
|
||||
typeIdentifier = typeIdentifier.rename(from, to),
|
||||
properties = properties.mapValues { (_, property) -> property.copy(type = property.type.rename(from, to)) },
|
||||
interfaces = interfaces.map { it.rename(from, to) },
|
||||
typeParameters = typeParameters.map { it.rename(from, to) })
|
||||
is RemoteTypeInformation.AnArray -> copy(componentType = componentType.rename(from, to))
|
||||
is RemoteTypeInformation.AnEnum -> copy(
|
||||
typeIdentifier = typeIdentifier.rename(from, to))
|
||||
else -> this
|
||||
}
|
||||
|
||||
protected fun RemoteTypeInformation.load(): Class<*> =
|
||||
typeLoader.load(listOf(this))[typeIdentifier]!!.asClass()
|
||||
|
||||
protected fun assertCanLoadAll(vararg types: RemoteTypeInformation) {
|
||||
assertTrue(typeLoader.load(types.asList()).keys.containsAll(types.map { it.typeIdentifier }))
|
||||
}
|
||||
|
||||
protected fun Class<*>.new(vararg constructorParams: Any?) =
|
||||
constructors[0].newInstance(*constructorParams)!!
|
||||
|
||||
protected fun Any.get(propertyName: String): Any =
|
||||
this::class.java.getMethod("get${propertyName.capitalize()}").invoke(this)
|
||||
}
|
||||
|
@ -2,13 +2,11 @@ package net.corda.serialization.internal.carpenter
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.serialization.internal.AllWhitelist
|
||||
import net.corda.serialization.internal.amqp.CompositeType
|
||||
import net.corda.serialization.internal.amqp.DeserializationInput
|
||||
import net.corda.serialization.internal.amqp.testutils.deserializeAndReturnEnvelope
|
||||
import org.junit.Test
|
||||
import java.io.NotSerializableException
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
@CordaSerializable
|
||||
interface I_ {
|
||||
@ -16,258 +14,96 @@ interface I_ {
|
||||
}
|
||||
|
||||
class CompositeMembers : AmqpCarpenterBase(AllWhitelist) {
|
||||
@Test
|
||||
fun bothKnown() {
|
||||
val testA = 10
|
||||
val testB = 20
|
||||
|
||||
@Test
|
||||
fun parentIsUnknown() {
|
||||
@CordaSerializable
|
||||
data class A(val a: Int)
|
||||
|
||||
@CordaSerializable
|
||||
data class B(val a: A, var b: Int)
|
||||
|
||||
val b = B(A(testA), testB)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b))
|
||||
val (_, envelope) = B(A(10), 20).roundTrip()
|
||||
|
||||
val amqpObj = obj.obj
|
||||
|
||||
assertEquals(testB, amqpObj.b)
|
||||
assertEquals(testA, amqpObj.a.a)
|
||||
assertEquals(2, obj.envelope.schema.types.size)
|
||||
require(obj.envelope.schema.types[0] is CompositeType)
|
||||
require(obj.envelope.schema.types[1] is CompositeType)
|
||||
|
||||
var amqpSchemaA: CompositeType? = null
|
||||
var amqpSchemaB: CompositeType? = null
|
||||
|
||||
for (type in obj.envelope.schema.types) {
|
||||
when (type.name.split("$").last()) {
|
||||
"A" -> amqpSchemaA = type as CompositeType
|
||||
"B" -> amqpSchemaB = type as CompositeType
|
||||
}
|
||||
}
|
||||
|
||||
require(amqpSchemaA != null)
|
||||
require(amqpSchemaB != null)
|
||||
|
||||
// Just ensure the amqp schema matches what we want before we go messing
|
||||
// around with the internals
|
||||
assertEquals(1, amqpSchemaA?.fields?.size)
|
||||
assertEquals("a", amqpSchemaA!!.fields[0].name)
|
||||
assertEquals("int", amqpSchemaA.fields[0].type)
|
||||
|
||||
assertEquals(2, amqpSchemaB?.fields?.size)
|
||||
assertEquals("a", amqpSchemaB!!.fields[0].name)
|
||||
assertEquals(classTestName("A"), amqpSchemaB.fields[0].type)
|
||||
assertEquals("b", amqpSchemaB.fields[1].name)
|
||||
assertEquals("int", amqpSchemaB.fields[1].type)
|
||||
|
||||
val metaSchema = obj.envelope.schema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
|
||||
// if we know all the classes there is nothing to really achieve here
|
||||
require(metaSchema.carpenterSchemas.isEmpty())
|
||||
require(metaSchema.dependsOn.isEmpty())
|
||||
require(metaSchema.dependencies.isEmpty())
|
||||
// We load an unknown class, B_mangled, which includes a reference to a known class, A.
|
||||
assertCanLoadAll(envelope.getMangled<B>())
|
||||
}
|
||||
|
||||
// you cannot have an element of a composite class we know about
|
||||
// that is unknown as that should be impossible. If we have the containing
|
||||
// class in the class path then we must have all of it's constituent elements
|
||||
@Test(expected = UncarpentableException::class)
|
||||
fun nestedIsUnknown() {
|
||||
val testA = 10
|
||||
val testB = 20
|
||||
|
||||
@Test
|
||||
fun bothAreUnknown() {
|
||||
@CordaSerializable
|
||||
data class A(override val a: Int) : I_
|
||||
|
||||
@CordaSerializable
|
||||
data class B(val a: A, var b: Int)
|
||||
|
||||
val b = B(A(testA), testB)
|
||||
val (_, envelope) = B(A(10), 20).roundTrip()
|
||||
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b))
|
||||
val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A")))
|
||||
|
||||
amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
// We load an unknown class, B_mangled, which includes a reference to an unknown class, A_mangled.
|
||||
// For this to work, we must include A_mangled in our set of classes to load.
|
||||
assertCanLoadAll(envelope.getMangled<B>().mangle<A>(), envelope.getMangled<A>())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ParentIsUnknown() {
|
||||
val testA = 10
|
||||
val testB = 20
|
||||
|
||||
fun oneIsUnknown() {
|
||||
@CordaSerializable
|
||||
data class A(override val a: Int) : I_
|
||||
|
||||
@CordaSerializable
|
||||
data class B(val a: A, var b: Int)
|
||||
|
||||
val b = B(A(testA), testB)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b))
|
||||
val (_, envelope) = B(A(10), 20).roundTrip()
|
||||
|
||||
val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("B")))
|
||||
val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
// We load an unknown class, B_mangled, which includes a reference to an unknown class, A_mangled.
|
||||
// This will fail, because A_mangled is not included in our set of classes to load.
|
||||
assertFailsWith<NotSerializableException> { assertCanLoadAll(envelope.getMangled<B>().mangle<A>()) }
|
||||
}
|
||||
|
||||
assertEquals(1, carpenterSchema.size)
|
||||
// See https://github.com/corda/corda/issues/4107
|
||||
@Test
|
||||
fun withUUID() {
|
||||
@CordaSerializable
|
||||
data class IOUStateData(
|
||||
val value: Int,
|
||||
val ref: UUID,
|
||||
val newValue: String? = null
|
||||
)
|
||||
|
||||
val metaCarpenter = MetaCarpenter(carpenterSchema, ClassCarpenterImpl(whitelist = AllWhitelist))
|
||||
|
||||
metaCarpenter.build()
|
||||
|
||||
require(mangleName(classTestName("B")) in metaCarpenter.objects)
|
||||
val uuid = UUID.randomUUID()
|
||||
val(_, envelope) = IOUStateData(10, uuid, "new value").roundTrip()
|
||||
val recarpented = envelope.getMangled<IOUStateData>().load()
|
||||
val instance = recarpented.new(null, uuid, 10)
|
||||
assertEquals(uuid, instance.get("ref"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun BothUnknown() {
|
||||
val testA = 10
|
||||
val testB = 20
|
||||
fun mapWithUnknown() {
|
||||
data class C(val a: Int)
|
||||
data class D(val m: Map<String, C>)
|
||||
val (_, envelope) = D(mapOf("c" to C(1))).roundTrip()
|
||||
|
||||
@CordaSerializable
|
||||
data class A(override val a: Int) : I_
|
||||
val infoForD = envelope.typeInformationFor<D>().mangle<C>()
|
||||
val mangledMap = envelope.typeInformation.values.find { it.typeIdentifier.name == "java.util.Map" }!!.mangle<C>()
|
||||
val mangledC = envelope.getMangled<C>()
|
||||
|
||||
@CordaSerializable
|
||||
data class B(val a: A, var b: Int)
|
||||
assertEquals(
|
||||
"java.util.Map<java.lang.String, ${mangledC.typeIdentifier.prettyPrint(false)}>",
|
||||
mangledMap.prettyPrint(false))
|
||||
|
||||
val b = B(A(testA), testB)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b))
|
||||
val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B")))
|
||||
val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
|
||||
// just verify we're in the expected initial state, A is carpentable, B is not because
|
||||
// it depends on A and the dependency chains are in place
|
||||
assertEquals(1, carpenterSchema.size)
|
||||
assertEquals(mangleName(classTestName("A")), carpenterSchema.carpenterSchemas.first().name)
|
||||
assertEquals(1, carpenterSchema.dependencies.size)
|
||||
require(mangleName(classTestName("B")) in carpenterSchema.dependencies)
|
||||
assertEquals(1, carpenterSchema.dependsOn.size)
|
||||
require(mangleName(classTestName("A")) in carpenterSchema.dependsOn)
|
||||
|
||||
val metaCarpenter = TestMetaCarpenter(carpenterSchema, ClassCarpenterImpl(whitelist = AllWhitelist))
|
||||
|
||||
assertEquals(0, metaCarpenter.objects.size)
|
||||
|
||||
// first iteration, carpent A, resolve deps and mark B as carpentable
|
||||
metaCarpenter.build()
|
||||
|
||||
// one build iteration should have carpetned up A and worked out that B is now buildable
|
||||
// given it's depedencies have been satisfied
|
||||
assertTrue(mangleName(classTestName("A")) in metaCarpenter.objects)
|
||||
assertFalse(mangleName(classTestName("B")) in metaCarpenter.objects)
|
||||
|
||||
assertEquals(1, carpenterSchema.carpenterSchemas.size)
|
||||
assertEquals(mangleName(classTestName("B")), carpenterSchema.carpenterSchemas.first().name)
|
||||
assertTrue(carpenterSchema.dependencies.isEmpty())
|
||||
assertTrue(carpenterSchema.dependsOn.isEmpty())
|
||||
|
||||
// second manual iteration, will carpent B
|
||||
metaCarpenter.build()
|
||||
require(mangleName(classTestName("A")) in metaCarpenter.objects)
|
||||
require(mangleName(classTestName("B")) in metaCarpenter.objects)
|
||||
|
||||
// and we must be finished
|
||||
assertTrue(carpenterSchema.carpenterSchemas.isEmpty())
|
||||
assertCanLoadAll(infoForD, mangledMap, mangledC)
|
||||
}
|
||||
|
||||
@Test(expected = UncarpentableException::class)
|
||||
@Suppress("UNUSED")
|
||||
fun nestedIsUnknownInherited() {
|
||||
val testA = 10
|
||||
val testB = 20
|
||||
val testC = 30
|
||||
|
||||
@CordaSerializable
|
||||
open class A(val a: Int)
|
||||
|
||||
@CordaSerializable
|
||||
class B(a: Int, var b: Int) : A(a)
|
||||
|
||||
@CordaSerializable
|
||||
data class C(val b: B, var c: Int)
|
||||
|
||||
val c = C(B(testA, testB), testC)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(c))
|
||||
|
||||
val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B")))
|
||||
|
||||
amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
}
|
||||
|
||||
@Test(expected = UncarpentableException::class)
|
||||
@Suppress("UNUSED")
|
||||
fun nestedIsUnknownInheritedUnknown() {
|
||||
val testA = 10
|
||||
val testB = 20
|
||||
val testC = 30
|
||||
|
||||
@CordaSerializable
|
||||
open class A(val a: Int)
|
||||
|
||||
@CordaSerializable
|
||||
class B(a: Int, var b: Int) : A(a)
|
||||
|
||||
@CordaSerializable
|
||||
data class C(val b: B, var c: Int)
|
||||
|
||||
val c = C(B(testA, testB), testC)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(c))
|
||||
|
||||
val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B")))
|
||||
|
||||
amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
}
|
||||
|
||||
@Suppress("UNUSED")
|
||||
@Test(expected = UncarpentableException::class)
|
||||
fun parentsIsUnknownWithUnknownInheritedMember() {
|
||||
val testA = 10
|
||||
val testB = 20
|
||||
val testC = 30
|
||||
|
||||
@CordaSerializable
|
||||
open class A(val a: Int)
|
||||
|
||||
@CordaSerializable
|
||||
class B(a: Int, var b: Int) : A(a)
|
||||
|
||||
@CordaSerializable
|
||||
data class C(val b: B, var c: Int)
|
||||
|
||||
val c = C(B(testA, testB), testC)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(c))
|
||||
|
||||
val carpenterSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B")))
|
||||
TestMetaCarpenter(carpenterSchema.carpenterSchema(
|
||||
ClassLoader.getSystemClassLoader()), ClassCarpenterImpl(whitelist = AllWhitelist))
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO serializer doesn't support inheritnace at the moment, when it does this should work
|
||||
@Test
|
||||
fun `inheritance`() {
|
||||
val testA = 10
|
||||
val testB = 20
|
||||
fun parameterisedNonCollectionWithUnknown() {
|
||||
data class C(val a: Int)
|
||||
data class NotAMap<K, V>(val key: K, val value: V)
|
||||
data class D(val m: NotAMap<String, C>)
|
||||
val (_, envelope) = D(NotAMap("c" , C(1))).roundTrip()
|
||||
|
||||
@CordaSerializable
|
||||
open class A(open val a: Int)
|
||||
val infoForD = envelope.typeInformationFor<D>().mangle<C>()
|
||||
val mangledNotAMap = envelope.typeInformationFor<NotAMap<String, C>>().mangle<C>()
|
||||
val mangledC = envelope.getMangled<C>()
|
||||
|
||||
@CordaSerializable
|
||||
class B(override val a: Int, val b: Int) : A (a)
|
||||
|
||||
val b = B(testA, testB)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b))
|
||||
|
||||
require(obj.obj is B)
|
||||
|
||||
val carpenterSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B")))
|
||||
val metaCarpenter = TestMetaCarpenter(carpenterSchema.carpenterSchema())
|
||||
|
||||
assertEquals(1, metaCarpenter.schemas.carpenterSchemas.size)
|
||||
assertEquals(mangleNames(classTestName("B")), metaCarpenter.schemas.carpenterSchemas.first().name)
|
||||
assertEquals(1, metaCarpenter.schemas.dependencies.size)
|
||||
assertTrue(mangleNames(classTestName("A")) in metaCarpenter.schemas.dependencies)
|
||||
assertCanLoadAll(infoForD, mangledNotAMap, mangledC)
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
|
@ -2,10 +2,9 @@ package net.corda.serialization.internal.carpenter
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.serialization.internal.AllWhitelist
|
||||
import net.corda.serialization.internal.amqp.DeserializationInput
|
||||
import org.junit.Test
|
||||
import kotlin.test.*
|
||||
import net.corda.serialization.internal.amqp.testutils.deserializeAndReturnEnvelope
|
||||
import java.io.NotSerializableException
|
||||
|
||||
@CordaSerializable
|
||||
interface J {
|
||||
@ -39,172 +38,68 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) {
|
||||
fun interfaceParent1() {
|
||||
class A(override val j: Int) : J
|
||||
|
||||
val testJ = 20
|
||||
val a = A(testJ)
|
||||
val (_, env) = A(20).roundTrip()
|
||||
val mangledA = env.getMangled<A>()
|
||||
|
||||
assertEquals(testJ, a.j)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
|
||||
val serSchema = obj.envelope.schema
|
||||
assertEquals(2, serSchema.types.size)
|
||||
val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
val carpentedA = mangledA.load()
|
||||
val carpentedInstance = carpentedA.new(20)
|
||||
|
||||
// since we're using an envelope generated by seilaising classes defined locally
|
||||
// it's extremely unlikely we'd need to carpent any classes
|
||||
assertEquals(0, l1.size)
|
||||
assertEquals(20, carpentedInstance.get("j"))
|
||||
|
||||
val mangleSchema = serSchema.mangleNames(listOf(classTestName("A")))
|
||||
val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
assertEquals(1, l2.size)
|
||||
|
||||
val aSchema = l2.carpenterSchemas.find { it.name == mangleName(classTestName("A")) }
|
||||
assertNotEquals(null, aSchema)
|
||||
assertEquals(mangleName(classTestName("A")), aSchema!!.name)
|
||||
assertEquals(1, aSchema.interfaces.size)
|
||||
assertEquals(J::class.java, aSchema.interfaces[0])
|
||||
|
||||
val aBuilder = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema)
|
||||
val objJ = aBuilder.constructors[0].newInstance(testJ)
|
||||
val j = objJ as J
|
||||
|
||||
assertEquals(aBuilder.getMethod("getJ").invoke(objJ), testJ)
|
||||
assertEquals(a.j, j.j)
|
||||
val asJ = carpentedInstance as J
|
||||
assertEquals(20, asJ.j)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun interfaceParent2() {
|
||||
class A(override val j: Int, val jj: Int) : J
|
||||
|
||||
val testJ = 20
|
||||
val testJJ = 40
|
||||
val a = A(testJ, testJJ)
|
||||
val (_, env) = A(23, 42).roundTrip()
|
||||
val carpentedA = env.getMangled<A>().load()
|
||||
val carpetedInstance = carpentedA.constructors[0].newInstance(23, 42)
|
||||
|
||||
assertEquals(testJ, a.j)
|
||||
assertEquals(testJJ, a.jj)
|
||||
assertEquals(23, carpetedInstance.get("j"))
|
||||
assertEquals(42, carpetedInstance.get("jj"))
|
||||
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
|
||||
|
||||
val serSchema = obj.envelope.schema
|
||||
|
||||
assertEquals(2, serSchema.types.size)
|
||||
|
||||
val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
|
||||
assertEquals(0, l1.size)
|
||||
|
||||
val mangleSchema = serSchema.mangleNames(listOf(classTestName("A")))
|
||||
val aName = mangleName(classTestName("A"))
|
||||
val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
|
||||
assertEquals(1, l2.size)
|
||||
|
||||
val aSchema = l2.carpenterSchemas.find { it.name == aName }
|
||||
|
||||
assertNotEquals(null, aSchema)
|
||||
|
||||
assertEquals(aName, aSchema!!.name)
|
||||
assertEquals(1, aSchema.interfaces.size)
|
||||
assertEquals(J::class.java, aSchema.interfaces[0])
|
||||
|
||||
val aBuilder = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema)
|
||||
val objJ = aBuilder.constructors[0].newInstance(testJ, testJJ)
|
||||
val j = objJ as J
|
||||
|
||||
assertEquals(aBuilder.getMethod("getJ").invoke(objJ), testJ)
|
||||
assertEquals(aBuilder.getMethod("getJj").invoke(objJ), testJJ)
|
||||
|
||||
assertEquals(a.j, j.j)
|
||||
val asJ = carpetedInstance as J
|
||||
assertEquals(23, asJ.j)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multipleInterfaces() {
|
||||
val testI = 20
|
||||
val testII = 40
|
||||
|
||||
class A(override val i: Int, override val ii: Int) : I, II
|
||||
|
||||
val a = A(testI, testII)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
|
||||
val (_, env) = A(23, 42).roundTrip()
|
||||
val carpentedA = env.getMangled<A>().load()
|
||||
val carpetedInstance = carpentedA.constructors[0].newInstance(23, 42)
|
||||
|
||||
val serSchema = obj.envelope.schema
|
||||
assertEquals(23, carpetedInstance.get("i"))
|
||||
assertEquals(42, carpetedInstance.get("ii"))
|
||||
|
||||
assertEquals(3, serSchema.types.size)
|
||||
val i = carpetedInstance as I
|
||||
val ii = carpetedInstance as II
|
||||
|
||||
val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
|
||||
// since we're using an envelope generated by serialising classes defined locally
|
||||
// it's extremely unlikely we'd need to carpent any classes
|
||||
assertEquals(0, l1.size)
|
||||
|
||||
// pretend we don't know the class we've been sent, i.e. it's unknown to the class loader, and thus
|
||||
// needs some carpentry
|
||||
val mangleSchema = serSchema.mangleNames(listOf(classTestName("A")))
|
||||
val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
val aName = mangleName(classTestName("A"))
|
||||
|
||||
assertEquals(1, l2.size)
|
||||
|
||||
val aSchema = l2.carpenterSchemas.find { it.name == aName }
|
||||
|
||||
assertNotEquals(null, aSchema)
|
||||
assertEquals(aName, aSchema!!.name)
|
||||
assertEquals(2, aSchema.interfaces.size)
|
||||
assertTrue(I::class.java in aSchema.interfaces)
|
||||
assertTrue(II::class.java in aSchema.interfaces)
|
||||
|
||||
val aBuilder = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema)
|
||||
val objA = aBuilder.constructors[0].newInstance(testI, testII)
|
||||
val i = objA as I
|
||||
val ii = objA as II
|
||||
|
||||
assertEquals(aBuilder.getMethod("getI").invoke(objA), testI)
|
||||
assertEquals(aBuilder.getMethod("getIi").invoke(objA), testII)
|
||||
assertEquals(a.i, i.i)
|
||||
assertEquals(a.ii, ii.ii)
|
||||
assertEquals(23, i.i)
|
||||
assertEquals(42, ii.ii)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nestedInterfaces() {
|
||||
class A(override val i: Int, override val iii: Int) : III
|
||||
|
||||
val testI = 20
|
||||
val testIII = 60
|
||||
val a = A(testI, testIII)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
|
||||
val (_, env) = A(23, 42).roundTrip()
|
||||
val carpentedA = env.getMangled<A>().load()
|
||||
val carpetedInstance = carpentedA.constructors[0].newInstance(23, 42)
|
||||
|
||||
val serSchema = obj.envelope.schema
|
||||
assertEquals(23, carpetedInstance.get("i"))
|
||||
assertEquals(42, carpetedInstance.get("iii"))
|
||||
|
||||
assertEquals(3, serSchema.types.size)
|
||||
val i = carpetedInstance as I
|
||||
val iii = carpetedInstance as III
|
||||
|
||||
val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
|
||||
// since we're using an envelope generated by serialising classes defined locally
|
||||
// it's extremely unlikely we'd need to carpent any classes
|
||||
assertEquals(0, l1.size)
|
||||
|
||||
val mangleSchema = serSchema.mangleNames(listOf(classTestName("A")))
|
||||
val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
val aName = mangleName(classTestName("A"))
|
||||
|
||||
assertEquals(1, l2.size)
|
||||
|
||||
val aSchema = l2.carpenterSchemas.find { it.name == aName }
|
||||
|
||||
assertNotEquals(null, aSchema)
|
||||
assertEquals(aName, aSchema!!.name)
|
||||
assertEquals(2, aSchema.interfaces.size)
|
||||
assertTrue(I::class.java in aSchema.interfaces)
|
||||
assertTrue(III::class.java in aSchema.interfaces)
|
||||
|
||||
val aBuilder = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema)
|
||||
val objA = aBuilder.constructors[0].newInstance(testI, testIII)
|
||||
val i = objA as I
|
||||
val iii = objA as III
|
||||
|
||||
assertEquals(aBuilder.getMethod("getI").invoke(objA), testI)
|
||||
assertEquals(aBuilder.getMethod("getIii").invoke(objA), testIII)
|
||||
assertEquals(a.i, i.i)
|
||||
assertEquals(a.i, iii.i)
|
||||
assertEquals(a.iii, iii.iii)
|
||||
assertEquals(23, i.i)
|
||||
assertEquals(23, iii.i)
|
||||
assertEquals(42, iii.iii)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -212,237 +107,60 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) {
|
||||
class A(override val i: Int) : I
|
||||
class B(override val i: I, override val iiii: Int) : IIII
|
||||
|
||||
val testI = 25
|
||||
val testIIII = 50
|
||||
val a = A(testI)
|
||||
val b = B(a, testIIII)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b))
|
||||
val (_, env) = B(A(23), 42).roundTrip()
|
||||
val carpentedA = env.getMangled<A>().load()
|
||||
val carpentedB = env.getMangled<B>().load()
|
||||
|
||||
val serSchema = obj.envelope.schema
|
||||
val carpentedAInstance = carpentedA.new(23)
|
||||
val carpentedBInstance = carpentedB.new(carpentedAInstance, 42)
|
||||
|
||||
// Expected classes are
|
||||
// * class A
|
||||
// * class A's interface (class I)
|
||||
// * class B
|
||||
// * class B's interface (class IIII)
|
||||
assertEquals(4, serSchema.types.size)
|
||||
|
||||
val mangleSchema = serSchema.mangleNames(listOf(classTestName("A"), classTestName("B")))
|
||||
val cSchema = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
val aName = mangleName(classTestName("A"))
|
||||
val bName = mangleName(classTestName("B"))
|
||||
|
||||
assertEquals(2, cSchema.size)
|
||||
|
||||
val aCarpenterSchema = cSchema.carpenterSchemas.find { it.name == aName }
|
||||
val bCarpenterSchema = cSchema.carpenterSchemas.find { it.name == bName }
|
||||
|
||||
assertNotEquals(null, aCarpenterSchema)
|
||||
assertNotEquals(null, bCarpenterSchema)
|
||||
|
||||
val cc = ClassCarpenterImpl(whitelist = AllWhitelist)
|
||||
val cc2 = ClassCarpenterImpl(whitelist = AllWhitelist)
|
||||
val bBuilder = cc.build(bCarpenterSchema!!)
|
||||
bBuilder.constructors[0].newInstance(a, testIIII)
|
||||
|
||||
val aBuilder = cc.build(aCarpenterSchema!!)
|
||||
val objA = aBuilder.constructors[0].newInstance(testI)
|
||||
|
||||
// build a second B this time using our constructed instance of A and not the
|
||||
// local one we pre defined
|
||||
bBuilder.constructors[0].newInstance(objA, testIIII)
|
||||
|
||||
// whittle and instantiate a different A with a new class loader
|
||||
val aBuilder2 = cc2.build(aCarpenterSchema)
|
||||
val objA2 = aBuilder2.constructors[0].newInstance(testI)
|
||||
|
||||
bBuilder.constructors[0].newInstance(objA2, testIIII)
|
||||
val iiii = carpentedBInstance as IIII
|
||||
assertEquals(23, iiii.i.i)
|
||||
assertEquals(42, iiii.iiii)
|
||||
}
|
||||
|
||||
// if we remove the nested interface we should get an error as it's impossible
|
||||
// to have a concrete class loaded without having access to all of it's elements
|
||||
@Test(expected = UncarpentableException::class)
|
||||
@Test
|
||||
fun memberInterface2() {
|
||||
class A(override val i: Int) : I
|
||||
class B(override val i: I, override val iiii: Int) : IIII
|
||||
|
||||
val testI = 25
|
||||
val testIIII = 50
|
||||
val a = A(testI)
|
||||
val b = B(a, testIIII)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b))
|
||||
val (_, env) = A(23).roundTrip()
|
||||
|
||||
val serSchema = obj.envelope.schema
|
||||
|
||||
// The classes we're expecting to find:
|
||||
// * class A
|
||||
// * class A's interface (class I)
|
||||
// * class B
|
||||
// * class B's interface (class IIII)
|
||||
assertEquals(4, serSchema.types.size)
|
||||
|
||||
// ignore the return as we expect this to throw
|
||||
serSchema.mangleNames(listOf(
|
||||
classTestName("A"), "${this.javaClass.`package`.name}.I")).carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
// if we remove the nested interface we should get an error as it's impossible
|
||||
// to have a concrete class loaded without having access to all of it's elements
|
||||
assertFailsWith<NotSerializableException> { assertCanLoadAll(env.getMangled<A>().mangle<I>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun interfaceAndImplementation() {
|
||||
class A(override val i: Int) : I
|
||||
|
||||
val testI = 25
|
||||
val a = A(testI)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
|
||||
val (_, env) = A(23).roundTrip()
|
||||
|
||||
val serSchema = obj.envelope.schema
|
||||
|
||||
// The classes we're expecting to find:
|
||||
// * class A
|
||||
// * class A's interface (class I)
|
||||
assertEquals(2, serSchema.types.size)
|
||||
|
||||
val amqpSchema = serSchema.mangleNames(listOf(classTestName("A"), "${this.javaClass.`package`.name}.I"))
|
||||
val aName = mangleName(classTestName("A"))
|
||||
val iName = mangleName("${this.javaClass.`package`.name}.I")
|
||||
val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
|
||||
// whilst there are two unknown classes within the envelope A depends on I so we can't construct a
|
||||
// schema for A until we have for I
|
||||
assertEquals(1, carpenterSchema.size)
|
||||
assertNotEquals(null, carpenterSchema.carpenterSchemas.find { it.name == iName })
|
||||
|
||||
// since we can't build A it should list I as a dependency
|
||||
assertTrue(aName in carpenterSchema.dependencies)
|
||||
assertEquals(1, carpenterSchema.dependencies[aName]!!.second.size)
|
||||
assertEquals(iName, carpenterSchema.dependencies[aName]!!.second[0])
|
||||
|
||||
// and conversly I should have A listed as a dependent
|
||||
assertTrue(iName in carpenterSchema.dependsOn)
|
||||
assertEquals(1, carpenterSchema.dependsOn[iName]!!.size)
|
||||
assertEquals(aName, carpenterSchema.dependsOn[iName]!![0])
|
||||
|
||||
val mc = MetaCarpenter(carpenterSchema, ClassCarpenterImpl(whitelist = AllWhitelist))
|
||||
mc.build()
|
||||
|
||||
assertEquals(0, mc.schemas.carpenterSchemas.size)
|
||||
assertEquals(0, mc.schemas.dependencies.size)
|
||||
assertEquals(0, mc.schemas.dependsOn.size)
|
||||
assertEquals(2, mc.objects.size)
|
||||
assertTrue(aName in mc.objects)
|
||||
assertTrue(iName in mc.objects)
|
||||
|
||||
mc.objects[aName]!!.constructors[0].newInstance(testI)
|
||||
// This time around we will succeed, because the mangled I is included in the type information to be loaded.
|
||||
assertCanLoadAll(env.getMangled<A>().mangle<I>(), env.getMangled<I>())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun twoInterfacesAndImplementation() {
|
||||
class A(override val i: Int, override val ii: Int) : I, II
|
||||
|
||||
val testI = 69
|
||||
val testII = 96
|
||||
val a = A(testI, testII)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
|
||||
|
||||
val amqpSchema = obj.envelope.schema.mangleNames(listOf(
|
||||
classTestName("A"),
|
||||
"${this.javaClass.`package`.name}.I",
|
||||
"${this.javaClass.`package`.name}.II"))
|
||||
|
||||
val aName = mangleName(classTestName("A"))
|
||||
val iName = mangleName("${this.javaClass.`package`.name}.I")
|
||||
val iiName = mangleName("${this.javaClass.`package`.name}.II")
|
||||
val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
|
||||
// there is nothing preventing us from carpenting up the two interfaces so
|
||||
// our initial list should contain both interface with A being dependent on both
|
||||
// and each having A as a dependent
|
||||
assertEquals(2, carpenterSchema.carpenterSchemas.size)
|
||||
assertNotNull(carpenterSchema.carpenterSchemas.find { it.name == iName })
|
||||
assertNotNull(carpenterSchema.carpenterSchemas.find { it.name == iiName })
|
||||
assertNull(carpenterSchema.carpenterSchemas.find { it.name == aName })
|
||||
|
||||
assertTrue(iName in carpenterSchema.dependsOn)
|
||||
assertEquals(1, carpenterSchema.dependsOn[iName]?.size)
|
||||
assertNotNull(carpenterSchema.dependsOn[iName]?.find({ it == aName }))
|
||||
|
||||
assertTrue(iiName in carpenterSchema.dependsOn)
|
||||
assertEquals(1, carpenterSchema.dependsOn[iiName]?.size)
|
||||
assertNotNull(carpenterSchema.dependsOn[iiName]?.find { it == aName })
|
||||
|
||||
assertTrue(aName in carpenterSchema.dependencies)
|
||||
assertEquals(2, carpenterSchema.dependencies[aName]!!.second.size)
|
||||
assertNotNull(carpenterSchema.dependencies[aName]!!.second.find { it == iName })
|
||||
assertNotNull(carpenterSchema.dependencies[aName]!!.second.find { it == iiName })
|
||||
|
||||
val mc = MetaCarpenter(carpenterSchema, ClassCarpenterImpl(whitelist = AllWhitelist))
|
||||
mc.build()
|
||||
|
||||
assertEquals(0, mc.schemas.carpenterSchemas.size)
|
||||
assertEquals(0, mc.schemas.dependencies.size)
|
||||
assertEquals(0, mc.schemas.dependsOn.size)
|
||||
assertEquals(3, mc.objects.size)
|
||||
assertTrue(aName in mc.objects)
|
||||
assertTrue(iName in mc.objects)
|
||||
assertTrue(iiName in mc.objects)
|
||||
val (_, env) = A(23, 42).roundTrip()
|
||||
assertCanLoadAll(
|
||||
env.getMangled<A>().mangle<I>().mangle<II>(),
|
||||
env.getMangled<I>(),
|
||||
env.getMangled<II>()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nestedInterfacesAndImplementation() {
|
||||
class A(override val i: Int, override val iii: Int) : III
|
||||
|
||||
val testI = 7
|
||||
val testIII = 11
|
||||
val a = A(testI, testIII)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
|
||||
|
||||
val amqpSchema = obj.envelope.schema.mangleNames(listOf(
|
||||
classTestName("A"),
|
||||
"${this.javaClass.`package`.name}.I",
|
||||
"${this.javaClass.`package`.name}.III"))
|
||||
|
||||
val aName = mangleName(classTestName("A"))
|
||||
val iName = mangleName("${this.javaClass.`package`.name}.I")
|
||||
val iiiName = mangleName("${this.javaClass.`package`.name}.III")
|
||||
val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader())
|
||||
|
||||
// Since A depends on III and III extends I we will have to construct them
|
||||
// in that reverse order (I -> III -> A)
|
||||
assertEquals(1, carpenterSchema.carpenterSchemas.size)
|
||||
assertNotNull(carpenterSchema.carpenterSchemas.find { it.name == iName })
|
||||
assertNull(carpenterSchema.carpenterSchemas.find { it.name == iiiName })
|
||||
assertNull(carpenterSchema.carpenterSchemas.find { it.name == aName })
|
||||
|
||||
// I has III as a direct dependent and A as an indirect one
|
||||
assertTrue(iName in carpenterSchema.dependsOn)
|
||||
assertEquals(2, carpenterSchema.dependsOn[iName]?.size)
|
||||
assertNotNull(carpenterSchema.dependsOn[iName]?.find({ it == iiiName }))
|
||||
assertNotNull(carpenterSchema.dependsOn[iName]?.find({ it == aName }))
|
||||
|
||||
// III has A as a dependent
|
||||
assertTrue(iiiName in carpenterSchema.dependsOn)
|
||||
assertEquals(1, carpenterSchema.dependsOn[iiiName]?.size)
|
||||
assertNotNull(carpenterSchema.dependsOn[iiiName]?.find { it == aName })
|
||||
|
||||
// conversly III depends on I
|
||||
assertTrue(iiiName in carpenterSchema.dependencies)
|
||||
assertEquals(1, carpenterSchema.dependencies[iiiName]!!.second.size)
|
||||
assertNotNull(carpenterSchema.dependencies[iiiName]!!.second.find { it == iName })
|
||||
|
||||
// and A depends on III and I
|
||||
assertTrue(aName in carpenterSchema.dependencies)
|
||||
assertEquals(2, carpenterSchema.dependencies[aName]!!.second.size)
|
||||
assertNotNull(carpenterSchema.dependencies[aName]!!.second.find { it == iiiName })
|
||||
assertNotNull(carpenterSchema.dependencies[aName]!!.second.find { it == iName })
|
||||
|
||||
val mc = MetaCarpenter(carpenterSchema, ClassCarpenterImpl(whitelist = AllWhitelist))
|
||||
mc.build()
|
||||
|
||||
assertEquals(0, mc.schemas.carpenterSchemas.size)
|
||||
assertEquals(0, mc.schemas.dependencies.size)
|
||||
assertEquals(0, mc.schemas.dependsOn.size)
|
||||
assertEquals(3, mc.objects.size)
|
||||
assertTrue(aName in mc.objects)
|
||||
assertTrue(iName in mc.objects)
|
||||
assertTrue(iiiName in mc.objects)
|
||||
val (_, env) = A(23, 42).roundTrip()
|
||||
assertCanLoadAll(
|
||||
env.getMangled<A>().mangle<I>().mangle<III>(),
|
||||
env.getMangled<I>(),
|
||||
env.getMangled<III>().mangle<I>()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,57 +1,24 @@
|
||||
package net.corda.serialization.internal.carpenter
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializableCalculatedProperty
|
||||
import net.corda.serialization.internal.AllWhitelist
|
||||
import net.corda.serialization.internal.amqp.CompositeType
|
||||
import net.corda.serialization.internal.amqp.DeserializationInput
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotEquals
|
||||
import net.corda.serialization.internal.amqp.testutils.deserializeAndReturnEnvelope
|
||||
|
||||
class MultiMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) {
|
||||
|
||||
@Test
|
||||
fun twoInts() {
|
||||
fun anIntAndALong() {
|
||||
@CordaSerializable
|
||||
data class A(val a: Int, val b: Int)
|
||||
data class A(val a: Int, val b: Long)
|
||||
|
||||
val testA = 10
|
||||
val testB = 20
|
||||
val a = A(testA, testB)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
|
||||
val (_, env) = A(23, 42).roundTrip()
|
||||
val carpentedInstance = env.getMangled<A>().load().new(23, 42)
|
||||
|
||||
val amqpObj = obj.obj
|
||||
|
||||
assertEquals(testA, amqpObj.a)
|
||||
assertEquals(testB, amqpObj.b)
|
||||
assertEquals(1, obj.envelope.schema.types.size)
|
||||
require(obj.envelope.schema.types[0] is CompositeType)
|
||||
|
||||
val amqpSchema = obj.envelope.schema.types[0] as CompositeType
|
||||
|
||||
assertEquals(2, amqpSchema.fields.size)
|
||||
assertEquals("a", amqpSchema.fields[0].name)
|
||||
assertEquals("int", amqpSchema.fields[0].type)
|
||||
assertEquals("b", amqpSchema.fields[1].name)
|
||||
assertEquals("int", amqpSchema.fields[1].type)
|
||||
|
||||
val carpenterSchema = CarpenterMetaSchema.newInstance()
|
||||
amqpSchema.carpenterSchema(
|
||||
classloader = ClassLoader.getSystemClassLoader(),
|
||||
carpenterSchemas = carpenterSchema,
|
||||
force = true)
|
||||
|
||||
assertEquals(1, carpenterSchema.size)
|
||||
val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }
|
||||
|
||||
assertNotEquals(null, aSchema)
|
||||
|
||||
val pinochio = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema!!)
|
||||
val p = pinochio.constructors[0].newInstance(testA, testB)
|
||||
|
||||
assertEquals(pinochio.getMethod("getA").invoke(p), amqpObj.a)
|
||||
assertEquals(pinochio.getMethod("getB").invoke(p), amqpObj.b)
|
||||
assertEquals(23, carpentedInstance.get("a"))
|
||||
assertEquals(42L, carpentedInstance.get("b"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -59,42 +26,65 @@ class MultiMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhi
|
||||
@CordaSerializable
|
||||
data class A(val a: Int, val b: String)
|
||||
|
||||
val testA = 10
|
||||
val testB = "twenty"
|
||||
val a = A(testA, testB)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
|
||||
val (_, env) = A(23, "skidoo").roundTrip()
|
||||
val carpentedInstance = env.getMangled<A>().load().new(23, "skidoo")
|
||||
|
||||
val amqpObj = obj.obj
|
||||
assertEquals(23, carpentedInstance.get("a"))
|
||||
assertEquals("skidoo", carpentedInstance.get("b"))
|
||||
}
|
||||
|
||||
assertEquals(testA, amqpObj.a)
|
||||
assertEquals(testB, amqpObj.b)
|
||||
assertEquals(1, obj.envelope.schema.types.size)
|
||||
require(obj.envelope.schema.types[0] is CompositeType)
|
||||
interface Parent {
|
||||
@get:SerializableCalculatedProperty
|
||||
val doubled: Int
|
||||
}
|
||||
|
||||
val amqpSchema = obj.envelope.schema.types[0] as CompositeType
|
||||
@Test
|
||||
fun calculatedValues() {
|
||||
data class C(val i: Int): Parent {
|
||||
@get:SerializableCalculatedProperty
|
||||
val squared = (i * i).toString()
|
||||
|
||||
assertEquals(2, amqpSchema.fields.size)
|
||||
assertEquals("a", amqpSchema.fields[0].name)
|
||||
assertEquals("int", amqpSchema.fields[0].type)
|
||||
assertEquals("b", amqpSchema.fields[1].name)
|
||||
assertEquals("string", amqpSchema.fields[1].type)
|
||||
override val doubled get() = i * 2
|
||||
}
|
||||
|
||||
val carpenterSchema = CarpenterMetaSchema.newInstance()
|
||||
amqpSchema.carpenterSchema(
|
||||
classloader = ClassLoader.getSystemClassLoader(),
|
||||
carpenterSchemas = carpenterSchema,
|
||||
force = true)
|
||||
val (amqpObj, envelope) = C(2).roundTrip()
|
||||
val remoteTypeInformation = envelope.typeInformationFor<C>()
|
||||
|
||||
assertEquals(1, carpenterSchema.size)
|
||||
val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }
|
||||
assertEquals("""
|
||||
C: Parent
|
||||
doubled: int
|
||||
i: int
|
||||
squared: String
|
||||
""".trimIndent(), remoteTypeInformation.prettyPrint())
|
||||
|
||||
assertNotEquals(null, aSchema)
|
||||
val pinochio = remoteTypeInformation.mangle<C>().load()
|
||||
assertNotEquals(pinochio.name, C::class.java.name)
|
||||
assertNotEquals(pinochio, C::class.java)
|
||||
|
||||
val pinochio = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema!!)
|
||||
val p = pinochio.constructors[0].newInstance(testA, testB)
|
||||
// Note that params are given in alphabetical order: doubled, i, squared
|
||||
val p = pinochio.new(4, 2, "4")
|
||||
|
||||
assertEquals(pinochio.getMethod("getA").invoke(p), amqpObj.a)
|
||||
assertEquals(pinochio.getMethod("getB").invoke(p), amqpObj.b)
|
||||
assertEquals(2, p.get("i"))
|
||||
assertEquals("4", p.get("squared"))
|
||||
assertEquals(4, p.get("doubled"))
|
||||
|
||||
val upcast = p as Parent
|
||||
assertEquals(upcast.doubled, amqpObj.doubled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun implementingClassDoesNotCalculateValue() {
|
||||
class C(override val doubled: Int): Parent
|
||||
|
||||
val (_, env) = C(5).roundTrip()
|
||||
|
||||
val pinochio = env.getMangled<C>().load()
|
||||
val p = pinochio.new(5)
|
||||
|
||||
assertEquals(5, p.get("doubled"))
|
||||
|
||||
val upcast = p as Parent
|
||||
assertEquals(5, upcast.doubled)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,205 +0,0 @@
|
||||
package net.corda.serialization.internal.carpenter
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.serialization.internal.AllWhitelist
|
||||
import net.corda.serialization.internal.amqp.CompositeType
|
||||
import net.corda.serialization.internal.amqp.DeserializationInput
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import net.corda.serialization.internal.amqp.testutils.deserializeAndReturnEnvelope
|
||||
|
||||
class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase(AllWhitelist) {
|
||||
@Test
|
||||
fun singleInteger() {
|
||||
@CordaSerializable
|
||||
data class A(val a: Int)
|
||||
|
||||
val test = 10
|
||||
val a = A(test)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
|
||||
val amqpObj = obj.obj
|
||||
|
||||
assertEquals(test, amqpObj.a)
|
||||
assertEquals(1, obj.envelope.schema.types.size)
|
||||
require(obj.envelope.schema.types[0] is CompositeType)
|
||||
|
||||
val amqpSchema = obj.envelope.schema.types[0] as CompositeType
|
||||
|
||||
assertEquals(1, amqpSchema.fields.size)
|
||||
assertEquals("a", amqpSchema.fields[0].name)
|
||||
assertEquals("int", amqpSchema.fields[0].type)
|
||||
|
||||
val carpenterSchema = CarpenterMetaSchema.newInstance()
|
||||
amqpSchema.carpenterSchema(
|
||||
classloader = ClassLoader.getSystemClassLoader(),
|
||||
carpenterSchemas = carpenterSchema,
|
||||
force = true)
|
||||
|
||||
val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!!
|
||||
val aBuilder = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema)
|
||||
val p = aBuilder.constructors[0].newInstance(test)
|
||||
|
||||
assertEquals(aBuilder.getMethod("getA").invoke(p), amqpObj.a)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleString() {
|
||||
@CordaSerializable
|
||||
data class A(val a: String)
|
||||
|
||||
val test = "ten"
|
||||
val a = A(test)
|
||||
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
|
||||
val amqpObj = obj.obj
|
||||
|
||||
assertEquals(test, amqpObj.a)
|
||||
assertEquals(1, obj.envelope.schema.types.size)
|
||||
require(obj.envelope.schema.types[0] is CompositeType)
|
||||
|
||||
val amqpSchema = obj.envelope.schema.types[0] as CompositeType
|
||||
val carpenterSchema = CarpenterMetaSchema.newInstance()
|
||||
amqpSchema.carpenterSchema(
|
||||
classloader = ClassLoader.getSystemClassLoader(),
|
||||
carpenterSchemas = carpenterSchema,
|
||||
force = true)
|
||||
|
||||
val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!!
|
||||
val aBuilder = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema)
|
||||
val p = aBuilder.constructors[0].newInstance(test)
|
||||
|
||||
assertEquals(aBuilder.getMethod("getA").invoke(p), amqpObj.a)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleLong() {
|
||||
@CordaSerializable
|
||||
data class A(val a: Long)
|
||||
|
||||
val test = 10L
|
||||
val a = A(test)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
|
||||
val amqpObj = obj.obj
|
||||
|
||||
assertEquals(test, amqpObj.a)
|
||||
assertEquals(1, obj.envelope.schema.types.size)
|
||||
require(obj.envelope.schema.types[0] is CompositeType)
|
||||
|
||||
val amqpSchema = obj.envelope.schema.types[0] as CompositeType
|
||||
|
||||
assertEquals(1, amqpSchema.fields.size)
|
||||
assertEquals("a", amqpSchema.fields[0].name)
|
||||
assertEquals("long", amqpSchema.fields[0].type)
|
||||
|
||||
val carpenterSchema = CarpenterMetaSchema.newInstance()
|
||||
amqpSchema.carpenterSchema(
|
||||
classloader = ClassLoader.getSystemClassLoader(),
|
||||
carpenterSchemas = carpenterSchema,
|
||||
force = true)
|
||||
|
||||
val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!!
|
||||
val aBuilder = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema)
|
||||
val p = aBuilder.constructors[0].newInstance(test)
|
||||
|
||||
assertEquals(aBuilder.getMethod("getA").invoke(p), amqpObj.a)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleShort() {
|
||||
@CordaSerializable
|
||||
data class A(val a: Short)
|
||||
|
||||
val test = 10.toShort()
|
||||
val a = A(test)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
|
||||
val amqpObj = obj.obj
|
||||
|
||||
assertEquals(test, amqpObj.a)
|
||||
assertEquals(1, obj.envelope.schema.types.size)
|
||||
require(obj.envelope.schema.types[0] is CompositeType)
|
||||
|
||||
val amqpSchema = obj.envelope.schema.types[0] as CompositeType
|
||||
|
||||
assertEquals(1, amqpSchema.fields.size)
|
||||
assertEquals("a", amqpSchema.fields[0].name)
|
||||
assertEquals("short", amqpSchema.fields[0].type)
|
||||
|
||||
val carpenterSchema = CarpenterMetaSchema.newInstance()
|
||||
amqpSchema.carpenterSchema(
|
||||
classloader = ClassLoader.getSystemClassLoader(),
|
||||
carpenterSchemas = carpenterSchema,
|
||||
force = true)
|
||||
|
||||
val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!!
|
||||
val aBuilder = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema)
|
||||
val p = aBuilder.constructors[0].newInstance(test)
|
||||
|
||||
assertEquals(aBuilder.getMethod("getA").invoke(p), amqpObj.a)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleDouble() {
|
||||
@CordaSerializable
|
||||
data class A(val a: Double)
|
||||
|
||||
val test = 10.0
|
||||
val a = A(test)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
|
||||
val amqpObj = obj.obj
|
||||
|
||||
assertEquals(test, amqpObj.a)
|
||||
assertEquals(1, obj.envelope.schema.types.size)
|
||||
require(obj.envelope.schema.types[0] is CompositeType)
|
||||
|
||||
val amqpSchema = obj.envelope.schema.types[0] as CompositeType
|
||||
|
||||
assertEquals(1, amqpSchema.fields.size)
|
||||
assertEquals("a", amqpSchema.fields[0].name)
|
||||
assertEquals("double", amqpSchema.fields[0].type)
|
||||
|
||||
val carpenterSchema = CarpenterMetaSchema.newInstance()
|
||||
amqpSchema.carpenterSchema(
|
||||
classloader = ClassLoader.getSystemClassLoader(),
|
||||
carpenterSchemas = carpenterSchema,
|
||||
force = true)
|
||||
|
||||
val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!!
|
||||
val aBuilder = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema)
|
||||
val p = aBuilder.constructors[0].newInstance(test)
|
||||
|
||||
assertEquals(aBuilder.getMethod("getA").invoke(p), amqpObj.a)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleFloat() {
|
||||
@CordaSerializable
|
||||
data class A(val a: Float)
|
||||
|
||||
val test = 10.0F
|
||||
val a = A(test)
|
||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
|
||||
val amqpObj = obj.obj
|
||||
|
||||
assertEquals(test, amqpObj.a)
|
||||
assertEquals(1, obj.envelope.schema.types.size)
|
||||
require(obj.envelope.schema.types[0] is CompositeType)
|
||||
|
||||
val amqpSchema = obj.envelope.schema.types[0] as CompositeType
|
||||
|
||||
assertEquals(1, amqpSchema.fields.size)
|
||||
assertEquals("a", amqpSchema.fields[0].name)
|
||||
assertEquals("float", amqpSchema.fields[0].type)
|
||||
|
||||
val carpenterSchema = CarpenterMetaSchema.newInstance()
|
||||
amqpSchema.carpenterSchema(
|
||||
classloader = ClassLoader.getSystemClassLoader(),
|
||||
carpenterSchemas = carpenterSchema,
|
||||
force = true)
|
||||
|
||||
val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!!
|
||||
val aBuilder = ClassCarpenterImpl(whitelist = AllWhitelist).build(aSchema)
|
||||
val p = aBuilder.constructors[0].newInstance(test)
|
||||
|
||||
assertEquals(aBuilder.getMethod("getA").invoke(p), amqpObj.a)
|
||||
}
|
||||
}
|
@ -54,7 +54,7 @@ class ClassCarpentingTypeLoaderTests {
|
||||
val person = personType.make("Arthur Putey", 42, address, listOf(previousAddress))
|
||||
val personJson = ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(person)
|
||||
.replace("\r\n", "\n")
|
||||
|
||||
|
||||
assertEquals("""
|
||||
{
|
||||
"name" : "Arthur Putey",
|
||||
|
@ -59,9 +59,9 @@ class LocalTypeModelTests {
|
||||
assertInformation<Nested>("""
|
||||
Nested(collectionHolder: StringKeyedCollectionHolder<Integer>?, intArray: int[], optionalParam: Short?)
|
||||
collectionHolder (optional): StringKeyedCollectionHolder<Integer>(list: List<Integer>, map: Map<String, Integer>, array: List<Integer>[]): CollectionHolder<String, Integer>
|
||||
array: List<Integer>[]
|
||||
list: List<Integer>
|
||||
map: Map<String, Integer>
|
||||
array: List<Integer>[]
|
||||
list: List<Integer>
|
||||
map: Map<String, Integer>
|
||||
intArray: int[]
|
||||
""")
|
||||
|
||||
|
@ -43,6 +43,19 @@ class TypeIdentifierTests {
|
||||
TypeIdentifier.forGenericType(fieldType, HasStringArray::class.java).prettyPrint())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `roundtrip`() {
|
||||
assertRoundtrips(Int::class.javaPrimitiveType!!)
|
||||
assertRoundtrips<Int>()
|
||||
assertRoundtrips<IntArray>()
|
||||
assertRoundtrips(List::class.java)
|
||||
assertRoundtrips<List<String>>()
|
||||
assertRoundtrips<Array<List<String>>>()
|
||||
assertRoundtrips<HasStringArray>()
|
||||
assertRoundtrips(HasArray::class.java)
|
||||
assertRoundtrips<HasArray<String>>()
|
||||
}
|
||||
|
||||
private fun assertIdentified(type: Type, expected: String) =
|
||||
assertEquals(expected, TypeIdentifier.forGenericType(type).prettyPrint())
|
||||
|
||||
@ -50,4 +63,12 @@ class TypeIdentifierTests {
|
||||
assertEquals(expected, TypeIdentifier.forGenericType(typeOf<T>()).prettyPrint())
|
||||
|
||||
private inline fun <reified T> typeOf() = object : TypeToken<T>() {}.type
|
||||
|
||||
private inline fun <reified T> assertRoundtrips() = assertRoundtrips(typeOf<T>())
|
||||
|
||||
private fun assertRoundtrips(original: Type) {
|
||||
val identifier = TypeIdentifier.forGenericType(original)
|
||||
val localType = identifier.getLocalType(classLoader = ClassLoader.getSystemClassLoader())
|
||||
assertIdentified(localType, identifier.prettyPrint())
|
||||
}
|
||||
}
|
@ -57,7 +57,7 @@ class InteractiveShellTest {
|
||||
}, input, FlowA::class.java, om)
|
||||
assertEquals(expected, output!!, input)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun flowStartSimple() {
|
||||
check("a: Hi there", "Hi there")
|
||||
|
Loading…
Reference in New Issue
Block a user