diff --git a/serialization-deterministic/build.gradle b/serialization-deterministic/build.gradle index 497745157b..229b2e179a 100644 --- a/serialization-deterministic/build.gradle +++ b/serialization-deterministic/build.gradle @@ -22,12 +22,12 @@ dependencies { // Configure these by hand. It should be a minimal subset of dependencies, // and without any obviously non-deterministic ones such as Hibernate. - // This dependency will become "compile" scoped in our published POM. + // These dependencies will become "compile" scoped in our published POM. // See publish.dependenciesFrom.defaultScope. deterministicLibraries project(path: ':core-deterministic', configuration: 'deterministicArtifacts') + deterministicLibraries "org.apache.qpid:proton-j:$protonj_version" // These "implementation" dependencies will become "runtime" scoped in our published POM. - implementation "org.apache.qpid:proton-j:$protonj_version" implementation "org.iq80.snappy:snappy:$snappy_version" implementation "com.google.guava:guava:$guava_version" } diff --git a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt index 2b1d66ab76..19a59bc9ba 100644 --- a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt +++ b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt @@ -13,11 +13,16 @@ import net.corda.serialization.internal.carpenter.Schema @Suppress("UNUSED") fun createSerializerFactoryFactory(): SerializerFactoryFactory = DeterministicSerializerFactoryFactory() +/** + * Creates a [ClassCarpenter] suitable for the DJVM, i.e. one that doesn't work. + */ +fun createClassCarpenter(context: SerializationContext): ClassCarpenter = DummyClassCarpenter(context.whitelist, context.deserializationClassLoader) + private class DeterministicSerializerFactoryFactory : SerializerFactoryFactory { override fun make(context: SerializationContext) = SerializerFactoryBuilder.build( whitelist = context.whitelist, - classCarpenter = DummyClassCarpenter(context.whitelist, context.deserializationClassLoader)) + classCarpenter = createClassCarpenter(context)) } private class DummyClassCarpenter( diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt index 878a294695..071dbf74b7 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt @@ -3,14 +3,21 @@ package net.corda.serialization.internal.amqp import net.corda.core.serialization.SerializationContext +import net.corda.serialization.internal.carpenter.ClassCarpenter import net.corda.serialization.internal.carpenter.ClassCarpenterImpl fun createSerializerFactoryFactory(): SerializerFactoryFactory = SerializerFactoryFactoryImpl() +fun createClassCarpenter(context: SerializationContext): ClassCarpenter = ClassCarpenterImpl( + whitelist = context.whitelist, + cl = context.deserializationClassLoader, + lenient = context.lenientCarpenterEnabled +) + open class SerializerFactoryFactoryImpl : SerializerFactoryFactory { override fun make(context: SerializationContext): SerializerFactory { return SerializerFactoryBuilder.build(context.whitelist, - ClassCarpenterImpl(context.whitelist, context.deserializationClassLoader, context.lenientCarpenterEnabled), + createClassCarpenter(context), mustPreserveDataWhenEvolving = context.preventDataLoss ) } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPTypeIdentifiers.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPTypeIdentifiers.kt index e9d99cfe8f..fbbbaddde5 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPTypeIdentifiers.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPTypeIdentifiers.kt @@ -29,7 +29,7 @@ object AMQPTypeIdentifiers { Float::class to "float", Double::class to "double", Decimal32::class to "decimal32", - Decimal64::class to "decimal62", + Decimal64::class to "decimal64", Decimal128::class to "decimal128", Date::class to "timestamp", UUID::class to "uuid", @@ -62,4 +62,4 @@ object AMQPTypeIdentifiers { private val primitiveByteArrayType = TypeIdentifier.ArrayOf(TypeIdentifier.forClass(Byte::class.javaPrimitiveType!!)) fun nameForType(type: Type): String = nameForType(TypeIdentifier.forGenericType(type)) -} \ No newline at end of file +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt index 3e2266f63a..63b6ce73b7 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt @@ -88,7 +88,9 @@ open class ArraySerializer(override val type: Type, factory: LocalSerializerFact context: SerializationContext ): Any { if (obj is List<*>) { - return obj.map { input.readObjectOrNull(it, schemas, elementType, context) }.toArrayOfType(elementType) + return obj.map { + input.readObjectOrNull(redescribe(it, elementType), schemas, elementType, context) + }.toArrayOfType(elementType) } else throw AMQPNotSerializableException(type, "Expected a List but found $obj") } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ComposableTypePropertySerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ComposableTypePropertySerializer.kt index aa9764fd1e..254dc1f422 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ComposableTypePropertySerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ComposableTypePropertySerializer.kt @@ -1,6 +1,7 @@ package net.corda.serialization.internal.amqp import net.corda.core.serialization.SerializationContext +import net.corda.serialization.internal.amqp.AMQPTypeIdentifiers.isPrimitive import net.corda.serialization.internal.model.* import org.apache.qpid.proton.amqp.Binary import org.apache.qpid.proton.codec.Data @@ -18,7 +19,7 @@ interface PropertyReadStrategy { * 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)) { + if (isPrimitive(typeIdentifier)) { when (typeIdentifier) { in characterTypes -> AMQPCharPropertyReadStrategy else -> AMQPPropertyReadStrategy @@ -47,7 +48,7 @@ interface PropertyWriteStrategy { fun make(name: String, propertyInformation: LocalPropertyInformation, factory: LocalSerializerFactory): PropertyWriteStrategy { val reader = PropertyReader.make(propertyInformation) val type = propertyInformation.type - return if (AMQPTypeIdentifiers.isPrimitive(type.typeIdentifier)) { + return if (isPrimitive(type.typeIdentifier)) { when (type.typeIdentifier) { in characterTypes -> AMQPCharPropertyWriteStategy(reader) else -> AMQPPropertyWriteStrategy(reader) @@ -199,7 +200,7 @@ class DescribedTypeReadStrategy(name: String, override fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any? = ifThrowsAppend({ nameForDebug }) { - input.readObjectOrNull(obj, schemas, type, context) + input.readObjectOrNull(redescribe(obj, type), schemas, type, context) } } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt index 0be0de6c23..471546369e 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt @@ -29,6 +29,11 @@ abstract class CustomSerializer : AMQPSerializer, SerializerFor { */ open val additionalSerializers: Iterable> = emptyList() + /** + * This custom serializer is also allowed to deserialize these classes. This allows us + * to deserialize objects into completely different types, e.g. `A` -> `sandbox.A`. + */ + open val deserializationAliases: Set> = emptySet() protected abstract val descriptor: Descriptor /** @@ -53,6 +58,14 @@ abstract class CustomSerializer : AMQPSerializer, SerializerFor { abstract fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) + /** + * [CustomSerializerRegistry.findCustomSerializer] will invoke this method on the [CustomSerializer] + * that it selects to give that serializer an opportunity to customise its behaviour. The serializer + * can also return `null` here, in which case [CustomSerializerRegistry] will proceed as if no + * serializer is available for [declaredType]. + */ + open fun specialiseFor(declaredType: Type): AMQPSerializer? = this + /** * This custom serializer represents a sort of symbolic link from a subclass to a super class, where the super * class custom serializer is responsible for the "on the wire" format but we want to create a reference to the @@ -110,7 +123,7 @@ abstract class CustomSerializer : AMQPSerializer, SerializerFor { */ abstract class CustomSerializerImp(protected val clazz: Class, protected val withInheritance: Boolean) : CustomSerializer() { override val type: Type get() = clazz - override val typeDescriptor: Symbol = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${AMQPTypeIdentifiers.nameForType(clazz)}") + override val typeDescriptor: Symbol = typeDescriptorFor(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 @@ -119,11 +132,13 @@ abstract class CustomSerializer : AMQPSerializer, SerializerFor { /** * Additional base features for a custom serializer for a particular class, that excludes subclasses. */ + @KeepForDJVM abstract class Is(clazz: Class) : CustomSerializerImp(clazz, false) /** * Additional base features for a custom serializer for all implementations of a particular interface or super class. */ + @KeepForDJVM abstract class Implements(clazz: Class) : CustomSerializerImp(clazz, true) /** @@ -133,6 +148,7 @@ abstract class CustomSerializer : AMQPSerializer, SerializerFor { * The proxy class must use only types which are either native AMQP or other types for which there are pre-registered * custom serializers. */ + @KeepForDJVM abstract class Proxy(clazz: Class, protected val proxyClass: Class

, protected val factory: LocalSerializerFactory, @@ -191,6 +207,7 @@ abstract class CustomSerializer : AMQPSerializer, SerializerFor { * @param maker A lambda for constructing an instance, that defaults to calling a constructor that expects a string. * @param unmaker A lambda that extracts the string value for an instance, that defaults to the [toString] method. */ + @KeepForDJVM abstract class ToString(clazz: Class, withInheritance: Boolean = false, private val maker: (String) -> T = clazz.getConstructor(String::class.java).let { `constructor` -> { string -> `constructor`.newInstance(string) } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializerRegistry.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializerRegistry.kt index 2081a8162f..d0e8700e08 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializerRegistry.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializerRegistry.kt @@ -44,7 +44,7 @@ interface CustomSerializerRegistry { * * @param clazz The actual class to look for a custom serializer for. * @param declaredType The declared type to look for a custom serializer for. - * @return The custom serializer handing the class, if found, or `null`. + * @return The custom serializer handling the class, if found, or `null`. * * @throws IllegalCustomSerializerException If a custom serializer identifies itself as the serializer for * a class annotated with [CordaSerializable], since all such classes should be serializable via standard object @@ -57,8 +57,10 @@ interface CustomSerializerRegistry { } class CachingCustomSerializerRegistry( - private val descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry) - : CustomSerializerRegistry { + private val descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry, + private val allowedFor: Set> +) : CustomSerializerRegistry { + constructor(descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry) : this(descriptorBasedSerializerRegistry, emptySet()) companion object { val logger = contextLogger() @@ -84,7 +86,7 @@ class CachingCustomSerializerRegistry( } private val customSerializersCache: MutableMap = DefaultCacheProvider.createCache() - private var customSerializers: List = emptyList() + private val customSerializers: MutableList = mutableListOf() /** * Register a custom serializer for any type that cannot be serialized or deserialized by the default serializer @@ -93,7 +95,7 @@ class CachingCustomSerializerRegistry( override fun register(customSerializer: CustomSerializer) { logger.trace("action=\"Registering custom serializer\", class=\"${customSerializer.type}\"") - if (!customSerializersCache.isEmpty()) { + if (customSerializersCache.isNotEmpty()) { logger.warn("Attempting to register custom serializer $customSerializer.type} in an active cache." + "All serializers should be registered before the cache comes into use.") } @@ -103,14 +105,23 @@ class CachingCustomSerializerRegistry( for (additional in customSerializer.additionalSerializers) { register(additional) } + + for (alias in customSerializer.deserializationAliases) { + val aliasDescriptor = typeDescriptorFor(alias) + if (aliasDescriptor != customSerializer.typeDescriptor) { + descriptorBasedSerializerRegistry[aliasDescriptor.toString()] = customSerializer + } + } + customSerializer } + } override fun registerExternal(customSerializer: CorDappCustomSerializer) { logger.trace("action=\"Registering external serializer\", class=\"${customSerializer.type}\"") - if (!customSerializersCache.isEmpty()) { + if (customSerializersCache.isNotEmpty()) { logger.warn("Attempting to register custom serializer ${customSerializer.type} in an active cache." + "All serializers must be registered before the cache comes into use.") } @@ -120,7 +131,7 @@ class CachingCustomSerializerRegistry( customSerializer } } - + override fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer? { val typeIdentifier = CustomSerializerIdentifier( TypeIdentifier.forClass(clazz), @@ -164,13 +175,21 @@ class CachingCustomSerializerRegistry( throw IllegalCustomSerializerException(declaredSerializers.first(), clazz) } - return declaredSerializers.first() + return declaredSerializers.first().let { + if (it is CustomSerializer) { + it.specialiseFor(declaredType) + } else { + it + } + } } private val Class<*>.isCustomSerializationForbidden: Boolean get() = when { AMQPTypeIdentifiers.isPrimitive(this) -> true isSubClassOf(CordaThrowable::class.java) -> false + allowedFor.any { it.isAssignableFrom(this) } -> false isAnnotationPresent(CordaSerializable::class.java) -> true else -> false } -} \ No newline at end of file +} + diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt index 503d370fd6..c21050bc8d 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt @@ -142,12 +142,12 @@ class DeserializationInput constructor( envelope) } - internal fun readObjectOrNull(obj: Any?, schema: SerializationSchemas, type: Type, context: SerializationContext + fun readObjectOrNull(obj: Any?, schema: SerializationSchemas, type: Type, context: SerializationContext ): Any? { return if (obj == null) null else readObject(obj, schema, type, context) } - internal fun readObject(obj: Any, schemas: SerializationSchemas, type: Type, context: SerializationContext): Any = + fun readObject(obj: Any, schemas: SerializationSchemas, type: Type, context: SerializationContext): Any = if (obj is DescribedType && ReferencedObject.DESCRIPTOR == obj.descriptor) { // It must be a reference to an instance that has already been read, cheaply and quickly returning it by reference. val objectIndex = (obj.described as UnsignedInteger).toInt() diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerFactory.kt index 8821860355..4755289cf6 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerFactory.kt @@ -17,6 +17,13 @@ interface EvolutionSerializerFactory { fun getEvolutionSerializer( remote: RemoteTypeInformation, local: LocalTypeInformation): AMQPSerializer? + + /** + * A mapping between Java object types and their equivalent Java primitive types. + * Predominantly for the sake of the DJVM sandbox where e.g. `char` will map to + * sandbox.java.lang.Character instead of java.lang.Character. + */ + val primitiveTypes: Map, Class<*>> } class EvolutionSerializationException(remoteTypeInformation: RemoteTypeInformation, reason: String) @@ -32,7 +39,9 @@ class EvolutionSerializationException(remoteTypeInformation: RemoteTypeInformati class DefaultEvolutionSerializerFactory( private val localSerializerFactory: LocalSerializerFactory, private val classLoader: ClassLoader, - private val mustPreserveDataWhenEvolving: Boolean): EvolutionSerializerFactory { + private val mustPreserveDataWhenEvolving: Boolean, + override val primitiveTypes: Map, Class<*>> +): EvolutionSerializerFactory { override fun getEvolutionSerializer(remote: RemoteTypeInformation, local: LocalTypeInformation): AMQPSerializer? = @@ -77,7 +86,7 @@ class DefaultEvolutionSerializerFactory( val localClass = localProperty.type.observedType.asClass() val remoteClass = remoteProperty.type.typeIdentifier.getLocalType(classLoader).asClass() - if (!localClass.isAssignableFrom(remoteClass) && remoteClass != localClass.kotlin.javaPrimitiveType) { + if (!localClass.isAssignableFrom(remoteClass) && remoteClass != primitiveTypes[localClass]) { throw EvolutionSerializationException(this, "Local type $localClass of property $name is not assignable from remote type $remoteClass") } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/LocalSerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/LocalSerializerFactory.kt index fe34660a68..bfd8863441 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/LocalSerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/LocalSerializerFactory.kt @@ -6,10 +6,13 @@ 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 net.corda.serialization.internal.model.TypeIdentifier.* +import net.corda.serialization.internal.model.TypeIdentifier.Companion.classLoaderFor import org.apache.qpid.proton.amqp.Symbol import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import java.util.* +import java.util.function.Function import javax.annotation.concurrent.ThreadSafe /** @@ -87,6 +90,7 @@ class DefaultLocalSerializerFactory( private val fingerPrinter: FingerPrinter, override val classloader: ClassLoader, private val descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry, + private val primitiveSerializerFactory: Function, AMQPSerializer>, private val customSerializerRegistry: CustomSerializerRegistry, private val onlyCustomSerializers: Boolean) : LocalSerializerFactory { @@ -137,9 +141,18 @@ class DefaultLocalSerializerFactory( serializersByTypeId.getOrPut(localTypeInformation.typeIdentifier) { val declaredClass = declaredType.asClass() + // Any Custom Serializer cached for a ParameterizedType can only be + // found by searching for that exact same type. Searching for its raw + // class will not work! + val declaredGenericType = if (declaredType !is ParameterizedType && localTypeInformation.typeIdentifier is Parameterised) { + localTypeInformation.typeIdentifier.getLocalType(classLoaderFor(declaredClass)) + } else { + declaredType + } + // can be useful to enable but will be *extremely* chatty if you do - logger.trace { "Get Serializer for $declaredClass ${declaredType.typeName}" } - customSerializerRegistry.findCustomSerializer(declaredClass, declaredType)?.apply { return@get this } + logger.trace { "Get Serializer for $declaredClass ${declaredGenericType.typeName}" } + customSerializerRegistry.findCustomSerializer(declaredClass, declaredGenericType)?.apply { return@get this } return when (localTypeInformation) { is LocalTypeInformation.ACollection -> makeDeclaredCollection(localTypeInformation) @@ -226,7 +239,7 @@ class DefaultLocalSerializerFactory( throw AMQPNotSerializableException( type, "Serializer does not support synthetic classes") - AMQPTypeIdentifiers.isPrimitive(typeInformation.typeIdentifier) -> AMQPPrimitiveSerializer(clazz) + AMQPTypeIdentifiers.isPrimitive(typeInformation.typeIdentifier) -> primitiveSerializerFactory.apply(clazz) else -> makeNonCustomSerializer(type, typeInformation, clazz) } } @@ -250,4 +263,4 @@ class DefaultLocalSerializerFactory( } } -} \ No newline at end of file +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/RemoteSerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/RemoteSerializerFactory.kt index e7b00f618a..6c057bfd71 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/RemoteSerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/RemoteSerializerFactory.kt @@ -3,7 +3,6 @@ package net.corda.serialization.internal.amqp import net.corda.core.serialization.SerializationContext import net.corda.core.utilities.contextLogger import net.corda.serialization.internal.model.* -import org.hibernate.type.descriptor.java.ByteTypeDescriptor import java.io.NotSerializableException /** diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Schema.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Schema.kt index f1bb00f7c0..36ac18bfe6 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Schema.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Schema.kt @@ -3,18 +3,46 @@ package net.corda.serialization.internal.amqp import net.corda.core.KeepForDJVM import net.corda.core.internal.uncheckedCast import net.corda.serialization.internal.CordaSerializationMagic -import org.apache.qpid.proton.amqp.DescribedType -import org.apache.qpid.proton.amqp.Symbol -import org.apache.qpid.proton.amqp.UnsignedInteger -import org.apache.qpid.proton.amqp.UnsignedLong +import net.corda.serialization.internal.amqp.AMQPTypeIdentifiers.isPrimitive +import net.corda.serialization.internal.model.TypeIdentifier +import net.corda.serialization.internal.model.TypeIdentifier.TopType +import net.corda.serialization.internal.model.TypeIdentifier.Companion.forGenericType +import org.apache.qpid.proton.amqp.* import org.apache.qpid.proton.codec.DescribedTypeConstructor import java.io.NotSerializableException -import net.corda.serialization.internal.carpenter.Field as CarpenterField -import net.corda.serialization.internal.carpenter.Schema as CarpenterSchema +import java.lang.reflect.Type const val DESCRIPTOR_DOMAIN: String = "net.corda" val amqpMagic = CordaSerializationMagic("corda".toByteArray() + byteArrayOf(1, 0)) +fun typeDescriptorFor(typeId: TypeIdentifier): Symbol = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${AMQPTypeIdentifiers.nameForType(typeId)}") +fun typeDescriptorFor(type: Type): Symbol = typeDescriptorFor(forGenericType(type)) + +/** + * Repackages a naked, non-primitive [obj] as a [DescribedType]. If [obj] is primitive, [Binary] or already + * an instance of [DescribedType]] then it is returned unchanged. This allows Corda to search for a serializer + * capable of handling instances of [type]. + */ +fun redescribe(obj: Any?, type: Type): Any? { + return if (obj == null || obj is DescribedType || obj is Binary || forGenericType(type).run { isPrimitive(this) || this == TopType }) { + obj + } else { + /** + * This must be a primitive [obj] that has a non-primitive [type]. + * Rewrap it with the required descriptor for further deserialization. + */ + RedescribedType(typeDescriptorFor(type), obj) + } +} + +private class RedescribedType( + private val descriptor: Symbol, + private val described: Any? +) : DescribedType { + override fun getDescriptor(): Symbol = descriptor + override fun getDescribed(): Any? = described +} + /** * This and the classes below are OO representations of the AMQP XML schema described in the specification. Their * [toString] representations generate the associated XML form. diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt index 7c5ec79c49..a6469b443c 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt @@ -7,9 +7,28 @@ import net.corda.serialization.internal.carpenter.ClassCarpenter import net.corda.serialization.internal.carpenter.ClassCarpenterImpl import net.corda.serialization.internal.model.* import java.io.NotSerializableException +import java.util.Collections.unmodifiableMap +import java.util.function.Function @KeepForDJVM object SerializerFactoryBuilder { + /** + * The standard mapping of Java object types to Java primitive types. + * The DJVM will need to override these, but probably not anyone else. + */ + @Suppress("unchecked_cast") + private val javaPrimitiveTypes: Map, Class<*>> = unmodifiableMap(listOf( + Boolean::class, + Byte::class, + Char::class, + Double::class, + Float::class, + Int::class, + Long::class, + Short::class + ).associate { + klazz -> klazz.javaObjectType to klazz.javaPrimitiveType + }) as Map, Class<*>> @JvmStatic fun build(whitelist: ClassWhitelist, classCarpenter: ClassCarpenter): SerializerFactory { @@ -89,17 +108,19 @@ object SerializerFactoryBuilder { fingerPrinter, classCarpenter.classloader, descriptorBasedSerializerRegistry, + Function { clazz -> AMQPPrimitiveSerializer(clazz) }, customSerializerRegistry, onlyCustomSerializers) - val typeLoader = ClassCarpentingTypeLoader( + val typeLoader: TypeLoader = ClassCarpentingTypeLoader( SchemaBuildingRemoteTypeCarpenter(classCarpenter), classCarpenter.classloader) val evolutionSerializerFactory = if (allowEvolution) DefaultEvolutionSerializerFactory( localSerializerFactory, classCarpenter.classloader, - mustPreserveDataWhenEvolving + mustPreserveDataWhenEvolving, + javaPrimitiveTypes ) else NoEvolutionSerializerFactory val remoteSerializerFactory = DefaultRemoteSerializerFactory( @@ -116,15 +137,17 @@ object SerializerFactoryBuilder { } object NoEvolutionSerializerFactory : EvolutionSerializerFactory { - override fun getEvolutionSerializer(remoteTypeInformation: RemoteTypeInformation, localTypeInformation: LocalTypeInformation): AMQPSerializer { + override fun getEvolutionSerializer(remote: RemoteTypeInformation, local: LocalTypeInformation): AMQPSerializer { throw NotSerializableException(""" Evolution not permitted. Remote: -${remoteTypeInformation.prettyPrint(false)} +${remote.prettyPrint(false)} Local: -${localTypeInformation.prettyPrint(false)} +${local.prettyPrint(false)} """) } + + override val primitiveTypes: Map, Class<*>> = emptyMap() } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeModel.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeModel.kt index 45bdc8794f..fb801c52d7 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeModel.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeModel.kt @@ -1,7 +1,5 @@ package net.corda.serialization.internal.model -import net.corda.core.serialization.ClassWhitelist -import net.corda.serialization.internal.amqp.* import java.lang.reflect.* /** @@ -54,7 +52,7 @@ class ConfigurableLocalTypeModel(private val typeModelConfiguration: LocalTypeMo private val typeInformationCache = DefaultCacheProvider.createCache() /** - * We need to provide the [TypeInformationBuilder] with a temporary local cache, so that it doesn't leak + * We need to provide the [LocalTypeInformationBuilder] with a temporary local cache, so that it doesn't leak * [LocalTypeInformation] with unpatched cycles into the global cache where other threads can access them * before we've patched the cycles up. */ diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt index 43f0329ab9..2697b107a8 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt @@ -63,6 +63,8 @@ sealed class TypeIdentifier { // This method has locking. So we memo the value here. private val systemClassLoader: ClassLoader = ClassLoader.getSystemClassLoader() + fun classLoaderFor(clazz: Class<*>): ClassLoader = clazz.classLoader ?: systemClassLoader + /** * Obtain the [TypeIdentifier] for an erased Java class. * @@ -206,7 +208,11 @@ sealed class TypeIdentifier { override fun toString() = "Parameterised(${prettyPrint()})" override fun getLocalType(classLoader: ClassLoader): Type { - val rawType = Class.forName(name, false, classLoader) + // We need to invoke ClassLoader.loadClass() directly, because + // the JVM will complain if Class.forName() returns a class + // that has a name other than the requested one. This will happen + // for "transformative" class loaders, i.e. `A` -> `sandbox.A`. + val rawType = classLoader.loadClass(name) if (rawType.typeParameters.size != parameters.size) { throw IncompatibleTypeIdentifierException( "Class $rawType expects ${rawType.typeParameters.size} type arguments, " + diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeModellingFingerPrinter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeModellingFingerPrinter.kt index b0314bbc2a..b0ac855d34 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeModellingFingerPrinter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeModellingFingerPrinter.kt @@ -4,7 +4,9 @@ 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 +import net.corda.serialization.internal.model.TypeIdentifier.* +import net.corda.serialization.internal.model.TypeIdentifier.Companion.classLoaderFor +import java.lang.reflect.ParameterizedType /** * A fingerprinter that fingerprints [LocalTypeInformation]. @@ -34,11 +36,15 @@ class TypeModellingFingerPrinter( private val cache: MutableMap = DefaultCacheProvider.createCache() override fun fingerprint(typeInformation: LocalTypeInformation): String = - cache.computeIfAbsent(typeInformation.typeIdentifier) { - FingerPrintingState( - customTypeDescriptorLookup, - FingerprintWriter(debugEnabled)).fingerprint(typeInformation) - } + /* + * We cannot use ConcurrentMap.computeIfAbsent() here because it requires + * that the map not be re-entered during the computation function. And + * the Fingerprinter cannot guarantee that. + */ + cache.getOrPut(typeInformation.typeIdentifier) { + FingerPrintingState(customTypeDescriptorLookup, FingerprintWriter(debugEnabled)) + .fingerprint(typeInformation) + } } /** @@ -224,7 +230,22 @@ private class FingerPrintingState( // 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() + val observedType = type.observedType + val observedClass = observedType.asClass() + + // Any Custom Serializer cached for a ParameterizedType can only be + // found by searching for that exact same type. Searching for its raw + // class will not work! + val observedGenericType = if (observedType !is ParameterizedType && type.typeIdentifier is Parameterised) { + type.typeIdentifier.getLocalType(classLoaderFor(observedClass)) + } else { + observedType + } + + val customTypeDescriptor = customSerializerRegistry.findCustomSerializer( + clazz = observedClass, + declaredType = observedGenericType + )?.typeDescriptor?.toString() if (customTypeDescriptor != null) writer.write(customTypeDescriptor) else defaultAction() }