uros calakovic uros calakovic - 4 months ago 13
Java Question

Create a Groovy class dynamically

Given a class name I would like to dynamically create a Groovy class add properties and methods to it. I create the new class using

instance = this.class.classLoader.parseClass(
"public class $name {}")


For methods I use

instance.metaClass."$it.key" = it.value


where it.key is a string (method name) and it.value is a closure. This is convenient because I can specify method parameter types and get type checking. However, I am not able to specify a dynamically created property type without assigning it a value. I can work around this by explicitly defining getter and setter for the property. This works, but it seems that neither metaClass.name = value nor metaClass.getName = {} actually create a field in the class because the Java field operator doesn't work for the created properties. Can I ad a property to a Groovy class and specify its type without assigning an initial value to it or explicitly defining getter and setter methods? Is there a way to add a new field to a Groovy class?
Here is the script:

class SomeClass {
Integer p1
String p2
}

class ClassBuilder {
def name
def instance
def properties
def methods

def ClassBuilder() {
properties = [:]
methods = [:]
}

def set_name(name) {
this.name = name
}

def add_property(name, type) {
properties[name] = type
}

def add_method(name, closure) {
methods[name] = closure
}

def get_instance() {
instance = this.class.classLoader.parseClass(
"public class $name {}")

properties.each {
instance.metaClass."$it.key" = null
//doesn't work
instance.metaClass."$it.key".type = it.value
}

methods.each {
instance.metaClass."$it.key" = it.value
}

return instance
}
}

builder = new ClassBuilder()

builder.set_name('MyClass')

builder.add_property('property1', String)
builder.add_property('property2', SomeClass)

builder.add_method('method1', {SomeClass obj -> println obj})
builder.add_method('setProperty2', {SomeClass obj -> this.property2 = obj})
builder.add_method('getProperty2', {return this.property2})

builder.add_method('method2', {return property1 + property2})

c = builder.get_instance()

i = c.newInstance()
i.property1 = new SomeClass()
i.property2 = 5

//i.method2() //throws GroovyCastException

//i.property2 = 'throws GroovyCastException'
//i.@property1 = 'throws MissingFieldException'

//No such field: property2 for class: MyClass
//i.@property2 = new SomeClass()

i.method1(new SomeClass())
//i.method1('throws MissingMethodException')


[Edit]

The use case is like this: I define an interface or a base class in Java. A user implements the interface or extends the base class in Groovy and the class is passed back to Java to be used by the main application. Users are not programmers so they define the class using a simple DSL and I construct the actual class using a builder. I am still experimenting with Groovy/JRuby and Java interop (new to both languages).

Answer

I have more or less been able to get it working by using GroovyClassLoader and SimpleTemplateEngine. here is the code:

class ClassBuilder {

    GroovyClassLoader loader
    String name
    Class cls
    def imports
    def fields
    def methods

    def ClassBuilder(GroovyClassLoader loader) {
        this.loader = loader
        imports = []
        fields = [:]
        methods = [:]
    }

    def setName(String name) {
        this.name = name
    }

    def addImport(Class importClass) {
        imports << "${importClass.getPackage().getName()}" +
                ".${importClass.getSimpleName()}"
    }

    def addField(String name, Class type) {
        fields[name] = type.simpleName
    }

    def addMethod(String name, Closure closure) {
        methods[name] = closure
    }

    def getCreatedClass() {

        def templateText = '''
<%imports.each {%>import $it\n <% } %> 
class $name
{
<%fields.each {%>    $it.value $it.key \n<% } %>
}
'''
        def data = [name: name, imports: imports, fields: fields]

        def engine = new groovy.text.SimpleTemplateEngine()
        def template = engine.createTemplate(templateText)
        def result = template.make(data)
        cls = loader.parseClass(result.toString())
        methods.each {
            cls.metaClass."$it.key" = it.value
        }
        return cls
    }
}

and here is an example of how I use it to create a class dynamically:

import java.util.Calendar
def builder = new ClassBuilder(this.class.classLoader)
builder.setName("MyClass");

builder.addImport(Calendar)

builder.addField('field1', Integer)
builder.addField('field2', Integer)

builder.addMethod('sum') { field1 + field2 }

builder.addMethod('product') { field1 * field2 }

builder.addMethod('testCalendar') {
    println Calendar.getInstance().getTime()
}

Class myClass = builder.getCreatedClass()
def myInstance = myClass.newInstance()

myInstance.field1 = 1
myInstance.field2 = 2

println myInstance.sum()
println myInstance.product()

myInstance.setField2(1500)
println myInstance.getField2()

myInstance.testCalendar()