Jan Eglinger
Jan Eglinger

Reputation: 4090

Lazy GString evaluation in Groovy closures

I try to understand why in the following snippet, the GString is evaluated fine if it's created inside the closure, but will throw an exception if I try to create the String outside and try to evaluate it inside the closures:

map1 = ['foo': 1, 'bar': 2]
map2 = ['foo': 3, 'bar': 4]

dynamicallyGeneratedString = "key1: ${->key1}, val1: ${->value1}, key2: ${->key2}, val2: ${->value2}"

map1.each { key1, value1 ->
    map2.each { key2, value2 ->
        println "key1: ${->key1}, val1: ${->value1}, key2: ${->key2}, val2: ${->value2}" // works as expected
        // println dynamicallyGeneratedString // throws MissingPropertyException
    }
}

The desired output in both cases would be:

key1: foo, val1: 1, key2: foo, val2: 3
key1: foo, val1: 1, key2: bar, val2: 4
key1: bar, val1: 2, key2: foo, val2: 3
key1: bar, val1: 2, key2: bar, val2: 4

My goal is to dynamically generate a String depending on some other conditions, and then to lazily evaluate its contents while looping through the maps.

Is this a valid approach at all?

Upvotes: 1

Views: 3361

Answers (3)

Glushiator
Glushiator

Reputation: 684

A bit late to the party but here it is.

I wrote the following class, I use it for dynamically building SQL queries without sacrificing security since fill's result is a GStringImpl instance and groovy.sql.Sql correctly transforms it into parametrized database query.

I wasn't sure if Closure's call method is thread-safe (since I am setting delegate property) so I added synchronized to the fill method.

class GTemplate {

    def compiledTemplate

    GTemplate(String templateSource) {
        compiledTemplate = new GroovyShell().evaluate('{-> """' + escape(templateSource) + '""" }')
    }

    def static GTemplate compile(String templateSource) {
        return new GTemplate(templateSource)
    }

    def synchronized fill(def args) {
        compiledTemplate.delegate = args
        return compiledTemplate.call()
    }

    def synchronized fill(Map<?,?> args) {
        compiledTemplate.delegate = args
        return compiledTemplate.call()
    }

    private static String escape(String str) {
        StringBuilder buf = new StringBuilder()
        for(char c : str) {
            if ((c == '"') || (c == '\\'))
                buf.append('\\')
            buf.append(c)
        }
        return buf.toString()
    }
}

map1 = ['foo': 1, 'bar': 2]
map2 = ['foo': 3, 'bar': 4]

dynamicallyGeneratedString = GTemplate.compile('key1: ${->key1}, val1: ${->value1}, key2: ${->key2}, val2: ${->value2}')

map1.each { key1, value1 ->
    map2.each { key2, value2 ->
        println dynamicallyGeneratedString.fill(key1: key1, value1: value1, key2: key2, value2: value2)
    }
}

Upvotes: 1

Jan Eglinger
Jan Eglinger

Reputation: 4090

In addition to using templating as suggested by @Vampire, I can think of two alternative ways of solving the task.

  • Re-assigning variables inside the closure:

    map1 = ['foo': 1, 'bar': 2]
    map2 = ['foo': 3, 'bar': 4]
    
    def k1, v1, k2, v2
    dynamicString = "key1: ${->k1}, val1: ${->v1}, key2: ${->k2}, val2: ${->v2}"
    
    map1.each { key1, value1 ->
        map2.each { key2, value2 ->
            k1 = key1
            v1 = value1
            k2 = key2
            v2 = value2
            println dynamicString
        }
    }
    
  • Function evaluation:

    map1 = ['foo': 1, 'bar': 2]
    map2 = ['foo': 3, 'bar': 4]
    
    def myfunc(key1, value1, key2, value2) {
        dynamicallyGeneratedString = "key1: ${key1}, val1: ${value1}, key2: ${key2}, val2: ${value2}"
    }
    
    map1.each { key1, value1 ->
        map2.each { key2, value2 ->
            println myfunc(key1, value1, key2, value2)
        }
    }
    

I guess it's just a matter of taste... (or are there any performance considerations I am missing?)

Upvotes: 2

Vampire
Vampire

Reputation: 38639

The problem is, when you create the GString, it stores the references to the variables. When you then try to evaluate it, those refernces point to nothing and you get the exception.

If you really want to do it that way, I think you have to use a template engine like with

println new groovy.text.GStringTemplateEngine().createTemplate(dynamicallyGeneratedString).make(key1: key1, value1: value1, key2: key2, value2: value2)

Upvotes: 1

Related Questions