mirror of
https://github.com/corda/corda.git
synced 2025-06-10 11:21:45 +00:00
CORDA-3157: Modify Corda's custom serialiser support for the DJVM.
Modify Corda's custom serialiser support for the DJVM.
This commit is contained in:
commit
f96105a014
@ -22,12 +22,12 @@ dependencies {
|
|||||||
// Configure these by hand. It should be a minimal subset of dependencies,
|
// Configure these by hand. It should be a minimal subset of dependencies,
|
||||||
// and without any obviously non-deterministic ones such as Hibernate.
|
// 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.
|
// See publish.dependenciesFrom.defaultScope.
|
||||||
deterministicLibraries project(path: ':core-deterministic', configuration: 'deterministicArtifacts')
|
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.
|
// 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 "org.iq80.snappy:snappy:$snappy_version"
|
||||||
implementation "com.google.guava:guava:$guava_version"
|
implementation "com.google.guava:guava:$guava_version"
|
||||||
}
|
}
|
||||||
|
@ -13,11 +13,16 @@ import net.corda.serialization.internal.carpenter.Schema
|
|||||||
@Suppress("UNUSED")
|
@Suppress("UNUSED")
|
||||||
fun createSerializerFactoryFactory(): SerializerFactoryFactory = DeterministicSerializerFactoryFactory()
|
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 {
|
private class DeterministicSerializerFactoryFactory : SerializerFactoryFactory {
|
||||||
override fun make(context: SerializationContext) =
|
override fun make(context: SerializationContext) =
|
||||||
SerializerFactoryBuilder.build(
|
SerializerFactoryBuilder.build(
|
||||||
whitelist = context.whitelist,
|
whitelist = context.whitelist,
|
||||||
classCarpenter = DummyClassCarpenter(context.whitelist, context.deserializationClassLoader))
|
classCarpenter = createClassCarpenter(context))
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DummyClassCarpenter(
|
private class DummyClassCarpenter(
|
||||||
|
@ -3,14 +3,21 @@
|
|||||||
package net.corda.serialization.internal.amqp
|
package net.corda.serialization.internal.amqp
|
||||||
|
|
||||||
import net.corda.core.serialization.SerializationContext
|
import net.corda.core.serialization.SerializationContext
|
||||||
|
import net.corda.serialization.internal.carpenter.ClassCarpenter
|
||||||
import net.corda.serialization.internal.carpenter.ClassCarpenterImpl
|
import net.corda.serialization.internal.carpenter.ClassCarpenterImpl
|
||||||
|
|
||||||
fun createSerializerFactoryFactory(): SerializerFactoryFactory = SerializerFactoryFactoryImpl()
|
fun createSerializerFactoryFactory(): SerializerFactoryFactory = SerializerFactoryFactoryImpl()
|
||||||
|
|
||||||
|
fun createClassCarpenter(context: SerializationContext): ClassCarpenter = ClassCarpenterImpl(
|
||||||
|
whitelist = context.whitelist,
|
||||||
|
cl = context.deserializationClassLoader,
|
||||||
|
lenient = context.lenientCarpenterEnabled
|
||||||
|
)
|
||||||
|
|
||||||
open class SerializerFactoryFactoryImpl : SerializerFactoryFactory {
|
open class SerializerFactoryFactoryImpl : SerializerFactoryFactory {
|
||||||
override fun make(context: SerializationContext): SerializerFactory {
|
override fun make(context: SerializationContext): SerializerFactory {
|
||||||
return SerializerFactoryBuilder.build(context.whitelist,
|
return SerializerFactoryBuilder.build(context.whitelist,
|
||||||
ClassCarpenterImpl(context.whitelist, context.deserializationClassLoader, context.lenientCarpenterEnabled),
|
createClassCarpenter(context),
|
||||||
mustPreserveDataWhenEvolving = context.preventDataLoss
|
mustPreserveDataWhenEvolving = context.preventDataLoss
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ object AMQPTypeIdentifiers {
|
|||||||
Float::class to "float",
|
Float::class to "float",
|
||||||
Double::class to "double",
|
Double::class to "double",
|
||||||
Decimal32::class to "decimal32",
|
Decimal32::class to "decimal32",
|
||||||
Decimal64::class to "decimal62",
|
Decimal64::class to "decimal64",
|
||||||
Decimal128::class to "decimal128",
|
Decimal128::class to "decimal128",
|
||||||
Date::class to "timestamp",
|
Date::class to "timestamp",
|
||||||
UUID::class to "uuid",
|
UUID::class to "uuid",
|
||||||
|
@ -88,7 +88,9 @@ open class ArraySerializer(override val type: Type, factory: LocalSerializerFact
|
|||||||
context: SerializationContext
|
context: SerializationContext
|
||||||
): Any {
|
): Any {
|
||||||
if (obj is List<*>) {
|
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")
|
} else throw AMQPNotSerializableException(type, "Expected a List but found $obj")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.corda.serialization.internal.amqp
|
package net.corda.serialization.internal.amqp
|
||||||
|
|
||||||
import net.corda.core.serialization.SerializationContext
|
import net.corda.core.serialization.SerializationContext
|
||||||
|
import net.corda.serialization.internal.amqp.AMQPTypeIdentifiers.isPrimitive
|
||||||
import net.corda.serialization.internal.model.*
|
import net.corda.serialization.internal.model.*
|
||||||
import org.apache.qpid.proton.amqp.Binary
|
import org.apache.qpid.proton.amqp.Binary
|
||||||
import org.apache.qpid.proton.codec.Data
|
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.
|
* Select the correct strategy for reading properties, based on the property type.
|
||||||
*/
|
*/
|
||||||
fun make(name: String, typeIdentifier: TypeIdentifier, type: Type): PropertyReadStrategy =
|
fun make(name: String, typeIdentifier: TypeIdentifier, type: Type): PropertyReadStrategy =
|
||||||
if (AMQPTypeIdentifiers.isPrimitive(typeIdentifier)) {
|
if (isPrimitive(typeIdentifier)) {
|
||||||
when (typeIdentifier) {
|
when (typeIdentifier) {
|
||||||
in characterTypes -> AMQPCharPropertyReadStrategy
|
in characterTypes -> AMQPCharPropertyReadStrategy
|
||||||
else -> AMQPPropertyReadStrategy
|
else -> AMQPPropertyReadStrategy
|
||||||
@ -47,7 +48,7 @@ interface PropertyWriteStrategy {
|
|||||||
fun make(name: String, propertyInformation: LocalPropertyInformation, factory: LocalSerializerFactory): PropertyWriteStrategy {
|
fun make(name: String, propertyInformation: LocalPropertyInformation, factory: LocalSerializerFactory): PropertyWriteStrategy {
|
||||||
val reader = PropertyReader.make(propertyInformation)
|
val reader = PropertyReader.make(propertyInformation)
|
||||||
val type = propertyInformation.type
|
val type = propertyInformation.type
|
||||||
return if (AMQPTypeIdentifiers.isPrimitive(type.typeIdentifier)) {
|
return if (isPrimitive(type.typeIdentifier)) {
|
||||||
when (type.typeIdentifier) {
|
when (type.typeIdentifier) {
|
||||||
in characterTypes -> AMQPCharPropertyWriteStategy(reader)
|
in characterTypes -> AMQPCharPropertyWriteStategy(reader)
|
||||||
else -> AMQPPropertyWriteStrategy(reader)
|
else -> AMQPPropertyWriteStrategy(reader)
|
||||||
@ -199,7 +200,7 @@ class DescribedTypeReadStrategy(name: String,
|
|||||||
|
|
||||||
override fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any? =
|
override fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any? =
|
||||||
ifThrowsAppend({ nameForDebug }) {
|
ifThrowsAppend({ nameForDebug }) {
|
||||||
input.readObjectOrNull(obj, schemas, type, context)
|
input.readObjectOrNull(redescribe(obj, type), schemas, type, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +29,11 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
|||||||
*/
|
*/
|
||||||
open val additionalSerializers: Iterable<CustomSerializer<out Any>> = emptyList()
|
open val additionalSerializers: Iterable<CustomSerializer<out Any>> = 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<Class<*>> = emptySet()
|
||||||
|
|
||||||
protected abstract val descriptor: Descriptor
|
protected abstract val descriptor: Descriptor
|
||||||
/**
|
/**
|
||||||
@ -53,6 +58,14 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
|||||||
abstract fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput,
|
abstract fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput,
|
||||||
context: SerializationContext)
|
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<T>? = this
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This custom serializer represents a sort of symbolic link from a subclass to a super class, where the super
|
* 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
|
* 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<T : Any> : AMQPSerializer<T>, SerializerFor {
|
|||||||
*/
|
*/
|
||||||
abstract class CustomSerializerImp<T : Any>(protected val clazz: Class<T>, protected val withInheritance: Boolean) : CustomSerializer<T>() {
|
abstract class CustomSerializerImp<T : Any>(protected val clazz: Class<T>, protected val withInheritance: Boolean) : CustomSerializer<T>() {
|
||||||
override val type: Type get() = clazz
|
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 fun writeClassInfo(output: SerializationOutput) {}
|
||||||
override val descriptor: Descriptor = Descriptor(typeDescriptor)
|
override val descriptor: Descriptor = Descriptor(typeDescriptor)
|
||||||
override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz
|
override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz
|
||||||
@ -119,11 +132,13 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
|||||||
/**
|
/**
|
||||||
* Additional base features for a custom serializer for a particular class, that excludes subclasses.
|
* Additional base features for a custom serializer for a particular class, that excludes subclasses.
|
||||||
*/
|
*/
|
||||||
|
@KeepForDJVM
|
||||||
abstract class Is<T : Any>(clazz: Class<T>) : CustomSerializerImp<T>(clazz, false)
|
abstract class Is<T : Any>(clazz: Class<T>) : CustomSerializerImp<T>(clazz, false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Additional base features for a custom serializer for all implementations of a particular interface or super class.
|
* Additional base features for a custom serializer for all implementations of a particular interface or super class.
|
||||||
*/
|
*/
|
||||||
|
@KeepForDJVM
|
||||||
abstract class Implements<T : Any>(clazz: Class<T>) : CustomSerializerImp<T>(clazz, true)
|
abstract class Implements<T : Any>(clazz: Class<T>) : CustomSerializerImp<T>(clazz, true)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -133,6 +148,7 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
|||||||
* The proxy class must use only types which are either native AMQP or other types for which there are pre-registered
|
* The proxy class must use only types which are either native AMQP or other types for which there are pre-registered
|
||||||
* custom serializers.
|
* custom serializers.
|
||||||
*/
|
*/
|
||||||
|
@KeepForDJVM
|
||||||
abstract class Proxy<T : Any, P : Any>(clazz: Class<T>,
|
abstract class Proxy<T : Any, P : Any>(clazz: Class<T>,
|
||||||
protected val proxyClass: Class<P>,
|
protected val proxyClass: Class<P>,
|
||||||
protected val factory: LocalSerializerFactory,
|
protected val factory: LocalSerializerFactory,
|
||||||
@ -191,6 +207,7 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
|||||||
* @param maker A lambda for constructing an instance, that defaults to calling a constructor that expects a string.
|
* @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.
|
* @param unmaker A lambda that extracts the string value for an instance, that defaults to the [toString] method.
|
||||||
*/
|
*/
|
||||||
|
@KeepForDJVM
|
||||||
abstract class ToString<T : Any>(clazz: Class<T>, withInheritance: Boolean = false,
|
abstract class ToString<T : Any>(clazz: Class<T>, withInheritance: Boolean = false,
|
||||||
private val maker: (String) -> T = clazz.getConstructor(String::class.java).let { `constructor` ->
|
private val maker: (String) -> T = clazz.getConstructor(String::class.java).let { `constructor` ->
|
||||||
{ string -> `constructor`.newInstance(string) }
|
{ string -> `constructor`.newInstance(string) }
|
||||||
|
@ -44,7 +44,7 @@ interface CustomSerializerRegistry {
|
|||||||
*
|
*
|
||||||
* @param clazz The actual class to look for a custom serializer for.
|
* @param clazz The actual class to look for a custom serializer for.
|
||||||
* @param declaredType The declared type 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
|
* @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
|
* a class annotated with [CordaSerializable], since all such classes should be serializable via standard object
|
||||||
@ -57,8 +57,10 @@ interface CustomSerializerRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CachingCustomSerializerRegistry(
|
class CachingCustomSerializerRegistry(
|
||||||
private val descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry)
|
private val descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry,
|
||||||
: CustomSerializerRegistry {
|
private val allowedFor: Set<Class<*>>
|
||||||
|
) : CustomSerializerRegistry {
|
||||||
|
constructor(descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry) : this(descriptorBasedSerializerRegistry, emptySet())
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val logger = contextLogger()
|
val logger = contextLogger()
|
||||||
@ -84,7 +86,7 @@ class CachingCustomSerializerRegistry(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val customSerializersCache: MutableMap<CustomSerializerIdentifier, CustomSerializerLookupResult> = DefaultCacheProvider.createCache()
|
private val customSerializersCache: MutableMap<CustomSerializerIdentifier, CustomSerializerLookupResult> = DefaultCacheProvider.createCache()
|
||||||
private var customSerializers: List<SerializerFor> = emptyList()
|
private val customSerializers: MutableList<SerializerFor> = mutableListOf()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a custom serializer for any type that cannot be serialized or deserialized by the default serializer
|
* 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<out Any>) {
|
override fun register(customSerializer: CustomSerializer<out Any>) {
|
||||||
logger.trace("action=\"Registering custom serializer\", class=\"${customSerializer.type}\"")
|
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." +
|
logger.warn("Attempting to register custom serializer $customSerializer.type} in an active cache." +
|
||||||
"All serializers should be registered before the cache comes into use.")
|
"All serializers should be registered before the cache comes into use.")
|
||||||
}
|
}
|
||||||
@ -103,14 +105,23 @@ class CachingCustomSerializerRegistry(
|
|||||||
for (additional in customSerializer.additionalSerializers) {
|
for (additional in customSerializer.additionalSerializers) {
|
||||||
register(additional)
|
register(additional)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (alias in customSerializer.deserializationAliases) {
|
||||||
|
val aliasDescriptor = typeDescriptorFor(alias)
|
||||||
|
if (aliasDescriptor != customSerializer.typeDescriptor) {
|
||||||
|
descriptorBasedSerializerRegistry[aliasDescriptor.toString()] = customSerializer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
customSerializer
|
customSerializer
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun registerExternal(customSerializer: CorDappCustomSerializer) {
|
override fun registerExternal(customSerializer: CorDappCustomSerializer) {
|
||||||
logger.trace("action=\"Registering external serializer\", class=\"${customSerializer.type}\"")
|
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." +
|
logger.warn("Attempting to register custom serializer ${customSerializer.type} in an active cache." +
|
||||||
"All serializers must be registered before the cache comes into use.")
|
"All serializers must be registered before the cache comes into use.")
|
||||||
}
|
}
|
||||||
@ -164,13 +175,21 @@ class CachingCustomSerializerRegistry(
|
|||||||
throw IllegalCustomSerializerException(declaredSerializers.first(), clazz)
|
throw IllegalCustomSerializerException(declaredSerializers.first(), clazz)
|
||||||
}
|
}
|
||||||
|
|
||||||
return declaredSerializers.first()
|
return declaredSerializers.first().let {
|
||||||
|
if (it is CustomSerializer<Any>) {
|
||||||
|
it.specialiseFor(declaredType)
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val Class<*>.isCustomSerializationForbidden: Boolean get() = when {
|
private val Class<*>.isCustomSerializationForbidden: Boolean get() = when {
|
||||||
AMQPTypeIdentifiers.isPrimitive(this) -> true
|
AMQPTypeIdentifiers.isPrimitive(this) -> true
|
||||||
isSubClassOf(CordaThrowable::class.java) -> false
|
isSubClassOf(CordaThrowable::class.java) -> false
|
||||||
|
allowedFor.any { it.isAssignableFrom(this) } -> false
|
||||||
isAnnotationPresent(CordaSerializable::class.java) -> true
|
isAnnotationPresent(CordaSerializable::class.java) -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,12 +142,12 @@ class DeserializationInput constructor(
|
|||||||
envelope)
|
envelope)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun readObjectOrNull(obj: Any?, schema: SerializationSchemas, type: Type, context: SerializationContext
|
fun readObjectOrNull(obj: Any?, schema: SerializationSchemas, type: Type, context: SerializationContext
|
||||||
): Any? {
|
): Any? {
|
||||||
return if (obj == null) null else readObject(obj, schema, type, context)
|
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) {
|
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.
|
// 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()
|
val objectIndex = (obj.described as UnsignedInteger).toInt()
|
||||||
|
@ -17,6 +17,13 @@ interface EvolutionSerializerFactory {
|
|||||||
fun getEvolutionSerializer(
|
fun getEvolutionSerializer(
|
||||||
remote: RemoteTypeInformation,
|
remote: RemoteTypeInformation,
|
||||||
local: LocalTypeInformation): AMQPSerializer<Any>?
|
local: LocalTypeInformation): AMQPSerializer<Any>?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<*>>
|
||||||
}
|
}
|
||||||
|
|
||||||
class EvolutionSerializationException(remoteTypeInformation: RemoteTypeInformation, reason: String)
|
class EvolutionSerializationException(remoteTypeInformation: RemoteTypeInformation, reason: String)
|
||||||
@ -32,7 +39,9 @@ class EvolutionSerializationException(remoteTypeInformation: RemoteTypeInformati
|
|||||||
class DefaultEvolutionSerializerFactory(
|
class DefaultEvolutionSerializerFactory(
|
||||||
private val localSerializerFactory: LocalSerializerFactory,
|
private val localSerializerFactory: LocalSerializerFactory,
|
||||||
private val classLoader: ClassLoader,
|
private val classLoader: ClassLoader,
|
||||||
private val mustPreserveDataWhenEvolving: Boolean): EvolutionSerializerFactory {
|
private val mustPreserveDataWhenEvolving: Boolean,
|
||||||
|
override val primitiveTypes: Map<Class<*>, Class<*>>
|
||||||
|
): EvolutionSerializerFactory {
|
||||||
|
|
||||||
override fun getEvolutionSerializer(remote: RemoteTypeInformation,
|
override fun getEvolutionSerializer(remote: RemoteTypeInformation,
|
||||||
local: LocalTypeInformation): AMQPSerializer<Any>? =
|
local: LocalTypeInformation): AMQPSerializer<Any>? =
|
||||||
@ -77,7 +86,7 @@ class DefaultEvolutionSerializerFactory(
|
|||||||
val localClass = localProperty.type.observedType.asClass()
|
val localClass = localProperty.type.observedType.asClass()
|
||||||
val remoteClass = remoteProperty.type.typeIdentifier.getLocalType(classLoader).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,
|
throw EvolutionSerializationException(this,
|
||||||
"Local type $localClass of property $name is not assignable from remote type $remoteClass")
|
"Local type $localClass of property $name is not assignable from remote type $remoteClass")
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,13 @@ import net.corda.core.utilities.contextLogger
|
|||||||
import net.corda.core.utilities.debug
|
import net.corda.core.utilities.debug
|
||||||
import net.corda.core.utilities.trace
|
import net.corda.core.utilities.trace
|
||||||
import net.corda.serialization.internal.model.*
|
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 org.apache.qpid.proton.amqp.Symbol
|
||||||
import java.lang.reflect.ParameterizedType
|
import java.lang.reflect.ParameterizedType
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.function.Function
|
||||||
import javax.annotation.concurrent.ThreadSafe
|
import javax.annotation.concurrent.ThreadSafe
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -87,6 +90,7 @@ class DefaultLocalSerializerFactory(
|
|||||||
private val fingerPrinter: FingerPrinter,
|
private val fingerPrinter: FingerPrinter,
|
||||||
override val classloader: ClassLoader,
|
override val classloader: ClassLoader,
|
||||||
private val descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry,
|
private val descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry,
|
||||||
|
private val primitiveSerializerFactory: Function<Class<*>, AMQPSerializer<Any>>,
|
||||||
private val customSerializerRegistry: CustomSerializerRegistry,
|
private val customSerializerRegistry: CustomSerializerRegistry,
|
||||||
private val onlyCustomSerializers: Boolean)
|
private val onlyCustomSerializers: Boolean)
|
||||||
: LocalSerializerFactory {
|
: LocalSerializerFactory {
|
||||||
@ -137,9 +141,18 @@ class DefaultLocalSerializerFactory(
|
|||||||
serializersByTypeId.getOrPut(localTypeInformation.typeIdentifier) {
|
serializersByTypeId.getOrPut(localTypeInformation.typeIdentifier) {
|
||||||
val declaredClass = declaredType.asClass()
|
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
|
// can be useful to enable but will be *extremely* chatty if you do
|
||||||
logger.trace { "Get Serializer for $declaredClass ${declaredType.typeName}" }
|
logger.trace { "Get Serializer for $declaredClass ${declaredGenericType.typeName}" }
|
||||||
customSerializerRegistry.findCustomSerializer(declaredClass, declaredType)?.apply { return@get this }
|
customSerializerRegistry.findCustomSerializer(declaredClass, declaredGenericType)?.apply { return@get this }
|
||||||
|
|
||||||
return when (localTypeInformation) {
|
return when (localTypeInformation) {
|
||||||
is LocalTypeInformation.ACollection -> makeDeclaredCollection(localTypeInformation)
|
is LocalTypeInformation.ACollection -> makeDeclaredCollection(localTypeInformation)
|
||||||
@ -226,7 +239,7 @@ class DefaultLocalSerializerFactory(
|
|||||||
throw AMQPNotSerializableException(
|
throw AMQPNotSerializableException(
|
||||||
type,
|
type,
|
||||||
"Serializer does not support synthetic classes")
|
"Serializer does not support synthetic classes")
|
||||||
AMQPTypeIdentifiers.isPrimitive(typeInformation.typeIdentifier) -> AMQPPrimitiveSerializer(clazz)
|
AMQPTypeIdentifiers.isPrimitive(typeInformation.typeIdentifier) -> primitiveSerializerFactory.apply(clazz)
|
||||||
else -> makeNonCustomSerializer(type, typeInformation, clazz)
|
else -> makeNonCustomSerializer(type, typeInformation, clazz)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package net.corda.serialization.internal.amqp
|
|||||||
import net.corda.core.serialization.SerializationContext
|
import net.corda.core.serialization.SerializationContext
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.serialization.internal.model.*
|
import net.corda.serialization.internal.model.*
|
||||||
import org.hibernate.type.descriptor.java.ByteTypeDescriptor
|
|
||||||
import java.io.NotSerializableException
|
import java.io.NotSerializableException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,18 +3,46 @@ package net.corda.serialization.internal.amqp
|
|||||||
import net.corda.core.KeepForDJVM
|
import net.corda.core.KeepForDJVM
|
||||||
import net.corda.core.internal.uncheckedCast
|
import net.corda.core.internal.uncheckedCast
|
||||||
import net.corda.serialization.internal.CordaSerializationMagic
|
import net.corda.serialization.internal.CordaSerializationMagic
|
||||||
import org.apache.qpid.proton.amqp.DescribedType
|
import net.corda.serialization.internal.amqp.AMQPTypeIdentifiers.isPrimitive
|
||||||
import org.apache.qpid.proton.amqp.Symbol
|
import net.corda.serialization.internal.model.TypeIdentifier
|
||||||
import org.apache.qpid.proton.amqp.UnsignedInteger
|
import net.corda.serialization.internal.model.TypeIdentifier.TopType
|
||||||
import org.apache.qpid.proton.amqp.UnsignedLong
|
import net.corda.serialization.internal.model.TypeIdentifier.Companion.forGenericType
|
||||||
|
import org.apache.qpid.proton.amqp.*
|
||||||
import org.apache.qpid.proton.codec.DescribedTypeConstructor
|
import org.apache.qpid.proton.codec.DescribedTypeConstructor
|
||||||
import java.io.NotSerializableException
|
import java.io.NotSerializableException
|
||||||
import net.corda.serialization.internal.carpenter.Field as CarpenterField
|
import java.lang.reflect.Type
|
||||||
import net.corda.serialization.internal.carpenter.Schema as CarpenterSchema
|
|
||||||
|
|
||||||
const val DESCRIPTOR_DOMAIN: String = "net.corda"
|
const val DESCRIPTOR_DOMAIN: String = "net.corda"
|
||||||
val amqpMagic = CordaSerializationMagic("corda".toByteArray() + byteArrayOf(1, 0))
|
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
|
* 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.
|
* [toString] representations generate the associated XML form.
|
||||||
|
@ -7,9 +7,28 @@ import net.corda.serialization.internal.carpenter.ClassCarpenter
|
|||||||
import net.corda.serialization.internal.carpenter.ClassCarpenterImpl
|
import net.corda.serialization.internal.carpenter.ClassCarpenterImpl
|
||||||
import net.corda.serialization.internal.model.*
|
import net.corda.serialization.internal.model.*
|
||||||
import java.io.NotSerializableException
|
import java.io.NotSerializableException
|
||||||
|
import java.util.Collections.unmodifiableMap
|
||||||
|
import java.util.function.Function
|
||||||
|
|
||||||
@KeepForDJVM
|
@KeepForDJVM
|
||||||
object SerializerFactoryBuilder {
|
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<*>, 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<*>, Class<*>>
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun build(whitelist: ClassWhitelist, classCarpenter: ClassCarpenter): SerializerFactory {
|
fun build(whitelist: ClassWhitelist, classCarpenter: ClassCarpenter): SerializerFactory {
|
||||||
@ -89,17 +108,19 @@ object SerializerFactoryBuilder {
|
|||||||
fingerPrinter,
|
fingerPrinter,
|
||||||
classCarpenter.classloader,
|
classCarpenter.classloader,
|
||||||
descriptorBasedSerializerRegistry,
|
descriptorBasedSerializerRegistry,
|
||||||
|
Function { clazz -> AMQPPrimitiveSerializer(clazz) },
|
||||||
customSerializerRegistry,
|
customSerializerRegistry,
|
||||||
onlyCustomSerializers)
|
onlyCustomSerializers)
|
||||||
|
|
||||||
val typeLoader = ClassCarpentingTypeLoader(
|
val typeLoader: TypeLoader = ClassCarpentingTypeLoader(
|
||||||
SchemaBuildingRemoteTypeCarpenter(classCarpenter),
|
SchemaBuildingRemoteTypeCarpenter(classCarpenter),
|
||||||
classCarpenter.classloader)
|
classCarpenter.classloader)
|
||||||
|
|
||||||
val evolutionSerializerFactory = if (allowEvolution) DefaultEvolutionSerializerFactory(
|
val evolutionSerializerFactory = if (allowEvolution) DefaultEvolutionSerializerFactory(
|
||||||
localSerializerFactory,
|
localSerializerFactory,
|
||||||
classCarpenter.classloader,
|
classCarpenter.classloader,
|
||||||
mustPreserveDataWhenEvolving
|
mustPreserveDataWhenEvolving,
|
||||||
|
javaPrimitiveTypes
|
||||||
) else NoEvolutionSerializerFactory
|
) else NoEvolutionSerializerFactory
|
||||||
|
|
||||||
val remoteSerializerFactory = DefaultRemoteSerializerFactory(
|
val remoteSerializerFactory = DefaultRemoteSerializerFactory(
|
||||||
@ -116,15 +137,17 @@ object SerializerFactoryBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object NoEvolutionSerializerFactory : EvolutionSerializerFactory {
|
object NoEvolutionSerializerFactory : EvolutionSerializerFactory {
|
||||||
override fun getEvolutionSerializer(remoteTypeInformation: RemoteTypeInformation, localTypeInformation: LocalTypeInformation): AMQPSerializer<Any> {
|
override fun getEvolutionSerializer(remote: RemoteTypeInformation, local: LocalTypeInformation): AMQPSerializer<Any> {
|
||||||
throw NotSerializableException("""
|
throw NotSerializableException("""
|
||||||
Evolution not permitted.
|
Evolution not permitted.
|
||||||
|
|
||||||
Remote:
|
Remote:
|
||||||
${remoteTypeInformation.prettyPrint(false)}
|
${remote.prettyPrint(false)}
|
||||||
|
|
||||||
Local:
|
Local:
|
||||||
${localTypeInformation.prettyPrint(false)}
|
${local.prettyPrint(false)}
|
||||||
""")
|
""")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val primitiveTypes: Map<Class<*>, Class<*>> = emptyMap()
|
||||||
}
|
}
|
@ -1,7 +1,5 @@
|
|||||||
package net.corda.serialization.internal.model
|
package net.corda.serialization.internal.model
|
||||||
|
|
||||||
import net.corda.core.serialization.ClassWhitelist
|
|
||||||
import net.corda.serialization.internal.amqp.*
|
|
||||||
import java.lang.reflect.*
|
import java.lang.reflect.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,7 +52,7 @@ class ConfigurableLocalTypeModel(private val typeModelConfiguration: LocalTypeMo
|
|||||||
private val typeInformationCache = DefaultCacheProvider.createCache<TypeIdentifier, LocalTypeInformation>()
|
private val typeInformationCache = DefaultCacheProvider.createCache<TypeIdentifier, LocalTypeInformation>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
* [LocalTypeInformation] with unpatched cycles into the global cache where other threads can access them
|
||||||
* before we've patched the cycles up.
|
* before we've patched the cycles up.
|
||||||
*/
|
*/
|
||||||
|
@ -63,6 +63,8 @@ sealed class TypeIdentifier {
|
|||||||
// This method has locking. So we memo the value here.
|
// This method has locking. So we memo the value here.
|
||||||
private val systemClassLoader: ClassLoader = ClassLoader.getSystemClassLoader()
|
private val systemClassLoader: ClassLoader = ClassLoader.getSystemClassLoader()
|
||||||
|
|
||||||
|
fun classLoaderFor(clazz: Class<*>): ClassLoader = clazz.classLoader ?: systemClassLoader
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtain the [TypeIdentifier] for an erased Java class.
|
* Obtain the [TypeIdentifier] for an erased Java class.
|
||||||
*
|
*
|
||||||
@ -206,7 +208,11 @@ sealed class TypeIdentifier {
|
|||||||
|
|
||||||
override fun toString() = "Parameterised(${prettyPrint()})"
|
override fun toString() = "Parameterised(${prettyPrint()})"
|
||||||
override fun getLocalType(classLoader: ClassLoader): Type {
|
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) {
|
if (rawType.typeParameters.size != parameters.size) {
|
||||||
throw IncompatibleTypeIdentifierException(
|
throw IncompatibleTypeIdentifierException(
|
||||||
"Class $rawType expects ${rawType.typeParameters.size} type arguments, " +
|
"Class $rawType expects ${rawType.typeParameters.size} type arguments, " +
|
||||||
|
@ -4,7 +4,9 @@ import com.google.common.hash.Hashing
|
|||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.core.utilities.toBase64
|
import net.corda.core.utilities.toBase64
|
||||||
import net.corda.serialization.internal.amqp.*
|
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].
|
* A fingerprinter that fingerprints [LocalTypeInformation].
|
||||||
@ -34,10 +36,14 @@ class TypeModellingFingerPrinter(
|
|||||||
private val cache: MutableMap<TypeIdentifier, String> = DefaultCacheProvider.createCache()
|
private val cache: MutableMap<TypeIdentifier, String> = DefaultCacheProvider.createCache()
|
||||||
|
|
||||||
override fun fingerprint(typeInformation: LocalTypeInformation): String =
|
override fun fingerprint(typeInformation: LocalTypeInformation): String =
|
||||||
cache.computeIfAbsent(typeInformation.typeIdentifier) {
|
/*
|
||||||
FingerPrintingState(
|
* We cannot use ConcurrentMap.computeIfAbsent() here because it requires
|
||||||
customTypeDescriptorLookup,
|
* that the map not be re-entered during the computation function. And
|
||||||
FingerprintWriter(debugEnabled)).fingerprint(typeInformation)
|
* 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
|
// Give any custom serializers loaded into the factory the chance to supply their own type-descriptors
|
||||||
private fun fingerprintWithCustomSerializerOrElse(type: LocalTypeInformation, defaultAction: () -> Unit) {
|
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)
|
if (customTypeDescriptor != null) writer.write(customTypeDescriptor)
|
||||||
else defaultAction()
|
else defaultAction()
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user