Merge pull request from corda/feature/kat/carpentEnum

CORDA-539 - Add enum support to the carpenter
This commit is contained in:
Katelyn Baker 2017-09-14 12:58:27 +01:00 committed by GitHub
commit 094187cfa1
9 changed files with 480 additions and 177 deletions
docs/source
node-api/src
main/kotlin/net/corda/nodeapi/internal/serialization/carpenter
test/kotlin/net/corda/nodeapi/internal/serialization/carpenter

@ -6,6 +6,8 @@ from the previous milestone release.
UNRELEASED
----------
* Adding enum support to the class carpenter
* ``ContractState::contract`` has been moved ``TransactionState::contract`` and it's type has changed to ``String`` in order to
support dynamic classloading of contract and contract constraints.

@ -20,11 +20,21 @@ interface SimpleFieldAccess {
operator fun get(name: String): Any?
}
class CarpenterClassLoader (parentClassLoader: ClassLoader = Thread.currentThread().contextClassLoader) :
class CarpenterClassLoader(parentClassLoader: ClassLoader = Thread.currentThread().contextClassLoader) :
ClassLoader(parentClassLoader) {
fun load(name: String, bytes: ByteArray) = defineClass(name, bytes, 0, bytes.size)
}
/**
* Which version of the java runtime are we constructing objects against
*/
private const val TARGET_VERSION = V1_8
private val jlEnum get() = Type.getInternalName(Enum::class.java)
private val jlString get() = Type.getInternalName(String::class.java)
private val jlObject get() = Type.getInternalName(Object::class.java)
private val jlClass get() = Type.getInternalName(Class::class.java)
/**
* A class carpenter generates JVM bytecodes for a class given a schema and then loads it into a sub-classloader.
* The generated classes have getters, a toString method and implement a simple property access interface. The
@ -107,49 +117,66 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
when (it) {
is InterfaceSchema -> generateInterface(it)
is ClassSchema -> generateClass(it)
is EnumSchema -> generateEnum(it)
}
}
assert (schema.name in _loaded)
assert(schema.name in _loaded)
return _loaded[schema.name]!!
}
private fun generateEnum(enumSchema: Schema): Class<*> {
return generate(enumSchema) { cw, schema ->
cw.apply {
visit(TARGET_VERSION, ACC_PUBLIC + ACC_FINAL + ACC_SUPER + ACC_ENUM, schema.jvmName,
"L$jlEnum<L${schema.jvmName};>;", jlEnum, null)
visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd()
generateFields(schema)
generateStaticEnumConstructor(schema)
generateEnumConstructor()
generateEnumValues(schema)
generateEnumValueOf(schema)
}.visitEnd()
}
}
private fun generateInterface(interfaceSchema: Schema): Class<*> {
return generate(interfaceSchema) { cw, schema ->
val interfaces = schema.interfaces.map { it.name.jvm }.toTypedArray()
with(cw) {
visit(V1_8, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, schema.jvmName, null, "java/lang/Object", interfaces)
cw.apply {
visit(TARGET_VERSION, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, schema.jvmName, null,
jlObject, interfaces)
visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd()
generateAbstractGetters(schema)
visitEnd()
}
}.visitEnd()
}
}
private fun generateClass(classSchema: Schema): Class<*> {
return generate(classSchema) { cw, schema ->
val superName = schema.superclass?.jvmName ?: "java/lang/Object"
val superName = schema.superclass?.jvmName ?: jlObject
val interfaces = schema.interfaces.map { it.name.jvm }.toMutableList()
if (SimpleFieldAccess::class.java !in schema.interfaces) interfaces.add(SimpleFieldAccess::class.java.name.jvm)
if (SimpleFieldAccess::class.java !in schema.interfaces) {
interfaces.add(SimpleFieldAccess::class.java.name.jvm)
}
with(cw) {
visit(V1_8, ACC_PUBLIC + ACC_SUPER, schema.jvmName, null, superName, interfaces.toTypedArray())
cw.apply {
visit(TARGET_VERSION, ACC_PUBLIC + ACC_SUPER, schema.jvmName, null, superName,
interfaces.toTypedArray())
visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd()
generateFields(schema)
generateConstructor(schema)
generateClassConstructor(schema)
generateGetters(schema)
if (schema.superclass == null)
generateGetMethod() // From SimplePropertyAccess
generateToString(schema)
visitEnd()
}
}.visitEnd()
}
}
@ -165,25 +192,26 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
}
private fun ClassWriter.generateFields(schema: Schema) {
schema.fields.forEach { it.value.generateField(this) }
schema.generateFields(this)
}
private fun ClassWriter.generateToString(schema: Schema) {
val toStringHelper = "com/google/common/base/MoreObjects\$ToStringHelper"
with(visitMethod(ACC_PUBLIC, "toString", "()Ljava/lang/String;", null, null)) {
with(visitMethod(ACC_PUBLIC, "toString", "()L$jlString;", null, null)) {
visitCode()
// com.google.common.base.MoreObjects.toStringHelper("TypeName")
visitLdcInsn(schema.name.split('.').last())
visitMethodInsn(INVOKESTATIC, "com/google/common/base/MoreObjects", "toStringHelper", "(Ljava/lang/String;)L$toStringHelper;", false)
visitMethodInsn(INVOKESTATIC, "com/google/common/base/MoreObjects", "toStringHelper",
"(L$jlString;)L$toStringHelper;", false)
// Call the add() methods.
for ((name, field) in schema.fieldsIncludingSuperclasses().entries) {
visitLdcInsn(name)
visitVarInsn(ALOAD, 0) // this
visitFieldInsn(GETFIELD, schema.jvmName, name, schema.descriptorsIncludingSuperclasses()[name])
visitMethodInsn(INVOKEVIRTUAL, toStringHelper, "add", "(Ljava/lang/String;${field.type})L$toStringHelper;", false)
visitMethodInsn(INVOKEVIRTUAL, toStringHelper, "add", "(L$jlString;${field.type})L$toStringHelper;", false)
}
// call toString() on the builder and return.
visitMethodInsn(INVOKEVIRTUAL, toStringHelper, "toString", "()Ljava/lang/String;", false)
visitMethodInsn(INVOKEVIRTUAL, toStringHelper, "toString", "()L$jlString;", false)
visitInsn(ARETURN)
visitMaxs(0, 0)
visitEnd()
@ -192,14 +220,14 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
private fun ClassWriter.generateGetMethod() {
val ourJvmName = ClassCarpenter::class.java.name.jvm
with(visitMethod(ACC_PUBLIC, "get", "(Ljava/lang/String;)Ljava/lang/Object;", null, null)) {
with(visitMethod(ACC_PUBLIC, "get", "(L$jlString;)L$jlObject;", null, null)) {
visitCode()
visitVarInsn(ALOAD, 0) // Load 'this'
visitVarInsn(ALOAD, 1) // Load the name argument
// Using this generic helper method is slow, as it relies on reflection. A faster way would be
// to use a tableswitch opcode, or just push back on the user and ask them to use actual reflection
// or MethodHandles (super fast reflection) to access the object instead.
visitMethodInsn(INVOKESTATIC, ourJvmName, "getField", "(Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object;", false)
visitMethodInsn(INVOKESTATIC, ourJvmName, "getField", "(L$jlObject;L$jlString;)L$jlObject;", false)
visitInsn(ARETURN)
visitMaxs(0, 0)
visitEnd()
@ -207,45 +235,113 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
}
private fun ClassWriter.generateGetters(schema: Schema) {
for ((name, type) in schema.fields) {
with(visitMethod(ACC_PUBLIC, "get" + name.capitalize(), "()" + type.descriptor, null, null)) {
@Suppress("UNCHECKED_CAST")
for ((name, type) in (schema.fields as Map<String, ClassField>)) {
visitMethod(ACC_PUBLIC, "get" + name.capitalize(), "()" + type.descriptor, null, null).apply {
type.addNullabilityAnnotation(this)
visitCode()
visitVarInsn(ALOAD, 0) // Load 'this'
visitFieldInsn(GETFIELD, schema.jvmName, name, type.descriptor)
when (type.field) {
java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE,
java.lang.Character.TYPE -> visitInsn(IRETURN)
java.lang.Character.TYPE -> visitInsn(IRETURN)
java.lang.Long.TYPE -> visitInsn(LRETURN)
java.lang.Double.TYPE -> visitInsn(DRETURN)
java.lang.Float.TYPE -> visitInsn(FRETURN)
else -> visitInsn(ARETURN)
}
visitMaxs(0, 0)
visitEnd()
}
}.visitEnd()
}
}
private fun ClassWriter.generateAbstractGetters(schema: Schema) {
for ((name, field) in schema.fields) {
@Suppress("UNCHECKED_CAST")
for ((name, field) in (schema.fields as Map<String, ClassField>)) {
val opcodes = ACC_ABSTRACT + ACC_PUBLIC
with(visitMethod(opcodes, "get" + name.capitalize(), "()${field.descriptor}", null, null)) {
// abstract method doesn't have any implementation so just end
visitEnd()
}
// abstract method doesn't have any implementation so just end
visitMethod(opcodes, "get" + name.capitalize(), "()${field.descriptor}", null, null).visitEnd()
}
}
private fun ClassWriter.generateConstructor(schema: Schema) {
with(visitMethod(
private fun ClassWriter.generateStaticEnumConstructor(schema: Schema) {
visitMethod(ACC_STATIC, "<clinit>", "()V", null, null).apply {
visitCode()
visitIntInsn(BIPUSH, schema.fields.size)
visitTypeInsn(ANEWARRAY, schema.jvmName)
visitInsn(DUP)
var idx = 0
schema.fields.forEach {
visitInsn(DUP)
visitIntInsn(BIPUSH, idx)
visitTypeInsn(NEW, schema.jvmName)
visitInsn(DUP)
visitLdcInsn(it.key)
visitIntInsn(BIPUSH, idx++)
visitMethodInsn(INVOKESPECIAL, schema.jvmName, "<init>", "(L$jlString;I)V", false)
visitInsn(DUP)
visitFieldInsn(PUTSTATIC, schema.jvmName, it.key, "L${schema.jvmName};")
visitInsn(AASTORE)
}
visitFieldInsn(PUTSTATIC, schema.jvmName, "\$VALUES", schema.asArray)
visitInsn(RETURN)
visitMaxs(0, 0)
}.visitEnd()
}
private fun ClassWriter.generateEnumValues(schema: Schema) {
visitMethod(ACC_PUBLIC + ACC_STATIC, "values", "()${schema.asArray}", null, null).apply {
visitCode()
visitFieldInsn(GETSTATIC, schema.jvmName, "\$VALUES", schema.asArray)
visitMethodInsn(INVOKEVIRTUAL, schema.asArray, "clone", "()L$jlObject;", false)
visitTypeInsn(CHECKCAST, schema.asArray)
visitInsn(ARETURN)
visitMaxs(0, 0)
}.visitEnd()
}
private fun ClassWriter.generateEnumValueOf(schema: Schema) {
visitMethod(ACC_PUBLIC + ACC_STATIC, "valueOf", "(L$jlString;)L${schema.jvmName};", null, null).apply {
visitCode()
visitLdcInsn(Type.getType("L${schema.jvmName};"))
visitVarInsn(ALOAD, 0)
visitMethodInsn(INVOKESTATIC, jlEnum, "valueOf", "(L$jlClass;L$jlString;)L$jlEnum;", true)
visitTypeInsn(CHECKCAST, schema.jvmName)
visitInsn(ARETURN)
visitMaxs(0, 0)
}.visitEnd()
}
private fun ClassWriter.generateEnumConstructor() {
visitMethod(ACC_PROTECTED, "<init>", "(L$jlString;I)V", "()V", null).apply {
visitParameter("\$enum\$name", ACC_SYNTHETIC)
visitParameter("\$enum\$ordinal", ACC_SYNTHETIC)
visitCode()
visitVarInsn(ALOAD, 0) // this
visitVarInsn(ALOAD, 1)
visitVarInsn(ILOAD, 2)
visitMethodInsn(INVOKESPECIAL, jlEnum, "<init>", "(L$jlString;I)V", false)
visitInsn(RETURN)
visitMaxs(0, 0)
}.visitEnd()
}
private fun ClassWriter.generateClassConstructor(schema: Schema) {
visitMethod(
ACC_PUBLIC,
"<init>",
"(" + schema.descriptorsIncludingSuperclasses().values.joinToString("") + ")V",
null,
null))
{
null).apply {
var idx = 0
schema.fields.values.forEach { it.visitParameter(this, idx++) }
visitCode()
@ -255,7 +351,7 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
visitVarInsn(ALOAD, 0)
val sc = schema.superclass
if (sc == null) {
visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false)
visitMethodInsn(INVOKESPECIAL, jlObject, "<init>", "()V", false)
} else {
var slot = 1
superclassFields.values.forEach { slot += load(slot, it) }
@ -265,7 +361,8 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
// Assign the fields from parameters.
var slot = 1 + superclassFields.size
for ((name, field) in schema.fields.entries) {
@Suppress("UNCHECKED_CAST")
for ((name, field) in (schema.fields as Map<String, ClassField>)) {
field.nullTest(this, slot)
visitVarInsn(ALOAD, 0) // Load 'this' onto the stack
@ -274,14 +371,13 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
}
visitInsn(RETURN)
visitMaxs(0, 0)
visitEnd()
}
}.visitEnd()
}
private fun MethodVisitor.load(slot: Int, type: Field): Int {
when (type.field) {
java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE,
java.lang.Character.TYPE -> visitVarInsn(ILOAD, slot)
java.lang.Character.TYPE -> visitVarInsn(ILOAD, slot)
java.lang.Long.TYPE -> visitVarInsn(LLOAD, slot)
java.lang.Double.TYPE -> visitVarInsn(DLOAD, slot)
java.lang.Float.TYPE -> visitVarInsn(FLOAD, slot)
@ -325,7 +421,8 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
}
companion object {
@JvmStatic @Suppress("UNUSED")
@JvmStatic
@Suppress("UNUSED")
fun getField(obj: Any, name: String): Any? = obj.javaClass.getMethod("get" + name.capitalize()).invoke(obj)
}
}

@ -1,11 +1,11 @@
package net.corda.nodeapi.internal.serialization.carpenter
class DuplicateNameException : RuntimeException (
class DuplicateNameException : RuntimeException(
"An attempt was made to register two classes with the same name within the same ClassCarpenter namespace.")
class InterfaceMismatchException(msg: String) : RuntimeException(msg)
class NullablePrimitiveException(msg: String) : RuntimeException(msg)
class UncarpentableException (name: String, field: String, type: String) :
Exception ("Class $name is loadable yet contains field $field of unknown type $type")
class UncarpentableException(name: String, field: String, type: String) :
Exception("Class $name is loadable yet contains field $field of unknown type $type")

@ -22,22 +22,19 @@ import net.corda.nodeapi.internal.serialization.amqp.TypeNotation
* in turn look up all of those classes in the [dependsOn] list, remove their dependency on the newly created class,
* and if that list is reduced to zero know we can now generate a [Schema] for them and carpent them up
*/
data class CarpenterSchemas (
data class CarpenterSchemas(
val carpenterSchemas: MutableList<Schema>,
val dependencies: MutableMap<String, Pair<TypeNotation, MutableList<String>>>,
val dependsOn: MutableMap<String, MutableList<String>>) {
companion object CarpenterSchemaConstructor {
fun newInstance(): CarpenterSchemas {
return CarpenterSchemas(
mutableListOf<Schema>(),
mutableMapOf<String, Pair<TypeNotation, MutableList<String>>>(),
mutableMapOf<String, MutableList<String>>())
return CarpenterSchemas(mutableListOf(), mutableMapOf(), mutableMapOf())
}
}
fun addDepPair(type: TypeNotation, dependant: String, dependee: String) {
dependsOn.computeIfAbsent(dependee, { mutableListOf<String>() }).add(dependant)
dependencies.computeIfAbsent(dependant, { Pair(type, mutableListOf<String>()) }).second.add(dependee)
dependsOn.computeIfAbsent(dependee, { mutableListOf() }).add(dependant)
dependencies.computeIfAbsent(dependant, { Pair(type, mutableListOf()) }).second.add(dependee)
}
val size
@ -56,23 +53,23 @@ data class CarpenterSchemas (
* @property cc a reference to the actual class carpenter we're using to constuct classes
* @property objects a list of carpented classes loaded into the carpenters class loader
*/
abstract class MetaCarpenterBase (val schemas : CarpenterSchemas, val cc : ClassCarpenter = ClassCarpenter()) {
abstract class MetaCarpenterBase(val schemas: CarpenterSchemas, val cc: ClassCarpenter = ClassCarpenter()) {
val objects = mutableMapOf<String, Class<*>>()
fun step(newObject: Schema) {
objects[newObject.name] = cc.build (newObject)
objects[newObject.name] = cc.build(newObject)
// go over the list of everything that had a dependency on the newly
// carpented class existing and remove it from their dependency list, If that
// list is now empty we have no impediment to carpenting that class up
schemas.dependsOn.remove(newObject.name)?.forEach { dependent ->
assert (newObject.name in schemas.dependencies[dependent]!!.second)
assert(newObject.name in schemas.dependencies[dependent]!!.second)
schemas.dependencies[dependent]?.second?.remove(newObject.name)
// we're out of blockers so we can now create the type
if (schemas.dependencies[dependent]?.second?.isEmpty() ?: false) {
(schemas.dependencies.remove (dependent)?.first as CompositeType).carpenterSchema (
if (schemas.dependencies[dependent]?.second?.isEmpty() == true) {
(schemas.dependencies.remove(dependent)?.first as CompositeType).carpenterSchema(
classloader = cc.classloader,
carpenterSchemas = schemas)
}
@ -81,25 +78,25 @@ abstract class MetaCarpenterBase (val schemas : CarpenterSchemas, val cc : Class
abstract fun build()
val classloader : ClassLoader
get() = cc.classloader
val classloader: ClassLoader
get() = cc.classloader
}
class MetaCarpenter(schemas : CarpenterSchemas,
cc : ClassCarpenter = ClassCarpenter()) : MetaCarpenterBase(schemas, cc) {
class MetaCarpenter(schemas: CarpenterSchemas,
cc: ClassCarpenter = ClassCarpenter()) : MetaCarpenterBase(schemas, cc) {
override fun build() {
while (schemas.carpenterSchemas.isNotEmpty()) {
val newObject = schemas.carpenterSchemas.removeAt(0)
step (newObject)
step(newObject)
}
}
}
class TestMetaCarpenter(schemas : CarpenterSchemas,
cc : ClassCarpenter = ClassCarpenter()) : MetaCarpenterBase(schemas, cc) {
class TestMetaCarpenter(schemas: CarpenterSchemas,
cc: ClassCarpenter = ClassCarpenter()) : MetaCarpenterBase(schemas, cc) {
override fun build() {
if (schemas.carpenterSchemas.isEmpty()) return
step (schemas.carpenterSchemas.removeAt(0))
step(schemas.carpenterSchemas.removeAt(0))
}
}

@ -1,148 +1,110 @@
package net.corda.nodeapi.internal.serialization.carpenter
import jdk.internal.org.objectweb.asm.Opcodes.*
import kotlin.collections.LinkedHashMap
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Type
import java.util.*
import org.objectweb.asm.Opcodes.*
/**
* A Schema represents a desired class.
* A Schema is the representation of an object the Carpenter can contsruct
*
* Known Sub Classes
* - [ClassSchema]
* - [InterfaceSchema]
* - [EnumSchema]
*/
abstract class Schema(
val name: String,
fields: Map<String, Field>,
var fields: Map<String, Field>,
val superclass: Schema? = null,
val interfaces: List<Class<*>> = emptyList())
{
private fun Map<String, Field>.descriptors() =
LinkedHashMap(this.mapValues { it.value.descriptor })
val interfaces: List<Class<*>> = emptyList(),
updater: (String, Field) -> Unit) {
private fun Map<String, Field>.descriptors() = LinkedHashMap(this.mapValues { it.value.descriptor })
/* Fix the order up front if the user didn't, inject the name into the field as it's
neater when iterating */
val fields = LinkedHashMap(fields.mapValues { it.value.copy(it.key, it.value.field) })
init {
fields.forEach { updater(it.key, it.value) }
// Fix the order up front if the user didn't, inject the name into the field as it's
// neater when iterating
fields = LinkedHashMap(fields)
}
fun fieldsIncludingSuperclasses(): Map<String, Field> =
(superclass?.fieldsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(fields)
fun descriptorsIncludingSuperclasses(): Map<String, String> =
fun descriptorsIncludingSuperclasses(): Map<String, String?> =
(superclass?.descriptorsIncludingSuperclasses() ?: emptyMap()) + fields.descriptors()
abstract fun generateFields(cw: ClassWriter)
val jvmName: String
get() = name.replace(".", "/")
val asArray: String
get() = "[L$jvmName;"
}
/**
* Represents a concrete object
*/
class ClassSchema(
name: String,
fields: Map<String, Field>,
superclass: Schema? = null,
interfaces: List<Class<*>> = emptyList()
) : Schema(name, fields, superclass, interfaces)
) : Schema(name, fields, superclass, interfaces, { name, field -> field.name = name }) {
override fun generateFields(cw: ClassWriter) {
cw.apply { fields.forEach { it.value.generateField(this) } }
}
}
/**
* Represents an interface. Carpented interfaces can be used within [ClassSchema]s
* if that class should be implementing that interface
*/
class InterfaceSchema(
name: String,
fields: Map<String, Field>,
superclass: Schema? = null,
interfaces: List<Class<*>> = emptyList()
) : Schema(name, fields, superclass, interfaces)
) : Schema(name, fields, superclass, interfaces, { name, field -> field.name = name }) {
override fun generateFields(cw: ClassWriter) {
cw.apply { fields.forEach { it.value.generateField(this) } }
}
}
/**
* Represents an enumerated type
*/
class EnumSchema(
name: String,
fields: Map<String, Field>
) : Schema(name, fields, null, emptyList(), { fieldName, field ->
(field as EnumField).name = fieldName
field.descriptor = "L${name.replace(".", "/")};"
}) {
override fun generateFields(cw: ClassWriter) {
with(cw) {
fields.forEach { it.value.generateField(this) }
visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC + ACC_SYNTHETIC,
"\$VALUES", asArray, null, null)
}
}
}
/**
* Factory object used by the serialiser when building [Schema]s based
* on an AMQP schema
*/
object CarpenterSchemaFactory {
fun newInstance (
fun newInstance(
name: String,
fields: Map<String, Field>,
superclass: Schema? = null,
interfaces: List<Class<*>> = emptyList(),
isInterface: Boolean = false
) : Schema =
if (isInterface) InterfaceSchema (name, fields, superclass, interfaces)
else ClassSchema (name, fields, superclass, interfaces)
): Schema =
if (isInterface) InterfaceSchema(name, fields, superclass, interfaces)
else ClassSchema(name, fields, superclass, interfaces)
}
abstract class Field(val field: Class<out Any?>) {
companion object {
const val unsetName = "Unset"
}
var name: String = unsetName
abstract val nullabilityAnnotation: String
val descriptor: String
get() = Type.getDescriptor(this.field)
val type: String
get() = if (this.field.isPrimitive) this.descriptor else "Ljava/lang/Object;"
fun generateField(cw: ClassWriter) {
val fieldVisitor = cw.visitField(ACC_PROTECTED + ACC_FINAL, name, descriptor, null, null)
fieldVisitor.visitAnnotation(nullabilityAnnotation, true).visitEnd()
fieldVisitor.visitEnd()
}
fun addNullabilityAnnotation(mv: MethodVisitor) {
mv.visitAnnotation(nullabilityAnnotation, true).visitEnd()
}
fun visitParameter(mv: MethodVisitor, idx: Int) {
with(mv) {
visitParameter(name, 0)
if (!field.isPrimitive) {
visitParameterAnnotation(idx, nullabilityAnnotation, true).visitEnd()
}
}
}
abstract fun copy(name: String, field: Class<out Any?>): Field
abstract fun nullTest(mv: MethodVisitor, slot: Int)
}
class NonNullableField(field: Class<out Any?>) : Field(field) {
override val nullabilityAnnotation = "Ljavax/annotation/Nonnull;"
constructor(name: String, field: Class<out Any?>) : this(field) {
this.name = name
}
override fun copy(name: String, field: Class<out Any?>) = NonNullableField(name, field)
override fun nullTest(mv: MethodVisitor, slot: Int) {
assert(name != unsetName)
if (!field.isPrimitive) {
with(mv) {
visitVarInsn(ALOAD, 0) // load this
visitVarInsn(ALOAD, slot) // load parameter
visitLdcInsn("param \"$name\" cannot be null")
visitMethodInsn(INVOKESTATIC,
"java/util/Objects",
"requireNonNull",
"(Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object;", false)
visitInsn(POP)
}
}
}
}
class NullableField(field: Class<out Any?>) : Field(field) {
override val nullabilityAnnotation = "Ljavax/annotation/Nullable;"
constructor(name: String, field: Class<out Any?>) : this(field) {
if (field.isPrimitive) {
throw NullablePrimitiveException (
"Field $name is primitive type ${Type.getDescriptor(field)} and thus cannot be nullable")
}
this.name = name
}
override fun copy(name: String, field: Class<out Any?>) = NullableField(name, field)
override fun nullTest(mv: MethodVisitor, slot: Int) {
assert(name != unsetName)
}
}
object FieldFactory {
fun newInstance (mandatory: Boolean, name: String, field: Class<out Any?>) =
if (mandatory) NonNullableField (name, field) else NullableField (name, field)
}

@ -0,0 +1,138 @@
package net.corda.nodeapi.internal.serialization.carpenter
import jdk.internal.org.objectweb.asm.Opcodes.*
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Type
abstract class Field(val field: Class<out Any?>) {
abstract var descriptor: String?
companion object {
const val unsetName = "Unset"
}
var name: String = unsetName
abstract val type: String
abstract fun generateField(cw: ClassWriter)
abstract fun visitParameter(mv: MethodVisitor, idx: Int)
}
/**
* Any field that can be a member of an object
*
* Known
* - [NullableField]
* - [NonNullableField]
*/
abstract class ClassField(field: Class<out Any?>) : Field(field) {
abstract val nullabilityAnnotation: String
abstract fun nullTest(mv: MethodVisitor, slot: Int)
override var descriptor = Type.getDescriptor(this.field)
override val type: String get() = if (this.field.isPrimitive) this.descriptor else "Ljava/lang/Object;"
fun addNullabilityAnnotation(mv: MethodVisitor) {
mv.visitAnnotation(nullabilityAnnotation, true).visitEnd()
}
override fun generateField(cw: ClassWriter) {
cw.visitField(ACC_PROTECTED + ACC_FINAL, name, descriptor, null, null).visitAnnotation(
nullabilityAnnotation, true).visitEnd()
}
override fun visitParameter(mv: MethodVisitor, idx: Int) {
with(mv) {
visitParameter(name, 0)
if (!field.isPrimitive) {
visitParameterAnnotation(idx, nullabilityAnnotation, true).visitEnd()
}
}
}
}
/**
* A member of a constructed class that can be assigned to null, the
* mandatory type for primitives, but also any member that cannot be
* null
*
* maps to AMQP mandatory = true fields
*/
open class NonNullableField(field: Class<out Any?>) : ClassField(field) {
override val nullabilityAnnotation = "Ljavax/annotation/Nonnull;"
constructor(name: String, field: Class<out Any?>) : this(field) {
this.name = name
}
override fun nullTest(mv: MethodVisitor, slot: Int) {
assert(name != unsetName)
if (!field.isPrimitive) {
with(mv) {
visitVarInsn(ALOAD, 0) // load this
visitVarInsn(ALOAD, slot) // load parameter
visitLdcInsn("param \"$name\" cannot be null")
visitMethodInsn(INVOKESTATIC,
"java/util/Objects",
"requireNonNull",
"(Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object;", false)
visitInsn(POP)
}
}
}
}
/**
* A member of a constructed class that can be assigned to null,
*
* maps to AMQP mandatory = false fields
*/
class NullableField(field: Class<out Any?>) : ClassField(field) {
override val nullabilityAnnotation = "Ljavax/annotation/Nullable;"
constructor(name: String, field: Class<out Any?>) : this(field) {
this.name = name
}
init {
if (field.isPrimitive) {
throw NullablePrimitiveException(
"Field $name is primitive type ${Type.getDescriptor(field)} and thus cannot be nullable")
}
}
override fun nullTest(mv: MethodVisitor, slot: Int) {
assert(name != unsetName)
}
}
/**
* Represents enum constants within an enum
*/
class EnumField : Field(Enum::class.java) {
override var descriptor: String? = null
override val type: String
get() = "Ljava/lang/Enum;"
override fun generateField(cw: ClassWriter) {
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC + ACC_ENUM, name,
descriptor, null, null).visitEnd()
}
override fun visitParameter(mv: MethodVisitor, idx: Int) {
mv.visitParameter(name, 0)
}
}
/**
* Constructs a Field Schema object of the correct type depending weather
* the AMQP schema indicates it's mandatory (non nullable) or not (nullable)
*/
object FieldFactory {
fun newInstance(mandatory: Boolean, name: String, field: Class<out Any?>) =
if (mandatory) NonNullableField(name, field) else NullableField(name, field)
}

@ -15,12 +15,12 @@ class ClassCarpenterTest {
val b: Int
}
val cc = ClassCarpenter()
private val cc = ClassCarpenter()
// We have to ignore synthetic fields even though ClassCarpenter doesn't create any because the JaCoCo
// coverage framework auto-magically injects one method and one field into every class loaded into the JVM.
val Class<*>.nonSyntheticFields: List<Field> get() = declaredFields.filterNot { it.isSynthetic }
val Class<*>.nonSyntheticMethods: List<Method> get() = declaredMethods.filterNot { it.isSynthetic }
private val Class<*>.nonSyntheticFields: List<Field> get() = declaredFields.filterNot { it.isSynthetic }
private val Class<*>.nonSyntheticMethods: List<Method> get() = declaredMethods.filterNot { it.isSynthetic }
@Test
fun empty() {

@ -3,6 +3,7 @@ package net.corda.nodeapi.internal.serialization.carpenter
import net.corda.nodeapi.internal.serialization.amqp.*
import net.corda.nodeapi.internal.serialization.amqp.Field
import net.corda.nodeapi.internal.serialization.amqp.Schema
import net.corda.nodeapi.internal.serialization.AllWhitelist
fun mangleName(name: String) = "${name}__carpenter"
@ -34,7 +35,8 @@ fun Schema.mangleNames(names: List<String>): Schema {
}
open class AmqpCarpenterBase {
var factory = testDefaultFactory()
var cc = ClassCarpenter()
var factory = SerializerFactory(AllWhitelist, cc.classloader)
fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz)
fun testName(): String = Thread.currentThread().stackTrace[2].methodName

@ -0,0 +1,105 @@
package net.corda.nodeapi.internal.serialization.carpenter
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class EnumClassTests : AmqpCarpenterBase() {
@Test
fun oneValue() {
val enumConstants = mapOf("A" to EnumField())
val schema = EnumSchema("gen.enum", enumConstants)
assertTrue(cc.build(schema).isEnum)
}
@Test
fun oneValueInstantiate() {
val enumConstants = mapOf("A" to EnumField())
val schema = EnumSchema("gen.enum", enumConstants)
val clazz = cc.build(schema)
assertTrue(clazz.isEnum)
assertEquals(enumConstants.size, clazz.enumConstants.size)
assertEquals("A", clazz.enumConstants.first().toString())
assertEquals(0, (clazz.enumConstants.first() as Enum<*>).ordinal)
assertEquals("A", (clazz.enumConstants.first() as Enum<*>).name)
}
@Test
fun twoValuesInstantiate() {
val enumConstants = mapOf("left" to EnumField(), "right" to EnumField())
val schema = EnumSchema("gen.enum", enumConstants)
val clazz = cc.build(schema)
assertTrue(clazz.isEnum)
assertEquals(enumConstants.size, clazz.enumConstants.size)
val left = clazz.enumConstants[0] as Enum<*>
val right = clazz.enumConstants[1] as Enum<*>
assertEquals(0, left.ordinal)
assertEquals("left", left.name)
assertEquals(1, right.ordinal)
assertEquals("right", right.name)
}
@Test
fun manyValues() {
val enumConstants = listOf("AAA", "BBB", "CCC", "DDD", "EEE", "FFF",
"GGG", "HHH", "III", "JJJ").associateBy({ it }, { EnumField() })
val schema = EnumSchema("gen.enum", enumConstants)
val clazz = cc.build(schema)
assertTrue(clazz.isEnum)
assertEquals(enumConstants.size, clazz.enumConstants.size)
var idx = 0
enumConstants.forEach {
val constant = clazz.enumConstants[idx] as Enum<*>
assertEquals(idx++, constant.ordinal)
assertEquals(it.key, constant.name)
}
}
@Test
fun assignment() {
val enumConstants = listOf("AAA", "BBB", "CCC", "DDD", "EEE", "FFF").associateBy({ it }, { EnumField() })
val schema = EnumSchema("gen.enum", enumConstants)
val clazz = cc.build(schema)
assertEquals("CCC", clazz.getMethod("valueOf", String::class.java).invoke(null, "CCC").toString())
assertEquals("CCC", (clazz.getMethod("valueOf", String::class.java).invoke(null, "CCC") as Enum<*>).name)
val ddd = clazz.getMethod("valueOf", String::class.java).invoke(null, "DDD") as Enum<*>
assertTrue(ddd::class.java.isEnum)
assertEquals("DDD", ddd.name)
assertEquals(3, ddd.ordinal)
}
// if anything goes wrong with this test it's going to end up throwing *some*
// exception, hence the lack of asserts
@Test
fun assignAndTest() {
val cc2 = ClassCarpenter()
val schema1 = EnumSchema("gen.enum",
listOf("AAA", "BBB", "CCC", "DDD", "EEE", "FFF").associateBy({ it }, { EnumField() }))
val enumClazz = cc2.build(schema1)
val schema2 = ClassSchema("gen.class",
mapOf(
"a" to NonNullableField(Int::class.java),
"b" to NonNullableField(enumClazz)))
val classClazz = cc2.build(schema2)
// make sure we can construct a class that has an enum we've constructed as a member
classClazz.constructors[0].newInstance(1, enumClazz.getMethod(
"valueOf", String::class.java).invoke(null, "BBB"))
}
}