diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index bccaf5c085..23a00b1ada 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,6 +6,8 @@ from the previous milestone release. UNRELEASED ---------- +* Make the serialisation finger-printer a pluggable entity rather than hard wiring into the factory + * Removed blacklisted word checks in Corda X.500 name to allow "Server" or "Node" to be use as part of the legal name. * Separated our pre-existing Artemis broker into an RPC broker and a P2P broker. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt index b6b20a204e..872709a119 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt @@ -31,7 +31,8 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory) "${type.componentType().typeName}$arrayType" } - override val typeDescriptor by lazy { Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}") } + override val typeDescriptor by lazy { + Symbol.valueOf("$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}") } internal val elementType: Type by lazy { type.componentType() } internal open val typeName by lazy { calcTypeName(type) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt index bd19e98599..5a6ab42f8e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt @@ -17,7 +17,9 @@ import kotlin.collections.Set */ class CollectionSerializer(val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer { override val type: Type = declaredType as? DeserializedParameterizedType ?: DeserializedParameterizedType.make(SerializerFactory.nameForType(declaredType)) - override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}") + override val typeDescriptor by lazy { + Symbol.valueOf("$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}") + } companion object { // NB: Order matters in this map, the most specific classes should be listed at the end diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt index f2ee28d01b..fd02997c49 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt @@ -60,7 +60,9 @@ abstract class CustomSerializer : AMQPSerializer, SerializerFor { override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.clazz override val type: Type get() = clazz - override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForDescriptors(superClassSerializer.typeDescriptor.toString(), nameForType(clazz))}") + override val typeDescriptor by lazy { + Symbol.valueOf("$DESCRIPTOR_DOMAIN:${SerializerFingerPrinter().fingerprintForDescriptors(superClassSerializer.typeDescriptor.toString(), nameForType(clazz))}") + } private val typeNotation: TypeNotation = RestrictedType( SerializerFactory.nameForType(clazz), null, diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt index a9bc0916b1..a6d9ebb055 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt @@ -39,7 +39,8 @@ class EnumEvolutionSerializer( factory: SerializerFactory, private val conversions: Map, private val ordinals: Map) : AMQPSerializer { - override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")!! + override val typeDescriptor = Symbol.valueOf( + "$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")!! companion object { private fun MutableMap.mapInPlace(f: (String) -> String) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt index 5678f094ed..c3d9fdd732 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt @@ -11,8 +11,9 @@ import java.lang.reflect.Type */ class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: SerializerFactory) : AMQPSerializer { override val type: Type = declaredType - override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")!! private val typeNotation: TypeNotation + override val typeDescriptor = Symbol.valueOf( + "$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")!! init { typeNotation = RestrictedType( diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/FingerPrinter.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/FingerPrinter.kt new file mode 100644 index 0000000000..e5cc478acb --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/FingerPrinter.kt @@ -0,0 +1,202 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import com.google.common.hash.Hasher +import com.google.common.hash.Hashing +import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.toBase64 +import java.io.NotSerializableException +import java.lang.reflect.* +import java.util.* + +/** + * Should be implemented by classes which wish to provide plugable fingerprinting og types for a [SerializerFactory] + */ +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 + + /** + * If required, associate an instance of the fingerprinter with a specific serializer factory + */ + fun setOwner(factory: SerializerFactory) +} + +/** + * Implementation of the finger printing mechanism used by default + */ +class SerializerFingerPrinter : FingerPrinter { + private var factory: SerializerFactory? = null + + private val ARRAY_HASH: String = "Array = true" + private val ENUM_HASH: String = "Enum = true" + private val ALREADY_SEEN_HASH: String = "Already seen = true" + private val NULLABLE_HASH: String = "Nullable = true" + private val NOT_NULLABLE_HASH: String = "Nullable = false" + private val ANY_TYPE_HASH: String = "Any type = true" + private val TYPE_VARIABLE_HASH: String = "Type variable = true" + private val WILDCARD_TYPE_HASH: String = "Wild card = true" + + private val logger by lazy { loggerFor() } + + override fun setOwner(factory: SerializerFactory) { + this.factory = factory + } + + /** + * 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 { + return fingerprintForType( + type, null, HashSet(), Hashing.murmur3_128().newHasher(), debugIndent = 1).hash().asBytes().toBase64() + } + + private fun isCollectionOrMap(type: Class<*>) = + (Collection::class.java.isAssignableFrom(type) || Map::class.java.isAssignableFrom(type)) + && !EnumSet::class.java.isAssignableFrom(type) + + internal fun fingerprintForDescriptors(vararg typeDescriptors: String): String { + val hasher = Hashing.murmur3_128().newHasher() + for (typeDescriptor in typeDescriptors) { + hasher.putUnencodedChars(typeDescriptor) + } + return hasher.hash().asBytes().toBase64() + } + + private fun Hasher.fingerprintWithCustomSerializerOrElse( + factory: SerializerFactory, + clazz: Class<*>, + declaredType: Type, + block: () -> Hasher): Hasher { + // Need to check if a custom serializer is applicable + val customSerializer = factory.findCustomSerializer(clazz, declaredType) + return if (customSerializer != null) { + putUnencodedChars(customSerializer.typeDescriptor) + } else { + block() + } + } + + // 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 fingerprintForType(type: Type, contextType: Type?, alreadySeen: MutableSet, + hasher: Hasher, debugIndent: Int = 1): Hasher { + // We don't include Example and Example where type is ? or T in this otherwise we + // generate different fingerprints for class Outer(val a: Inner) 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 and Example to have the same fingerprint + return if ((type in alreadySeen) + && (type !is SerializerFactory.AnyType) + && (type !is TypeVariable<*>) + && (type !is WildcardType)) { + hasher.putUnencodedChars(ALREADY_SEEN_HASH) + } else { + alreadySeen += type + try { + when (type) { + is ParameterizedType -> { + // Hash the rawType + params + val clazz = type.rawType as Class<*> + + val startingHash = if (isCollectionOrMap(clazz)) { + hasher.putUnencodedChars(clazz.name) + } else { + hasher.fingerprintWithCustomSerializerOrElse(factory!!, clazz, type) { + fingerprintForObject(type, type, alreadySeen, hasher, factory!!, debugIndent + 1) + } + } + + // ... and concatenate the type data for each parameter type. + type.actualTypeArguments.fold(startingHash) { orig, paramType -> + fingerprintForType(paramType, type, alreadySeen, orig, debugIndent + 1) + } + } + // 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), creates a different fingerprint from the generic type itself (Example) + // + // On serialization Example is treated as Example, 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<*> -> { + hasher.putUnencodedChars("?").putUnencodedChars(ANY_TYPE_HASH) + } + is Class<*> -> { + if (type.isArray) { + fingerprintForType(type.componentType, contextType, alreadySeen, hasher, debugIndent + 1) + .putUnencodedChars(ARRAY_HASH) + } else if (SerializerFactory.isPrimitive(type)) { + hasher.putUnencodedChars(type.name) + } else if (isCollectionOrMap(type)) { + hasher.putUnencodedChars(type.name) + } else if (type.isEnum) { + // ensures any change to the enum (adding constants) will trigger the need for evolution + hasher.apply { + type.enumConstants.forEach { + putUnencodedChars(it.toString()) + } + }.putUnencodedChars(type.name).putUnencodedChars(ENUM_HASH) + } else { + hasher.fingerprintWithCustomSerializerOrElse(factory!!, type, type) { + if (type.kotlin.objectInstance != null) { + // TODO: name collision is too likely for kotlin objects, we need to introduce some reference + // to the CorDapp but maybe reference to the JAR in the short term. + hasher.putUnencodedChars(type.name) + } else { + fingerprintForObject(type, type, alreadySeen, hasher, factory!!, debugIndent + 1) + } + } + } + } + // Hash the element type + some array hash + is GenericArrayType -> { + fingerprintForType(type.genericComponentType, contextType, alreadySeen, + hasher, debugIndent + 1).putUnencodedChars(ARRAY_HASH) + } + else -> throw NotSerializableException("Don't know how to hash") + } + } catch (e: NotSerializableException) { + val msg = "${e.message} -> $type" + logger.error(msg, e) + throw NotSerializableException(msg) + } + } + } + + private fun fingerprintForObject( + type: Type, + contextType: Type?, + alreadySeen: MutableSet, + hasher: Hasher, + factory: SerializerFactory, + debugIndent: Int = 0): Hasher { + // Hash the class + properties + interfaces + val name = type.asClass()?.name + ?: throw NotSerializableException("Expected only Class or ParameterizedType but found $type") + + propertiesForSerialization(constructorForDeserialization(type), contextType ?: type, factory) + .serializationOrder + .fold(hasher.putUnencodedChars(name)) { orig, prop -> + fingerprintForType(prop.getter.resolvedType, type, alreadySeen, orig, debugIndent + 1) + .putUnencodedChars(prop.getter.name) + .putUnencodedChars(if (prop.getter.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH) + } + interfacesForSerialization(type, factory).map { fingerprintForType(it, type, alreadySeen, hasher, debugIndent + 1) } + return hasher + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt index 5472074d62..138326dde6 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt @@ -19,7 +19,8 @@ private typealias MapCreationFunction = (Map<*, *>) -> Map<*, *> */ class MapSerializer(private val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer { override val type: Type = declaredType as? DeserializedParameterizedType ?: DeserializedParameterizedType.make(SerializerFactory.nameForType(declaredType)) - override val typeDescriptor: Symbol = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}") + override val typeDescriptor: Symbol = Symbol.valueOf( + "$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}") companion object { // NB: Order matters in this map, the most specific classes should be listed at the end diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt index fa723fd3de..e4dddec1da 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt @@ -31,7 +31,8 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS private val typeName = nameForType(clazz) - override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}") + override val typeDescriptor = Symbol.valueOf( + "$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}") // We restrict to only those annotated or whitelisted private val interfaces = interfacesForSerialization(clazz, factory) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt index f5edd923c9..285a1f5d54 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt @@ -1,19 +1,13 @@ package net.corda.nodeapi.internal.serialization.amqp -import com.google.common.hash.Hasher -import com.google.common.hash.Hashing import net.corda.core.internal.uncheckedCast import net.corda.nodeapi.internal.serialization.CordaSerializationMagic -import net.corda.core.utilities.loggerFor -import net.corda.core.utilities.toBase64 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 org.apache.qpid.proton.codec.DescribedTypeConstructor import java.io.NotSerializableException -import java.lang.reflect.* -import java.util.* import net.corda.nodeapi.internal.serialization.carpenter.Field as CarpenterField import net.corda.nodeapi.internal.serialization.carpenter.Schema as CarpenterSchema @@ -92,7 +86,14 @@ data class Descriptor(val name: Symbol?, val code: UnsignedLong? = null) : Descr } } -data class Field(val name: String, val type: String, val requires: List, val default: String?, val label: String?, val mandatory: Boolean, val multiple: Boolean) : DescribedType { +data class Field( + val name: String, + val type: String, + val requires: List, + val default: String?, + val label: String?, + val mandatory: Boolean, + val multiple: Boolean) : DescribedType { companion object : DescribedTypeConstructor { val DESCRIPTOR = AMQPDescriptorRegistry.FIELD.amqpDescriptor @@ -302,162 +303,4 @@ data class ReferencedObject(private val refCounter: Int) : DescribedType { override fun toString(): String = "" } -private val ARRAY_HASH: String = "Array = true" -private val ENUM_HASH: String = "Enum = true" -private val ALREADY_SEEN_HASH: String = "Already seen = true" -private val NULLABLE_HASH: String = "Nullable = true" -private val NOT_NULLABLE_HASH: String = "Nullable = false" -private val ANY_TYPE_HASH: String = "Any type = true" -private val TYPE_VARIABLE_HASH: String = "Type variable = true" -private val WILDCARD_TYPE_HASH: String = "Wild card = true" -private val logger by lazy { loggerFor() } - -/** - * 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. - */ -// TODO: write tests -internal fun fingerprintForType(type: Type, factory: SerializerFactory): String { - return fingerprintForType(type, null, HashSet(), Hashing.murmur3_128().newHasher(), factory).hash().asBytes().toBase64() -} - -internal fun fingerprintForDescriptors(vararg typeDescriptors: String): String { - val hasher = Hashing.murmur3_128().newHasher() - for (typeDescriptor in typeDescriptors) { - hasher.putUnencodedChars(typeDescriptor) - } - return hasher.hash().asBytes().toBase64() -} - -private fun Hasher.fingerprintWithCustomSerializerOrElse(factory: SerializerFactory, clazz: Class<*>, declaredType: Type, block: () -> Hasher): Hasher { - // Need to check if a custom serializer is applicable - val customSerializer = factory.findCustomSerializer(clazz, declaredType) - return if (customSerializer != null) { - putUnencodedChars(customSerializer.typeDescriptor) - } else { - block() - } -} - -// 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 fingerprintForType(type: Type, contextType: Type?, alreadySeen: MutableSet, - hasher: Hasher, factory: SerializerFactory, debugIndent: Int = 1): Hasher { - // We don't include Example and Example where type is ? or T in this otherwise we - // generate different fingerprints for class Outer(val a: Inner) 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 and Example to have the same fingerprint - return if ((type in alreadySeen) - && (type !is SerializerFactory.AnyType) - && (type !is TypeVariable<*>) - && (type !is WildcardType)) { - hasher.putUnencodedChars(ALREADY_SEEN_HASH) - } else { - alreadySeen += type - try { - when (type) { - is ParameterizedType -> { - // Hash the rawType + params - val clazz = type.rawType as Class<*> - - val startingHash = if (isCollectionOrMap(clazz)) { - hasher.putUnencodedChars(clazz.name) - } else { - hasher.fingerprintWithCustomSerializerOrElse(factory, clazz, type) { - fingerprintForObject(type, type, alreadySeen, hasher, factory, debugIndent+1) - } - } - - // ... and concatenate the type data for each parameter type. - type.actualTypeArguments.fold(startingHash) { orig, paramType -> - fingerprintForType(paramType, type, alreadySeen, orig, factory, debugIndent+1) - } - } - // 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), creates a different fingerprint from the generic type itself (Example) - // - // On serialization Example is treated as Example, 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<*> -> { - hasher.putUnencodedChars("?").putUnencodedChars(ANY_TYPE_HASH) - } - is Class<*> -> { - if (type.isArray) { - fingerprintForType(type.componentType, contextType, alreadySeen, hasher, factory, debugIndent+1) - .putUnencodedChars(ARRAY_HASH) - } else if (SerializerFactory.isPrimitive(type)) { - hasher.putUnencodedChars(type.name) - } else if (isCollectionOrMap(type)) { - hasher.putUnencodedChars(type.name) - } else if (type.isEnum) { - // ensures any change to the enum (adding constants) will trigger the need for evolution - hasher.apply { - type.enumConstants.forEach { - putUnencodedChars(it.toString()) - } - }.putUnencodedChars(type.name).putUnencodedChars(ENUM_HASH) - } else { - hasher.fingerprintWithCustomSerializerOrElse(factory, type, type) { - if (type.kotlin.objectInstance != null) { - // TODO: name collision is too likely for kotlin objects, we need to introduce some reference - // to the CorDapp but maybe reference to the JAR in the short term. - hasher.putUnencodedChars(type.name) - } else { - fingerprintForObject(type, type, alreadySeen, hasher, factory, debugIndent+1) - } - } - } - } - // Hash the element type + some array hash - is GenericArrayType -> { - fingerprintForType(type.genericComponentType, contextType, alreadySeen, - hasher, factory, debugIndent+1).putUnencodedChars(ARRAY_HASH) - } - else -> throw NotSerializableException("Don't know how to hash") - } - } catch (e: NotSerializableException) { - val msg = "${e.message} -> $type" - logger.error(msg, e) - throw NotSerializableException(msg) - } - } -} - -private fun isCollectionOrMap(type: Class<*>) = (Collection::class.java.isAssignableFrom(type) || Map::class.java.isAssignableFrom(type)) && - !EnumSet::class.java.isAssignableFrom(type) - -private fun fingerprintForObject( - type: Type, - contextType: Type?, - alreadySeen: MutableSet, - hasher: Hasher, - factory: SerializerFactory, - debugIndent: Int = 0): Hasher { - // Hash the class + properties + interfaces - val name = type.asClass()?.name ?: throw NotSerializableException("Expected only Class or ParameterizedType but found $type") - - propertiesForSerialization(constructorForDeserialization(type), contextType ?: type, factory) - .serializationOrder - .fold(hasher.putUnencodedChars(name)) { orig, prop -> - fingerprintForType(prop.getter.resolvedType, type, alreadySeen, orig, factory, debugIndent+1) - .putUnencodedChars(prop.getter.name) - .putUnencodedChars(if (prop.getter.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH) - } - interfacesForSerialization(type, factory).map { fingerprintForType(it, type, alreadySeen, hasher, factory, debugIndent+1) } - return hasher -} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt index 7bd8178a6d..011b04dbf7 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt @@ -40,7 +40,13 @@ data class FactorySchemaAndDescriptor(val schemas: SerializationSchemas, val typ open class SerializerFactory( val whitelist: ClassWhitelist, cl: ClassLoader, - private val evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter()) { + private val evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), + val fingerPrinter: FingerPrinter = SerializerFingerPrinter()) { + + init { + fingerPrinter.setOwner(this) + } + private val serializersByType = ConcurrentHashMap>() private val serializersByDescriptor = ConcurrentHashMap>() private val customSerializers = CopyOnWriteArrayList() diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SingletonSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SingletonSerializer.kt index 0c70a18935..b54bb94393 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SingletonSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SingletonSerializer.kt @@ -10,7 +10,8 @@ import java.lang.reflect.Type * want converting back to that singleton instance on the receiving JVM. */ class SingletonSerializer(override val type: Class<*>, val singleton: Any, factory: SerializerFactory) : AMQPSerializer { - override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}") + override val typeDescriptor = Symbol.valueOf( + "$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}") private val interfaces = interfacesForSerialization(type, factory) diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ErrorMessageTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ErrorMessageTests.java index 7cd21c4f21..11db9fc643 100644 --- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ErrorMessageTests.java +++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ErrorMessageTests.java @@ -32,10 +32,12 @@ public class ErrorMessageTests { @Test public void testJavaConstructorAnnotations() { EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter(); + FingerPrinter fingerPrinter = new SerializerFingerPrinter(); SerializerFactory factory1 = new SerializerFactory( AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), - evolutionSerialiserGetter); + evolutionSerialiserGetter, + fingerPrinter); SerializationOutput ser = new SerializationOutput(factory1); diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaGenericsTest.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaGenericsTest.java index feb416e991..9c206ad8cd 100644 --- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaGenericsTest.java +++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaGenericsTest.java @@ -29,7 +29,8 @@ public class JavaGenericsTest { SerializerFactory factory = new SerializerFactory( AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), - new EvolutionSerializerGetter()); + new EvolutionSerializerGetter(), + new SerializerFingerPrinter()); SerializationOutput ser = new SerializationOutput(factory); SerializedBytes bytes = ser.serialize(a1); @@ -44,7 +45,8 @@ public class JavaGenericsTest { SerializerFactory factory = new SerializerFactory( AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), - new EvolutionSerializerGetter()); + new EvolutionSerializerGetter(), + new SerializerFingerPrinter()); return (new SerializationOutput(factory)).serialize(a); } @@ -59,7 +61,8 @@ public class JavaGenericsTest { SerializerFactory factory = new SerializerFactory( AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), - new EvolutionSerializerGetter()); + new EvolutionSerializerGetter(), + new SerializerFingerPrinter()); DeserializationInput des = new DeserializationInput(factory); return des.deserialize(bytes, A.class); @@ -83,7 +86,8 @@ public class JavaGenericsTest { SerializerFactory factory = new SerializerFactory( AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), - new EvolutionSerializerGetter()); + new EvolutionSerializerGetter(), + new SerializerFingerPrinter()); SerializedBytes bytes = forceWildcardSerializeFactory(new A(new Inner(29)), factory); Inner i = (Inner)forceWildcardDeserializeFactory(bytes, factory).getT(); diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaPrivatePropertyTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaPrivatePropertyTests.java index b13cc2cc48..2ee89945f6 100644 --- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaPrivatePropertyTests.java +++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaPrivatePropertyTests.java @@ -76,9 +76,9 @@ public class JavaPrivatePropertyTests { @Test public void singlePrivateBooleanWithConstructor() throws NotSerializableException, NoSuchFieldException, IllegalAccessException { - EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter(); SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), - evolutionSerializerGetter); + new EvolutionSerializerGetter(), + new SerializerFingerPrinter()); SerializationOutput ser = new SerializationOutput(factory); DeserializationInput des = new DeserializationInput(factory); @@ -89,9 +89,10 @@ public class JavaPrivatePropertyTests { @Test public void singlePrivateBooleanWithNoConstructor() throws NotSerializableException, NoSuchFieldException, IllegalAccessException { - EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter(); SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), - evolutionSerializerGetter); + new EvolutionSerializerGetter(), + new SerializerFingerPrinter()); + SerializationOutput ser = new SerializationOutput(factory); DeserializationInput des = new DeserializationInput(factory); @@ -103,9 +104,9 @@ public class JavaPrivatePropertyTests { @Test public void testCapitilsationOfIs() throws NotSerializableException, NoSuchFieldException, IllegalAccessException { - EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter(); SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), - evolutionSerializerGetter); + new EvolutionSerializerGetter(), + new SerializerFingerPrinter()); SerializationOutput ser = new SerializationOutput(factory); DeserializationInput des = new DeserializationInput(factory); @@ -119,9 +120,9 @@ public class JavaPrivatePropertyTests { @Test public void singlePrivateIntWithBoolean() throws NotSerializableException, NoSuchFieldException, IllegalAccessException { - EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter(); SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), - evolutionSerializerGetter); + new EvolutionSerializerGetter(), + new SerializerFingerPrinter()); SerializationOutput ser = new SerializationOutput(factory); DeserializationInput des = new DeserializationInput(factory); @@ -134,9 +135,10 @@ public class JavaPrivatePropertyTests { @Test public void singlePrivateWithConstructor() throws NotSerializableException, NoSuchFieldException, IllegalAccessException { - EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter(); SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), - evolutionSerializerGetter); + new EvolutionSerializerGetter(), + new SerializerFingerPrinter()); + SerializationOutput ser = new SerializationOutput(factory); DeserializationInput des = new DeserializationInput(factory); @@ -164,9 +166,11 @@ public class JavaPrivatePropertyTests { @Test public void singlePrivateWithConstructorAndGetter() throws NotSerializableException, NoSuchFieldException, IllegalAccessException { - EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter(); SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, - ClassLoader.getSystemClassLoader(), evolutionSerializerGetter); + ClassLoader.getSystemClassLoader(), + new EvolutionSerializerGetter(), + new SerializerFingerPrinter()); + SerializationOutput ser = new SerializationOutput(factory); DeserializationInput des = new DeserializationInput(factory); diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerialiseEnumTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerialiseEnumTests.java index a64f9c3d9e..9be95ca396 100644 --- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerialiseEnumTests.java +++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerialiseEnumTests.java @@ -29,9 +29,9 @@ public class JavaSerialiseEnumTests { public void testJavaConstructorAnnotations() throws NotSerializableException { Bra bra = new Bra(Bras.UNDERWIRE); - EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter(); SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), - evolutionSerialiserGetter); + new EvolutionSerializerGetter(), + new SerializerFingerPrinter()); SerializationOutput ser = new SerializationOutput(factory1); SerializedBytes bytes = ser.serialize(bra); } diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java index 4379718003..0441b2720f 100644 --- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java +++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java @@ -173,10 +173,13 @@ public class JavaSerializationOutputTests { private Object serdes(Object obj) throws NotSerializableException { EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter(); + FingerPrinter fingerPrinter = new SerializerFingerPrinter(); SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), - evolutionSerialiserGetter); + evolutionSerialiserGetter, + fingerPrinter); SerializerFactory factory2 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), - evolutionSerialiserGetter); + evolutionSerialiserGetter, + fingerPrinter); SerializationOutput ser = new SerializationOutput(factory1); SerializedBytes bytes = ser.serialize(obj); diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ListsSerializationJavaTest.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ListsSerializationJavaTest.java index c77f452b33..153681fa6a 100644 --- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ListsSerializationJavaTest.java +++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ListsSerializationJavaTest.java @@ -125,9 +125,12 @@ public class ListsSerializationJavaTest { // Have to have own version as Kotlin inline functions cannot be easily called from Java private static void assertEqualAfterRoundTripSerialization(T container, Class clazz) throws Exception { - EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter(); - SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), - evolutionSerialiserGetter); + EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter(); + FingerPrinter fingerPrinter = new SerializerFingerPrinter(); + SerializerFactory factory1 = new SerializerFactory( + AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), + evolutionSerializerGetter, + fingerPrinter); SerializationOutput ser = new SerializationOutput(factory1); SerializedBytes bytes = ser.serialize(container); DeserializationInput des = new DeserializationInput(factory1); diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/SetterConstructorTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/SetterConstructorTests.java index defd86d948..a0ab7276dd 100644 --- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/SetterConstructorTests.java +++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/SetterConstructorTests.java @@ -110,10 +110,12 @@ public class SetterConstructorTests { @Test public void serialiseC() throws NotSerializableException { EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter(); + FingerPrinter fingerPrinter = new SerializerFingerPrinter(); SerializerFactory factory1 = new SerializerFactory( AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), - evolutionSerialiserGetter); + evolutionSerialiserGetter, + fingerPrinter); SerializationOutput ser = new SerializationOutput(factory1); @@ -184,10 +186,12 @@ public class SetterConstructorTests { @Test public void deserialiseC() throws NotSerializableException { EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter(); + FingerPrinter fingerPrinter = new SerializerFingerPrinter(); SerializerFactory factory1 = new SerializerFactory( AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), - evolutionSerialiserGetter); + evolutionSerialiserGetter, + fingerPrinter); C cPre1 = new C(); @@ -251,10 +255,12 @@ public class SetterConstructorTests { @Test public void serialiseOuterAndInner() throws NotSerializableException { EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter(); + FingerPrinter fingerPrinter = new SerializerFingerPrinter(); SerializerFactory factory1 = new SerializerFactory( AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), - evolutionSerialiserGetter); + evolutionSerialiserGetter, + fingerPrinter); Inner1 i1 = new Inner1("Hello"); Inner2 i2 = new Inner2(); @@ -277,10 +283,12 @@ public class SetterConstructorTests { @Test public void typeMistmatch() throws NotSerializableException { EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter(); + FingerPrinter fingerPrinter = new SerializerFingerPrinter(); SerializerFactory factory1 = new SerializerFactory( AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), - evolutionSerialiserGetter); + evolutionSerialiserGetter, + fingerPrinter); TypeMismatch tm = new TypeMismatch(); tm.setA(10); @@ -293,10 +301,12 @@ public class SetterConstructorTests { @Test public void typeMistmatch2() throws NotSerializableException { EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter(); + FingerPrinter fingerPrinter = new SerializerFingerPrinter(); SerializerFactory factory1 = new SerializerFactory( AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), - evolutionSerialiserGetter); + evolutionSerialiserGetter, + fingerPrinter); TypeMismatch2 tm = new TypeMismatch2(); tm.setA("10"); diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/FingerPrinterTesting.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/FingerPrinterTesting.kt new file mode 100644 index 0000000000..9b5ceb24df --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/FingerPrinterTesting.kt @@ -0,0 +1,55 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import org.junit.Test +import java.lang.reflect.Type +import kotlin.test.assertEquals +import net.corda.nodeapi.internal.serialization.AllWhitelist + +class FingerPrinterTesting : FingerPrinter { + private var index = 0 + private val cache = mutableMapOf() + + override fun fingerprint(type: Type): String { + return cache.computeIfAbsent(type) { index++.toString() } + } + + override fun setOwner(factory: SerializerFactory) { + return + } + + @Suppress("UNUSED") + fun changeFingerprint(type: Type) { + cache.computeIfAbsent(type) { "" }.apply { index++.toString() } + } +} + +class FingerPrinterTestingTests { + companion object { + val VERBOSE = true + } + @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)) + } + + @Test + fun worksAsReplacement() { + data class C (val a: Int, val b: Long) + + val factory = SerializerFactory( + AllWhitelist, + ClassLoader.getSystemClassLoader(), + EvolutionSerializerGetterTesting(), + FingerPrinterTesting()) + + val blob = TestSerializationOutput(VERBOSE, factory).serializeAndReturnSchema(C(1, 2L)) + + assertEquals (1, blob.schema.types.size) + assertEquals ("", blob.schema.types[0].descriptor.toString()) + } + +} \ No newline at end of file