Transform Kotlin's EmptyList, EmptySet and EmptyMap into Java classes (#1550)

* Transform Kotlin's EmptyList, EmptySet and EmptyMap into Java classes before serialising them.
* Transform Kotlin's EmptyList, EmptySet and EmptyMap to their unmodifiable Java equivalents.
This commit is contained in:
Chris Rankin
2017-09-26 08:33:30 +01:00
committed by GitHub
parent be0e7a8877
commit 8cc091b3e1
8 changed files with 240 additions and 36 deletions

View File

@ -36,7 +36,7 @@ buildscript {
ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion") ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion")
ext.fileupload_version = '1.3.2' ext.fileupload_version = '1.3.2'
ext.junit_version = '4.12' ext.junit_version = '4.12'
ext.mockito_version = '1.10.19' ext.mockito_version = '2.10.0'
ext.jopt_simple_version = '5.0.2' ext.jopt_simple_version = '5.0.2'
ext.jansi_version = '1.14' ext.jansi_version = '1.14'
ext.hibernate_version = '5.2.6.Final' ext.hibernate_version = '5.2.6.Final'

View File

@ -25,9 +25,22 @@ import java.util.*
class CordaClassResolver(serializationContext: SerializationContext) : DefaultClassResolver() { class CordaClassResolver(serializationContext: SerializationContext) : DefaultClassResolver() {
val whitelist: ClassWhitelist = TransientClassWhiteList(serializationContext.whitelist) val whitelist: ClassWhitelist = TransientClassWhiteList(serializationContext.whitelist)
/*
* These classes are assignment-compatible Java equivalents of Kotlin classes.
* The point is that we do not want to send Kotlin types "over the wire" via RPC.
*/
private val javaAliases: Map<Class<*>, Class<*>> = mapOf(
listOf<Any>().javaClass to Collections.emptyList<Any>().javaClass,
setOf<Any>().javaClass to Collections.emptySet<Any>().javaClass,
mapOf<Any, Any>().javaClass to Collections.emptyMap<Any, Any>().javaClass
)
private fun typeForSerializationOf(type: Class<*>): Class<*> = javaAliases[type] ?: type
/** Returns the registration for the specified class, or null if the class is not registered. */ /** Returns the registration for the specified class, or null if the class is not registered. */
override fun getRegistration(type: Class<*>): Registration? { override fun getRegistration(type: Class<*>): Registration? {
return super.getRegistration(type) ?: checkClass(type) val targetType = typeForSerializationOf(type)
return super.getRegistration(targetType) ?: checkClass(targetType)
} }
private var whitelistEnabled = true private var whitelistEnabled = true
@ -61,9 +74,9 @@ class CordaClassResolver(serializationContext: SerializationContext) : DefaultCl
} }
override fun registerImplicit(type: Class<*>): Registration { override fun registerImplicit(type: Class<*>): Registration {
val targetType = typeForSerializationOf(type)
val objectInstance = try { val objectInstance = try {
type.kotlin.objectInstance targetType.kotlin.objectInstance
} catch (t: Throwable) { } catch (t: Throwable) {
null // objectInstance will throw if the type is something like a lambda null // objectInstance will throw if the type is something like a lambda
} }
@ -74,17 +87,21 @@ class CordaClassResolver(serializationContext: SerializationContext) : DefaultCl
kryo.references = true kryo.references = true
val serializer = when { val serializer = when {
objectInstance != null -> KotlinObjectSerializer(objectInstance) objectInstance != null -> KotlinObjectSerializer(objectInstance)
kotlin.jvm.internal.Lambda::class.java.isAssignableFrom(type) -> // Kotlin lambdas extend this class and any captured variables are stored in synthetic fields kotlin.jvm.internal.Lambda::class.java.isAssignableFrom(targetType) -> // Kotlin lambdas extend this class and any captured variables are stored in synthetic fields
FieldSerializer<Any>(kryo, type).apply { setIgnoreSyntheticFields(false) } FieldSerializer<Any>(kryo, targetType).apply { setIgnoreSyntheticFields(false) }
Throwable::class.java.isAssignableFrom(type) -> ThrowableSerializer(kryo, type) Throwable::class.java.isAssignableFrom(targetType) -> ThrowableSerializer(kryo, targetType)
else -> kryo.getDefaultSerializer(type) else -> kryo.getDefaultSerializer(targetType)
} }
return register(Registration(type, serializer, NAME.toInt())) return register(Registration(targetType, serializer, NAME.toInt()))
} finally { } finally {
kryo.references = references kryo.references = references
} }
} }
override fun writeName(output: Output, type: Class<*>, registration: Registration) {
super.writeName(output, registration.type ?: type, registration)
}
// Trivial Serializer which simply returns the given instance, which we already know is a Kotlin object // Trivial Serializer which simply returns the given instance, which we already know is a Kotlin object
private class KotlinObjectSerializer(private val objectInstance: Any) : Serializer<Any>() { private class KotlinObjectSerializer(private val objectInstance: Any) : Serializer<Any>() {
override fun read(kryo: Kryo, input: Input, type: Class<Any>): Any = objectInstance override fun read(kryo: Kryo, input: Input, type: Class<Any>): Any = objectInstance
@ -128,10 +145,6 @@ interface MutableClassWhitelist : ClassWhitelist {
fun add(entry: Class<*>) fun add(entry: Class<*>)
} }
object EmptyWhitelist : ClassWhitelist {
override fun hasListed(type: Class<*>): Boolean = false
}
class BuiltInExceptionsWhitelist : ClassWhitelist { class BuiltInExceptionsWhitelist : ClassWhitelist {
companion object { companion object {
private val packageName = "^(?:java|kotlin)(?:[.]|$)".toRegex() private val packageName = "^(?:java|kotlin)(?:[.]|$)".toRegex()

View File

@ -19,16 +19,14 @@ class DefaultWhitelist : CordaPluginRegistry() {
Notification::class.java, Notification::class.java,
Notification.Kind::class.java, Notification.Kind::class.java,
ArrayList::class.java, ArrayList::class.java,
listOf<Any>().javaClass, // EmptyList
Pair::class.java, Pair::class.java,
ByteArray::class.java, ByteArray::class.java,
UUID::class.java, UUID::class.java,
LinkedHashSet::class.java, LinkedHashSet::class.java,
setOf<Unit>().javaClass, // EmptySet
Currency::class.java, Currency::class.java,
listOf(Unit).javaClass, // SingletonList listOf(Unit).javaClass, // SingletonList
setOf(Unit).javaClass, // SingletonSet setOf(Unit).javaClass, // SingletonSet
mapOf(Unit to Unit).javaClass, // SingletonSet mapOf(Unit to Unit).javaClass, // SingletonMap
NetworkHostAndPort::class.java, NetworkHostAndPort::class.java,
SimpleString::class.java, SimpleString::class.java,
KryoException::class.java, // TODO: Will be removed when we migrate away from Kryo KryoException::class.java, // TODO: Will be removed when we migrate away from Kryo

View File

@ -3,12 +3,13 @@ package net.corda.nodeapi.internal.serialization
import com.esotericsoftware.kryo.* import com.esotericsoftware.kryo.*
import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.util.DefaultClassResolver
import com.esotericsoftware.kryo.util.MapReferenceResolver import com.esotericsoftware.kryo.util.MapReferenceResolver
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.verify
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.node.services.AttachmentStorage import net.corda.core.node.services.AttachmentStorage
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.*
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationFactory
import net.corda.core.serialization.SerializedBytes
import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.ByteSequence
import net.corda.nodeapi.internal.AttachmentsClassLoader import net.corda.nodeapi.internal.AttachmentsClassLoader
import net.corda.nodeapi.internal.AttachmentsClassLoaderTests import net.corda.nodeapi.internal.AttachmentsClassLoaderTests
@ -19,6 +20,7 @@ import org.junit.rules.ExpectedException
import java.lang.IllegalStateException import java.lang.IllegalStateException
import java.sql.Connection import java.sql.Connection
import java.util.* import java.util.*
import kotlin.test.*
@CordaSerializable @CordaSerializable
enum class Foo { enum class Foo {
@ -56,7 +58,6 @@ open class SerializableViaSubInterface : SerializableSubInterface
class SerializableViaSuperSubInterface : SerializableViaSubInterface() class SerializableViaSuperSubInterface : SerializableViaSubInterface()
@CordaSerializable @CordaSerializable
class CustomSerializable : KryoSerializable { class CustomSerializable : KryoSerializable {
override fun read(kryo: Kryo?, input: Input?) { override fun read(kryo: Kryo?, input: Input?) {
@ -79,7 +80,17 @@ class DefaultSerializableSerializer : Serializer<DefaultSerializable>() {
} }
} }
object EmptyWhitelist : ClassWhitelist {
override fun hasListed(type: Class<*>): Boolean = false
}
class CordaClassResolverTests { class CordaClassResolverTests {
private companion object {
val emptyListClass = listOf<Any>().javaClass
val emptySetClass = setOf<Any>().javaClass
val emptyMapClass = mapOf<Any, Any>().javaClass
}
val factory: SerializationFactory = object : SerializationFactory() { val factory: SerializationFactory = object : SerializationFactory() {
override fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T { override fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T {
TODO("not implemented") TODO("not implemented")
@ -88,7 +99,6 @@ class CordaClassResolverTests {
override fun <T : Any> serialize(obj: T, context: SerializationContext): SerializedBytes<T> { override fun <T : Any> serialize(obj: T, context: SerializationContext): SerializedBytes<T> {
TODO("not implemented") TODO("not implemented")
} }
} }
private val emptyWhitelistContext: SerializationContext = SerializationContextImpl(KryoHeaderV0_1, this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, SerializationContext.UseCase.P2P) private val emptyWhitelistContext: SerializationContext = SerializationContextImpl(KryoHeaderV0_1, this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, SerializationContext.UseCase.P2P)
@ -197,6 +207,69 @@ class CordaClassResolverTests {
resolver.getRegistration(HashSet::class.java) resolver.getRegistration(HashSet::class.java)
} }
@Test
fun `Kotlin EmptyList not registered`() {
val resolver = CordaClassResolver(allButBlacklistedContext)
assertNull(resolver.getRegistration(emptyListClass))
}
@Test
fun `Kotlin EmptyList registers as Java emptyList`() {
val javaEmptyListClass = Collections.emptyList<Any>().javaClass
val kryo = mock<Kryo>()
val resolver = CordaClassResolver(allButBlacklistedContext).apply { setKryo(kryo) }
whenever(kryo.getDefaultSerializer(javaEmptyListClass)).thenReturn(DefaultSerializableSerializer())
val registration = resolver.registerImplicit(emptyListClass)
assertNotNull(registration)
assertEquals(javaEmptyListClass, registration.type)
assertEquals(DefaultClassResolver.NAME.toInt(), registration.id)
verify(kryo).getDefaultSerializer(javaEmptyListClass)
assertEquals(registration, resolver.getRegistration(emptyListClass))
}
@Test
fun `Kotlin EmptySet not registered`() {
val resolver = CordaClassResolver(allButBlacklistedContext)
assertNull(resolver.getRegistration(emptySetClass))
}
@Test
fun `Kotlin EmptySet registers as Java emptySet`() {
val javaEmptySetClass = Collections.emptySet<Any>().javaClass
val kryo = mock<Kryo>()
val resolver = CordaClassResolver(allButBlacklistedContext).apply { setKryo(kryo) }
whenever(kryo.getDefaultSerializer(javaEmptySetClass)).thenReturn(DefaultSerializableSerializer())
val registration = resolver.registerImplicit(emptySetClass)
assertNotNull(registration)
assertEquals(javaEmptySetClass, registration.type)
assertEquals(DefaultClassResolver.NAME.toInt(), registration.id)
verify(kryo).getDefaultSerializer(javaEmptySetClass)
assertEquals(registration, resolver.getRegistration(emptySetClass))
}
@Test
fun `Kotlin EmptyMap not registered`() {
val resolver = CordaClassResolver(allButBlacklistedContext)
assertNull(resolver.getRegistration(emptyMapClass))
}
@Test
fun `Kotlin EmptyMap registers as Java emptyMap`() {
val javaEmptyMapClass = Collections.emptyMap<Any, Any>().javaClass
val kryo = mock<Kryo>()
val resolver = CordaClassResolver(allButBlacklistedContext).apply { setKryo(kryo) }
whenever(kryo.getDefaultSerializer(javaEmptyMapClass)).thenReturn(DefaultSerializableSerializer())
val registration = resolver.registerImplicit(emptyMapClass)
assertNotNull(registration)
assertEquals(javaEmptyMapClass, registration.type)
assertEquals(DefaultClassResolver.NAME.toInt(), registration.id)
verify(kryo).getDefaultSerializer(javaEmptyMapClass)
assertEquals(registration, resolver.getRegistration(emptyMapClass))
}
open class SubHashSet<E> : HashSet<E>() open class SubHashSet<E> : HashSet<E>()
@Test @Test
fun `Check blacklisted subclass`() { fun `Check blacklisted subclass`() {

View File

@ -25,9 +25,8 @@ import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
import java.time.Instant import java.time.Instant
import kotlin.test.assertEquals import java.util.Collections
import kotlin.test.assertNotNull import kotlin.test.*
import kotlin.test.assertTrue
class KryoTests : TestDependencyInjectionBase() { class KryoTests : TestDependencyInjectionBase() {
private lateinit var factory: SerializationFactory private lateinit var factory: SerializationFactory
@ -113,6 +112,27 @@ class KryoTests : TestDependencyInjectionBase() {
assertThat(deserialised).isSameAs(TestSingleton) assertThat(deserialised).isSameAs(TestSingleton)
} }
@Test
fun `check Kotlin EmptyList can be serialised`() {
val deserialisedList: List<Int> = emptyList<Int>().serialize(factory, context).deserialize(factory, context)
assertEquals(0, deserialisedList.size)
assertEquals<Any>(Collections.emptyList<Int>().javaClass, deserialisedList.javaClass)
}
@Test
fun `check Kotlin EmptySet can be serialised`() {
val deserialisedSet: Set<Int> = emptySet<Int>().serialize(factory, context).deserialize(factory, context)
assertEquals(0, deserialisedSet.size)
assertEquals<Any>(Collections.emptySet<Int>().javaClass, deserialisedSet.javaClass)
}
@Test
fun `check Kotlin EmptyMap can be serialised`() {
val deserialisedMap: Map<Int, Int> = emptyMap<Int, Int>().serialize(factory, context).deserialize(factory, context)
assertEquals(0, deserialisedMap.size)
assertEquals<Any>(Collections.emptyMap<Int, Int>().javaClass, deserialisedMap.javaClass)
}
@Test @Test
fun `InputStream serialisation`() { fun `InputStream serialisation`() {
val rubbish = ByteArray(12345, { (it * it * 0.12345).toByte() }) val rubbish = ByteArray(12345, { (it * it * 0.12345).toByte() })

View File

@ -1,18 +1,23 @@
package net.corda.nodeapi.internal.serialization package net.corda.nodeapi.internal.serialization
import net.corda.core.serialization.CordaSerializable import com.esotericsoftware.kryo.Kryo
import net.corda.core.serialization.SerializedBytes import com.esotericsoftware.kryo.util.DefaultClassResolver
import net.corda.core.serialization.deserialize import net.corda.core.serialization.*
import net.corda.core.serialization.serialize
import net.corda.node.services.statemachine.SessionData import net.corda.node.services.statemachine.SessionData
import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.TestDependencyInjectionBase
import net.corda.testing.amqpSpecific import net.corda.testing.amqpSpecific
import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions
import org.junit.Assert.*
import org.junit.Test import org.junit.Test
import java.io.ByteArrayOutputStream
import java.io.NotSerializableException import java.io.NotSerializableException
import kotlin.test.assertEquals import java.nio.charset.StandardCharsets.*
import java.util.*
class ListsSerializationTest : TestDependencyInjectionBase() { class ListsSerializationTest : TestDependencyInjectionBase() {
private companion object {
val javaEmptyListClass = Collections.emptyList<Any>().javaClass
}
@Test @Test
fun `check list can be serialized as root of serialization graph`() { fun `check list can be serialized as root of serialization graph`() {
@ -23,16 +28,32 @@ class ListsSerializationTest : TestDependencyInjectionBase() {
@Test @Test
fun `check list can be serialized as part of SessionData`() { fun `check list can be serialized as part of SessionData`() {
run { run {
val sessionData = SessionData(123, listOf(1)) val sessionData = SessionData(123, listOf(1))
assertEqualAfterRoundTripSerialization(sessionData) assertEqualAfterRoundTripSerialization(sessionData)
} }
run { run {
val sessionData = SessionData(123, listOf(1, 2)) val sessionData = SessionData(123, listOf(1, 2))
assertEqualAfterRoundTripSerialization(sessionData) assertEqualAfterRoundTripSerialization(sessionData)
} }
run {
val sessionData = SessionData(123, emptyList<Int>())
assertEqualAfterRoundTripSerialization(sessionData)
}
}
@Test
fun `check empty list serialises as Java emptyList`() {
val nameID = 0
val serializedForm = emptyList<Int>().serialize()
val output = ByteArrayOutputStream().apply {
write(KryoHeaderV0_1.bytes)
write(DefaultClassResolver.NAME + 2)
write(nameID)
write(javaEmptyListClass.name.toAscii())
write(Kryo.NOT_NULL.toInt())
}
assertArrayEquals(output.toByteArray(), serializedForm.bytes)
} }
@CordaSerializable @CordaSerializable
@ -56,3 +77,7 @@ internal inline fun<reified T : Any> assertEqualAfterRoundTripSerialization(obj:
assertEquals(obj, deserializedInstance) assertEquals(obj, deserializedInstance)
} }
internal fun String.toAscii(): ByteArray = toByteArray(US_ASCII).apply {
this[lastIndex] = (this[lastIndex] + 0x80).toByte()
}

View File

@ -1,18 +1,25 @@
package net.corda.nodeapi.internal.serialization package net.corda.nodeapi.internal.serialization
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.util.DefaultClassResolver
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.node.services.statemachine.SessionData import net.corda.node.services.statemachine.SessionData
import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.TestDependencyInjectionBase
import net.corda.testing.amqpSpecific import net.corda.testing.amqpSpecific
import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions
import org.junit.Assert.assertArrayEquals
import org.junit.Test import org.junit.Test
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import java.io.ByteArrayOutputStream
import java.io.NotSerializableException import java.io.NotSerializableException
import java.util.*
class MapsSerializationTest : TestDependencyInjectionBase() { class MapsSerializationTest : TestDependencyInjectionBase() {
private companion object {
private val smallMap = mapOf("foo" to "bar", "buzz" to "bull") val javaEmptyMapClass = Collections.emptyMap<Any, Any>().javaClass
val smallMap = mapOf("foo" to "bar", "buzz" to "bull")
}
@Test @Test
fun `check EmptyMap serialization`() = amqpSpecific<MapsSerializationTest>("kotlin.collections.EmptyMap is not enabled for Kryo serialization") { fun `check EmptyMap serialization`() = amqpSpecific<MapsSerializationTest>("kotlin.collections.EmptyMap is not enabled for Kryo serialization") {
@ -54,4 +61,18 @@ class MapsSerializationTest : TestDependencyInjectionBase() {
MyKey(10.0) to MyValue(X500Name("CN=ten"))) MyKey(10.0) to MyValue(X500Name("CN=ten")))
assertEqualAfterRoundTripSerialization(myMap) assertEqualAfterRoundTripSerialization(myMap)
} }
@Test
fun `check empty map serialises as Java emptytMap`() {
val nameID = 0
val serializedForm = emptyMap<Int, Int>().serialize()
val output = ByteArrayOutputStream().apply {
write(KryoHeaderV0_1.bytes)
write(DefaultClassResolver.NAME + 2)
write(nameID)
write(javaEmptyMapClass.name.toAscii())
write(Kryo.NOT_NULL.toInt())
}
assertArrayEquals(output.toByteArray(), serializedForm.bytes)
}
} }

View File

@ -0,0 +1,54 @@
package net.corda.nodeapi.internal.serialization
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.util.DefaultClassResolver
import net.corda.core.serialization.serialize
import net.corda.node.services.statemachine.SessionData
import net.corda.testing.TestDependencyInjectionBase
import org.junit.Assert.*
import org.junit.Test
import java.io.ByteArrayOutputStream
import java.util.*
class SetsSerializationTest : TestDependencyInjectionBase() {
private companion object {
val javaEmptySetClass = Collections.emptySet<Any>().javaClass
}
@Test
fun `check set can be serialized as root of serialization graph`() {
assertEqualAfterRoundTripSerialization(emptySet<Int>())
assertEqualAfterRoundTripSerialization(setOf(1))
assertEqualAfterRoundTripSerialization(setOf(1, 2))
}
@Test
fun `check set can be serialized as part of SessionData`() {
run {
val sessionData = SessionData(123, setOf(1))
assertEqualAfterRoundTripSerialization(sessionData)
}
run {
val sessionData = SessionData(123, setOf(1, 2))
assertEqualAfterRoundTripSerialization(sessionData)
}
run {
val sessionData = SessionData(123, emptySet<Int>())
assertEqualAfterRoundTripSerialization(sessionData)
}
}
@Test
fun `check empty set serialises as Java emptySet`() {
val nameID = 0
val serializedForm = emptySet<Int>().serialize()
val output = ByteArrayOutputStream().apply {
write(KryoHeaderV0_1.bytes)
write(DefaultClassResolver.NAME + 2)
write(nameID)
write(javaEmptySetClass.name.toAscii())
write(Kryo.NOT_NULL.toInt())
}
assertArrayEquals(output.toByteArray(), serializedForm.bytes)
}
}