Reputation: 4301
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
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:
where we are stopped in a breakpoint right before the first toString
call on the template. Now compare this to the 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