diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt index e309882d7b..dede33eb92 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt @@ -415,7 +415,7 @@ private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: Muta }.putUnencodedChars(type.name).putUnencodedChars(ENUM_HASH) } else { hasher.fingerprintWithCustomSerializerOrElse(factory, type, type) { - if (type.kotlin.objectInstance != null) { + if (type.objectInstance() != null) { // TODO: name collision is too likely for kotlin objects, we need to introduce some reference // to the CorDapp but maybe reference to the JAR in the short term. hasher.putUnencodedChars(type.name) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt index 435498648a..887eaa78fd 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt @@ -531,3 +531,48 @@ fun ClassWhitelist.hasAnnotationInHierarchy(type: Class<*>): Boolean { || type.interfaces.any { hasAnnotationInHierarchy(it) } || (type.superclass != null && hasAnnotationInHierarchy(type.superclass)) } + +/** + * By default use Kotlin reflection and gran the objectInstance. However, that doesn't play nicely with nested + * private objects. Even setting the accessibility override (setAccessible) still causes an + * [IllegalAccessException] when attempting to retrieve the value of the INSTANCE field. + * + * Whichever reference to the class Kotlin reflection uses, override (set from setAccessible) on that field + * isn't set even when it was explicitly set as accessible before calling into the kotlin reflection routines. + * + * For example + * + * clazz.getDeclaredField("INSTANCE")?.apply { + * isAccessible = true + * kotlin.objectInstance // This throws as the INSTANCE field isn't accessible + * } + * + * Therefore default back to good old java reflection and simply look for the INSTANCE field as we are never going + * to serialize a companion object. + * + * As such, if objectInstance fails access, revert to Java reflection and try that + */ +fun Class<*>.objectInstance() = + try { + this.kotlin.objectInstance + } catch (e: IllegalAccessException) { + // Check it really is an object (i.e. it has no constructor) + if (constructors.isNotEmpty()) null + else { + try { + this.getDeclaredField("INSTANCE")?.let { field -> + // and must be marked as both static and final (>0 means they're set) + if (modifiers and Modifier.STATIC == 0 || modifiers and Modifier.FINAL == 0) null + else { + val accessibility = field.isAccessible + field.isAccessible = true + val obj = field.get(null) + field.isAccessible = accessibility + obj + } + } + } catch (e: NoSuchFieldException) { + null + } + } + } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt index 7bd8178a6d..472e3a89ef 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt @@ -261,12 +261,15 @@ open class SerializerFactory( // Don't need to check the whitelist since each element will come back through the whitelisting process. if (clazz.componentType.isPrimitive) PrimArraySerializer.make(type, this) else ArraySerializer.make(type, this) - } else if (clazz.kotlin.objectInstance != null) { - whitelist.requireWhitelisted(clazz) - SingletonSerializer(clazz, clazz.kotlin.objectInstance!!, this) } else { - whitelist.requireWhitelisted(type) - ObjectSerializer(type, this) + val singleton = clazz.objectInstance() + if (singleton != null) { + whitelist.requireWhitelisted(clazz) + SingletonSerializer(clazz, singleton, this) + } else { + whitelist.requireWhitelisted(type) + ObjectSerializer(type, this) + } } } } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt index 28cf60f2f0..75a5abce6a 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt @@ -46,6 +46,24 @@ import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue +object AckWrapper { + object Ack + + fun serialize() { + val factory = testDefaultFactoryNoEvolution() + SerializationOutput(factory).serialize(Ack) + } +} + +object PrivateAckWrapper { + private object Ack + + fun serialize() { + val factory = testDefaultFactoryNoEvolution() + SerializationOutput(factory).serialize(Ack) + } +} + class SerializationOutputTests { private companion object { val BOB_IDENTITY = TestIdentity(BOB_NAME, 80).identity @@ -1115,4 +1133,16 @@ class SerializationOutputTests { // were the issue not fixed we'd blow up here SerializationOutput(factory).serialize(c) } + + @Test + fun nestedObjects() { + // The "test" is that this doesn't throw, anything else is a success + AckWrapper.serialize() + } + + @Test + fun privateNestedObjects() { + // The "test" is that this doesn't throw, anything else is a success + PrivateAckWrapper.serialize() + } } \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt index cfccafbc4d..2424101214 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt @@ -305,7 +305,8 @@ object SimmFlow { @Suspendable private fun agreePortfolio(portfolio: Portfolio): SignedTransaction { logger.info("Handshake finished, awaiting Simm offer") - require(offer.dealBeingOffered.portfolio == portfolio.refs) + + require(offer.dealBeingOffered.portfolio.toSet() == portfolio.refs.toSet()) val seller = TwoPartyDealFlow.Instigator( replyToSession,