CORDA-540: Allow lists as root object in AMQP serialization graph

This commit is contained in:
Viktor Kolomeyko
2017-09-05 13:33:00 +01:00
committed by Viktor Kolomeyko
parent ebc9cacb53
commit cd912f8add
3 changed files with 52 additions and 8 deletions

View File

@ -18,18 +18,42 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali
override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}" override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}"
companion object { companion object {
private val supportedTypes: Map<Class<out Collection<*>>, (List<*>) -> Collection<*>> = mapOf( // NB: Order matters in this map, the most specific classes should be listed at the end
private val supportedTypes: Map<Class<out Collection<*>>, (List<*>) -> Collection<*>> = Collections.unmodifiableMap(linkedMapOf(
Collection::class.java to { list -> Collections.unmodifiableCollection(list) }, Collection::class.java to { list -> Collections.unmodifiableCollection(list) },
List::class.java to { list -> Collections.unmodifiableList(list) }, List::class.java to { list -> Collections.unmodifiableList(list) },
Set::class.java to { list -> Collections.unmodifiableSet(LinkedHashSet(list)) }, Set::class.java to { list -> Collections.unmodifiableSet(LinkedHashSet(list)) },
SortedSet::class.java to { list -> Collections.unmodifiableSortedSet(TreeSet(list)) }, SortedSet::class.java to { list -> Collections.unmodifiableSortedSet(TreeSet(list)) },
NavigableSet::class.java to { list -> Collections.unmodifiableNavigableSet(TreeSet(list)) }, NavigableSet::class.java to { list -> Collections.unmodifiableNavigableSet(TreeSet(list)) },
NonEmptySet::class.java to { list -> NonEmptySet.copyOf(list) } NonEmptySet::class.java to { list -> NonEmptySet.copyOf(list) }
) ))
private fun findConcreteType(clazz: Class<*>): (List<*>) -> Collection<*> { private fun findConcreteType(clazz: Class<*>): (List<*>) -> Collection<*> {
return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported collection type $clazz.") return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported collection type $clazz.")
} }
fun deriveParameterizedType(declaredType: Type, declaredClass: Class<*>, actualClass: Class<*>?): ParameterizedType {
if(supportedTypes.containsKey(declaredClass)) {
// Simple case - it is already known to be a collection.
@Suppress("UNCHECKED_CAST")
return deriveParametrizedType(declaredType, declaredClass as Class<out Collection<*>>)
}
else if (actualClass != null && Collection::class.java.isAssignableFrom(actualClass)) {
// Declared class is not collection, but [actualClass] is - represent it accordingly.
val collectionClass = findMostSuitableCollectionType(actualClass)
return deriveParametrizedType(declaredType, collectionClass)
}
throw NotSerializableException("Cannot derive collection type for declaredType: '$declaredType', declaredClass: '$declaredClass', actualClass: '$actualClass'")
}
private fun deriveParametrizedType(declaredType: Type, collectionClass: Class<out Collection<*>>): ParameterizedType =
(declaredType as? ParameterizedType) ?: DeserializedParameterizedType(collectionClass, arrayOf(SerializerFactory.AnyType))
private fun findMostSuitableCollectionType(actualClass: Class<*>): Class<out Collection<*>> =
supportedTypes.keys.findLast { it.isAssignableFrom(actualClass) }!!
} }
private val concreteBuilder: (List<*>) -> Collection<*> = findConcreteType(declaredType.rawType as Class<*>) private val concreteBuilder: (List<*>) -> Collection<*> = findConcreteType(declaredType.rawType as Class<*>)

View File

@ -4,6 +4,7 @@ import com.google.common.primitives.Primitives
import com.google.common.reflect.TypeResolver import com.google.common.reflect.TypeResolver
import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.ClassWhitelist
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.nodeapi.internal.serialization.amqp.CollectionSerializer.Companion.deriveParameterizedType
import net.corda.nodeapi.internal.serialization.carpenter.* import net.corda.nodeapi.internal.serialization.carpenter.*
import org.apache.qpid.proton.amqp.* import org.apache.qpid.proton.amqp.*
import java.io.NotSerializableException import java.io.NotSerializableException
@ -69,10 +70,15 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
val actualType: Type = inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType val actualType: Type = inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType
val serializer = when { val serializer = when {
(Collection::class.java.isAssignableFrom(declaredClass)) -> { (Collection::class.java.isAssignableFrom(declaredClass) ||
serializersByType.computeIfAbsent(declaredType) { // declared class may not be set to Collection, but actual class could be a collection.
CollectionSerializer(declaredType as? ParameterizedType ?: DeserializedParameterizedType( // In this case use of CollectionSerializer is perfectly appropriate.
declaredClass, arrayOf(AnyType), null), this) (actualClass != null && Collection::class.java.isAssignableFrom(actualClass))) -> {
val declaredTypeAmended= deriveParameterizedType(declaredType, declaredClass, actualClass)
serializersByType.computeIfAbsent(declaredTypeAmended) {
CollectionSerializer(declaredTypeAmended, this)
} }
} }
Map::class.java.isAssignableFrom(declaredClass) -> serializersByType.computeIfAbsent(declaredClass) { Map::class.java.isAssignableFrom(declaredClass) -> serializersByType.computeIfAbsent(declaredClass) {

View File

@ -3,19 +3,33 @@ package net.corda.nodeapi.internal.serialization
import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.node.services.statemachine.SessionData
import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.TestDependencyInjectionBase
import net.corda.testing.kryoSpecific
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class ListsSerializationTest : TestDependencyInjectionBase() { class ListsSerializationTest : TestDependencyInjectionBase() {
@Test @Test
fun `check list can be serialized as root of serialization graph`() = kryoSpecific<ListsSerializationTest>("AMQP doesn't support lists as the root of serialization graph"){ fun `check list can be serialized as root of serialization graph`() {
assertEqualAfterRoundTripSerialization(listOf(1)) assertEqualAfterRoundTripSerialization(listOf(1))
assertEqualAfterRoundTripSerialization(listOf(1, 2)) assertEqualAfterRoundTripSerialization(listOf(1, 2))
} }
@Test
fun `check list can be serialized as part of SessionData`() {
run {
val sessionData = SessionData(123, listOf(1))
assertEqualAfterRoundTripSerialization(sessionData)
}
run {
val sessionData = SessionData(123, listOf(1, 2))
assertEqualAfterRoundTripSerialization(sessionData)
}
}
private inline fun<reified T : Any> assertEqualAfterRoundTripSerialization(obj: T) { private inline fun<reified T : Any> assertEqualAfterRoundTripSerialization(obj: T) {
val serializedForm: SerializedBytes<T> = obj.serialize() val serializedForm: SerializedBytes<T> = obj.serialize()