Merge pull request #1473 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
9 changed files with 480 additions and 177 deletions

View File

@ -6,6 +6,8 @@ from the previous milestone release.
UNRELEASED 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 * ``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. support dynamic classloading of contract and contract constraints.

View File

@ -20,11 +20,21 @@ interface SimpleFieldAccess {
operator fun get(name: String): Any? operator fun get(name: String): Any?
} }
class CarpenterClassLoader (parentClassLoader: ClassLoader = Thread.currentThread().contextClassLoader) : class CarpenterClassLoader(parentClassLoader: ClassLoader = Thread.currentThread().contextClassLoader) :
ClassLoader(parentClassLoader) { ClassLoader(parentClassLoader) {
fun load(name: String, bytes: ByteArray) = defineClass(name, bytes, 0, bytes.size) 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. * 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 * 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) { when (it) {
is InterfaceSchema -> generateInterface(it) is InterfaceSchema -> generateInterface(it)
is ClassSchema -> generateClass(it) is ClassSchema -> generateClass(it)
is EnumSchema -> generateEnum(it)
} }
} }
assert (schema.name in _loaded) assert(schema.name in _loaded)
return _loaded[schema.name]!! 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<*> { private fun generateInterface(interfaceSchema: Schema): Class<*> {
return generate(interfaceSchema) { cw, schema -> return generate(interfaceSchema) { cw, schema ->
val interfaces = schema.interfaces.map { it.name.jvm }.toTypedArray() val interfaces = schema.interfaces.map { it.name.jvm }.toTypedArray()
with(cw) { cw.apply {
visit(V1_8, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, schema.jvmName, null, "java/lang/Object", interfaces) visit(TARGET_VERSION, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, schema.jvmName, null,
jlObject, interfaces)
visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd() visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd()
generateAbstractGetters(schema) generateAbstractGetters(schema)
}.visitEnd()
visitEnd()
}
} }
} }
private fun generateClass(classSchema: Schema): Class<*> { private fun generateClass(classSchema: Schema): Class<*> {
return generate(classSchema) { cw, schema -> 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() 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) { cw.apply {
visit(V1_8, ACC_PUBLIC + ACC_SUPER, schema.jvmName, null, superName, interfaces.toTypedArray()) visit(TARGET_VERSION, ACC_PUBLIC + ACC_SUPER, schema.jvmName, null, superName,
interfaces.toTypedArray())
visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd() visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd()
generateFields(schema) generateFields(schema)
generateConstructor(schema) generateClassConstructor(schema)
generateGetters(schema) generateGetters(schema)
if (schema.superclass == null) if (schema.superclass == null)
generateGetMethod() // From SimplePropertyAccess generateGetMethod() // From SimplePropertyAccess
generateToString(schema) generateToString(schema)
}.visitEnd()
visitEnd()
}
} }
} }
@ -165,25 +192,26 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
} }
private fun ClassWriter.generateFields(schema: Schema) { private fun ClassWriter.generateFields(schema: Schema) {
schema.fields.forEach { it.value.generateField(this) } schema.generateFields(this)
} }
private fun ClassWriter.generateToString(schema: Schema) { private fun ClassWriter.generateToString(schema: Schema) {
val toStringHelper = "com/google/common/base/MoreObjects\$ToStringHelper" 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() visitCode()
// com.google.common.base.MoreObjects.toStringHelper("TypeName") // com.google.common.base.MoreObjects.toStringHelper("TypeName")
visitLdcInsn(schema.name.split('.').last()) 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. // Call the add() methods.
for ((name, field) in schema.fieldsIncludingSuperclasses().entries) { for ((name, field) in schema.fieldsIncludingSuperclasses().entries) {
visitLdcInsn(name) visitLdcInsn(name)
visitVarInsn(ALOAD, 0) // this visitVarInsn(ALOAD, 0) // this
visitFieldInsn(GETFIELD, schema.jvmName, name, schema.descriptorsIncludingSuperclasses()[name]) 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. // call toString() on the builder and return.
visitMethodInsn(INVOKEVIRTUAL, toStringHelper, "toString", "()Ljava/lang/String;", false) visitMethodInsn(INVOKEVIRTUAL, toStringHelper, "toString", "()L$jlString;", false)
visitInsn(ARETURN) visitInsn(ARETURN)
visitMaxs(0, 0) visitMaxs(0, 0)
visitEnd() visitEnd()
@ -192,14 +220,14 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
private fun ClassWriter.generateGetMethod() { private fun ClassWriter.generateGetMethod() {
val ourJvmName = ClassCarpenter::class.java.name.jvm 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() visitCode()
visitVarInsn(ALOAD, 0) // Load 'this' visitVarInsn(ALOAD, 0) // Load 'this'
visitVarInsn(ALOAD, 1) // Load the name argument visitVarInsn(ALOAD, 1) // Load the name argument
// Using this generic helper method is slow, as it relies on reflection. A faster way would be // 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 // 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. // 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) visitInsn(ARETURN)
visitMaxs(0, 0) visitMaxs(0, 0)
visitEnd() visitEnd()
@ -207,45 +235,113 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
} }
private fun ClassWriter.generateGetters(schema: Schema) { private fun ClassWriter.generateGetters(schema: Schema) {
for ((name, type) in schema.fields) { @Suppress("UNCHECKED_CAST")
with(visitMethod(ACC_PUBLIC, "get" + name.capitalize(), "()" + type.descriptor, null, null)) { for ((name, type) in (schema.fields as Map<String, ClassField>)) {
visitMethod(ACC_PUBLIC, "get" + name.capitalize(), "()" + type.descriptor, null, null).apply {
type.addNullabilityAnnotation(this) type.addNullabilityAnnotation(this)
visitCode() visitCode()
visitVarInsn(ALOAD, 0) // Load 'this' visitVarInsn(ALOAD, 0) // Load 'this'
visitFieldInsn(GETFIELD, schema.jvmName, name, type.descriptor) visitFieldInsn(GETFIELD, schema.jvmName, name, type.descriptor)
when (type.field) { when (type.field) {
java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE, 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.Long.TYPE -> visitInsn(LRETURN)
java.lang.Double.TYPE -> visitInsn(DRETURN) java.lang.Double.TYPE -> visitInsn(DRETURN)
java.lang.Float.TYPE -> visitInsn(FRETURN) java.lang.Float.TYPE -> visitInsn(FRETURN)
else -> visitInsn(ARETURN) else -> visitInsn(ARETURN)
} }
visitMaxs(0, 0) visitMaxs(0, 0)
visitEnd() }.visitEnd()
}
} }
} }
private fun ClassWriter.generateAbstractGetters(schema: Schema) { 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 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
// abstract method doesn't have any implementation so just end visitMethod(opcodes, "get" + name.capitalize(), "()${field.descriptor}", null, null).visitEnd()
visitEnd()
}
} }
} }
private fun ClassWriter.generateConstructor(schema: Schema) { private fun ClassWriter.generateStaticEnumConstructor(schema: Schema) {
with(visitMethod( 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, ACC_PUBLIC,
"<init>", "<init>",
"(" + schema.descriptorsIncludingSuperclasses().values.joinToString("") + ")V", "(" + schema.descriptorsIncludingSuperclasses().values.joinToString("") + ")V",
null, null,
null)) null).apply {
{
var idx = 0 var idx = 0
schema.fields.values.forEach { it.visitParameter(this, idx++) } schema.fields.values.forEach { it.visitParameter(this, idx++) }
visitCode() visitCode()
@ -255,7 +351,7 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
visitVarInsn(ALOAD, 0) visitVarInsn(ALOAD, 0)
val sc = schema.superclass val sc = schema.superclass
if (sc == null) { if (sc == null) {
visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false) visitMethodInsn(INVOKESPECIAL, jlObject, "<init>", "()V", false)
} else { } else {
var slot = 1 var slot = 1
superclassFields.values.forEach { slot += load(slot, it) } superclassFields.values.forEach { slot += load(slot, it) }
@ -265,7 +361,8 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
// Assign the fields from parameters. // Assign the fields from parameters.
var slot = 1 + superclassFields.size 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) field.nullTest(this, slot)
visitVarInsn(ALOAD, 0) // Load 'this' onto the stack visitVarInsn(ALOAD, 0) // Load 'this' onto the stack
@ -274,14 +371,13 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
} }
visitInsn(RETURN) visitInsn(RETURN)
visitMaxs(0, 0) visitMaxs(0, 0)
visitEnd() }.visitEnd()
}
} }
private fun MethodVisitor.load(slot: Int, type: Field): Int { private fun MethodVisitor.load(slot: Int, type: Field): Int {
when (type.field) { when (type.field) {
java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE, 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.Long.TYPE -> visitVarInsn(LLOAD, slot)
java.lang.Double.TYPE -> visitVarInsn(DLOAD, slot) java.lang.Double.TYPE -> visitVarInsn(DLOAD, slot)
java.lang.Float.TYPE -> visitVarInsn(FLOAD, slot) java.lang.Float.TYPE -> visitVarInsn(FLOAD, slot)
@ -325,7 +421,8 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
} }
companion object { companion object {
@JvmStatic @Suppress("UNUSED") @JvmStatic
@Suppress("UNUSED")
fun getField(obj: Any, name: String): Any? = obj.javaClass.getMethod("get" + name.capitalize()).invoke(obj) fun getField(obj: Any, name: String): Any? = obj.javaClass.getMethod("get" + name.capitalize()).invoke(obj)
} }
} }

View File

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

View File

@ -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, * 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 * 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 carpenterSchemas: MutableList<Schema>,
val dependencies: MutableMap<String, Pair<TypeNotation, MutableList<String>>>, val dependencies: MutableMap<String, Pair<TypeNotation, MutableList<String>>>,
val dependsOn: MutableMap<String, MutableList<String>>) { val dependsOn: MutableMap<String, MutableList<String>>) {
companion object CarpenterSchemaConstructor { companion object CarpenterSchemaConstructor {
fun newInstance(): CarpenterSchemas { fun newInstance(): CarpenterSchemas {
return CarpenterSchemas( return CarpenterSchemas(mutableListOf(), mutableMapOf(), mutableMapOf())
mutableListOf<Schema>(),
mutableMapOf<String, Pair<TypeNotation, MutableList<String>>>(),
mutableMapOf<String, MutableList<String>>())
} }
} }
fun addDepPair(type: TypeNotation, dependant: String, dependee: String) { fun addDepPair(type: TypeNotation, dependant: String, dependee: String) {
dependsOn.computeIfAbsent(dependee, { mutableListOf<String>() }).add(dependant) dependsOn.computeIfAbsent(dependee, { mutableListOf() }).add(dependant)
dependencies.computeIfAbsent(dependant, { Pair(type, mutableListOf<String>()) }).second.add(dependee) dependencies.computeIfAbsent(dependant, { Pair(type, mutableListOf()) }).second.add(dependee)
} }
val size 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 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 * @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<*>>() val objects = mutableMapOf<String, Class<*>>()
fun step(newObject: Schema) { 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 // 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 // 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 // list is now empty we have no impediment to carpenting that class up
schemas.dependsOn.remove(newObject.name)?.forEach { dependent -> 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) schemas.dependencies[dependent]?.second?.remove(newObject.name)
// we're out of blockers so we can now create the type // we're out of blockers so we can now create the type
if (schemas.dependencies[dependent]?.second?.isEmpty() ?: false) { if (schemas.dependencies[dependent]?.second?.isEmpty() == true) {
(schemas.dependencies.remove (dependent)?.first as CompositeType).carpenterSchema ( (schemas.dependencies.remove(dependent)?.first as CompositeType).carpenterSchema(
classloader = cc.classloader, classloader = cc.classloader,
carpenterSchemas = schemas) carpenterSchemas = schemas)
} }
@ -81,25 +78,25 @@ abstract class MetaCarpenterBase (val schemas : CarpenterSchemas, val cc : Class
abstract fun build() abstract fun build()
val classloader : ClassLoader val classloader: ClassLoader
get() = cc.classloader get() = cc.classloader
} }
class MetaCarpenter(schemas : CarpenterSchemas, class MetaCarpenter(schemas: CarpenterSchemas,
cc : ClassCarpenter = ClassCarpenter()) : MetaCarpenterBase(schemas, cc) { cc: ClassCarpenter = ClassCarpenter()) : MetaCarpenterBase(schemas, cc) {
override fun build() { override fun build() {
while (schemas.carpenterSchemas.isNotEmpty()) { while (schemas.carpenterSchemas.isNotEmpty()) {
val newObject = schemas.carpenterSchemas.removeAt(0) val newObject = schemas.carpenterSchemas.removeAt(0)
step (newObject) step(newObject)
} }
} }
} }
class TestMetaCarpenter(schemas : CarpenterSchemas, class TestMetaCarpenter(schemas: CarpenterSchemas,
cc : ClassCarpenter = ClassCarpenter()) : MetaCarpenterBase(schemas, cc) { cc: ClassCarpenter = ClassCarpenter()) : MetaCarpenterBase(schemas, cc) {
override fun build() { override fun build() {
if (schemas.carpenterSchemas.isEmpty()) return if (schemas.carpenterSchemas.isEmpty()) return
step (schemas.carpenterSchemas.removeAt(0)) step(schemas.carpenterSchemas.removeAt(0))
} }
} }

View File

@ -1,148 +1,110 @@
package net.corda.nodeapi.internal.serialization.carpenter 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.ClassWriter
import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes.*
import org.objectweb.asm.Type
import java.util.*
/** /**
* 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( abstract class Schema(
val name: String, val name: String,
fields: Map<String, Field>, var fields: Map<String, Field>,
val superclass: Schema? = null, val superclass: Schema? = null,
val interfaces: List<Class<*>> = emptyList()) val interfaces: List<Class<*>> = emptyList(),
{ updater: (String, Field) -> Unit) {
private fun Map<String, Field>.descriptors() = private fun Map<String, Field>.descriptors() = LinkedHashMap(this.mapValues { it.value.descriptor })
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 init {
neater when iterating */ fields.forEach { updater(it.key, it.value) }
val fields = LinkedHashMap(fields.mapValues { it.value.copy(it.key, it.value.field) })
// 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> = fun fieldsIncludingSuperclasses(): Map<String, Field> =
(superclass?.fieldsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(fields) (superclass?.fieldsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(fields)
fun descriptorsIncludingSuperclasses(): Map<String, String> = fun descriptorsIncludingSuperclasses(): Map<String, String?> =
(superclass?.descriptorsIncludingSuperclasses() ?: emptyMap()) + fields.descriptors() (superclass?.descriptorsIncludingSuperclasses() ?: emptyMap()) + fields.descriptors()
abstract fun generateFields(cw: ClassWriter)
val jvmName: String val jvmName: String
get() = name.replace(".", "/") get() = name.replace(".", "/")
val asArray: String
get() = "[L$jvmName;"
} }
/**
* Represents a concrete object
*/
class ClassSchema( class ClassSchema(
name: String, name: String,
fields: Map<String, Field>, fields: Map<String, Field>,
superclass: Schema? = null, superclass: Schema? = null,
interfaces: List<Class<*>> = emptyList() 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( class InterfaceSchema(
name: String, name: String,
fields: Map<String, Field>, fields: Map<String, Field>,
superclass: Schema? = null, superclass: Schema? = null,
interfaces: List<Class<*>> = emptyList() 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 { object CarpenterSchemaFactory {
fun newInstance ( fun newInstance(
name: String, name: String,
fields: Map<String, Field>, fields: Map<String, Field>,
superclass: Schema? = null, superclass: Schema? = null,
interfaces: List<Class<*>> = emptyList(), interfaces: List<Class<*>> = emptyList(),
isInterface: Boolean = false isInterface: Boolean = false
) : Schema = ): Schema =
if (isInterface) InterfaceSchema (name, fields, superclass, interfaces) if (isInterface) InterfaceSchema(name, fields, superclass, interfaces)
else ClassSchema (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)
}

View File

@ -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)
}

View File

@ -15,12 +15,12 @@ class ClassCarpenterTest {
val b: Int 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 // 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. // 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 } private val Class<*>.nonSyntheticFields: List<Field> get() = declaredFields.filterNot { it.isSynthetic }
val Class<*>.nonSyntheticMethods: List<Method> get() = declaredMethods.filterNot { it.isSynthetic } private val Class<*>.nonSyntheticMethods: List<Method> get() = declaredMethods.filterNot { it.isSynthetic }
@Test @Test
fun empty() { fun empty() {

View File

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

View File

@ -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"))
}
}