Bhanuchander Udhayakumar
Bhanuchander Udhayakumar

Reputation: 1621

Groovy closure does not modify a value in delegated object

The Groovy code shown below contains closures and a method. That format changed label and expected to receive that change label shows the problem and the requirement.

def method (String a, Closure c) {
    Query q = new Query()
    q.a = a
    c.delegate = q
    c.call()
    def str = q.str
}
class Query
{
    def str
    def a
    void key (String str, Closure cls) {
        this.str = str
        Pass p = new Pass()
        p.a=a
        cls.delegate=p
        cls.call()
        def val=p.a      // Expcted to receive that change
        println val

    } 
    class Pass
    {
        String a
    } 
}

method("got") {
    key ("got"){
        a=a.toUpperCase() // Format Changed here
        println a

    }
}

Actual Output is :

GOT
got

But my expected output is:

GOT
GOT

Why that a = a.toUpperCase() doesn't change a value in p object after the cls.call()? How to pass this change ?

Upvotes: 2

Views: 896

Answers (1)

Szymon Stepniak
Szymon Stepniak

Reputation: 42184

You have to change delegate resolving strategy for cls in key(String str, Closure cls) method to:

cls.resolveStrategy = Closure.DELEGATE_FIRST

Default strategy is Closure.OWNER_FIRST. In case of a Groovy script it means that the owner of this closure is an instance of a class that was generated by Groovy to run the script. In case of your Groovy script this class looks like this:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import groovy.lang.Binding;
import groovy.lang.Closure;
import groovy.lang.Script;
import org.codehaus.groovy.runtime.GeneratedClosure;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.runtime.callsite.CallSite;

public class my_groovy_script extends Script {
    public my_groovy_script() {
        CallSite[] var1 = $getCallSiteArray();
    }

    public my_groovy_script(Binding context) {
        CallSite[] var2 = $getCallSiteArray();
        super(context);
    }

    public static void main(String... args) {
        CallSite[] var1 = $getCallSiteArray();
        var1[0].call(InvokerHelper.class, my_groovy_script.class, args);
    }

    public Object run() {
        CallSite[] var1 = $getCallSiteArray();
        class _run_closure1 extends Closure implements GeneratedClosure {
            public _run_closure1(Object _thisObject) {
                CallSite[] var3 = $getCallSiteArray();
                super(my_groovy_script.this, _thisObject);
            }

            public Object doCall(Object it) {
                CallSite[] var2 = $getCallSiteArray();
                class _closure2 extends Closure implements GeneratedClosure {
                    public _closure2(Object _thisObject) {
                        CallSite[] var3 = $getCallSiteArray();
                        super(_run_closure1.this, _thisObject);
                    }

                    public Object doCall(Object it) {
                        CallSite[] var2 = $getCallSiteArray();
                        Object var3 = var2[0].call(var2[1].callGroovyObjectGetProperty(this));
                        ScriptBytecodeAdapter.setGroovyObjectProperty(var3, _closure2.class, this, (String)"a");
                        return var2[2].callCurrent(this, var2[3].callGroovyObjectGetProperty(this));
                    }

                    public Object doCall() {
                        CallSite[] var1 = $getCallSiteArray();
                        return this.doCall((Object)null);
                    }
                }

                return var2[0].callCurrent(this, "got", new _closure2(this.getThisObject()));
            }

            public Object doCall() {
                CallSite[] var1 = $getCallSiteArray();
                return this.doCall((Object)null);
            }
        }

        return var1[1].callCurrent(this, "got", new _run_closure1(this));
    }

    public Object method(String a, Closure c) {
        CallSite[] var3 = $getCallSiteArray();
        Query q = (Query)ScriptBytecodeAdapter.castToType(var3[2].callConstructor(Query.class), Query.class);
        ScriptBytecodeAdapter.setGroovyObjectProperty(a, my_groovy_script.class, q, (String)"a");
        ScriptBytecodeAdapter.setGroovyObjectProperty(q, my_groovy_script.class, c, (String)"delegate");
        var3[3].call(c);
        Object str = var3[4].callGroovyObjectGetProperty(q);
        return str;
    }
}

As you can see every Groovy script is actually a class that extends groovy.lang.Script class. There is one important thing about this class - it overrides:

If you take a look at the source code of both methods you will see that it uses binding object to store and access all variables in scope of the closure. That's why the closure you pass to Query.key(String str, Closure cls) does not modify a a field of class Pass but instead it creates a local binding a with a value GOT. You can change this behavior by changing Closure's resolve strategy to Closure.DELEGATE_FIRST. This will do the trick because you explicitly set cls.delegate to p instance so the closure will firstly look for a field a in p instance. I hope it helps.

Updated Groovy script

def method(String a, Closure c) {
    Query q = new Query()
    q.a = a
    c.delegate = q
    c.call()
    def str = q.str
}

class Query {
    def str
    def a

    void key(String str, Closure cls) {
        this.str = str
        Pass p = new Pass()
        p.a = a
        cls.delegate = p
        cls.resolveStrategy = Closure.DELEGATE_FIRST
        cls.call()
        def val = p.a      // Expcted to receive that change
        println val

    }

    class Pass {
        String a
    }
}

method("got") {
    key("got") {
        a = a.toUpperCase() // Format Changed here
        println a

    }

Output

GOT
GOT

Upvotes: 2

Related Questions