How do I bind all methods of a certain name in an Object into a template via the binding map?

1.6k views Asked by At

A normal action in Groovy templates is to bind a named object into the scope of the template like this:

map.put("someObject",object)
template.bind(map)

Then, in the template I can reference and use 'someObject' like this:

someObject.helloWorld()
someObject.helloWorld("Hi Everyone!")
someObject.helloWorld("Hi", "Everyone!")

Inside the template, Groovy also allows me to define a method handle as a first-class variable in the template like this:

someObject = someObject.&helloWorld

Then, I can do these without referring to the object and method name:

 someObject() 
 someObject("Hello World")
 someObject("Hello", "World") 

How can I bind the method reference like this at the 'template.bind(map)' stage along with auto-resolving all the parameter combinations like the '.&' operator in the script provides?

Using a MethodClosure does not work — Here is a simple test and the error I get

class TestMethodClass {
        public void test() {
            System.out.println("test()");
        }

        public void test(Object arg) {
            System.out.println("test( " + arg + " )");
        }

        public void test(Object arg1, Object arg2) {
            System.out.println("test( " + arg1 + ", " + arg2 + " )");
        }
    }

    String basic = "<%" +
        " def mc1=testInstance.&test;" +
        "println \"mc1 class ${mc1.getClass()}\";" +
        "println \"mc1 metaclass ${mc1.getMetaClass()}\";" +
        "println mc1.getClass();" +
        "mc1();" +
        "mc1('var1');" +
        "mc1('var1', 'var2');" +
        "testMethod();" +
        " %>";

    Map<Object, Object> bindings = new HashMap<>();
    bindings.put("testInstance", new TestMethodClass());
    bindings.put("testMethod", new MethodClosure(new TestMethodClass(), "test"));

    TemplateEngine engine = new GStringTemplateEngine();
    Template t = engine.createTemplate(basic);
    String result = t.make(bindings).toString();

Error

mc1 class class org.codehaus.groovy.runtime.MethodClosure
mc1 metaclass org.codehaus.groovy.runtime.HandleMetaClass@6069db50[groovy.lang.MetaClassImpl@6069db50[class org.codehaus.groovy.runtime.MethodClosure]]
class org.codehaus.groovy.runtime.MethodClosure
test()
test( var1 )
test( var1, var2 )

groovy.lang.MissingMethodException: No signature of method: groovy.lang.Binding.testMethod() is applicable for argument types: () values: []

A user suggests I just use '.call(..)'

"testMethod.call();" +
"testMethod.call(1);" +
"testMethod.call(1,2);" +

But that defeats the purpose. In that case I might as well just bind the object instead of 'testMethod' and use it normally in Groovy template with regular method calls. So that is not the solution here.

The solution will create a binding such that testMethod() can be called just like this, and resolved for all overloaded methods, just like the "mc1=testInstance.&test" provides.

The mc1 is a MethodClosure and 'mc1=testInstance.&test' does some magic that I want to do that magic at the binding stage!

The metaclass of mc1 is a 'HandleMetaClass'. I can also custom set the metaclass of the methodclosure from the Java side. I just want to know how to do that to get the same behaviour. The Groovy is doing that in the template (from Java side in the template interpreter) and so I want to do it the same, in advance.

Note that normally, the streaming template creates its own delegate already. When the template code 'def mc1=testInstance.&test;' is interpreted, the Groovy compiler/interpreter uses that delegate when creating the MethodClosure with a HandleMetaClass, and then installs that in the existing delegate.

The proper answer then does not install a replacement delegate as per @Dany answer below, but instead works with the existing delegate and creates the correct objects to facilitate usage of mc1 without the '.call' syntax.

4

There are 4 answers

2
skadya On BEST ANSWER

You can achieve your desired behavior by changing the ResolveStrategy to OWNER_FIRST. Since you want to access bounded closure directly (without . notation), you need to bind the closure method to the "owner" (template object itself) instead of supplying via bindings map (delegate).

Your modified example:

String basic = "<%" +
        " def mc1=testInstance.&test;" +
        "println \"mc1 class ${mc1.getClass()}\";" +
        "println \"mc1 metaclass ${mc1.getMetaClass()}\";" +
        "println mc1.getClass();" +
        "mc1();" +
        "mc1('var1');" +
        "mc1('var1', 'var2');" +
        "testMethod();" +
        "testMethod('var1');" +
        " %>";

TemplateEngine engine = new GStringTemplateEngine();

TestMethodClass instance = new TestMethodClass();

// Prepare binding map
Map<String, Object> bindings = new HashMap<>();
bindings.put("testInstance", instance);

Template t = engine.createTemplate(basic);

Closure<?> make = (Closure<?>) t.make(bindings); // cast as closure

int resolveStrategy = make.getResolveStrategy();
make.setResolveStrategy(Closure.OWNER_FIRST);

// set method closure which you want to invoke directly (without .
// notation). This is more or less same what you pass via binding map
// but too verbose. You can encapsulate this inside a nice static                 
// method
InvokerHelper.setProperty(
    make.getOwner(), "testMethod", new MethodClosure(instance, "test")
);

make.setResolveStrategy(resolveStrategy);
String result = make.toString();

Hoping, this meet your requirements.

1
blackdrag On

someObject.&helloWorld is a MethodClosure instance and the code translates into new MethodClosure(someObject, "helloWorld") (MetodClosure is from the org.codehaus.groovy.runtime package). This way you can prepare the map in Java.

1
Dany On

This will work:

"<% " +
"def mc1=testInstance.&test;" +
"mc1();" +
"mc1('var1');" +
"mc1('var1', 'var2');" +
"testMethod.call();" +
"testMethod.call(1);" +
"testMethod.call(1,2);" +
" %>"

testMethod.call(...) works because it is translated to getProperty('testMethod').invokeMethod('call', ...). The testMethod property is defined in the binding, and it is of MethodClosure type which has call method defined.

However testMethod(...) is translated to invokeMethod('testMethod', ...). It fails because there is no testMethod method defined and you cannot define it via binding.

EDIT

Main.java:

public class Main {
    public static void main(String[] args) throws Throwable {
        String basic = "<%" +
                " def mc1=testInstance.&test;" +
                "mc1();" +
                "mc1('var1');" +
                "mc1('var1', 'var2');" +
                "testMethod();" +
                "testMethod(1);" +
                "testMethod(2,3);" +
                " %>";

        Map<Object, Object> bindings = new HashMap<>();
        bindings.put("testInstance", new TestMethodClass());
        bindings.put("testMethod", new MethodClosure(new TestMethodClass(), "test"));

        TemplateEngine engine = new GStringTemplateEngine();
        Template t = engine.createTemplate(basic);
        Closure make = (Closure) t.make();
        make.setDelegate(new MyDelegate(bindings));
        String result = make.toString();
    }
}   

MyDelegate.groovy:

class MyDelegate extends Binding {

  MyDelegate(Map binding) {
    super(binding)
  }

  def invokeMethod(String name, Object args) {
    getProperty(name).call(args)
  }
}

Or MyDelegate.java:

public class MyDelegate extends Binding{

    public MyDelegate(Map binding) {
        super(binding);
    }

    public Object invokeMethod(String name, Object args) {
        return DefaultGroovyMethods.invokeMethod(getProperty(name), "call", args);
    }
}
1
Alessio Stalla On

It is the 'def' expression that has special semantics. Adding:

def testMethod = testMethod;

just before invoking testMethod() does the trick in your example. I don't think you can achieve the same result with bindings alone.

EDIT: to clarify, since the testMethod object is not changed in any way when the def expression is evaluated, probably you cannot construct testMethod in such a way that it is automatically registered as a method by providing it as a binding. You might have more luck by playing with the template's metaclass.

Writable templateImpl = t.make(bindings);
MetaClass metaClass = ((Closure) templateImpl).getMetaClass();