mirror of
https://github.com/corda/corda.git
synced 2025-04-06 19:07:08 +00:00
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:
parent
be0e7a8877
commit
8cc091b3e1
@ -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'
|
||||
|
@ -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
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
@ -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 {
|
||||
@ -56,7 +58,6 @@ open class SerializableViaSubInterface : SerializableSubInterface
|
||||
|
||||
class SerializableViaSuperSubInterface : SerializableViaSubInterface()
|
||||
|
||||
|
||||
@CordaSerializable
|
||||
class CustomSerializable : KryoSerializable {
|
||||
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 {
|
||||
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")
|
||||
@ -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)
|
||||
@ -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`() {
|
||||
|
@ -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
|
||||
@ -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() })
|
||||
|
@ -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`() {
|
||||
@ -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
|
||||
@ -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()
|
||||
}
|
||||
|
@ -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") {
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user