From ed2b2b02ca8c2b33eb0b1ca56341db08984c46f8 Mon Sep 17 00:00:00 2001
From: Katelyn Baker <katelyn.baker@r3.com>
Date: Thu, 24 Aug 2017 13:48:07 +0100
Subject: [PATCH] Add support for serialising enum types - Part 1

Part 2 will address the carpenting of enum classes
---
 .../amqp/DeserializationInput.kt              |  79 ++++----
 .../amqp/DeserializedParameterizedType.kt     |   3 +-
 .../serialization/amqp/EnumSerializer.kt      |  47 +++++
 .../serialization/amqp/MapSerializer.kt       |   2 +-
 .../internal/serialization/amqp/Schema.kt     |  25 ++-
 .../serialization/amqp/SerializerFactory.kt   |  28 +--
 .../amqp/JavaSerialiseEnumTests.java          |  36 ++++
 .../amqp/JavaSerializationOutputTests.java    |  12 +-
 .../serialization/amqp/AMQPTestUtils.kt       |   1 -
 .../internal/serialization/amqp/EnumTests.kt  | 191 ++++++++++++++++++
 .../serialization/amqp/EnumTests.changedEnum1 | Bin 0 -> 611 bytes
 .../serialization/amqp/EnumTests.changedEnum2 | Bin 0 -> 616 bytes
 12 files changed, 353 insertions(+), 71 deletions(-)
 create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt
 create mode 100644 node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerialiseEnumTests.java
 create mode 100644 node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt
 create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumTests.changedEnum1
 create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumTests.changedEnum2

diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt
index d5668cc494..7f6a938bb5 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt
@@ -25,7 +25,7 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
     private val objectHistory: MutableList<Any> = mutableListOf()
 
     internal companion object {
-        val BYTES_NEEDED_TO_PEEK: Int = 23
+        private val BYTES_NEEDED_TO_PEEK: Int = 23
 
         fun peekSize(bytes: ByteArray): Int {
             // There's an 8 byte header, and then a 0 byte plus descriptor followed by constructor
@@ -57,7 +57,6 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
     inline internal fun <reified T : Any> deserializeAndReturnEnvelope(bytes: SerializedBytes<T>): ObjectAndEnvelope<T> =
             deserializeAndReturnEnvelope(bytes, T::class.java)
 
-
     @Throws(NotSerializableException::class)
     private fun getEnvelope(bytes: ByteSequence): Envelope {
         // Check that the lead bytes match expected header
@@ -94,20 +93,16 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
      * be deserialized and a schema describing the types of the objects.
      */
     @Throws(NotSerializableException::class)
-    fun <T : Any> deserialize(bytes: ByteSequence, clazz: Class<T>): T {
-        return des {
-            val envelope = getEnvelope(bytes)
-            clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz))
-        }
+    fun <T : Any> deserialize(bytes: ByteSequence, clazz: Class<T>): T = des {
+        val envelope = getEnvelope(bytes)
+        clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz))
     }
 
     @Throws(NotSerializableException::class)
-    internal fun <T : Any> deserializeAndReturnEnvelope(bytes: SerializedBytes<T>, clazz: Class<T>): ObjectAndEnvelope<T> {
-        return des {
-            val envelope = getEnvelope(bytes)
-            // Now pick out the obj and schema from the envelope.
-            ObjectAndEnvelope(clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)), envelope)
-        }
+    fun <T : Any> deserializeAndReturnEnvelope(bytes: SerializedBytes<T>, clazz: Class<T>): ObjectAndEnvelope<T> = des {
+        val envelope = getEnvelope(bytes)
+        // Now pick out the obj and schema from the envelope.
+        ObjectAndEnvelope(clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)), envelope)
     }
 
     internal fun readObjectOrNull(obj: Any?, schema: Schema, type: Type): Any? {
@@ -115,36 +110,36 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
     }
 
     internal fun readObject(obj: Any, schema: Schema, type: Type): Any =
-        if (obj is DescribedType && ReferencedObject.DESCRIPTOR == obj.descriptor) {
-            // It must be a reference to an instance that has already been read, cheaply and quickly returning it by reference.
-            val objectIndex = (obj.described as UnsignedInteger).toInt()
-            if (objectIndex !in 0..objectHistory.size)
-                throw NotSerializableException("Retrieval of existing reference failed. Requested index $objectIndex " +
-                        "is outside of the bounds for the list of size: ${objectHistory.size}")
+            if (obj is DescribedType && ReferencedObject.DESCRIPTOR == obj.descriptor) {
+                // It must be a reference to an instance that has already been read, cheaply and quickly returning it by reference.
+                val objectIndex = (obj.described as UnsignedInteger).toInt()
+                if (objectIndex !in 0..objectHistory.size)
+                    throw NotSerializableException("Retrieval of existing reference failed. Requested index $objectIndex " +
+                            "is outside of the bounds for the list of size: ${objectHistory.size}")
 
-            val objectRetrieved = objectHistory[objectIndex]
-            if (!objectRetrieved::class.java.isSubClassOf(type.asClass()!!))
-                throw NotSerializableException("Existing reference type mismatch. Expected: '$type', found: '${objectRetrieved::class.java}'")
-            objectRetrieved
-        }
-        else {
-            val objectRead = when (obj) {
-                is DescribedType -> {
-                    // Look up serializer in factory by descriptor
-                    val serializer = serializerFactory.get(obj.descriptor, schema)
-                    if (serializer.type != type && with(serializer.type) { !isSubClassOf(type) && !materiallyEquivalentTo(type) })
-                        throw NotSerializableException("Described type with descriptor ${obj.descriptor} was " +
-                                "expected to be of type $type but was ${serializer.type}")
-                    serializer.readObject(obj.described, schema, this)
+                val objectRetrieved = objectHistory[objectIndex]
+                if (!objectRetrieved::class.java.isSubClassOf(type))
+                    throw NotSerializableException("Existing reference type mismatch. Expected: '$type', found: '${objectRetrieved::class.java}'")
+                objectRetrieved
+            } else {
+                val objectRead = when (obj) {
+                    is DescribedType -> {
+                        // Look up serializer in factory by descriptor
+                        val serializer = serializerFactory.get(obj.descriptor, schema)
+                        if (serializer.type != type && with(serializer.type) { !isSubClassOf(type) && !materiallyEquivalentTo(type) })
+                            throw NotSerializableException("Described type with descriptor ${obj.descriptor} was " +
+                                    "expected to be of type $type but was ${serializer.type}")
+                        serializer.readObject(obj.described, schema, this)
+                    }
+                    is Binary -> obj.array
+                    else -> obj // this will be the case for primitive types like [boolean] et al.
                 }
-                is Binary -> obj.array
-                else -> obj // this will be the case for primitive types like [boolean] et al.
+
+                // Store the reference in case we need it later on.
+                // Skip for primitive types as they are too small and overhead of referencing them will be much higher than their content
+                if (type.asClass()?.isPrimitive != true) objectHistory.add(objectRead)
+                objectRead
             }
-            // Store the reference in case we need it later on.
-            // Skip for primitive types as they are too small and overhead of referencing them will be much higher than their content
-            if (suitableForObjectReference(objectRead.javaClass)) objectHistory.add(objectRead)
-            objectRead
-        }
 
     /**
      * TODO: Currently performs rather basic checks aimed in particular at [java.util.List<Command<?>>] and
@@ -152,5 +147,5 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
      * In the future tighter control might be needed
      */
     private fun Type.materiallyEquivalentTo(that: Type): Boolean =
-        asClass() == that.asClass() && that is ParameterizedType
-}
\ No newline at end of file
+            asClass() == that.asClass() && that is ParameterizedType
+}
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializedParameterizedType.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializedParameterizedType.kt
index 3209866821..96b2d729d4 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializedParameterizedType.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializedParameterizedType.kt
@@ -53,6 +53,7 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p
             var typeStart = 0
             var needAType = true
             var skippingWhitespace = false
+
             while (pos < params.length) {
                 if (params[pos] == '<') {
                     val typeEnd = pos++
@@ -102,7 +103,7 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p
                         } else if (!skippingWhitespace && (params[pos] == '.' || params[pos].isJavaIdentifierPart())) {
                             pos++
                         } else {
-                            throw NotSerializableException("Invalid character in middle of type: ${params[pos]}")
+                            throw NotSerializableException("Invalid character ${params[pos]} in middle of type $params at idx $pos")
                         }
                     }
                 }
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt
new file mode 100644
index 0000000000..ce648b7eeb
--- /dev/null
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt
@@ -0,0 +1,47 @@
+package net.corda.nodeapi.internal.serialization.amqp
+
+import org.apache.qpid.proton.codec.Data
+import java.lang.reflect.Type
+import java.io.NotSerializableException
+
+class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: SerializerFactory) : AMQPSerializer<Any> {
+    override val type: Type = declaredType
+    override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}"
+    private val typeNotation: TypeNotation
+
+    init {
+        typeNotation = RestrictedType(
+                SerializerFactory.nameForType(declaredType),
+                null, emptyList(), "enum", Descriptor(typeDescriptor, null),
+                declaredClass.enumConstants.zip(IntRange(0, declaredClass.enumConstants.size)).map {
+                    Choice(it.first.toString(), it.second.toString())
+                })
+    }
+
+    override fun writeClassInfo(output: SerializationOutput) {
+        output.writeTypeNotations(typeNotation)
+    }
+
+    override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
+        val enumName = (obj as List<*>)[0] as String
+        val enumOrd = obj[1] as Int
+        val fromOrd = type.asClass()!!.enumConstants[enumOrd]
+
+        if (enumName != fromOrd?.toString()) {
+            throw NotSerializableException("Deserializing obj as enum $type with value $enumName.$enumOrd but "
+                    + "ordinality has changed")
+        }
+        return fromOrd
+    }
+
+    override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
+        if (obj !is Enum<*>) throw NotSerializableException("Serializing $obj as enum when it isn't")
+
+        data.withDescribed(typeNotation.descriptor) {
+            withList {
+                data.putString(obj.name)
+                data.putInt(obj.ordinal)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt
index 8b74459fe5..314c9b6aa9 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt
@@ -13,7 +13,7 @@ import kotlin.collections.map
 /**
  * Serialization / deserialization of certain supported [Map] types.
  */
-class MapSerializer(val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer<Any> {
+class MapSerializer(private val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer<Any> {
     override val type: Type = declaredType as? DeserializedParameterizedType ?: DeserializedParameterizedType.make(declaredType.toString())
     override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}"
 
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 6a506685c6..7b2aaa41d4 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
@@ -252,7 +252,12 @@ data class CompositeType(override val name: String, override val label: String?,
     }
 }
 
-data class RestrictedType(override val name: String, override val label: String?, override val provides: List<String>, val source: String, override val descriptor: Descriptor, val choices: List<Choice>) : TypeNotation() {
+data class RestrictedType(override val name: String,
+                          override val label: String?,
+                          override val provides: List<String>,
+                          val source: String,
+                          override val descriptor: Descriptor,
+                          val choices: List<Choice>) : TypeNotation() {
     companion object : DescribedTypeConstructor<RestrictedType> {
         val DESCRIPTOR = DescriptorRegistry.RESTRICTED_TYPE.amqpDescriptor
 
@@ -290,6 +295,9 @@ data class RestrictedType(override val name: String, override val label: String?
         }
         sb.append(">\n")
         sb.append("  $descriptor\n")
+        choices.forEach {
+            sb.append("  $it\n")
+        }
         sb.append("</type>")
         return sb.toString()
     }
@@ -403,14 +411,13 @@ private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: Muta
             if (type is SerializerFactory.AnyType) {
                 hasher.putUnencodedChars(ANY_TYPE_HASH)
             } else if (type is Class<*>) {
-                if (type.isArray) {
-                    fingerprintForType(type.componentType, contextType, alreadySeen, hasher, factory).putUnencodedChars(ARRAY_HASH)
-                } else if (SerializerFactory.isPrimitive(type)) {
-                    hasher.putUnencodedChars(type.name)
-                } else if (isCollectionOrMap(type)) {
-                    hasher.putUnencodedChars(type.name)
-                } else {
-                    hasher.fingerprintWithCustomSerializerOrElse(factory, type, type) {
+                when {
+                    type.isArray -> fingerprintForType(type.componentType, contextType, alreadySeen, hasher, factory).putUnencodedChars(ARRAY_HASH)
+                    SerializerFactory.isPrimitive(type) ||
+                    isCollectionOrMap(type) ||
+                    type.isEnum -> hasher.putUnencodedChars(type.name)
+                    else ->
+                        hasher.fingerprintWithCustomSerializerOrElse(factory, type, type) {
                         if (type.kotlin.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.
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 c0b8b19c3f..4538711459 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
@@ -21,7 +21,6 @@ data class schemaAndDescriptor (val schema: Schema, val typeDescriptor: Any)
 /**
  * Factory of serializers designed to be shared across threads and invocations.
  */
-// TODO: enums
 // TODO: object references - need better fingerprinting?
 // TODO: class references? (e.g. cheat with repeated descriptors using a long encoding, like object ref proposal)
 // TODO: Inner classes etc. Should we allow? Currently not considered.
@@ -66,18 +65,20 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
 
         val actualType: Type = inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType
 
-        val serializer = if (Collection::class.java.isAssignableFrom(declaredClass)) {
-            serializersByType.computeIfAbsent(declaredType) {
-                CollectionSerializer(declaredType as? ParameterizedType ?: DeserializedParameterizedType(
-                        declaredClass, arrayOf(AnyType), null), this)
+        val serializer = when {
+            (Collection::class.java.isAssignableFrom(declaredClass)) -> { serializersByType.computeIfAbsent(declaredType) {
+                    CollectionSerializer(declaredType as? ParameterizedType ?: DeserializedParameterizedType(
+                            declaredClass, arrayOf(AnyType), null), this)
+                }
             }
-        } else if (Map::class.java.isAssignableFrom(declaredClass)) {
-            serializersByType.computeIfAbsent(declaredClass) {
+            Map::class.java.isAssignableFrom(declaredClass) -> serializersByType.computeIfAbsent(declaredClass) {
                 makeMapSerializer(declaredType as? ParameterizedType ?: DeserializedParameterizedType(
                         declaredClass, arrayOf(AnyType, AnyType), null))
             }
-        } else {
-            makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType)
+            Enum::class.java.isAssignableFrom(declaredClass) -> serializersByType.computeIfAbsent(declaredClass) {
+                EnumSerializer(actualType, actualClass ?: declaredClass, this)
+            }
+            else -> makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType)
         }
 
         serializersByDescriptor.putIfAbsent(serializer.typeDescriptor, serializer)
@@ -248,8 +249,9 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
     }
 
     internal fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer<Any>? {
-        // e.g. Imagine if we provided a Map serializer this way, then it won't work if the declared type is AbstractMap, only Map.
-        // Otherwise it needs to inject additional schema for a RestrictedType source of the super type.  Could be done, but do we need it?
+        // e.g. Imagine if we provided a Map serializer this way, then it won't work if the declared type is
+        // AbstractMap, only Map. Otherwise it needs to inject additional schema for a RestrictedType source of the
+        // super type.  Could be done, but do we need it?
         for (customSerializer in customSerializers) {
             if (customSerializer.isSerializerFor(clazz)) {
                 val declaredSuperClass = declaredType.asClass()?.superclass
@@ -258,7 +260,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
                 } else {
                     // Make a subclass serializer for the subclass and return that...
                     @Suppress("UNCHECKED_CAST")
-                    return CustomSerializer.SubClass<Any>(clazz, customSerializer as CustomSerializer<Any>)
+                    return CustomSerializer.SubClass(clazz, customSerializer as CustomSerializer<Any>)
                 }
             }
         }
@@ -277,7 +279,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
             (!whitelist.hasListed(clazz) && !hasAnnotationInHierarchy(clazz))
 
     // Recursively check the class, interfaces and superclasses for our annotation.
-    internal fun hasAnnotationInHierarchy(type: Class<*>): Boolean {
+    private fun hasAnnotationInHierarchy(type: Class<*>): Boolean {
         return type.isAnnotationPresent(CordaSerializable::class.java) ||
                 type.interfaces.any { hasAnnotationInHierarchy(it) }
                 || (type.superclass != null && hasAnnotationInHierarchy(type.superclass))
diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerialiseEnumTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerialiseEnumTests.java
new file mode 100644
index 0000000000..e0b65ad27c
--- /dev/null
+++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerialiseEnumTests.java
@@ -0,0 +1,36 @@
+package net.corda.nodeapi.internal.serialization.amqp;
+
+import org.junit.Test;
+
+import net.corda.nodeapi.internal.serialization.AllWhitelist;
+import net.corda.core.serialization.SerializedBytes;
+
+import java.io.NotSerializableException;
+
+public class JavaSerialiseEnumTests {
+
+    public enum Bras {
+        TSHIRT, UNDERWIRE, PUSHUP, BRALETTE, STRAPLESS, SPORTS, BACKLESS, PADDED
+    }
+
+    private static class Bra {
+        private final Bras bra;
+
+        private Bra(Bras bra) {
+            this.bra = bra;
+        }
+
+        public Bras getBra() {
+            return this.bra;
+        }
+    }
+
+    @Test
+    public void testJavaConstructorAnnotations() throws NotSerializableException {
+        Bra bra = new Bra(Bras.UNDERWIRE);
+
+        SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader());
+        SerializationOutput ser = new SerializationOutput(factory1);
+        SerializedBytes<Object> bytes = ser.serialize(bra);
+    }
+}
diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java
index 171b9cbe72..34ca1abaa6 100644
--- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java
+++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java
@@ -25,15 +25,17 @@ public class JavaSerializationOutputTests {
         }
 
         @ConstructorForDeserialization
-        public Foo(String fred, int count) {
+        private Foo(String fred, int count) {
             this.bob = fred;
             this.count = count;
         }
 
+        @SuppressWarnings("unused")
         public String getFred() {
             return bob;
         }
 
+        @SuppressWarnings("unused")
         public int getCount() {
             return count;
         }
@@ -61,15 +63,17 @@ public class JavaSerializationOutputTests {
         private final String bob;
         private final int count;
 
-        public UnAnnotatedFoo(String fred, int count) {
+        private UnAnnotatedFoo(String fred, int count) {
             this.bob = fred;
             this.count = count;
         }
 
+        @SuppressWarnings("unused")
         public String getFred() {
             return bob;
         }
 
+        @SuppressWarnings("unused")
         public int getCount() {
             return count;
         }
@@ -97,7 +101,7 @@ public class JavaSerializationOutputTests {
         private final String fred;
         private final Integer count;
 
-        public BoxedFoo(String fred, Integer count) {
+        private BoxedFoo(String fred, Integer count) {
             this.fred = fred;
             this.count = count;
         }
@@ -134,7 +138,7 @@ public class JavaSerializationOutputTests {
         private final String fred;
         private final Integer count;
 
-        public BoxedFooNotNull(String fred, Integer count) {
+        private BoxedFooNotNull(String fred, Integer count) {
             this.fred = fred;
             this.count = count;
         }
diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt
index 0494497138..4f9b7b6872 100644
--- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt
+++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt
@@ -6,7 +6,6 @@ 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())
 
diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt
new file mode 100644
index 0000000000..5293f02e21
--- /dev/null
+++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt
@@ -0,0 +1,191 @@
+package net.corda.nodeapi.internal.serialization.amqp
+
+import org.junit.Test
+import java.time.DayOfWeek
+
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+
+import java.io.File
+import java.io.NotSerializableException
+
+import net.corda.core.serialization.SerializedBytes
+
+class EnumTests {
+    enum class Bras {
+        TSHIRT, UNDERWIRE, PUSHUP, BRALETTE, STRAPLESS, SPORTS, BACKLESS, PADDED
+    }
+
+    // The state of the OldBras enum when the tests in changedEnum1 were serialised
+    //      - use if the test file needs regenerating
+    //enum class OldBras {
+    //    TSHIRT, UNDERWIRE, PUSHUP, BRALETTE
+    //}
+
+    // the new state, SPACER has been added to change the ordinality
+    enum class OldBras {
+        SPACER, TSHIRT, UNDERWIRE, PUSHUP, BRALETTE
+    }
+
+    // The state of the OldBras2 enum when the tests in changedEnum2 were serialised
+    //      - use if the test file needs regenerating
+    //enum class OldBras2 {
+    //    TSHIRT, UNDERWIRE, PUSHUP, BRALETTE
+    //}
+
+    // the new state, note in the test we serialised with value UNDERWIRE so the spacer
+    // occuring after this won't have changed the ordinality of our serialised value
+    // and thus should still be deserialisable
+    enum class OldBras2 {
+        TSHIRT, UNDERWIRE, PUSHUP, SPACER, BRALETTE, SPACER2
+    }
+
+
+    enum class BrasWithInit (val someList: List<Int>) {
+        TSHIRT(emptyList()),
+        UNDERWIRE(listOf(1, 2, 3)),
+        PUSHUP(listOf(100, 200)),
+        BRALETTE(emptyList())
+    }
+
+    private val brasTestName = "${this.javaClass.name}\$Bras"
+
+    companion object {
+        /**
+         * If you want to see the schema encoded into the envelope after serialisation change this to true
+         */
+        private const val VERBOSE = false
+    }
+
+    @Suppress("NOTHING_TO_INLINE")
+    inline private fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz"
+
+    private val sf1 = testDefaultFactory()
+
+    @Test
+    fun serialiseSimpleTest() {
+        data class C(val c: Bras)
+
+        val schema = TestSerializationOutput(VERBOSE, sf1).serializeAndReturnSchema(C(Bras.UNDERWIRE)).schema
+
+        assertEquals(2, schema.types.size)
+        val schema_c = schema.types.find { it.name == classTestName("C") } as CompositeType
+        val schema_bras = schema.types.find { it.name == brasTestName } as RestrictedType
+
+        assertNotNull(schema_c)
+        assertNotNull(schema_bras)
+
+        assertEquals(1, schema_c.fields.size)
+        assertEquals("c", schema_c.fields.first().name)
+        assertEquals(brasTestName, schema_c.fields.first().type)
+
+        assertEquals(8, schema_bras.choices.size)
+        Bras.values().forEach {
+            val bra = it
+            assertNotNull (schema_bras.choices.find { it.name == bra.name })
+        }
+    }
+
+    @Test
+    fun deserialiseSimpleTest() {
+        data class C(val c: Bras)
+
+        val objAndEnvelope = DeserializationInput(sf1).deserializeAndReturnEnvelope(
+                TestSerializationOutput(VERBOSE, sf1).serialize(C(Bras.UNDERWIRE)))
+
+        val obj = objAndEnvelope.obj
+        val schema = objAndEnvelope.envelope.schema
+
+        assertEquals(2, schema.types.size)
+        val schema_c = schema.types.find { it.name == classTestName("C") } as CompositeType
+        val schema_bras = schema.types.find { it.name == brasTestName } as RestrictedType
+
+        assertEquals(1, schema_c.fields.size)
+        assertEquals("c", schema_c.fields.first().name)
+        assertEquals(brasTestName, schema_c.fields.first().type)
+
+        assertEquals(8, schema_bras.choices.size)
+        Bras.values().forEach {
+            val bra = it
+            assertNotNull (schema_bras.choices.find { it.name == bra.name })
+        }
+
+        // Test the actual deserialised object
+        assertEquals(obj.c, Bras.UNDERWIRE)
+    }
+
+    @Test
+    fun multiEnum() {
+        data class Support (val top: Bras, val day : DayOfWeek)
+        data class WeeklySupport (val tops: List<Support>)
+
+        val week = WeeklySupport (listOf(
+            Support (Bras.PUSHUP, DayOfWeek.MONDAY),
+            Support (Bras.UNDERWIRE, DayOfWeek.WEDNESDAY),
+            Support (Bras.PADDED, DayOfWeek.SUNDAY)))
+
+        val obj = DeserializationInput(sf1).deserialize(TestSerializationOutput(VERBOSE, sf1).serialize(week))
+
+        assertEquals(week.tops[0].top, obj.tops[0].top)
+        assertEquals(week.tops[0].day, obj.tops[0].day)
+        assertEquals(week.tops[1].top, obj.tops[1].top)
+        assertEquals(week.tops[1].day, obj.tops[1].day)
+        assertEquals(week.tops[2].top, obj.tops[2].top)
+        assertEquals(week.tops[2].day, obj.tops[2].day)
+    }
+
+    @Test
+    fun enumWithInit() {
+        data class C(val c: BrasWithInit)
+
+        val c = C (BrasWithInit.PUSHUP)
+        val obj = DeserializationInput(sf1).deserialize(TestSerializationOutput(VERBOSE, sf1).serialize(c))
+
+        assertEquals(c.c, obj.c)
+    }
+
+    @Test(expected = NotSerializableException::class)
+    fun changedEnum1() {
+        val path = EnumTests::class.java.getResource("EnumTests.changedEnum1")
+        val f = File(path.toURI())
+
+        data class C (val a: OldBras)
+
+        // Original version of the class for the serialised version of this class
+        //
+        // val a = OldBras.TSHIRT
+        // val sc = SerializationOutput(sf1).serialize(C(a))
+        // f.writeBytes(sc.bytes)
+        // println(path)
+
+        val sc2 = f.readBytes()
+
+        // we expect this to throw
+        DeserializationInput(sf1).deserialize(SerializedBytes<C>(sc2))
+    }
+
+    @Test
+    fun changedEnum2() {
+        val path = EnumTests::class.java.getResource("EnumTests.changedEnum2")
+        val f = File(path.toURI())
+
+        data class C (val a: OldBras2)
+
+        // DO NOT CHANGE THIS, it's important we serialise with a value that doesn't
+        // change position in the upated enum class
+        val a = OldBras2.UNDERWIRE
+
+        // Original version of the class for the serialised version of this class
+        //
+        // val sc = SerializationOutput(sf1).serialize(C(a))
+        // f.writeBytes(sc.bytes)
+        // println(path)
+
+        val sc2 = f.readBytes()
+
+        // we expect this to throw
+        val obj = DeserializationInput(sf1).deserialize(SerializedBytes<C>(sc2))
+
+        assertEquals(a, obj.a)
+    }
+}
\ No newline at end of file
diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumTests.changedEnum1 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumTests.changedEnum1
new file mode 100644
index 0000000000000000000000000000000000000000..800c35bf17112cdedf65d275b65ee62dfe8ef86c
GIT binary patch
literal 611
zcmbtR%}&BF9B(Th8jZe#AznxpCvY(#j$sv$2#yjGZw;$Z7#(iY#GQDRzDXa!69+F|
zd=NVi6-`Wx9@;d&{(oQjkHg5P5CE{!2dEbSp8-H!{YF3@3YJ*oY~r*$nACc9?W%S;
z3#!XBy$u(<J*n4K1%8>s9(SqTna@V$%6v8@kLeSw-srhqBnxEFYI}rynjvilU$t50
z3fCX5D}84{@@k!z7bbxvLZ8tkx41}HBxqp8EaEiaDNT4NEV{T~T39?R2#b@rG@ep1
zVSWbKrKW>Tzb$oyepAd*BWOrD)hS9y<=J23^a9_FXp9}F;TqL~dMo@}(!nyvbW5Z9
zz2Hw9anxPTkcw*0>_@`m4g3Jfo#FL4_C{?FOK5L=Wt9GK&>Hq7JlVkak=%B@Mi&#J
G8I^a0ZNftU

literal 0
HcmV?d00001

diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumTests.changedEnum2 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumTests.changedEnum2
new file mode 100644
index 0000000000000000000000000000000000000000..5f42911837200014df1478b9581254c04a68435d
GIT binary patch
literal 616
zcmbtR-AcnS7)@%oDTuy<3@^)8hcaQ1RwEsb{d6trWoV;Tx^!#fWWCy}<W2Gj-YMRB
z;d_{RQL7+`UL=H*^L^(WPLQM_fdGKDIzYYv_y~YDQEv^1Jw~|^6pZ7JbJR_SOFnKp
zD?8}$<yB}}lU`3A!Y{loZ{zuj+0@Dhqcr5!!(HAgcsrWd@`tC`z398xgQcFP^x&(~
zD}>?mhwxh6g_oLqs)@7Ggu#-Kl6hoAjMJ2n*vM!akvPf;j}m5(*<x;Bc0coI#<ON{
zOV|w!3&3oiAY4jr$bIcM*^s>a^r_49hJZw%zuFnap`DV<#0Xh->7_2;>i^$~u+XW>
zS+4ya@uymlyw=O1hTPUf-J3dn*AuW+!FQ|W3D~ZD6(eJR>iA;;%?hsi-n3n7fISZj
Gc<>G~Tf@)*

literal 0
HcmV?d00001