Skip to content

Commit

Permalink
Transform Kotlin's EmptyList, EmptySet and EmptyMap into Java classes (
Browse files Browse the repository at this point in the history
…corda#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.
  • Loading branch information
chrisr3 authored Sep 26, 2017
1 parent be0e7a8 commit 8cc091b
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 36 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ buildscript {
ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion")
ext.fileupload_version = '1.3.2'
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.jansi_version = '1.14'
ext.hibernate_version = '5.2.6.Final'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,22 @@ import java.util.*
class CordaClassResolver(serializationContext: SerializationContext) : DefaultClassResolver() {
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. */
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
Expand Down Expand Up @@ -61,9 +74,9 @@ class CordaClassResolver(serializationContext: SerializationContext) : DefaultCl
}

override fun registerImplicit(type: Class<*>): Registration {

val targetType = typeForSerializationOf(type)
val objectInstance = try {
type.kotlin.objectInstance
targetType.kotlin.objectInstance
} catch (t: Throwable) {
null // objectInstance will throw if the type is something like a lambda
}
Expand All @@ -74,17 +87,21 @@ class CordaClassResolver(serializationContext: SerializationContext) : DefaultCl
kryo.references = true
val serializer = when {
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
FieldSerializer<Any>(kryo, type).apply { setIgnoreSyntheticFields(false) }
Throwable::class.java.isAssignableFrom(type) -> ThrowableSerializer(kryo, type)
else -> kryo.getDefaultSerializer(type)
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, targetType).apply { setIgnoreSyntheticFields(false) }
Throwable::class.java.isAssignableFrom(targetType) -> ThrowableSerializer(kryo, targetType)
else -> kryo.getDefaultSerializer(targetType)
}
return register(Registration(type, serializer, NAME.toInt()))
return register(Registration(targetType, serializer, NAME.toInt()))
} finally {
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
private class KotlinObjectSerializer(private val objectInstance: Any) : Serializer<Any>() {
override fun read(kryo: Kryo, input: Input, type: Class<Any>): Any = objectInstance
Expand Down Expand Up @@ -128,10 +145,6 @@ interface MutableClassWhitelist : ClassWhitelist {
fun add(entry: Class<*>)
}

object EmptyWhitelist : ClassWhitelist {
override fun hasListed(type: Class<*>): Boolean = false
}

class BuiltInExceptionsWhitelist : ClassWhitelist {
companion object {
private val packageName = "^(?:java|kotlin)(?:[.]|$)".toRegex()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,14 @@ class DefaultWhitelist : CordaPluginRegistry() {
Notification::class.java,
Notification.Kind::class.java,
ArrayList::class.java,
listOf<Any>().javaClass, // EmptyList
Pair::class.java,
ByteArray::class.java,
UUID::class.java,
LinkedHashSet::class.java,
setOf<Unit>().javaClass, // EmptySet
Currency::class.java,
listOf(Unit).javaClass, // SingletonList
setOf(Unit).javaClass, // SingletonSet
mapOf(Unit to Unit).javaClass, // SingletonSet
mapOf(Unit to Unit).javaClass, // SingletonMap
NetworkHostAndPort::class.java,
SimpleString::class.java,
KryoException::class.java, // TODO: Will be removed when we migrate away from Kryo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ package net.corda.nodeapi.internal.serialization
import com.esotericsoftware.kryo.*
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.util.DefaultClassResolver
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.serialization.CordaSerializable
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationFactory
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.*
import net.corda.core.utilities.ByteSequence
import net.corda.nodeapi.internal.AttachmentsClassLoader
import net.corda.nodeapi.internal.AttachmentsClassLoaderTests
Expand All @@ -19,6 +20,7 @@ import org.junit.rules.ExpectedException
import java.lang.IllegalStateException
import java.sql.Connection
import java.util.*
import kotlin.test.*

@CordaSerializable
enum class Foo {
Expand Down Expand Up @@ -56,7 +58,6 @@ open class SerializableViaSubInterface : SerializableSubInterface

class SerializableViaSuperSubInterface : SerializableViaSubInterface()


@CordaSerializable
class CustomSerializable : KryoSerializable {
override fun read(kryo: Kryo?, input: Input?) {
Expand All @@ -79,7 +80,17 @@ class DefaultSerializableSerializer : Serializer<DefaultSerializable>() {
}
}

object EmptyWhitelist : ClassWhitelist {
override fun hasListed(type: Class<*>): Boolean = false
}

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() {
override fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T {
TODO("not implemented")
Expand All @@ -88,7 +99,6 @@ class CordaClassResolverTests {
override fun <T : Any> serialize(obj: T, context: SerializationContext): SerializedBytes<T> {
TODO("not implemented")
}

}

private val emptyWhitelistContext: SerializationContext = SerializationContextImpl(KryoHeaderV0_1, this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, SerializationContext.UseCase.P2P)
Expand Down Expand Up @@ -197,6 +207,69 @@ class CordaClassResolverTests {
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>()
@Test
fun `Check blacklisted subclass`() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,8 @@ import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.time.Instant
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import java.util.Collections
import kotlin.test.*

class KryoTests : TestDependencyInjectionBase() {
private lateinit var factory: SerializationFactory
Expand Down Expand Up @@ -113,6 +112,27 @@ class KryoTests : TestDependencyInjectionBase() {
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
fun `InputStream serialisation`() {
val rubbish = ByteArray(12345, { (it * it * 0.12345).toByte() })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package net.corda.nodeapi.internal.serialization

import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.util.DefaultClassResolver
import net.corda.core.serialization.*
import net.corda.node.services.statemachine.SessionData
import net.corda.testing.TestDependencyInjectionBase
import net.corda.testing.amqpSpecific
import org.assertj.core.api.Assertions
import org.junit.Assert.*
import org.junit.Test
import java.io.ByteArrayOutputStream
import java.io.NotSerializableException
import kotlin.test.assertEquals
import java.nio.charset.StandardCharsets.*
import java.util.*

class ListsSerializationTest : TestDependencyInjectionBase() {
private companion object {
val javaEmptyListClass = Collections.emptyList<Any>().javaClass
}

@Test
fun `check list can be serialized as root of serialization graph`() {
Expand All @@ -23,16 +28,32 @@ class ListsSerializationTest : TestDependencyInjectionBase() {

@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)
}
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
Expand All @@ -55,4 +76,8 @@ internal inline fun<reified T : Any> assertEqualAfterRoundTripSerialization(obj:
val deserializedInstance = serializedForm.deserialize()

assertEquals(obj, deserializedInstance)
}
}

internal fun String.toAscii(): ByteArray = toByteArray(US_ASCII).apply {
this[lastIndex] = (this[lastIndex] + 0x80).toByte()
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
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.serialize
import net.corda.node.services.statemachine.SessionData
import net.corda.testing.TestDependencyInjectionBase
import net.corda.testing.amqpSpecific
import org.assertj.core.api.Assertions
import org.junit.Assert.assertArrayEquals
import org.junit.Test
import org.bouncycastle.asn1.x500.X500Name
import java.io.ByteArrayOutputStream
import java.io.NotSerializableException
import java.util.*

class MapsSerializationTest : TestDependencyInjectionBase() {

private val smallMap = mapOf("foo" to "bar", "buzz" to "bull")
private companion object {
val javaEmptyMapClass = Collections.emptyMap<Any, Any>().javaClass
val smallMap = mapOf("foo" to "bar", "buzz" to "bull")
}

@Test
fun `check EmptyMap serialization`() = amqpSpecific<MapsSerializationTest>("kotlin.collections.EmptyMap is not enabled for Kryo serialization") {
Expand Down Expand Up @@ -54,4 +61,18 @@ class MapsSerializationTest : TestDependencyInjectionBase() {
MyKey(10.0) to MyValue(X500Name("CN=ten")))
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)
}
}
Loading

0 comments on commit 8cc091b

Please sign in to comment.