diff --git a/build.gradle b/build.gradle index c293e7c88a..1c69429c03 100644 --- a/build.gradle +++ b/build.gradle @@ -382,20 +382,20 @@ artifactory { defaults { // Root project applies the plugin (for this block) but does not need to be published - if(project != rootProject) { + if (project != rootProject) { publications(project.extensions.publish.name()) } } } } -task generateApi(type: net.corda.plugins.GenerateApi){ +task generateApi(type: net.corda.plugins.GenerateApi) { baseName = "api-corda" } // This exists to reduce CI build time when the envvar is set (can save up to 40 minutes) -if(file('corda-docs-only-build').exists() || (System.getenv('CORDA_DOCS_ONLY_BUILD') != null)) { - if(file('corda-docs-only-build').exists()) { +if (file('corda-docs-only-build').exists() || (System.getenv('CORDA_DOCS_ONLY_BUILD') != null)) { + if (file('corda-docs-only-build').exists()) { logger.info("Tests are disabled due to presence of file 'corda-docs-only-build' in the project root") } else { logger.info("Tests are disabled due to the presence of envvar CORDA_DOCS_ONLY_BUILD") @@ -407,7 +407,7 @@ if(file('corda-docs-only-build').exists() || (System.getenv('CORDA_DOCS_ONLY_BUI } it.afterEvaluate { - if(it.tasks.findByName("integrationTest") != null) { + if (it.tasks.findByName("integrationTest") != null) { integrationTest { exclude '*/**' } @@ -415,7 +415,7 @@ if(file('corda-docs-only-build').exists() || (System.getenv('CORDA_DOCS_ONLY_BUI } it.afterEvaluate { - if(it.tasks.findByName("smokeTest") != null) { + if (it.tasks.findByName("smokeTest") != null) { smokeTest { exclude '*/**' } diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt index 1f82119356..aa0e6618aa 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt @@ -10,6 +10,7 @@ import net.corda.core.serialization.internal.SerializationEnvironmentImpl import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.serialization.internal.* import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme +import net.corda.serialization.internal.amqp.AccessOrderLinkedHashMap import net.corda.serialization.internal.amqp.SerializerFactory import net.corda.serialization.internal.amqp.amqpMagic import net.corda.serialization.internal.amqp.custom.RxNotificationSerializer @@ -21,12 +22,12 @@ import java.util.concurrent.ConcurrentHashMap */ class AMQPClientSerializationScheme( cordappCustomSerializers: Set>, - serializerFactoriesForContexts: MutableMap, SerializerFactory> + serializerFactoriesForContexts: AccessOrderLinkedHashMap, SerializerFactory> ) : AbstractAMQPSerializationScheme(cordappCustomSerializers, serializerFactoriesForContexts) { - constructor(cordapps: List) : this(cordapps.customSerializers, ConcurrentHashMap()) + constructor(cordapps: List) : this(cordapps.customSerializers, AccessOrderLinkedHashMap { 128 }) @Suppress("UNUSED") - constructor() : this(emptySet(), ConcurrentHashMap()) + constructor() : this(emptySet(), AccessOrderLinkedHashMap { 128 }) companion object { /** Call from main only. */ @@ -52,7 +53,7 @@ class AMQPClientSerializationScheme( } override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory { - return SerializerFactory(context.whitelist, ClassLoader.getSystemClassLoader(), context.lenientCarpenterEnabled).apply { + return SerializerFactory(context.whitelist, context.deserializationClassLoader, context.lenientCarpenterEnabled).apply { register(RpcClientObservableSerializer) register(RpcClientCordaFutureSerializer(this)) register(RxNotificationSerializer(this)) diff --git a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/LocalSerializationRule.kt b/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/LocalSerializationRule.kt index 184f7f72d9..05c8c3bc5c 100644 --- a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/LocalSerializationRule.kt +++ b/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/LocalSerializationRule.kt @@ -2,12 +2,13 @@ package net.corda.deterministic.common import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationContext.UseCase.* +import net.corda.core.serialization.SerializationContext.UseCase.P2P import net.corda.core.serialization.SerializationCustomSerializer import net.corda.core.serialization.internal.SerializationEnvironmentImpl import net.corda.core.serialization.internal._contextSerializationEnv import net.corda.serialization.internal.* import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme +import net.corda.serialization.internal.amqp.AccessOrderLinkedHashMap import net.corda.serialization.internal.amqp.SerializerFactory import net.corda.serialization.internal.amqp.amqpMagic import org.junit.rules.TestRule @@ -21,13 +22,13 @@ class LocalSerializationRule(private val label: String) : TestRule { private companion object { private val AMQP_P2P_CONTEXT = SerializationContextImpl( - amqpMagic, - LocalSerializationRule::class.java.classLoader, - GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()), - emptyMap(), - true, - P2P, - null + amqpMagic, + LocalSerializationRule::class.java.classLoader, + GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()), + emptyMap(), + true, + P2P, + null ) } @@ -59,7 +60,7 @@ class LocalSerializationRule(private val label: String) : TestRule { private fun createTestSerializationEnv(): SerializationEnvironmentImpl { val factory = SerializationFactoryImpl(mutableMapOf()).apply { - registerScheme(AMQPSerializationScheme(emptySet(), mutableMapOf())) + registerScheme(AMQPSerializationScheme(emptySet(), AccessOrderLinkedHashMap(128))) } return object : SerializationEnvironmentImpl(factory, AMQP_P2P_CONTEXT) { override fun toString() = "testSerializationEnv($label)" @@ -67,8 +68,8 @@ class LocalSerializationRule(private val label: String) : TestRule { } private class AMQPSerializationScheme( - cordappCustomSerializers: Set>, - serializerFactoriesForContexts: MutableMap, SerializerFactory> + cordappCustomSerializers: Set>, + serializerFactoriesForContexts: AccessOrderLinkedHashMap, SerializerFactory> ) : AbstractAMQPSerializationScheme(cordappCustomSerializers, serializerFactoriesForContexts) { override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory { throw UnsupportedOperationException() diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt index cb6a4f86d0..c1332c8954 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt @@ -174,7 +174,6 @@ interface SerializationContext { /** * Helper method to return a new context based on this context with the deserialization class loader changed. */ - @DeleteForDJVM fun withClassLoader(classLoader: ClassLoader): SerializationContext /** diff --git a/node/src/main/kotlin/net/corda/node/serialization/amqp/AMQPServerSerializationScheme.kt b/node/src/main/kotlin/net/corda/node/serialization/amqp/AMQPServerSerializationScheme.kt index 2f28d692a2..e1183be6b7 100644 --- a/node/src/main/kotlin/net/corda/node/serialization/amqp/AMQPServerSerializationScheme.kt +++ b/node/src/main/kotlin/net/corda/node/serialization/amqp/AMQPServerSerializationScheme.kt @@ -6,6 +6,7 @@ import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationCustomSerializer import net.corda.serialization.internal.CordaSerializationMagic import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme +import net.corda.serialization.internal.amqp.AccessOrderLinkedHashMap import net.corda.serialization.internal.amqp.SerializerFactory import net.corda.serialization.internal.amqp.custom.RxNotificationSerializer import java.util.concurrent.ConcurrentHashMap @@ -16,11 +17,11 @@ import java.util.concurrent.ConcurrentHashMap */ class AMQPServerSerializationScheme( cordappCustomSerializers: Set>, - serializerFactoriesForContexts: MutableMap, SerializerFactory> + serializerFactoriesForContexts: AccessOrderLinkedHashMap, SerializerFactory> ) : AbstractAMQPSerializationScheme(cordappCustomSerializers, serializerFactoriesForContexts) { - constructor(cordapps: List) : this(cordapps.customSerializers, ConcurrentHashMap()) + constructor(cordapps: List) : this(cordapps.customSerializers, AccessOrderLinkedHashMap { 128 }) - constructor() : this(emptySet(), ConcurrentHashMap()) + constructor() : this(emptySet(), AccessOrderLinkedHashMap { 128 }) override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory { throw UnsupportedOperationException() diff --git a/node/src/test/kotlin/net/corda/node/internal/serialization/RoundTripObservableSerializerTests.kt b/node/src/test/kotlin/net/corda/node/internal/serialization/RoundTripObservableSerializerTests.kt index 0620f0f115..7289a9a959 100644 --- a/node/src/test/kotlin/net/corda/node/internal/serialization/RoundTripObservableSerializerTests.kt +++ b/node/src/test/kotlin/net/corda/node/internal/serialization/RoundTripObservableSerializerTests.kt @@ -13,6 +13,7 @@ import net.corda.node.internal.serialization.testutils.serializationContext import net.corda.node.serialization.amqp.RpcServerObservableSerializer import net.corda.node.services.messaging.ObservableSubscription import net.corda.nodeapi.RPCApi +import net.corda.serialization.internal.amqp.AccessOrderLinkedHashMap import net.corda.serialization.internal.amqp.DeserializationInput import net.corda.serialization.internal.amqp.SerializationOutput import org.apache.activemq.artemis.api.core.SimpleString @@ -59,7 +60,7 @@ class RoundTripObservableSerializerTests { @Test fun roundTripTest1() { val serializationScheme = AMQPRoundTripRPCSerializationScheme( - serializationContext, emptySet(), ConcurrentHashMap()) + serializationContext, emptySet(), AccessOrderLinkedHashMap { 128 }) // Fake up a message ID, needs to be used on both "sides". The server setting it in the subscriptionMap, // the client as a property of the deserializer which, in the actual RPC client, is pulled off of diff --git a/node/src/test/kotlin/net/corda/node/internal/serialization/testutils/AMQPTestSerialiationScheme.kt b/node/src/test/kotlin/net/corda/node/internal/serialization/testutils/AMQPTestSerialiationScheme.kt index 82fb3eaa70..c397920197 100644 --- a/node/src/test/kotlin/net/corda/node/internal/serialization/testutils/AMQPTestSerialiationScheme.kt +++ b/node/src/test/kotlin/net/corda/node/internal/serialization/testutils/AMQPTestSerialiationScheme.kt @@ -11,6 +11,7 @@ import net.corda.serialization.internal.CordaSerializationMagic import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme import net.corda.serialization.internal.amqp.SerializerFactory import net.corda.serialization.internal.AllWhitelist +import net.corda.serialization.internal.amqp.AccessOrderLinkedHashMap import net.corda.client.rpc.internal.ObservableContext as ClientObservableContext /** @@ -22,7 +23,7 @@ import net.corda.client.rpc.internal.ObservableContext as ClientObservableContex class AMQPRoundTripRPCSerializationScheme( private val serializationContext: SerializationContext, cordappCustomSerializers: Set>, - serializerFactoriesForContexts: MutableMap, SerializerFactory>) + serializerFactoriesForContexts: AccessOrderLinkedHashMap, SerializerFactory>) : AbstractAMQPSerializationScheme( cordappCustomSerializers, serializerFactoriesForContexts ) { diff --git a/samples/bank-of-corda-demo/src/test/kotlin/net/corda/serialization/internal/amqp/custom/OptionalSerializerTest.kt b/samples/bank-of-corda-demo/src/test/kotlin/net/corda/serialization/internal/amqp/custom/OptionalSerializerTest.kt new file mode 100644 index 0000000000..058f56fe66 --- /dev/null +++ b/samples/bank-of-corda-demo/src/test/kotlin/net/corda/serialization/internal/amqp/custom/OptionalSerializerTest.kt @@ -0,0 +1,39 @@ +package net.corda.serialization.internal.amqp.custom + +import net.corda.serialization.internal.amqp.SerializerFactory +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.CoreMatchers.nullValue +import org.junit.Assert +import org.junit.Test +import org.mockito.Mockito +import java.util.* + +class OptionalSerializerTest { + @Test + fun `should convert optional with item to proxy`() { + val opt = Optional.of("GenericTestString") + val proxy = OptionalSerializer(Mockito.mock(SerializerFactory::class.java)).toProxy(opt) + Assert.assertThat(proxy.item, `is`("GenericTestString")) + } + + @Test + fun `should convert optional without item to empty proxy`() { + val opt = Optional.ofNullable(null) + val proxy = OptionalSerializer(Mockito.mock(SerializerFactory::class.java)).toProxy(opt) + Assert.assertThat(proxy.item, `is`(nullValue())) + } + + @Test + fun `should convert proxy without item to empty optional `() { + val proxy = OptionalSerializer.OptionalProxy(null) + val opt = OptionalSerializer(Mockito.mock(SerializerFactory::class.java)).fromProxy(proxy) + Assert.assertThat(opt.isPresent, `is`(false)) + } + + @Test + fun `should convert proxy with item to empty optional `() { + val proxy = OptionalSerializer.OptionalProxy("GenericTestString") + val opt = OptionalSerializer(Mockito.mock(SerializerFactory::class.java)).fromProxy(proxy) + Assert.assertThat(opt.get(), `is`("GenericTestString")) + } +} \ No newline at end of file diff --git a/serialization-deterministic/build.gradle b/serialization-deterministic/build.gradle index 28f9059a09..c0c8ccb600 100644 --- a/serialization-deterministic/build.gradle +++ b/serialization-deterministic/build.gradle @@ -49,6 +49,7 @@ task patchSerialization(type: Zip, dependsOn: serializationJarTask) { exclude 'net/corda/serialization/internal/DefaultWhitelist*' exclude 'net/corda/serialization/internal/amqp/AMQPSerializerFactories*' exclude 'net/corda/serialization/internal/amqp/AMQPStreams*' + exclude 'net/corda/serialization/internal/amqp/AMQPSerializationThreadContext*' } reproducibleFileOrder = true diff --git a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationThreadContext.kt b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationThreadContext.kt new file mode 100644 index 0000000000..723dda762d --- /dev/null +++ b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationThreadContext.kt @@ -0,0 +1,6 @@ +@file:JvmName("AMQPSerializationThreadContext") +package net.corda.serialization.internal.amqp + +fun getContextClassLoader(): ClassLoader { + return ClassLoader.getSystemClassLoader() +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt index 10336f96dd..f35c808077 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt @@ -12,10 +12,12 @@ import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.* import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.contextLogger -import net.corda.serialization.internal.* +import net.corda.serialization.internal.CordaSerializationMagic +import net.corda.serialization.internal.DefaultWhitelist +import net.corda.serialization.internal.MutableClassWhitelist +import net.corda.serialization.internal.SerializationScheme import java.lang.reflect.Modifier import java.util.* -import java.util.concurrent.ConcurrentHashMap val AMQP_ENABLED get() = SerializationDefaults.P2P_CONTEXT.preferredSerializationVersion == amqpMagic @@ -37,12 +39,12 @@ interface SerializerFactoryFactory { @KeepForDJVM abstract class AbstractAMQPSerializationScheme( - private val cordappCustomSerializers: Set>, - private val serializerFactoriesForContexts: MutableMap, SerializerFactory>, - val sff: SerializerFactoryFactory = createSerializerFactoryFactory() + private val cordappCustomSerializers: Set>, + private val serializerFactoriesForContexts: AccessOrderLinkedHashMap, SerializerFactory>, + val sff: SerializerFactoryFactory = createSerializerFactoryFactory() ) : SerializationScheme { @DeleteForDJVM - constructor(cordapps: List) : this(cordapps.customSerializers, ConcurrentHashMap()) + constructor(cordapps: List) : this(cordapps.customSerializers, AccessOrderLinkedHashMap(128)) // TODO: This method of initialisation for the Whitelist and plugin serializers will have to change // when we have per-cordapp contexts and dynamic app reloading but for now it's the easiest way @@ -59,26 +61,27 @@ abstract class AbstractAMQPSerializationScheme( val scanSpec: String? = System.getProperty(SCAN_SPEC_PROP_NAME) if (scanSpec == null) { - logger.info ("scanSpec not set, not scanning for Custom Serializers") + logger.debug("scanSpec not set, not scanning for Custom Serializers") emptyList() } else { - logger.info ("scanSpec = \"$scanSpec\", scanning for Custom Serializers") + logger.debug("scanSpec = \"$scanSpec\", scanning for Custom Serializers") scanClasspathForSerializers(scanSpec) } } @StubOutForDJVM private fun scanClasspathForSerializers(scanSpec: String): List> = - this::class.java.classLoader.let { cl -> - FastClasspathScanner(scanSpec).addClassLoader(cl).scan() - .getNamesOfClassesImplementing(SerializationCustomSerializer::class.java) - .map { cl.loadClass(it).asSubclass(SerializationCustomSerializer::class.java) } - .filterNot { Modifier.isAbstract(it.modifiers) } - .map { it.kotlin.objectOrNewInstance() } - } + this::class.java.classLoader.let { cl -> + FastClasspathScanner(scanSpec).addClassLoader(cl).scan() + .getNamesOfClassesImplementing(SerializationCustomSerializer::class.java) + .map { cl.loadClass(it).asSubclass(SerializationCustomSerializer::class.java) } + .filterNot { Modifier.isAbstract(it.modifiers) } + .map { it.kotlin.objectOrNewInstance() } + } @DeleteForDJVM - val List.customSerializers get() = flatMap { it.serializationCustomSerializers }.toSet() + val List.customSerializers + get() = flatMap { it.serializationCustomSerializers }.toSet() } // Parameter "context" is unused directly but passed in by reflection. Removing it will cause failures. @@ -100,6 +103,7 @@ abstract class AbstractAMQPSerializationScheme( register(net.corda.serialization.internal.amqp.custom.ZoneIdSerializer(this)) register(net.corda.serialization.internal.amqp.custom.OffsetTimeSerializer(this)) register(net.corda.serialization.internal.amqp.custom.OffsetDateTimeSerializer(this)) + register(net.corda.serialization.internal.amqp.custom.OptionalSerializer(this)) register(net.corda.serialization.internal.amqp.custom.YearSerializer(this)) register(net.corda.serialization.internal.amqp.custom.YearMonthSerializer(this)) register(net.corda.serialization.internal.amqp.custom.MonthDaySerializer(this)) @@ -126,7 +130,7 @@ abstract class AbstractAMQPSerializationScheme( factory.registerExternal(CorDappCustomSerializer(customSerializer, factory)) } } else { - logger.info("Custom Serializer list loaded - not scanning classpath") + logger.debug("Custom Serializer list loaded - not scanning classpath") cordappCustomSerializers.forEach { customSerializer -> factory.registerExternal(CorDappCustomSerializer(customSerializer, factory)) } @@ -153,32 +157,41 @@ abstract class AbstractAMQPSerializationScheme( protected abstract fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory // Not used as a simple direct import to facilitate testing - open val publicKeySerializer : CustomSerializer<*> = net.corda.serialization.internal.amqp.custom.PublicKeySerializer + open val publicKeySerializer: CustomSerializer<*> = net.corda.serialization.internal.amqp.custom.PublicKeySerializer private fun getSerializerFactory(context: SerializationContext): SerializerFactory { - return serializerFactoriesForContexts.computeIfAbsent(Pair(context.whitelist, context.deserializationClassLoader)) { - when (context.useCase) { - SerializationContext.UseCase.Checkpoint -> - throw IllegalStateException("AMQP should not be used for checkpoint serialization.") - SerializationContext.UseCase.RPCClient -> - rpcClientSerializerFactory(context) - SerializationContext.UseCase.RPCServer -> - rpcServerSerializerFactory(context) - else -> sff.make(context) - }.also { - registerCustomSerializers(context, it) + return synchronized(serializerFactoriesForContexts) { + serializerFactoriesForContexts.computeIfAbsent(Pair(context.whitelist, context.deserializationClassLoader)) { + when (context.useCase) { + SerializationContext.UseCase.Checkpoint -> + throw IllegalStateException("AMQP should not be used for checkpoint serialization.") + SerializationContext.UseCase.RPCClient -> + rpcClientSerializerFactory(context) + SerializationContext.UseCase.RPCServer -> + rpcServerSerializerFactory(context) + else -> sff.make(context) + }.also { + registerCustomSerializers(context, it) + } } } } override fun deserialize(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): T { - val serializerFactory = getSerializerFactory(context) + var contextToUse = context + if (context.useCase == SerializationContext.UseCase.RPCClient) { + contextToUse = context.withClassLoader(getContextClassLoader()) + } + val serializerFactory = getSerializerFactory(contextToUse) return DeserializationInput(serializerFactory).deserialize(byteSequence, clazz, context) } override fun serialize(obj: T, context: SerializationContext): SerializedBytes { - val serializerFactory = getSerializerFactory(context) - + var contextToUse = context + if (context.useCase == SerializationContext.UseCase.RPCClient) { + contextToUse = context.withClassLoader(getContextClassLoader()) + } + val serializerFactory = getSerializerFactory(contextToUse) return SerializationOutput(serializerFactory).serialize(obj, context) } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationThreadContext.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationThreadContext.kt new file mode 100644 index 0000000000..3f8bedfbc4 --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationThreadContext.kt @@ -0,0 +1,6 @@ +@file:JvmName("AMQPSerializationThreadContext") +package net.corda.serialization.internal.amqp + +fun getContextClassLoader(): ClassLoader { + return Thread.currentThread().contextClassLoader +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AccessOrderLinkedHashMap.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AccessOrderLinkedHashMap.kt new file mode 100644 index 0000000000..d7e50afa4d --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AccessOrderLinkedHashMap.kt @@ -0,0 +1,11 @@ +package net.corda.serialization.internal.amqp +import net.corda.core.KeepForDJVM + +@KeepForDJVM +class AccessOrderLinkedHashMap(private val maxSize: Int) : LinkedHashMap(16, 0.75f, true) { + constructor(loader: () -> Int) : this(loader.invoke()) + + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean { + return this.size > maxSize + } +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt index 9e9edffbf7..4a8605d295 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt @@ -276,7 +276,7 @@ internal fun propertiesForSerializationFromConstructor( "in the Java compiler. Alternately, provide a proxy serializer " + "(SerializationCustomSerializer) if recompiling isn't an option") - Pair(PrivatePropertyReader(field, type), field.genericType) + Pair(PrivatePropertyReader(field, type), resolveTypeVariables(field.genericType, type)) } this += PropertyAccessorConstructor( diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt index 20f5ae54c4..bba29c007a 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt @@ -140,7 +140,7 @@ open class SerializerFactory( } } Enum::class.java.isAssignableFrom(actualClass ?: declaredClass) -> { - logger.debug("class=[${actualClass?.simpleName} | $declaredClass] is an enumeration " + logger.info("class=[${actualClass?.simpleName} | $declaredClass] is an enumeration " + "declaredType=${declaredType.typeName} " + "isEnum=${declaredType::class.java.isEnum}") diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/OptionalSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/OptionalSerializer.kt new file mode 100644 index 0000000000..632cc4c511 --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/OptionalSerializer.kt @@ -0,0 +1,24 @@ +package net.corda.serialization.internal.amqp.custom + +import net.corda.core.KeepForDJVM +import net.corda.serialization.internal.amqp.CustomSerializer +import net.corda.serialization.internal.amqp.SerializerFactory +import java.time.OffsetTime +import java.util.* + +/** + * A serializer for [OffsetTime] that uses a proxy object to write out the time and zone offset. + */ +class OptionalSerializer(factory: SerializerFactory) : CustomSerializer.Proxy, OptionalSerializer.OptionalProxy>(Optional::class.java, OptionalProxy::class.java, factory) { + + public override fun toProxy(obj: java.util.Optional<*>): OptionalProxy { + return OptionalProxy(obj.orElse(null)) + } + + public override fun fromProxy(proxy: OptionalProxy): Optional<*> { + return Optional.ofNullable(proxy.item) + } + + @KeepForDJVM + data class OptionalProxy(val item: Any?) +} \ No newline at end of file diff --git a/serialization/src/test/java/net/corda/serialization/internal/amqp/DummyOptional.java b/serialization/src/test/java/net/corda/serialization/internal/amqp/DummyOptional.java new file mode 100644 index 0000000000..9bd1f65142 --- /dev/null +++ b/serialization/src/test/java/net/corda/serialization/internal/amqp/DummyOptional.java @@ -0,0 +1,37 @@ +package net.corda.serialization.internal.amqp; + +import net.corda.core.serialization.CordaSerializable; + +import java.util.Objects; + +@CordaSerializable +public class DummyOptional { + + private final T item; + + public boolean isPresent() { + return item != null; + } + + public T get() { + return item; + } + + public DummyOptional(T item) { + this.item = item; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DummyOptional that = (DummyOptional) o; + return Objects.equals(item, that.item); + } + + @Override + public int hashCode() { + + return Objects.hash(item); + } +} \ No newline at end of file diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/AbstractAMQPSerializationSchemeTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/AbstractAMQPSerializationSchemeTest.kt new file mode 100644 index 0000000000..8c38cbeb26 --- /dev/null +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/AbstractAMQPSerializationSchemeTest.kt @@ -0,0 +1,76 @@ +package net.corda.serialization.internal.amqp + +import net.corda.core.serialization.ClassWhitelist +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.deserialize +import net.corda.core.utilities.ByteSequence +import net.corda.serialization.internal.AllWhitelist +import net.corda.serialization.internal.CordaSerializationMagic +import net.corda.serialization.internal.SerializationContextImpl +import net.corda.serialization.internal.amqp.testutils.serializationProperties +import net.corda.testing.core.SerializationEnvironmentRule +import org.hamcrest.CoreMatchers +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.Matchers +import org.junit.Assert +import org.junit.Rule +import org.junit.Test +import java.net.URLClassLoader +import java.util.concurrent.ThreadLocalRandom +import java.util.stream.IntStream + +class AbstractAMQPSerializationSchemeTest { + + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + + @Test + fun `number of cached factories must be bounded by maxFactories`() { + val genesisContext = SerializationContextImpl( + ByteSequence.of(byteArrayOf('c'.toByte(), 'o'.toByte(), 'r'.toByte(), 'd'.toByte(), 'a'.toByte(), 0.toByte(), 0.toByte(), 1.toByte())), + ClassLoader.getSystemClassLoader(), + AllWhitelist, + serializationProperties, + false, + SerializationContext.UseCase.RPCClient, + null) + + + val factory = TestSerializerFactory(TESTING_CONTEXT.whitelist, TESTING_CONTEXT.deserializationClassLoader) + val maxFactories = 512 + val backingMap = AccessOrderLinkedHashMap, SerializerFactory>({ maxFactories }) + val scheme = object : AbstractAMQPSerializationScheme(emptySet(), backingMap, createSerializerFactoryFactory()) { + override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory { + return factory + } + + override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory { + return factory + } + + override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean { + return true + } + + } + + + IntStream.range(0, 2048).parallel().forEach { + val context = if (ThreadLocalRandom.current().nextBoolean()) { + genesisContext.withClassLoader(URLClassLoader(emptyArray())) + } else { + genesisContext + } + val testString = "TEST${ThreadLocalRandom.current().nextInt()}" + val serialized = scheme.serialize(testString, context) + val deserialized = serialized.deserialize(context = context, serializationFactory = testSerialization.serializationFactory) + Assert.assertThat(testString, `is`(deserialized)) + Assert.assertThat(backingMap.size, `is`(Matchers.lessThanOrEqualTo(maxFactories))) + } + Assert.assertThat(backingMap.size, CoreMatchers.`is`(Matchers.lessThanOrEqualTo(maxFactories))) + } +} + + + diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/CorDappSerializerTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/CorDappSerializerTests.kt index 18f0ac5b76..55aa7ca1a8 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/CorDappSerializerTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/CorDappSerializerTests.kt @@ -1,15 +1,11 @@ package net.corda.serialization.internal.amqp -import org.junit.Test import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.SerializationCustomSerializer import net.corda.serialization.internal.AllWhitelist -import net.corda.serialization.internal.amqp.testutils.deserializeAndReturnEnvelope -import net.corda.serialization.internal.amqp.testutils.deserialize -import net.corda.serialization.internal.amqp.testutils.serializeAndReturnSchema -import net.corda.serialization.internal.amqp.testutils.serialize -import net.corda.serialization.internal.amqp.testutils.testDefaultFactory +import net.corda.serialization.internal.amqp.testutils.* import org.assertj.core.api.Assertions +import org.junit.Test import java.io.NotSerializableException import kotlin.test.assertEquals diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/OptionalSerializationTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/OptionalSerializationTests.kt new file mode 100644 index 0000000000..10b154863f --- /dev/null +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/OptionalSerializationTests.kt @@ -0,0 +1,36 @@ +package net.corda.serialization.internal.amqp + +import net.corda.serialization.internal.AllWhitelist +import net.corda.serialization.internal.amqp.custom.OptionalSerializer +import net.corda.serialization.internal.amqp.testutils.TestSerializationOutput +import net.corda.serialization.internal.amqp.testutils.deserialize +import net.corda.serialization.internal.amqp.testutils.testDefaultFactory +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.equalTo +import org.junit.Assert +import org.junit.Test +import java.util.* + +class OptionalSerializationTests { + + @Test + fun setupEnclosedSerializationTest() { + @Test + fun `java optionals should serialize`() { + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(OptionalSerializer(factory)) + val obj = Optional.ofNullable("YES") + val bytes = TestSerializationOutput(true, factory).serialize(obj) + val deserializerFactory = testDefaultFactory().apply { + register(OptionalSerializer(this)) + } + + val deserialized = DeserializationInput(factory).deserialize(bytes) + val deserialized2 = DeserializationInput(deserializerFactory).deserialize(bytes) + Assert.assertThat(deserialized, `is`(equalTo(deserialized2))) + Assert.assertThat(obj, `is`(equalTo(deserialized2))) + } + + `java optionals should serialize`() + } +} \ No newline at end of file diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt index 5a368b8304..c0ffdfc04a 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt @@ -510,6 +510,12 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi serdes(obj, SerializerFactory(EmptyWhitelist, ClassLoader.getSystemClassLoader())) } + @Test + fun `generics from java are supported`() { + val obj = DummyOptional("YES") + serdes(obj, SerializerFactory(EmptyWhitelist, ClassLoader.getSystemClassLoader())) + } + @Test fun `test throwables serialize`() { val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) @@ -969,6 +975,7 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi class Spike private constructor(val a: String) { constructor() : this("a") + override fun equals(other: Any?): Boolean = other is Spike && other.a == this.a override fun hashCode(): Int = a.hashCode() } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationSchemaTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationSchemaTests.kt index 7f71b60ae6..a5f4925f24 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationSchemaTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationSchemaTests.kt @@ -2,11 +2,11 @@ package net.corda.serialization.internal.amqp import net.corda.core.serialization.* import net.corda.core.utilities.ByteSequence -import net.corda.serialization.internal.* import net.corda.serialization.internal.BuiltInExceptionsWhitelist +import net.corda.serialization.internal.CordaSerializationMagic import net.corda.serialization.internal.GlobalTransientClassWhiteList +import net.corda.serialization.internal.SerializationContextImpl import org.junit.Test -import java.util.concurrent.ConcurrentHashMap import kotlin.test.assertEquals // Make sure all serialization calls in this test don't get stomped on by anything else @@ -46,7 +46,7 @@ class TestSerializerFactoryFactory : SerializerFactoryFactoryImpl() { } } -class AMQPTestSerializationScheme : AbstractAMQPSerializationScheme(emptySet(), ConcurrentHashMap(), TestSerializerFactoryFactory()) { +class AMQPTestSerializationScheme : AbstractAMQPSerializationScheme(emptySet(), AccessOrderLinkedHashMap { 128 }, TestSerializerFactoryFactory()) { override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory { throw UnsupportedOperationException() } @@ -91,7 +91,7 @@ class SerializationSchemaTests { val c = C(1) val testSerializationFactory = TestSerializationFactory() - val expectedCustomSerializerCount = 40 + val expectedCustomSerializerCount = 41 assertEquals(0, testFactory.registerCount) c.serialize(testSerializationFactory, TESTING_CONTEXT)