diff --git a/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt b/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt index 7641b9c6ea..bc29f684ed 100644 --- a/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt +++ b/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt @@ -71,7 +71,7 @@ class ClassCarpenter { /** * A Schema represents a desired class. */ - class Schema(val name: String, fields: Map>, val superclass: Schema? = null, val interfaces: List> = emptyList()) { + open class Schema(val name: String, fields: Map>, val superclass: Schema? = null, val interfaces: List> = emptyList()) { val fields = LinkedHashMap(fields) // Fix the order up front if the user didn't. val descriptors = fields.map { it.key to Type.getDescriptor(it.value) }.toMap() @@ -79,6 +79,20 @@ class ClassCarpenter { fun descriptorsIncludingSuperclasses(): Map = (superclass?.descriptorsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(descriptors) } + class ClassSchema( + name: String, + fields: Map>, + superclass: Schema? = null, + interfaces: List> = emptyList() + ) : Schema (name, fields, superclass, interfaces) + + class InterfaceSchema( + name: String, + fields: Map>, + superclass: Schema? = null, + interfaces: List> = emptyList() + ) : Schema (name, fields, superclass, interfaces) + class DuplicateName : RuntimeException("An attempt was made to register two classes with the same name within the same ClassCarpenter namespace.") class InterfaceMismatch(msg: String) : RuntimeException(msg) @@ -111,11 +125,16 @@ class ClassCarpenter { hierarchy += cursor cursor = cursor.superclass } - hierarchy.reversed().forEach { generateClass(it) } + hierarchy.reversed().forEach { + when (it) { + is ClassSchema -> generateClass(it) + is InterfaceSchema -> generateInterface(it) + } + } return _loaded[schema.name]!! } - private fun generateClass(schema: Schema): Class<*> { + private fun generateClass(schema: ClassSchema): Class<*> { val jvmName = schema.name.jvm // Lazy: we could compute max locals/max stack ourselves, it'd be faster. val cw = ClassWriter(ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS) @@ -137,6 +156,25 @@ class ClassCarpenter { return clazz } + private fun generateInterface(schema: InterfaceSchema): Class<*> { + val jvmName = schema.name.jvm + // Lazy: we could compute max locals/max stack ourselves, it'd be faster. + val cw = ClassWriter (ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS) + with(cw) { + val interfaces = schema.interfaces.map { it.name.jvm }.toTypedArray() + + visit(V1_8, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, jvmName, null, "java/lang/Object", interfaces) + + generateAbstractGetters(schema) + + visitEnd() + } + val clazz = classloader.load(schema.name, cw.toByteArray()) + + _loaded[schema.name] = clazz + return clazz + } + private fun ClassWriter.generateFields(schema: Schema) { for ((name, desc) in schema.descriptors) { visitField(ACC_PROTECTED + ACC_FINAL, name, desc, null, null).visitEnd() @@ -202,6 +240,17 @@ class ClassCarpenter { } } + private fun ClassWriter.generateAbstractGetters(schema: InterfaceSchema) { + for ((name, type) in schema.fields) { + val descriptor = schema.descriptors[name] + val opcodes = ACC_ABSTRACT + ACC_PUBLIC + with (visitMethod(opcodes, "get" + name.capitalize(), "()" + descriptor, null, null)) { + // abstract method doesn't have any implementation so just end + visitEnd() + } + } + } + private fun ClassWriter.generateConstructor(jvmName: String, schema: Schema) { with(visitMethod(ACC_PUBLIC, "", "(" + schema.descriptorsIncludingSuperclasses().values.joinToString("") + ")V", null, null)) { visitCode() @@ -262,7 +311,8 @@ class ClassCarpenter { method.name.startsWith("get") -> method.name.substring(3).decapitalize() else -> throw InterfaceMismatch("Requested interfaces must consist only of methods that start with 'get': ${itf.name}.${method.name}") } - if (fieldNameFromItf !in allFields) + + if ((schema is ClassSchema) and (fieldNameFromItf !in allFields)) throw InterfaceMismatch("Interface ${itf.name} requires a field named ${fieldNameFromItf} but that isn't found in the schema or any superclass schemas") } } diff --git a/experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt b/experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt index 5ffe32e51d..2e6b6bebd8 100644 --- a/experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt +++ b/experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt @@ -21,7 +21,7 @@ class ClassCarpenterTest { @Test fun empty() { - val clazz = cc.build(ClassCarpenter.Schema("gen.EmptyClass", emptyMap(), null)) + val clazz = cc.build(ClassCarpenter.ClassSchema("gen.EmptyClass", emptyMap(), null)) assertEquals(0, clazz.nonSyntheticFields.size) assertEquals(2, clazz.nonSyntheticMethods.size) // get, toString assertEquals(0, clazz.declaredConstructors[0].parameterCount) @@ -30,7 +30,7 @@ class ClassCarpenterTest { @Test fun prims() { - val clazz = cc.build(ClassCarpenter.Schema("gen.Prims", mapOf( + val clazz = cc.build(ClassCarpenter.ClassSchema("gen.Prims", mapOf( "anIntField" to Int::class.javaPrimitiveType!!, "aLongField" to Long::class.javaPrimitiveType!!, "someCharField" to Char::class.javaPrimitiveType!!, @@ -65,7 +65,7 @@ class ClassCarpenterTest { } private fun genPerson(): Pair, Any> { - val clazz = cc.build(ClassCarpenter.Schema("gen.Person", mapOf( + val clazz = cc.build(ClassCarpenter.ClassSchema("gen.Person", mapOf( "age" to Int::class.javaPrimitiveType!!, "name" to String::class.java ))) @@ -88,14 +88,14 @@ class ClassCarpenterTest { @Test(expected = ClassCarpenter.DuplicateName::class) fun duplicates() { - cc.build(ClassCarpenter.Schema("gen.EmptyClass", emptyMap(), null)) - cc.build(ClassCarpenter.Schema("gen.EmptyClass", emptyMap(), null)) + cc.build(ClassCarpenter.ClassSchema("gen.EmptyClass", emptyMap(), null)) + cc.build(ClassCarpenter.ClassSchema("gen.EmptyClass", emptyMap(), null)) } @Test fun `can refer to each other`() { val (clazz1, i) = genPerson() - val clazz2 = cc.build(ClassCarpenter.Schema("gen.Referee", mapOf( + val clazz2 = cc.build(ClassCarpenter.ClassSchema("gen.Referee", mapOf( "ref" to clazz1 ))) val i2 = clazz2.constructors[0].newInstance(i) @@ -104,8 +104,8 @@ class ClassCarpenterTest { @Test fun superclasses() { - val schema1 = ClassCarpenter.Schema("gen.A", mapOf("a" to String::class.java)) - val schema2 = ClassCarpenter.Schema("gen.B", mapOf("b" to String::class.java), schema1) + val schema1 = ClassCarpenter.ClassSchema("gen.A", mapOf("a" to String::class.java)) + val schema2 = ClassCarpenter.ClassSchema("gen.B", mapOf("b" to String::class.java), schema1) val clazz = cc.build(schema2) val i = clazz.constructors[0].newInstance("xa", "xb") as SimpleFieldAccess assertEquals("xa", i["a"]) @@ -115,8 +115,8 @@ class ClassCarpenterTest { @Test fun interfaces() { - val schema1 = ClassCarpenter.Schema("gen.A", mapOf("a" to String::class.java)) - val schema2 = ClassCarpenter.Schema("gen.B", mapOf("b" to Int::class.java), schema1, interfaces = listOf(DummyInterface::class.java)) + val schema1 = ClassCarpenter.ClassSchema("gen.A", mapOf("a" to String::class.java)) + val schema2 = ClassCarpenter.ClassSchema("gen.B", mapOf("b" to Int::class.java), schema1, interfaces = listOf(DummyInterface::class.java)) val clazz = cc.build(schema2) val i = clazz.constructors[0].newInstance("xa", 1) as DummyInterface assertEquals("xa", i.a) @@ -125,10 +125,92 @@ class ClassCarpenterTest { @Test(expected = ClassCarpenter.InterfaceMismatch::class) fun `mismatched interface`() { - val schema1 = ClassCarpenter.Schema("gen.A", mapOf("a" to String::class.java)) - val schema2 = ClassCarpenter.Schema("gen.B", mapOf("c" to Int::class.java), schema1, interfaces = listOf(DummyInterface::class.java)) + val schema1 = ClassCarpenter.ClassSchema("gen.A", mapOf("a" to String::class.java)) + val schema2 = ClassCarpenter.ClassSchema("gen.B", mapOf("c" to Int::class.java), schema1, interfaces = listOf(DummyInterface::class.java)) val clazz = cc.build(schema2) val i = clazz.constructors[0].newInstance("xa", 1) as DummyInterface assertEquals(1, i.b) } -} \ No newline at end of file + + @Test + fun `generate interface`() { + val schema1 = ClassCarpenter.InterfaceSchema("gen.Interface", mapOf("a" to Int::class.java)) + val iface = cc.build(schema1) + + assert (iface.isInterface()) + assert (iface.constructors.isEmpty()) + assertEquals (iface.declaredMethods.size, 1) + assertEquals (iface.declaredMethods[0].name, "getA") + + val schema2 = ClassCarpenter.ClassSchema("gen.Derived", mapOf("a" to Int::class.java), interfaces = listOf (iface)) + val clazz = cc.build(schema2) + val testA = 42 + val i = clazz.constructors[0].newInstance(testA) as SimpleFieldAccess + + assertEquals(testA, i["a"]) + } + + @Test + fun `generate multiple interfaces`() { + val iFace1 = ClassCarpenter.InterfaceSchema("gen.Interface1", mapOf("a" to Int::class.java, "b" to String::class.java)) + val iFace2 = ClassCarpenter.InterfaceSchema("gen.Interface2", mapOf("c" to Int::class.java, "d" to String::class.java)) + + val class1 = ClassCarpenter.ClassSchema( + "gen.Derived", + mapOf( + "a" to Int::class.java, + "b" to String::class.java, + "c" to Int::class.java, + "d" to String::class.java), + interfaces = listOf (cc.build (iFace1), cc.build (iFace2))) + + val clazz = cc.build(class1) + val testA = 42 + val testB = "don't touch me, I'm scared" + val testC = 0xDEAD + val testD = "wibble" + val i = clazz.constructors[0].newInstance(testA, testB, testC, testD) as SimpleFieldAccess + + assertEquals(testA, i["a"]) + assertEquals(testB, i["b"]) + assertEquals(testC, i["c"]) + assertEquals(testD, i["d"]) + } + + @Test + fun `interface implementing interface`() { + val iFace1 = ClassCarpenter.InterfaceSchema( + "gen.Interface1", + mapOf ( + "a" to Int::class.java, + "b" to String::class.java)) + + val iFace2 = ClassCarpenter.InterfaceSchema( + "gen.Interface2", + mapOf( + "c" to Int::class.java, + "d" to String::class.java), + interfaces = listOf (cc.build (iFace1))) + + val class1 = ClassCarpenter.ClassSchema( + "gen.Derived", + mapOf( + "a" to Int::class.java, + "b" to String::class.java, + "c" to Int::class.java, + "d" to String::class.java), + interfaces = listOf (cc.build (iFace2))) + + val clazz = cc.build(class1) + val testA = 99 + val testB = "green is not a creative colour" + val testC = 7 + val testD = "I like jam" + val i = clazz.constructors[0].newInstance(testA, testB, testC, testD) as SimpleFieldAccess + + assertEquals(testA, i["a"]) + assertEquals(testB, i["b"]) + assertEquals(testC, i["c"]) + assertEquals(testD, i["d"]) + } +}