Skip to content

Commit

Permalink
CORDA-2919: JacksonSupport, for CordaSerializable classes, improved t…
Browse files Browse the repository at this point in the history
…o only uses those properties that are part of Corda serialisation (corda#5397)
  • Loading branch information
shamsasari authored Aug 27, 2019
1 parent fc5cd62 commit 90284a6
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 235 deletions.
2 changes: 2 additions & 0 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
package net.corda.client.jackson.internal

import com.fasterxml.jackson.annotation.*
import com.fasterxml.jackson.annotation.JsonAutoDetect.Value
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility
import com.fasterxml.jackson.annotation.JsonCreator.Mode.DISABLED
import com.fasterxml.jackson.annotation.JsonInclude.Include
import com.fasterxml.jackson.core.JsonGenerator
Expand All @@ -12,10 +14,14 @@ import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.cfg.MapperConfig
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier
import com.fasterxml.jackson.databind.deser.ContextualDeserializer
import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer
import com.fasterxml.jackson.databind.deser.std.FromStringDeserializer
import com.fasterxml.jackson.databind.introspect.AnnotatedClass
import com.fasterxml.jackson.databind.introspect.BasicClassIntrospector
import com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.node.IntNode
import com.fasterxml.jackson.databind.node.ObjectNode
Expand All @@ -32,7 +38,6 @@ import net.corda.core.flows.StateMachineRunId
import net.corda.core.identity.*
import net.corda.core.internal.DigitalSignatureWithCert
import net.corda.core.internal.createComponentGroups
import net.corda.core.internal.kotlinObjectInstance
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.serialization.SerializedBytes
Expand All @@ -56,7 +61,13 @@ class CordaModule : SimpleModule("corda-core") {
override fun setupModule(context: SetupContext) {
super.setupModule(context)

// For classes which are annotated with CordaSerializable we want to use the same set of properties as the Corda serilasation scheme.
// To do that we use CordaSerializableClassIntrospector to first turn on field visibility for these classes (the Jackson default is
// private fields are not included) and then we use CordaSerializableBeanSerializerModifier to remove any extra properties that Jackson
// might pick up.
context.setClassIntrospector(CordaSerializableClassIntrospector(context))
context.addBeanSerializerModifier(CordaSerializableBeanSerializerModifier())

context.addBeanDeserializerModifier(AmountBeanDeserializerModifier())

context.setMixInAnnotations(PartyAndCertificate::class.java, PartyAndCertificateMixin::class.java)
Expand Down Expand Up @@ -88,9 +99,22 @@ class CordaModule : SimpleModule("corda-core") {
}
}

/**
* Use the same properties that AMQP serialization uses if the POJO is @CordaSerializable
*/
private class CordaSerializableClassIntrospector(private val context: Module.SetupContext) : BasicClassIntrospector() {
override fun constructPropertyCollector(
config: MapperConfig<*>?,
ac: AnnotatedClass?,
type: JavaType,
forSerialization: Boolean,
mutatorPrefix: String?
): POJOPropertiesCollector {
if (hasCordaSerializable(type.rawClass)) {
// Adjust the field visibility of CordaSerializable classes on the fly as they are encountered.
context.configOverride(type.rawClass).visibility = Value.defaultVisibility().withFieldVisibility(Visibility.ANY)
}
return super.constructPropertyCollector(config, ac, type, forSerialization, mutatorPrefix)
}
}

private class CordaSerializableBeanSerializerModifier : BeanSerializerModifier() {
// We need to pass in a SerializerFactory when scanning for properties, but don't actually do any serialisation so any will do.
private val serializerFactory = SerializerFactoryBuilder.build(AllWhitelist, javaClass.classLoader)
Expand All @@ -99,17 +123,10 @@ private class CordaSerializableBeanSerializerModifier : BeanSerializerModifier()
beanDesc: BeanDescription,
beanProperties: MutableList<BeanPropertyWriter>): MutableList<BeanPropertyWriter> {
val beanClass = beanDesc.beanClass
if (hasCordaSerializable(beanClass) && beanClass.kotlinObjectInstance == null && !SerializeAsToken::class.java.isAssignableFrom(beanClass)) {
if (hasCordaSerializable(beanClass) && !SerializeAsToken::class.java.isAssignableFrom(beanClass)) {
val typeInformation = serializerFactory.getTypeInformation(beanClass)
val properties = typeInformation.propertiesOrEmptyMap
val amqpProperties = properties.mapNotNull { (name, property) ->
if (property.isCalculated) null else name
}
val propertyRenames = beanDesc.findProperties().associateBy({ it.name }, { it.internalName })
(amqpProperties - propertyRenames.values).let {
check(it.isEmpty()) { "Jackson didn't provide serialisers for $it" }
}
beanProperties.removeIf { propertyRenames[it.name] !in amqpProperties }
val propertyNames = typeInformation.propertiesOrEmptyMap.mapNotNull { if (it.value.isCalculated) null else it.key }
beanProperties.removeIf { it.name !in propertyNames }
}
return beanProperties
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import net.corda.core.node.services.NetworkParametersService
import net.corda.core.node.services.TransactionStorage
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 net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.SignedTransaction
Expand Down Expand Up @@ -658,6 +659,15 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
assertThat(mapper.convertValue<NonCtorPropertiesData>(json)).isEqualTo(data)
}

@Test
fun `LinearState where the linearId property does not match the backing field`() {
val funkyLinearState = FunkyLinearState(UniqueIdentifier())
// As a sanity check, show that this is a valid CordaSerializable class
assertThat(funkyLinearState.serialize().deserialize()).isEqualTo(funkyLinearState)
val json = mapper.valueToTree<ObjectNode>(funkyLinearState)
assertThat(mapper.convertValue<FunkyLinearState>(json)).isEqualTo(funkyLinearState)
}

@Test
fun `kotlin object`() {
val json = mapper.valueToTree<ObjectNode>(KotlinObject)
Expand Down Expand Up @@ -713,6 +723,11 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
val nonCtor: Int get() = value
}

private data class FunkyLinearState(private val linearID: UniqueIdentifier) : LinearState {
override val linearId: UniqueIdentifier get() = linearID
override val participants: List<AbstractParty> get() = emptyList()
}

private object KotlinObject

@CordaSerializable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package net.corda.node.services.rpc

import co.paralleluniverse.fibers.Stack
import co.paralleluniverse.strands.Strand
import com.fasterxml.jackson.annotation.JsonAutoDetect
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility
import com.fasterxml.jackson.annotation.JsonFormat
Expand Down Expand Up @@ -363,9 +362,12 @@ class CheckpointDumper(private val checkpointStorage: CheckpointStorage, private
override fun changeProperties(config: SerializationConfig,
beanDesc: BeanDescription,
beanProperties: MutableList<BeanPropertyWriter>): MutableList<BeanPropertyWriter> {
// Remove references to any node singletons
beanProperties.removeIf { it.type.isTypeOrSubTypeOf(SerializeAsToken::class.java) }
if (FlowLogic::class.java.isAssignableFrom(beanDesc.beanClass)) {
if (SerializeAsToken::class.java.isAssignableFrom(beanDesc.beanClass)) {
// Do not serialise node singletons
// TODO This will cause the singleton to appear as an empty object. Ideally we don't want it to appear at all but this will
// have to do for now.
beanProperties.clear()
} else if (FlowLogic::class.java.isAssignableFrom(beanDesc.beanClass)) {
beanProperties.removeIf {
it.type.isTypeOrSubTypeOf(ProgressTracker::class.java) || it.name == "_stateMachine" || it.name == "deprecatedPartySessionMap"
}
Expand Down
Loading

0 comments on commit 90284a6

Please sign in to comment.