User1000547
User1000547

Reputation: 4301

SimpleTemplate created with binding.variables terminates execution when converted to a String

I was trying to implement the solution found in this answer, where a template string is resolved against this.binding.variables.

The first part of this script is a more standard Map-based solution, such as is found in the docs (it's mainly to demonstrate that this overall approach is functional).

However, from what I can gather, the template bound against this.binding.variables puts a hard stop on execution when it's converted to a string, either explicitly with a cast or toString() call, or implicitly (by being sent to stdout). No exception is thrown (at least, not that I can catch) and the finally block never executes, nor does any subsequent code. Interestingly, when the bound template is passed directly to println, output is produced. I don't know enough about the internals of println to hazard a guess as to what's going on there but it doesn't crash immediately (but no further code is executed).

text = 'i am a ${interpolated} string'
interpolated = 'runtime'

engine = new groovy.text.SimpleTemplateEngine()
template = engine.createTemplate(text)

makeWithMap = template.make([interpolated : interpolated])
// these all work fine
println(makeWithMap)
println(makeWithMap.toString())
println((String) makeWithMap)

try {
    makeWithBindingVariables = template.make(this.binding.variables)
    // any of the below lines of code will terminate execution
    // this WILL produce output, but stop executing after:
    println(makeWithBindingVariables)
    // these will NOT produce output, and will stop executing:
    println(makeWithBindingVariables.toString())
    println((String) makeWithBindingVariables)
} catch (Exception e) { // nothing is caught
    println("am dead ${e}")
} finally { // we never get here, either
    println("how did I break a finally block?")
}
// this won't be reached if any of the above are called
println("All done")

If I add this.binding.variables.each { println(it) } directly after makeWithBindingVariables is created, I get the following output:

args=[Ljava.lang.String;@366ac49b
text=i am a ${interpolated} string
interpolated=runtime
engine=groovy.text.SimpleTemplateEngine@6ad59d92
template=groovy.text.SimpleTemplateEngine$SimpleTemplate@56f0cc85
makeWithMap=i am a runtime string
makeWithBindingVariables=i am a runtime string

If I println(this.binding.variables) directly:

[args:[], text:i am a ${interpolated} string, interpolated:runtime, engine:groovy.text.SimpleTemplateEngine@22295ec4, template:groovy.text.SimpleTemplateEngine$SimpleTemplate@5adb0db3, makeWithMap:i am a runtime string, makeWithBindingVariables:i am a runtime string]

(NB printing this.binding.variables also crashes the program)

What is going on here? The fact that I've managed to craft an object that salts the earth when converted to a string doesn't really make any sense to me, so I have to assume that I'm either doing something drastically wrong or fundamentally misunderstanding something (possibly both).

This behavior can be reproduced in an online interpreter here

Upvotes: 1

Views: 379

Answers (1)

Matias Bjarland
Matias Bjarland

Reputation: 4482

First of all, very interesting observation. This is definitely unintuitive.

So I ran this in a debugger in IntelliJ and turns out the following are the relevant parts of the involved groovy code (under groovy 3.0.6):

// SimpleTemplateEngine
public Writable make(final Map map) {
    return new Writable() {
        /**
          * Write the template document with the set binding applied to the writer.
          *
          * @see groovy.lang.Writable#writeTo(java.io.Writer)
          */
        public Writer writeTo(Writer writer) {
            Binding binding;
            if (map == null)
                binding = new Binding();
            else
                binding = new Binding(map);
            Script scriptObject = InvokerHelper.createScript(script.getClass(), binding);
            PrintWriter pw = new PrintWriter(writer);
            scriptObject.setProperty("out", pw);
            scriptObject.run();
            pw.flush();
            return writer;
        }

        /**
          * Convert the template and binding into a result String.
          *
          * @see java.lang.Object#toString()
          */
        public String toString() {
            Writer sw = new StringBuilderWriter();
            writeTo(sw);
            return sw.toString();
        }
    };
}

Where the println(makeWithBindingVariables) will call the toString method which in turn will execute the writeTo method.

And running this in a debugger we see that the script does indeed not exit where the output stopped. The plot thickens...

We do however get a clue from this:

first breakpoint

where we are stopped in a breakpoint right before the first toString call on the template. Now compare this to the second breakpoint:

second breakpoint

and specifically, look at the out variable object identity in the script binding in the watches section at the bottom right. It went from 2804 to 2813.

In other words, the execution of the template toString has executed the template and in the process replaced the groovy scripts default out binding variable (which is System.out) thus rendering any further output invisible.

So the script is not exiting...it is just producing output in a place where you can no longer see it.

This can be remedied by copying the binding:

    def makeWithBindingVariables = template.make(this.binding.variables.clone())

which prevents the template from replacing the groovy script execution environment out variable.

Again, not intuitive and required some serious digging, but at least it explains the behavior.

As an additional detail, the groovy println and I'm assuming other methods do things like this:

  public void println(Object value) {
    Object object;
    try {
      object = this.getProperty("out");
    } catch (MissingPropertyException var4) {
      DefaultGroovyMethods.println(System.out, value);
      return;
    }

Note the use of out.

We can also reproduce this with a simpler script:

println "foo"
def sw = new org.apache.groovy.io.StringBuilderWriter()
def pw = new PrintWriter(sw)
out = pw
println "bar first"
System.out.println("bar again, sw contains: ${sw}")

which, when executed prints:

─➤ groovy test.groovy
foo
bar again, sw contains: bar first

where the first println after replacing out is not printed to std out, but we can force writing to std out using java's System.out.println and we can see that the replaced print writer did capture our vanished println.

Upvotes: 1

Related Questions