mirror of
https://github.com/corda/corda.git
synced 2025-04-16 07:27:17 +00:00
CORDA-539 - Add enum support to the carpenter
If the serializer is going to support enumerated types then the class carpenter also has to Refactor the Carpenter schema and fields to add an enum type, add code in the carpenter to generate enum's and of course add tests
This commit is contained in:
parent
15aa4036b6
commit
c48a37a080
@ -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.
|
||||
|
||||
|
@ -25,6 +25,16 @@ class CarpenterClassLoader (parentClassLoader: ClassLoader = Thread.currentThrea
|
||||
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 const val jlEnum = "java/lang/Enum"
|
||||
private const val jlString = "java/lang/String"
|
||||
private const val jlObject = "java/lang/Object"
|
||||
private const val jlClass = "java/lang/Class"
|
||||
|
||||
/**
|
||||
* 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,6 +117,7 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
|
||||
when (it) {
|
||||
is InterfaceSchema -> generateInterface(it)
|
||||
is ClassSchema -> generateClass(it)
|
||||
is EnumSchema -> generateEnum(it)
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,41 +126,55 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
|
||||
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)
|
||||
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 +190,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 +218,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,8 +233,9 @@ 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'
|
||||
@ -222,30 +249,97 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
|
||||
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 +349,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 +359,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,8 +369,7 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
|
||||
}
|
||||
visitInsn(RETURN)
|
||||
visitMaxs(0, 0)
|
||||
visitEnd()
|
||||
}
|
||||
}.visitEnd()
|
||||
}
|
||||
|
||||
private fun MethodVisitor.load(slot: Int, type: Field): Int {
|
||||
|
@ -1,53 +1,104 @@
|
||||
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())
|
||||
val interfaces: List<Class<*>> = emptyList(),
|
||||
updater : (String, Field) -> Unit)
|
||||
{
|
||||
private fun Map<String, Field>.descriptors() =
|
||||
LinkedHashMap(this.mapValues { it.value.descriptor })
|
||||
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 build [Schema]s based
|
||||
* on an AMQP schema
|
||||
*/
|
||||
object CarpenterSchemaFactory {
|
||||
fun newInstance (
|
||||
fun newInstance(
|
||||
name: String,
|
||||
fields: Map<String, Field>,
|
||||
superclass: Schema? = null,
|
||||
@ -58,91 +109,3 @@ object CarpenterSchemaFactory {
|
||||
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
|
||||
import java.beans.BeanDescriptor
|
||||
import java.util.*
|
||||
|
||||
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) {
|
||||
if (field.isPrimitive) {
|
||||
throw NullablePrimitiveException (
|
||||
"Field $name is primitive type ${Type.getDescriptor(field)} and thus cannot be nullable")
|
||||
}
|
||||
|
||||
this.name = name
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
cc.build(schema)
|
||||
}
|
||||
|
||||
@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"))
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user