mirror of
https://github.com/corda/corda.git
synced 2025-01-21 03:55:00 +00:00
CORDA-908 - Support private properties in AMQP serialization (#2336)
CORDA-908 - Support private properties in AMQP serialization * Review comments * Fix tests * Review Comments * review comments * review comments
This commit is contained in:
parent
979d7f2c63
commit
cacdba872e
@ -16,12 +16,24 @@ Unreleased
|
||||
That is the ability to alter an enum constant and, as long as certain rules are followed and the correct
|
||||
annotations applied, have older and newer instances of that enumeration be understood.
|
||||
|
||||
* **AMQP Enabled**
|
||||
|
||||
AMQP Serialization is now enabled for both peer to peer communication and writing states to the vault. This change
|
||||
* **AMQP Enabled**:
|
||||
AMQP Serialization is now enabled for both peer to peer communication and the writing of states to the vault. This change
|
||||
brings a stable format Corda can support internally throughout it's lifetime that meets the needs of Corda and our
|
||||
users.
|
||||
|
||||
Details on the AMQP serialization framework can be found in the :doc:`serialization` document :ref:`here <amqp_ref>`.
|
||||
This provides an introduction and overview of the framework whilst more specific details on object evolution as it relates to
|
||||
serialization is similarly found in pages :doc:`serialization-default-evolution` and :doc:`serialization-enum-evolution`
|
||||
respectively. Recommendations on how best to code CorDapps using your own :ref:`custom types <amqp_custom_types_ref>`.
|
||||
|
||||
.. note:: This release delivers the bulk of our transition from Kryo serialisation to AMQP serialisation. This means that many of the restrictions
|
||||
that were documented in previous versions of Corda are now enforced. (https://docs.corda.net/releases/release-V1.0/serialization.html).
|
||||
|
||||
In particular, you are advised to review the section titled "Custom Types". To aid with the transition, we have included support
|
||||
in this release for default construction of objects and their instantiation through getters as well as objects with inaccessible
|
||||
private fields but it is not guaranteed that this support will continue into future versions; the restrictions documented at the
|
||||
link above are the canonical source.
|
||||
|
||||
* **Custom Serializers**
|
||||
|
||||
To allow interop with third party libraries that cannot be recompiled we add functionality that allows custom serializers
|
||||
|
@ -1,6 +1,8 @@
|
||||
Object serialization
|
||||
====================
|
||||
|
||||
.. contents::
|
||||
|
||||
What is serialization (and deserialization)?
|
||||
--------------------------------------------
|
||||
|
||||
@ -52,7 +54,7 @@ was a compelling use case for the definition and development of a custom format
|
||||
|
||||
#. A desire to have a schema describing what has been serialized along-side the actual data:
|
||||
|
||||
#. To assist with versioning, both in terms of being able to interpret long ago archivEd data (e.g. trades from
|
||||
#. To assist with versioning, both in terms of being able to interpret long ago archived data (e.g. trades from
|
||||
a decade ago, long after the code has changed) and between differing code versions.
|
||||
#. To make it easier to write user interfaces that can navigate the serialized form of data.
|
||||
#. To support cross platform (non-JVM) interaction, where the format of a class file is not so easily interpreted.
|
||||
@ -76,7 +78,7 @@ Finally, for the checkpointing of flows Corda will continue to use the existing
|
||||
|
||||
This separation of serialization schemes into different contexts allows us to use the most suitable framework for that context rather than
|
||||
attempting to force a one size fits all approach. Where ``Kryo`` is more suited to the serialization of a programs stack frames, being more flexible
|
||||
than our AMQP framework in what it can construct and serialize, that flexibility makes it exceptionally difficult to make secure. Conversly
|
||||
than our AMQP framework in what it can construct and serialize, that flexibility makes it exceptionally difficult to make secure. Conversely
|
||||
our AMQP framework allows us to concentrate on a robust a secure framework that can be reasoned about thus made safer with far fewer unforeseen
|
||||
security holes.
|
||||
|
||||
@ -282,22 +284,6 @@ serialised form
|
||||
|
||||
val e2 = e.serialize().deserialize() // e2.c will be 20, not 100!!!
|
||||
|
||||
.. warning:: Private properties in Kotlin classes render the class unserializable *unless* a public
|
||||
getter is manually defined. For example:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
data class C(val a: Int, private val b: Int) {
|
||||
// Without this C cannot be serialized
|
||||
public fun getB() = b
|
||||
}
|
||||
|
||||
.. note:: This is particularly relevant as IDE's can often point out where they believe a
|
||||
property can be made private without knowing this can break Corda serialization. Should
|
||||
this happen then a run time warning will be generated when the class fails to serialize
|
||||
|
||||
Setter Instantiation
|
||||
''''''''''''''''''''
|
||||
|
||||
@ -330,6 +316,75 @@ For example:
|
||||
public void setC(int c) { this.c = c; }
|
||||
}
|
||||
|
||||
Inaccessible Private Properties
|
||||
```````````````````````````````
|
||||
|
||||
Whilst the Corda AMQP serialization framework supports private object properties without publicly
|
||||
accessible getter methods this development idiom is strongly discouraged.
|
||||
|
||||
For example.
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
Kotlin:
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
data class C(val a: Int, private val b: Int)
|
||||
|
||||
Java:
|
||||
|
||||
.. sourcecode:: Java
|
||||
|
||||
class C {
|
||||
public Integer a;
|
||||
private Integer b;
|
||||
|
||||
C(Integer a, Integer b) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
}
|
||||
}
|
||||
|
||||
When designing stateful objects is should be remembered that they are not, despite appearances, traditional
|
||||
programmatic constructs. They are signed over, transformed, serialised, and relationally mapped. As such,
|
||||
all elements should be publicly accessible by design
|
||||
|
||||
.. warning:: IDEs will indiciate erroneously that properties can be given something other than public
|
||||
visibility. Ignore this as whilst it will work, as discussed above there are many reasons why this isn't
|
||||
a good idea and those are beyond the scope of the IDEs inference rules
|
||||
|
||||
Providing a public getter, as per the following example, is acceptable
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
Kotlin:
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
data class C(val a: Int, private val b: Int) {
|
||||
public fun getB() = b
|
||||
}
|
||||
|
||||
Java:
|
||||
|
||||
.. sourcecode:: Java
|
||||
|
||||
class C {
|
||||
public Integer a;
|
||||
private Integer b;
|
||||
|
||||
C(Integer a, Integer b) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
}
|
||||
|
||||
public Integer getB() {
|
||||
return b;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Enums
|
||||
`````
|
||||
|
||||
|
@ -96,7 +96,7 @@ class EvolutionSerializer(
|
||||
old.fields.forEach {
|
||||
val returnType = it.getTypeAsClass(factory.classloader)
|
||||
oldArgs[it.name] = OldParam(
|
||||
returnType, idx++, PropertySerializer.make(it.name, null, returnType, factory))
|
||||
returnType, idx++, PropertySerializer.make(it.name, PublicPropertyReader(null), returnType, factory))
|
||||
}
|
||||
|
||||
val readers = constructor.parameters.map {
|
||||
|
@ -26,6 +26,8 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
|
||||
propertiesForSerialization(kotlinConstructor, clazz, factory)
|
||||
}
|
||||
|
||||
fun getPropertySerializers() = propertySerializers
|
||||
|
||||
private val typeName = nameForType(clazz)
|
||||
|
||||
override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")
|
||||
|
@ -1,17 +1,74 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import org.apache.qpid.proton.amqp.Binary
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Type
|
||||
import java.lang.reflect.Field
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.jvm.javaGetter
|
||||
import kotlin.reflect.jvm.kotlinProperty
|
||||
|
||||
abstract class PropertyReader {
|
||||
abstract fun read(obj: Any?): Any?
|
||||
abstract fun isNullable(): Boolean
|
||||
}
|
||||
|
||||
class PublicPropertyReader(private val readMethod: Method?) : PropertyReader() {
|
||||
init {
|
||||
readMethod?.isAccessible = true
|
||||
}
|
||||
|
||||
private fun Method.returnsNullable(): Boolean {
|
||||
try {
|
||||
val returnTypeString = this.declaringClass.kotlin.memberProperties.firstOrNull { it.javaGetter == this }?.returnType?.toString() ?: "?"
|
||||
return returnTypeString.endsWith('?') || returnTypeString.endsWith('!')
|
||||
} catch (e: kotlin.reflect.jvm.internal.KotlinReflectionInternalError) {
|
||||
// This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue is: https://youtrack.jetbrains.com/issue/KT-13077
|
||||
// TODO: Revisit this when Kotlin issue is fixed.
|
||||
|
||||
loggerFor<PropertySerializer>().error("Unexpected internal Kotlin error", e)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override fun read(obj: Any?): Any? {
|
||||
return readMethod!!.invoke(obj)
|
||||
}
|
||||
|
||||
override fun isNullable(): Boolean = readMethod?.returnsNullable() ?: false
|
||||
}
|
||||
|
||||
class PrivatePropertyReader(val field: Field, parentType: Type) : PropertyReader() {
|
||||
init {
|
||||
loggerFor<PropertySerializer>().warn("Create property Serializer for private property '${field.name}' not "
|
||||
+ "exposed by a getter on class '$parentType'\n"
|
||||
+ "\tNOTE: This behaviour will be deprecated at some point in the future and a getter required")
|
||||
}
|
||||
|
||||
override fun read(obj: Any?): Any? {
|
||||
field.isAccessible = true
|
||||
val rtn = field.get(obj)
|
||||
field.isAccessible = false
|
||||
return rtn
|
||||
}
|
||||
|
||||
override fun isNullable() = try {
|
||||
field.kotlinProperty?.returnType?.isMarkedNullable ?: false
|
||||
} catch (e: kotlin.reflect.jvm.internal.KotlinReflectionInternalError) {
|
||||
// This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue is: https://youtrack.jetbrains.com/issue/KT-13077
|
||||
// TODO: Revisit this when Kotlin issue is fixed.
|
||||
loggerFor<PropertySerializer>().error("Unexpected internal Kotlin error", e)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Base class for serialization of a property of an object.
|
||||
*/
|
||||
sealed class PropertySerializer(val name: String, val readMethod: Method?, val resolvedType: Type) {
|
||||
sealed class PropertySerializer(val name: String, val propertyReader: PropertyReader, val resolvedType: Type) {
|
||||
abstract fun writeClassInfo(output: SerializationOutput)
|
||||
abstract fun writeProperty(obj: Any?, data: Data, output: SerializationOutput)
|
||||
abstract fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput): Any?
|
||||
@ -44,25 +101,11 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
|
||||
}
|
||||
|
||||
private fun generateMandatory(): Boolean {
|
||||
return isJVMPrimitive || readMethod?.returnsNullable() == false
|
||||
}
|
||||
|
||||
private fun Method.returnsNullable(): Boolean {
|
||||
try {
|
||||
val returnTypeString = this.declaringClass.kotlin.memberProperties.firstOrNull { it.javaGetter == this }?.returnType?.toString() ?: "?"
|
||||
return returnTypeString.endsWith('?') || returnTypeString.endsWith('!')
|
||||
} catch (e: kotlin.reflect.jvm.internal.KotlinReflectionInternalError) {
|
||||
// This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue is: https://youtrack.jetbrains.com/issue/KT-13077
|
||||
// TODO: Revisit this when Kotlin issue is fixed.
|
||||
logger.error("Unexpected internal Kotlin error", e)
|
||||
return true
|
||||
}
|
||||
return isJVMPrimitive || !(propertyReader.isNullable())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
fun make(name: String, readMethod: Method?, resolvedType: Type, factory: SerializerFactory): PropertySerializer {
|
||||
readMethod?.isAccessible = true
|
||||
fun make(name: String, readMethod: PropertyReader, resolvedType: Type, factory: SerializerFactory): PropertySerializer {
|
||||
if (SerializerFactory.isPrimitive(resolvedType)) {
|
||||
return when (resolvedType) {
|
||||
Char::class.java, Character::class.java -> AMQPCharPropertySerializer(name, readMethod)
|
||||
@ -78,7 +121,8 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
|
||||
* A property serializer for a complex type (another object).
|
||||
*/
|
||||
class DescribedTypePropertySerializer(
|
||||
name: String, readMethod: Method?,
|
||||
name: String,
|
||||
readMethod: PropertyReader,
|
||||
resolvedType: Type,
|
||||
private val lazyTypeSerializer: () -> AMQPSerializer<*>) : PropertySerializer(name, readMethod, resolvedType) {
|
||||
// This is lazy so we don't get an infinite loop when a method returns an instance of the class.
|
||||
@ -90,12 +134,15 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
|
||||
}
|
||||
}
|
||||
|
||||
override fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput): Any? = ifThrowsAppend({ nameForDebug }) {
|
||||
override fun readProperty(
|
||||
obj: Any?,
|
||||
schemas: SerializationSchemas,
|
||||
input: DeserializationInput): Any? = ifThrowsAppend({ nameForDebug }) {
|
||||
input.readObjectOrNull(obj, schemas, resolvedType)
|
||||
}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) = ifThrowsAppend({ nameForDebug }) {
|
||||
output.writeObjectOrNull(readMethod!!.invoke(obj), data, resolvedType)
|
||||
output.writeObjectOrNull(propertyReader.read(obj), data, resolvedType)
|
||||
}
|
||||
|
||||
private val nameForDebug = "$name(${resolvedType.typeName})"
|
||||
@ -104,7 +151,10 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
|
||||
/**
|
||||
* A property serializer for most AMQP primitive type (Int, String, etc).
|
||||
*/
|
||||
class AMQPPrimitivePropertySerializer(name: String, readMethod: Method?, resolvedType: Type) : PropertySerializer(name, readMethod, resolvedType) {
|
||||
class AMQPPrimitivePropertySerializer(
|
||||
name: String,
|
||||
readMethod: PropertyReader,
|
||||
resolvedType: Type) : PropertySerializer(name, readMethod, resolvedType) {
|
||||
override fun writeClassInfo(output: SerializationOutput) {}
|
||||
|
||||
override fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput): Any? {
|
||||
@ -112,7 +162,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
|
||||
}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) {
|
||||
val value = readMethod!!.invoke(obj)
|
||||
val value = propertyReader.read(obj)
|
||||
if (value is ByteArray) {
|
||||
data.putObject(Binary(value))
|
||||
} else {
|
||||
@ -126,7 +176,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
|
||||
* value of the character is stored in numeric UTF-16 form and on deserialisation requires explicit
|
||||
* casting back to a char otherwise it's treated as an Integer and a TypeMismatch occurs
|
||||
*/
|
||||
class AMQPCharPropertySerializer(name: String, readMethod: Method?) :
|
||||
class AMQPCharPropertySerializer(name: String, readMethod: PropertyReader) :
|
||||
PropertySerializer(name, readMethod, Character::class.java) {
|
||||
override fun writeClassInfo(output: SerializationOutput) {}
|
||||
|
||||
@ -135,7 +185,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
|
||||
}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) {
|
||||
val input = readMethod!!.invoke(obj)
|
||||
val input = propertyReader.read(obj)
|
||||
if (input != null) data.putShort((input as Char).toShort()) else data.putNull()
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
import kotlin.reflect.jvm.isAccessible
|
||||
import kotlin.reflect.jvm.javaType
|
||||
import kotlin.reflect.jvm.kotlinProperty
|
||||
|
||||
/**
|
||||
* Annotation indicating a constructor to be used to reconstruct instances of a class during deserialization.
|
||||
@ -100,15 +101,8 @@ private fun <T : Any> propertiesForSerializationFromConstructor(
|
||||
for (param in kotlinConstructor.parameters) {
|
||||
val name = param.name ?: throw NotSerializableException("Constructor parameter of $clazz has no name.")
|
||||
|
||||
val matchingProperty = properties[name] ?:
|
||||
try {
|
||||
clazz.getDeclaredField(param.name)
|
||||
throw NotSerializableException("Property '$name' or its getter is non public, this renders class '$clazz' unserializable")
|
||||
} catch (e: NoSuchFieldException) {
|
||||
throw NotSerializableException("No property matching constructor parameter named '$name' of '$clazz'. " +
|
||||
"If using Java, check that you have the -parameters option specified in the Java compiler. " +
|
||||
"Alternately, provide a proxy serializer (SerializationCustomSerializer) if recompiling isn't an option")
|
||||
}
|
||||
if (name in properties) {
|
||||
val matchingProperty = properties[name]!!
|
||||
|
||||
// Check that the method has a getter in java.
|
||||
val getter = matchingProperty.readMethod ?: throw NotSerializableException("Property has no getter method for $name of $clazz. " +
|
||||
@ -116,10 +110,22 @@ private fun <T : Any> propertiesForSerializationFromConstructor(
|
||||
"Alternately, provide a proxy serializer (SerializationCustomSerializer) if recompiling isn't an option")
|
||||
val returnType = resolveTypeVariables(getter.genericReturnType, type)
|
||||
if (constructorParamTakesReturnTypeOfGetter(returnType, getter.genericReturnType, param)) {
|
||||
rc += PropertySerializer.make(name, getter, returnType, factory)
|
||||
rc += PropertySerializer.make(name, PublicPropertyReader(getter), returnType, factory)
|
||||
} else {
|
||||
throw NotSerializableException("Property type $returnType for $name of $clazz differs from constructor parameter type ${param.type.javaType}")
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
val field = (clazz.getDeclaredField(param.name))
|
||||
|
||||
rc += PropertySerializer.make(name, PrivatePropertyReader(field, type), field.genericType, factory)
|
||||
} catch (e: NoSuchFieldException) {
|
||||
throw NotSerializableException("No property matching constructor parameter named '$name' of '$clazz'. " +
|
||||
"If using Java, check that you have the -parameters option specified in the Java compiler. " +
|
||||
"Alternately, provide a proxy serializer (SerializationCustomSerializer) if recompiling isn't an option")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return ConstructorDestructorMethods(rc, emptyList())
|
||||
@ -146,8 +152,8 @@ private fun propertiesForSerializationFromSetters(
|
||||
// the getter / setter vs property as if there is a difference then that property isn't reported
|
||||
// by the BEAN inspector and thus we don't consider that case here
|
||||
|
||||
getters += PropertySerializer.make(property.key, getter, resolveTypeVariables(getter.genericReturnType, type),
|
||||
factory)
|
||||
getters += PropertySerializer.make(property.key, PublicPropertyReader(getter),
|
||||
resolveTypeVariables(getter.genericReturnType, type), factory)
|
||||
setters += setter
|
||||
}
|
||||
|
||||
@ -159,15 +165,22 @@ private fun constructorParamTakesReturnTypeOfGetter(getterReturnType: Type, rawG
|
||||
return typeToken.isSupertypeOf(getterReturnType) || typeToken.isSupertypeOf(rawGetterReturnType)
|
||||
}
|
||||
|
||||
private fun propertiesForSerializationFromAbstract(clazz: Class<*>, type: Type, factory: SerializerFactory): ConstructorDestructorMethods {
|
||||
private fun propertiesForSerializationFromAbstract(
|
||||
clazz: Class<*>,
|
||||
type: Type,
|
||||
factory: SerializerFactory): ConstructorDestructorMethods {
|
||||
// Kotlin reflection doesn't work with Java getters the way you might expect, so we drop back to good ol' beans.
|
||||
val properties = Introspector.getBeanInfo(clazz).propertyDescriptors.filter { it.name != "class" }.sortedBy { it.name }.filterNot { it is IndexedPropertyDescriptor }
|
||||
val properties = Introspector.getBeanInfo(clazz).propertyDescriptors
|
||||
.filter { it.name != "class" }
|
||||
.sortedBy { it.name }
|
||||
.filterNot { it is IndexedPropertyDescriptor }
|
||||
val rc: MutableList<PropertySerializer> = ArrayList(properties.size)
|
||||
for (property in properties) {
|
||||
// Check that the method has a getter in java.
|
||||
val getter = property.readMethod ?: throw NotSerializableException("Property has no getter method for ${property.name} of $clazz.")
|
||||
val getter = property.readMethod ?: throw NotSerializableException(
|
||||
"Property has no getter method for ${property.name} of $clazz.")
|
||||
val returnType = resolveTypeVariables(getter.genericReturnType, type)
|
||||
rc += PropertySerializer.make(property.name, getter, returnType, factory)
|
||||
rc += PropertySerializer.make(property.name, PublicPropertyReader(getter), returnType, factory)
|
||||
}
|
||||
return ConstructorDestructorMethods(rc, emptyList())
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
|
||||
val transformsCache = ConcurrentHashMap<String, EnumMap<TransformTypes, MutableList<Transform>>>()
|
||||
|
||||
open val classCarpenter = ClassCarpenter(cl, whitelist)
|
||||
|
||||
val classloader: ClassLoader
|
||||
get() = classCarpenter.classloader
|
||||
|
||||
@ -381,3 +382,4 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
|
||||
override fun toString(): String = "?"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<T
|
||||
val constructor = constructorForDeserialization(obj.javaClass)
|
||||
val props = propertiesForSerialization(constructor, obj.javaClass, factory)
|
||||
for (prop in props.getters) {
|
||||
extraProperties[prop.name] = prop.readMethod!!.invoke(obj)
|
||||
extraProperties[prop.name] = prop.propertyReader.read(obj)
|
||||
}
|
||||
} catch (e: NotSerializableException) {
|
||||
logger.warn("Unexpected exception", e)
|
||||
|
@ -2,10 +2,12 @@ package net.corda.nodeapi.internal.serialization.amqp;
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.AllWhitelist;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.NotSerializableException;
|
||||
|
||||
@Ignore("Current behaviour allows for the serialization of objects with private members, this will be disallowed at some point in the future")
|
||||
public class ErrorMessageTests {
|
||||
private String errMsg(String property, String testname) {
|
||||
return "Property '"
|
||||
|
@ -0,0 +1,79 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp;
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.AllWhitelist;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.NotSerializableException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class JavaPrivatePropertyTests {
|
||||
static class C {
|
||||
private String a;
|
||||
|
||||
C(String a) { this.a = a; }
|
||||
}
|
||||
|
||||
static class C2 {
|
||||
private String a;
|
||||
|
||||
C2(String a) { this.a = a; }
|
||||
|
||||
public String getA() { return a; }
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singlePrivateWithConstructor() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
|
||||
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader());
|
||||
SerializationOutput ser = new SerializationOutput(factory);
|
||||
DeserializationInput des = new DeserializationInput(factory);
|
||||
|
||||
C c = new C("dripping taps");
|
||||
C c2 = des.deserialize(ser.serialize(c), C.class);
|
||||
|
||||
assertEquals (c.a, c2.a);
|
||||
|
||||
//
|
||||
// Now ensure we actually got a private property serializer
|
||||
//
|
||||
Field f = SerializerFactory.class.getDeclaredField("serializersByDescriptor");
|
||||
f.setAccessible(true);
|
||||
|
||||
ConcurrentHashMap<Object, AMQPSerializer<Object>> serializersByDescriptor =
|
||||
(ConcurrentHashMap<Object, AMQPSerializer<Object>>) f.get(factory);
|
||||
|
||||
assertEquals(1, serializersByDescriptor.size());
|
||||
ObjectSerializer cSerializer = ((ObjectSerializer)serializersByDescriptor.values().toArray()[0]);
|
||||
assertEquals(1, cSerializer.getPropertySerializers().component1().size());
|
||||
Object[] propertyReaders = cSerializer.getPropertySerializers().component1().toArray();
|
||||
assertTrue (((PropertySerializer)propertyReaders[0]).getPropertyReader() instanceof PrivatePropertyReader);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singlePrivateWithConstructorAndGetter()
|
||||
throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
|
||||
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader());
|
||||
SerializationOutput ser = new SerializationOutput(factory);
|
||||
DeserializationInput des = new DeserializationInput(factory);
|
||||
|
||||
C2 c = new C2("dripping taps");
|
||||
C2 c2 = des.deserialize(ser.serialize(c), C2.class);
|
||||
|
||||
assertEquals (c.a, c2.a);
|
||||
|
||||
//
|
||||
// Now ensure we actually got a private property serializer
|
||||
//
|
||||
Field f = SerializerFactory.class.getDeclaredField("serializersByDescriptor");
|
||||
f.setAccessible(true);
|
||||
ConcurrentHashMap<Object, AMQPSerializer<Object>> serializersByDescriptor =
|
||||
(ConcurrentHashMap<Object, AMQPSerializer<Object>>) f.get(factory);
|
||||
|
||||
assertEquals(1, serializersByDescriptor.size());
|
||||
ObjectSerializer cSerializer = ((ObjectSerializer)serializersByDescriptor.values().toArray()[0]);
|
||||
assertEquals(1, cSerializer.getPropertySerializers().component1().size());
|
||||
Object[] propertyReaders = cSerializer.getPropertySerializers().component1().toArray();
|
||||
assertTrue (((PropertySerializer)propertyReaders[0]).getPropertyReader() instanceof PublicPropertyReader);
|
||||
}
|
||||
}
|
@ -1,10 +1,8 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import net.corda.nodeapi.internal.serialization.AllWhitelist
|
||||
import net.corda.nodeapi.internal.serialization.EmptyWhitelist
|
||||
import java.io.NotSerializableException
|
||||
|
||||
fun testDefaultFactory() = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
|
||||
fun testDefaultFactoryWithWhitelist() = SerializerFactory(EmptyWhitelist, ClassLoader.getSystemClassLoader())
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import java.io.NotSerializableException
|
||||
|
||||
@ -12,6 +13,8 @@ class ErrorMessagesTests {
|
||||
private fun errMsg(property:String, testname: String) =
|
||||
"Property '$property' or its getter is non public, this renders class 'class $testname\$C' unserializable -> class $testname\$C"
|
||||
|
||||
// Java allows this to be set at the class level yet Kotlin doesn't for some reason
|
||||
@Ignore("Current behaviour allows for the serialization of objects with private members, this will be disallowed at some point in the future")
|
||||
@Test
|
||||
fun privateProperty() {
|
||||
data class C(private val a: Int)
|
||||
@ -25,6 +28,8 @@ class ErrorMessagesTests {
|
||||
}.isInstanceOf(NotSerializableException::class.java).hasMessage(errMsg("a", testname))
|
||||
}
|
||||
|
||||
// Java allows this to be set at the class level yet Kotlin doesn't for some reason
|
||||
@Ignore("Current behaviour allows for the serialization of objects with private members, this will be disallowed at some point in the future")
|
||||
@Test
|
||||
fun privateProperty2() {
|
||||
data class C(val a: Int, private val b: Int)
|
||||
@ -38,6 +43,8 @@ class ErrorMessagesTests {
|
||||
}.isInstanceOf(NotSerializableException::class.java).hasMessage(errMsg("b", testname))
|
||||
}
|
||||
|
||||
// Java allows this to be set at the class level yet Kotlin doesn't for some reason
|
||||
@Ignore("Current behaviour allows for the serialization of objects with private members, this will be disallowed at some point in the future")
|
||||
@Test
|
||||
fun privateProperty3() {
|
||||
// despite b being private, the getter we've added is public and thus allows for the serialisation
|
||||
@ -54,6 +61,8 @@ class ErrorMessagesTests {
|
||||
val c = DeserializationInput(sf).deserialize(bytes)
|
||||
}
|
||||
|
||||
// Java allows this to be set at the class level yet Kotlin doesn't for some reason
|
||||
@Ignore("Current behaviour allows for the serialization of objects with private members, this will be disallowed at some point in the future")
|
||||
@Test
|
||||
fun protectedProperty() {
|
||||
data class C(protected val a: Int)
|
||||
|
@ -0,0 +1,117 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import org.junit.Test
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class PrivatePropertyTests {
|
||||
private val factory = testDefaultFactory()
|
||||
|
||||
@Test
|
||||
fun testWithOnePrivateProperty() {
|
||||
data class C(private val b: String)
|
||||
|
||||
val c1 = C("Pants are comfortable sometimes")
|
||||
val c2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(c1))
|
||||
assertEquals(c1, c2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWithOnePrivatePropertyNullableNotNull() {
|
||||
data class C(private val b: String?)
|
||||
|
||||
val c1 = C("Pants are comfortable sometimes")
|
||||
val c2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(c1))
|
||||
assertEquals(c1, c2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWithOnePrivatePropertyNullableNull() {
|
||||
data class C(private val b: String?)
|
||||
|
||||
val c1 = C(null)
|
||||
val c2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(c1))
|
||||
assertEquals(c1, c2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWithOnePublicOnePrivateProperty() {
|
||||
data class C(val a: Int, private val b: Int)
|
||||
|
||||
val c1 = C(1, 2)
|
||||
val c2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(c1))
|
||||
assertEquals(c1, c2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWithOnePublicOnePrivateProperty2() {
|
||||
data class C(val a: Int, private val b: Int)
|
||||
|
||||
val c1 = C(1, 2)
|
||||
val schemaAndBlob = SerializationOutput(factory).serializeAndReturnSchema(c1)
|
||||
assertEquals(1, schemaAndBlob.schema.types.size)
|
||||
|
||||
val field = SerializerFactory::class.java.getDeclaredField("serializersByDescriptor")
|
||||
field.isAccessible = true
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val serializersByDescriptor = field.get(factory) as ConcurrentHashMap<Any, AMQPSerializer<Any>>
|
||||
|
||||
val schemaDescriptor = schemaAndBlob.schema.types.first().descriptor.name
|
||||
serializersByDescriptor.filterKeys { (it as Symbol) == schemaDescriptor }.values.apply {
|
||||
assertEquals(1, this.size)
|
||||
assertTrue(this.first() is ObjectSerializer)
|
||||
val propertySerializers = (this.first() as ObjectSerializer).propertySerializers.getters.toList()
|
||||
assertEquals(2, propertySerializers.size)
|
||||
// a was public so should have a synthesised getter
|
||||
assertTrue(propertySerializers[0].propertyReader is PublicPropertyReader)
|
||||
|
||||
// b is private and thus won't have teh getter so we'll have reverted
|
||||
// to using reflection to remove the inaccessible property
|
||||
assertTrue(propertySerializers[1].propertyReader is PrivatePropertyReader)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetterMakesAPublicReader() {
|
||||
data class C(val a: Int, private val b: Int) {
|
||||
@Suppress("UNUSED")
|
||||
fun getB() = b
|
||||
}
|
||||
|
||||
val c1 = C(1, 2)
|
||||
val schemaAndBlob = SerializationOutput(factory).serializeAndReturnSchema(c1)
|
||||
assertEquals(1, schemaAndBlob.schema.types.size)
|
||||
|
||||
val field = SerializerFactory::class.java.getDeclaredField("serializersByDescriptor")
|
||||
field.isAccessible = true
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val serializersByDescriptor = field.get(factory) as ConcurrentHashMap<Any, AMQPSerializer<Any>>
|
||||
|
||||
val schemaDescriptor = schemaAndBlob.schema.types.first().descriptor.name
|
||||
serializersByDescriptor.filterKeys { (it as Symbol) == schemaDescriptor }.values.apply {
|
||||
assertEquals(1, this.size)
|
||||
assertTrue(this.first() is ObjectSerializer)
|
||||
val propertySerializers = (this.first() as ObjectSerializer).propertySerializers.getters.toList()
|
||||
assertEquals(2, propertySerializers.size)
|
||||
|
||||
// as before, a is public so we'll use the getter method
|
||||
assertTrue(propertySerializers[0].propertyReader is PublicPropertyReader)
|
||||
|
||||
// the getB() getter explicitly added means we should use the "normal" public
|
||||
// method reader rather than the private oen
|
||||
assertTrue(propertySerializers[1].propertyReader is PublicPropertyReader)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNested() {
|
||||
data class Inner(private val a: Int)
|
||||
data class Outer(private val i: Inner)
|
||||
|
||||
val c1 = Outer(Inner(1010101))
|
||||
val c2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(c1))
|
||||
assertEquals(c1, c2)
|
||||
}
|
||||
}
|
@ -35,9 +35,16 @@ fun Schema.mangleNames(names: List<String>): Schema {
|
||||
return Schema(types = newTypes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom implementation of a [SerializerFactory] where we need to give it a class carpenter
|
||||
* rather than have it create its own
|
||||
*/
|
||||
class SerializerFactoryExternalCarpenter(override val classCarpenter: ClassCarpenter)
|
||||
: SerializerFactory (classCarpenter.whitelist, classCarpenter.classloader)
|
||||
|
||||
open class AmqpCarpenterBase(whitelist: ClassWhitelist) {
|
||||
var cc = ClassCarpenter(whitelist = whitelist)
|
||||
var factory = SerializerFactory(AllWhitelist, cc.classloader)
|
||||
var factory = SerializerFactoryExternalCarpenter(cc)
|
||||
|
||||
fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz)
|
||||
fun testName(): String = Thread.currentThread().stackTrace[2].methodName
|
||||
|
Loading…
Reference in New Issue
Block a user