Some additional serializers to fill in the blanks vs. our default whitelist (#1490)

This commit is contained in:
Rick Parker 2017-09-14 09:14:09 +01:00 committed by GitHub
parent b102900c90
commit 988e3e5007
15 changed files with 250 additions and 23 deletions

View File

@ -64,7 +64,7 @@ abstract class SerializationFactory {
val priorContext = _currentFactory.get()
_currentFactory.set(this)
try {
return block()
return this.block()
} finally {
_currentFactory.set(priorContext)
}

View File

@ -54,6 +54,11 @@ abstract class AbstractAMQPSerializationScheme : SerializationScheme {
register(net.corda.nodeapi.internal.serialization.amqp.custom.ClassSerializer(this))
register(net.corda.nodeapi.internal.serialization.amqp.custom.X509CertificateHolderSerializer)
register(net.corda.nodeapi.internal.serialization.amqp.custom.PartyAndCertificateSerializer(factory))
register(net.corda.nodeapi.internal.serialization.amqp.custom.StringBufferSerializer)
register(net.corda.nodeapi.internal.serialization.amqp.custom.SimpleStringSerializer)
register(net.corda.nodeapi.internal.serialization.amqp.custom.InputStreamSerializer)
register(net.corda.nodeapi.internal.serialization.amqp.custom.BitSetSerializer(this))
register(net.corda.nodeapi.internal.serialization.amqp.custom.EnumSetSerializer(this))
}
val customizer = AMQPSerializationCustomization(factory)
pluginRegistries.forEach { it.customizeSerialization(customizer) }

View File

@ -29,6 +29,11 @@ abstract class CustomSerializer<T> : AMQPSerializer<T> {
*/
abstract val schemaForDocumentation: Schema
/**
* Whether subclasses using this serializer via inheritance should have a mapping in the schema.
*/
open val revealSubclassesInSchema: Boolean = false
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
data.withDescribed(descriptor) {
@Suppress("UNCHECKED_CAST")

View File

@ -86,7 +86,7 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p
if (pos == typeStart) {
skippingWhitespace = false
if (params[pos].isWhitespace()) {
typeStart = pos++
typeStart = ++pos
} else if (!needAType) {
throw NotSerializableException("Not expecting a type")
} else if (params[pos] == '?') {

View File

@ -18,7 +18,7 @@ private typealias MapCreationFunction = (Map<*, *>) -> Map<*, *>
*/
class MapSerializer(private val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer<Any> {
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: Symbol = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")
companion object {
// NB: Order matters in this map, the most specific classes should be listed at the end
@ -29,7 +29,11 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial
NavigableMap::class.java to { map -> Collections.unmodifiableNavigableMap(TreeMap(map)) },
// concrete classes for user convenience
LinkedHashMap::class.java to { map -> LinkedHashMap(map) },
TreeMap::class.java to { map -> TreeMap(map) }
TreeMap::class.java to { map -> TreeMap(map) },
EnumMap::class.java to { map ->
@Suppress("UNCHECKED_CAST")
EnumMap(map as Map<EnumJustUsedForCasting, Any>)
}
))
private fun findConcreteType(clazz: Class<*>): MapCreationFunction {
@ -95,6 +99,10 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial
private fun readEntry(schema: Schema, input: DeserializationInput, entry: Map.Entry<Any?, Any?>) =
input.readObjectOrNull(entry.key, schema, declaredType.actualTypeArguments[0]) to
input.readObjectOrNull(entry.value, schema, declaredType.actualTypeArguments[1])
// Cannot use * as a bound for EnumMap and EnumSet since * is not an enum. So, we use a sample enum instead.
// We don't actually care about the type, we just need to make the compiler happier.
internal enum class EnumJustUsedForCasting { NOT_USED }
}
internal fun Class<*>.checkSupportedMapType() {

View File

@ -468,7 +468,8 @@ private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: Muta
}
}
private fun isCollectionOrMap(type: Class<*>) = Collection::class.java.isAssignableFrom(type) || Map::class.java.isAssignableFrom(type)
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<Type>, hasher: Hasher, factory: SerializerFactory): Hasher {
// Hash the class + properties + interfaces

View File

@ -13,7 +13,7 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import javax.annotation.concurrent.ThreadSafe
data class schemaAndDescriptor(val schema: Schema, val typeDescriptor: Any)
data class FactorySchemaAndDescriptor(val schema: Schema, val typeDescriptor: Any)
/**
* Factory of serializers designed to be shared across threads and invocations.
@ -22,8 +22,8 @@ data class schemaAndDescriptor(val schema: Schema, val typeDescriptor: Any)
// TODO: maybe support for caching of serialized form of some core types for performance
// TODO: profile for performance in general
// TODO: use guava caches etc so not unbounded
// TODO: do we need to support a transient annotation to exclude certain properties?
// TODO: allow definition of well known types that are left out of the schema.
// TODO: migrate some core types to unsigned integer descriptor
// TODO: document and alert to the fact that classes cannot default superclass/interface properties otherwise they are "erased" due to matching with constructor.
// TODO: type name prefixes for interfaces and abstract classes? Or use label?
// TODO: generic types should define restricted type alias with source of the wildcarded version, I think, if we're to generate classes from schema
@ -64,7 +64,8 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
// Declared class may not be set to Collection, but actual class could be a collection.
// In this case use of CollectionSerializer is perfectly appropriate.
(Collection::class.java.isAssignableFrom(declaredClass) ||
(actualClass != null && Collection::class.java.isAssignableFrom(actualClass))) -> {
(actualClass != null && Collection::class.java.isAssignableFrom(actualClass))) &&
!EnumSet::class.java.isAssignableFrom(actualClass ?: declaredClass) -> {
val declaredTypeAmended= CollectionSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass)
serializersByType.computeIfAbsent(declaredTypeAmended) {
CollectionSerializer(declaredTypeAmended, this)
@ -79,7 +80,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
makeMapSerializer(declaredTypeAmended)
}
}
Enum::class.java.isAssignableFrom(declaredClass) -> serializersByType.computeIfAbsent(declaredClass) {
Enum::class.java.isAssignableFrom(actualClass ?: declaredClass) -> serializersByType.computeIfAbsent(actualClass ?: declaredClass) {
EnumSerializer(actualType, actualClass ?: declaredClass, this)
}
else -> makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType)
@ -164,7 +165,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
@Throws(NotSerializableException::class)
fun get(typeDescriptor: Any, schema: Schema): AMQPSerializer<Any> {
return serializersByDescriptor[typeDescriptor] ?: {
processSchema(schemaAndDescriptor(schema, typeDescriptor))
processSchema(FactorySchemaAndDescriptor(schema, typeDescriptor))
serializersByDescriptor[typeDescriptor] ?: throw NotSerializableException(
"Could not find type matching descriptor $typeDescriptor.")
}()
@ -188,7 +189,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
* Iterate over an AMQP schema, for each type ascertain weather it's on ClassPath of [classloader] amd
* if not use the [ClassCarpenter] to generate a class to use in it's place
*/
private fun processSchema(schema: schemaAndDescriptor, sentinel: Boolean = false) {
private fun processSchema(schema: FactorySchemaAndDescriptor, sentinel: Boolean = false) {
val carpenterSchemas = CarpenterSchemas.newInstance()
for (typeNotation in schema.schema.types) {
try {
@ -234,8 +235,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
} else {
findCustomSerializer(clazz, declaredType) ?: run {
if (type.isArray()) {
// Allow Object[] since this can be quite common (i.e. an untyped array)
if (type.componentType() != Object::class.java) whitelisted(type.componentType())
// Don't need to check the whitelist since each element will come back through the whitelisting process.
if (clazz.componentType.isPrimitive) PrimArraySerializer.make(type, this)
else ArraySerializer.make(type, this)
} else if (clazz.kotlin.objectInstance != null) {
@ -256,7 +256,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
for (customSerializer in customSerializers) {
if (customSerializer.isSerializerFor(clazz)) {
val declaredSuperClass = declaredType.asClass()?.superclass
if (declaredSuperClass == null || !customSerializer.isSerializerFor(declaredSuperClass)) {
if (declaredSuperClass == null || !customSerializer.isSerializerFor(declaredSuperClass) || !customSerializer.revealSubclassesInSchema) {
return customSerializer
} else {
// Make a subclass serializer for the subclass and return that...

View File

@ -0,0 +1,17 @@
package net.corda.nodeapi.internal.serialization.amqp.custom
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
import java.util.*
/**
* A serializer that writes out a [BitSet] as an integer number of bits, plus the necessary number of bytes to encode that
* many bits.
*/
class BitSetSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<BitSet, BitSetSerializer.BitSetProxy>(BitSet::class.java, BitSetProxy::class.java, factory) {
override fun toProxy(obj: BitSet): BitSetProxy = BitSetProxy(obj.toByteArray())
override fun fromProxy(proxy: BitSetProxy): BitSet = BitSet.valueOf(proxy.bytes)
data class BitSetProxy(val bytes: ByteArray)
}

View File

@ -0,0 +1,34 @@
package net.corda.nodeapi.internal.serialization.amqp.custom
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
import net.corda.nodeapi.internal.serialization.amqp.MapSerializer
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
import java.util.*
@Suppress("UNCHECKED_CAST")
/**
* A serializer that writes out an [EnumSet] as a type, plus list of instances in the set.
*/
class EnumSetSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<EnumSet<*>, EnumSetSerializer.EnumSetProxy>(EnumSet::class.java, EnumSetProxy::class.java, factory) {
override val additionalSerializers: Iterable<CustomSerializer<out Any>> = listOf(ClassSerializer(factory))
override fun toProxy(obj: EnumSet<*>): EnumSetProxy = EnumSetProxy(elementType(obj), obj.toList())
private fun elementType(set: EnumSet<*>): Class<*> {
return if (set.isEmpty()) {
EnumSet.complementOf(set as EnumSet<MapSerializer.EnumJustUsedForCasting>).first().javaClass
} else {
set.first().javaClass
}
}
override fun fromProxy(proxy: EnumSetProxy): EnumSet<*> {
return if (proxy.elements.isEmpty()) {
EnumSet.noneOf(proxy.clazz as Class<MapSerializer.EnumJustUsedForCasting>)
} else {
EnumSet.copyOf(proxy.elements as List<MapSerializer.EnumJustUsedForCasting>)
}
}
data class EnumSetProxy(val clazz: Class<*>, val elements: List<Any>)
}

View File

@ -0,0 +1,41 @@
package net.corda.nodeapi.internal.serialization.amqp.custom
import net.corda.nodeapi.internal.serialization.amqp.*
import org.apache.qpid.proton.amqp.Binary
import org.apache.qpid.proton.codec.Data
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.lang.reflect.Type
/**
* A serializer that writes out the content of an input stream as bytes and deserializes into a [ByteArrayInputStream].
*/
object InputStreamSerializer : CustomSerializer.Implements<InputStream>(InputStream::class.java) {
override val revealSubclassesInSchema: Boolean = true
override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), SerializerFactory.primitiveTypeName(ByteArray::class.java)!!, descriptor, emptyList())))
override fun writeDescribedObject(obj: InputStream, data: Data, type: Type, output: SerializationOutput) {
val startingSize = maxOf(4096, obj.available() + 1)
var buffer = ByteArray(startingSize)
var pos = 0
while (true) {
val numberOfBytesRead = obj.read(buffer, pos, buffer.size - pos)
if (numberOfBytesRead != -1) {
pos += numberOfBytesRead
// If the buffer is now full, resize it.
if (pos == buffer.size) {
buffer = buffer.copyOf(buffer.size + maxOf(4096, obj.available() + 1))
}
} else {
data.putBinary(Binary(buffer, 0, pos))
break
}
}
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): InputStream {
val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray
return ByteArrayInputStream(bits)
}
}

View File

@ -0,0 +1,9 @@
package net.corda.nodeapi.internal.serialization.amqp.custom
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
import org.apache.activemq.artemis.api.core.SimpleString
/**
* A serializer for [SimpleString].
*/
object SimpleStringSerializer : CustomSerializer.ToString<SimpleString>(SimpleString::class.java)

View File

@ -0,0 +1,8 @@
package net.corda.nodeapi.internal.serialization.amqp.custom
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
/**
* A serializer for [StringBuffer].
*/
object StringBufferSerializer : CustomSerializer.ToString<StringBuffer>(StringBuffer::class.java)

View File

@ -13,6 +13,8 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<T
private val logger = loggerFor<ThrowableSerializer>()
}
override val revealSubclassesInSchema: Boolean = true
override val additionalSerializers: Iterable<CustomSerializer<out Any>> = listOf(StackTraceElementSerializer(factory))
override fun toProxy(obj: Throwable): ThrowableProxy {

View File

@ -2,12 +2,14 @@ package net.corda.nodeapi.internal.serialization.amqp.custom
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
import java.time.*
import java.time.ZoneId
/**
* A serializer for [ZoneId] that uses a proxy object to write out the string form.
*/
class ZoneIdSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<ZoneId, ZoneIdSerializer.ZoneIdProxy>(ZoneId::class.java, ZoneIdProxy::class.java, factory) {
override val revealSubclassesInSchema: Boolean = true
override fun toProxy(obj: ZoneId): ZoneIdProxy = ZoneIdProxy(obj.id)
override fun fromProxy(proxy: ZoneIdProxy): ZoneId = ZoneId.of(proxy.id)

View File

@ -20,6 +20,7 @@ import net.corda.testing.BOB_IDENTITY
import net.corda.testing.MEGA_CORP
import net.corda.testing.MEGA_CORP_PUBKEY
import net.corda.testing.withTestSerialization
import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.qpid.proton.amqp.*
import org.apache.qpid.proton.codec.DecoderImpl
import org.apache.qpid.proton.codec.EncoderImpl
@ -27,6 +28,7 @@ import org.junit.Assert.assertNotSame
import org.junit.Assert.assertSame
import org.junit.Ignore
import org.junit.Test
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.NotSerializableException
import java.math.BigDecimal
@ -180,7 +182,7 @@ class SerializationOutputTests {
assertTrue(Objects.deepEquals(desObj, desObj2) == expectDeserializedEqual)
// TODO: add some schema assertions to check correctly formed.
return desObj2
return desObj
}
@Test
@ -355,7 +357,7 @@ class SerializationOutputTests {
serdes(obj)
}
@Test(expected = NotSerializableException::class)
@Test
fun `test TreeMap`() {
val obj = TreeMap<Int, Foo>()
obj[456] = Foo("Fred", 123)
@ -720,8 +722,18 @@ class SerializationOutputTests {
serdes(obj, factory, factory2)
}
// TODO: ignored due to Proton-J bug https://issues.apache.org/jira/browse/PROTON-1551
@Ignore
@Test
fun `test month serialize`() {
val obj = Month.APRIL
serdes(obj)
}
@Test
fun `test day of week serialize`() {
val obj = DayOfWeek.FRIDAY
serdes(obj)
}
@Test
fun `test certificate holder serialize`() {
val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
@ -734,8 +746,6 @@ class SerializationOutputTests {
serdes(obj, factory, factory2)
}
// TODO: ignored due to Proton-J bug https://issues.apache.org/jira/browse/PROTON-1551
@Ignore
@Test
fun `test party and certificate serialize`() {
val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
@ -806,7 +816,6 @@ class SerializationOutputTests {
data class Bob(val byteArrays: List<ByteArray>)
@Ignore("Causes DeserializedParameterizedType.make() to fail")
@Test
fun `test list of byte arrays`() {
val a = ByteArray(1)
@ -815,7 +824,9 @@ class SerializationOutputTests {
val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
serdes(obj, factory, factory2)
val obj2 = serdes(obj, factory, factory2, false, false)
assertNotSame(obj2.byteArrays[0], obj2.byteArrays[2])
}
data class Vic(val a: List<String>, val b: List<String>)
@ -874,4 +885,88 @@ class SerializationOutputTests {
val objCopy = serdes(obj, factory, factory2, false, false)
assertNotSame(objCopy.a, objCopy.b)
}
@Test
fun `test StringBuffer serialize`() {
val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.StringBufferSerializer)
val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.StringBufferSerializer)
val obj = StringBuffer("Bob")
val obj2 = serdes(obj, factory, factory2, false, false)
assertEquals(obj.toString(), obj2.toString())
}
@Test
fun `test SimpleString serialize`() {
val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.SimpleStringSerializer)
val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.SimpleStringSerializer)
val obj = SimpleString("Bob")
serdes(obj, factory, factory2)
}
@Test
fun `test kotlin Unit serialize`() {
val obj = Unit
serdes(obj)
}
@Test
fun `test kotlin Pair serialize`() {
val obj = Pair("a", 3)
serdes(obj)
}
@Test
fun `test InputStream serialize`() {
val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.InputStreamSerializer)
val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.InputStreamSerializer)
val bytes = ByteArray(10) { it.toByte() }
val obj = ByteArrayInputStream(bytes)
val obj2 = serdes(obj, factory, factory2, expectedEqual = false, expectDeserializedEqual = false)
val obj3 = ByteArrayInputStream(bytes) // Can't use original since the stream pointer has moved.
assertEquals(obj3.available(), obj2.available())
assertEquals(obj3.read(), obj2.read())
}
@Test
fun `test EnumSet serialize`() {
val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.EnumSetSerializer(factory))
val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.EnumSetSerializer(factory2))
val obj = EnumSet.of(Month.APRIL, Month.AUGUST)
serdes(obj, factory, factory2)
}
@Test
fun `test BitSet serialize`() {
val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.BitSetSerializer(factory))
val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.BitSetSerializer(factory2))
val obj = BitSet.valueOf(kotlin.ByteArray(16) { it.toByte() }).get(0, 123)
serdes(obj, factory, factory2)
}
@Test
fun `test EnumMap serialize`() {
val obj = EnumMap<Month, Int>(Month::class.java)
obj[Month.APRIL] = Month.APRIL.value
obj[Month.AUGUST] = Month.AUGUST.value
serdes(obj)
}
}