mirror of
https://github.com/corda/corda.git
synced 2025-06-16 14:18:20 +00:00
Merge pull request #1473 from corda/feature/kat/carpentEnum
CORDA-539 - Add enum support to the carpenter
This commit is contained in:
@ -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.
|
||||||
|
|
||||||
|
@ -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,8 +235,9 @@ 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'
|
||||||
@ -222,30 +251,97 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
|
|||||||
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
|
||||||
visitEnd()
|
visitMethod(opcodes, "get" + name.capitalize(), "()${field.descriptor}", null, null).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,8 +371,7 @@ 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 {
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -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 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() {
|
||||||
|
@ -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
|
||||||
|
@ -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"))
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user