JoeG JoeG - 2 months ago 16
Groovy Question

Late binding Groovy String?

I am moving the text for user interactions to an external groovy config file. I have had no problems "Slurping" this until the need arose to handle text such as this:

text:"Enter the port number used by the program (${defaultPort}): "


Is there any way to read this value into a String or GString and then do the binding to active program values (such as defaultPort)?

In the code that will process this the "defaultValue" can be set to more than one value. For instance if using http then that value might be 80, but if using https it might be 443.

I realize SimpleTemplateEngine would work, but is there an easier way?

Two solutions that don't work:

1)

text:{ value -> "Enter the port number used by the program ($value)" }


Ok, actually this does work! However, I can't (or don't think I can) use this approach as this is the exception case, the vast majority of uses will just read the text. This approach requires you call it like a method which would break all the other uses.

I tried reading it as a Java String (single quoted) and making a GString from that. I won't even show that code - it is New Years, not April Fools! Suffice to say I couldn't make it work!

2)

I also tried variants of this:

def defaultPort = determinePort()
def textVal = myConfig.text // tried single and double quotes in the config file
def newVal = "${writer -> writer << textVal}"


Thanks for any help.

Adding more detail based on answer from loteq

In case it was clear, here is a more detailed sketch of what I would like to do:

// questions would be in a file parseable by ConfigSlurper:
someIdentifier {
questions =[1:[text:'Enter http or https ',
anotherField: 'some value',
method2Use: 'getInput'],
2:[text:'Enter the port number used by the program (${defaultPort}): ',
anotherField: 'some value',
method2Use: 'getInput']
]
}

def myConfig = new ConfigSlurper().parse(questionConfigFile.groovy)
def questions = myConfig.someIdentifier?.questions
questions?.each{ number, details ->
// need a way in here to have the answer of the first question
// determine a value of either 80 or 443 (80 = http, 443 = https)
// and use that to substitute into the 'text' of the second question
}


Sorry if this is too much detail - I was hoping it might help.

Answer

Why don't you try a ConfigSlurper approach?

String configScript='''
text1="Enter the port number used by the program (${defaultPort}): "
text2="Normal text"
'''

def sl=new ConfigSlurper()

sl.setBinding(
defaultPort:8080
)

def cfg=sl.parse(configScript)

cfg.each{println it} 

Results:

text1=Enter the port number used by the program (8080): 
text2=Normal text

UPDATE, based on more detail in second part of question

You can use groovy's dynamic dispatch capabilities to quite nicely handle both strings and Closures.

// questions would be in a file parseable by ConfigSlurper:
someIdentifier {
    questions =[1:[text:'Enter http or https ',
        anotherField: 'some value',
        method2Use: 'getInput'
        responseBinding: 'protocol'
        ],
                2:[text:{binding->"Enter the port number used by the program (${binding.defaultPort}): ",
        anotherField: 'some value',
        method2Use: 'getInput'
        responseBinding: 'port'
        ]
    ]
}

def myConfig = new ConfigSlurper().parse(questionConfigFile.groovy)
def questions = myConfig.someIdentifier?.questions
def binding=[:]
questions?.each{ number, details ->
    this."processResponse$number"(this."${details.method2Use}"(details.text,details,binding))
}

void processResponse1(Map binding) {
     def defaultPorts =[
         https:443,
         http:80
     ]
     binding.defaultPort=defaultPorts[binding.protocol]
}

void processResponse2(Map binding) {
    //
}

Map getInput(String prompt, Map interactionDetails, Map binding) {
    binding[interactionDetails.responseBinding] = readInput(prompt)
    binding
}

Map getInput(Closure<String> prompt, Map interactionDetails, Map binding) {
    binding[interactionDetails.responseBinding] = readInput(prompt(binding))
    binding
}

UPDATE

A radically different approach and, in my opinion, way cleaner, is defining a DSL:

def configScript='''
    someIdentifier = {
        protocol=getInput("Enter http or https")
        port=getInput("Enter the port used for $protocol (${defaultPorts[protocol]}):")
    }
'''

You would still use a configSlurper to parse the files, but you would then run the closure someIdentifier against a delegate that would implement:

class Builder {
    def defaultPorts =[
         https:'443',
         http:'80'
    ]
    String protocol
    String port

    def getInput(String prompt) {
        System.console().readLine(prompt)
    }

    void setPort(String p) {
        port = p ?: defaultPorts[protocol]
    }

    String toString() {
        """
        |protocol=$protocol
        |port=$port
        """.stripMargin()
    }
}

then:

def sl=new ConfigSlurper()

def cfg=sl.parse(configScript)
def id=cfg.someIdentifier.clone()
def builder=new Builder()
id.delegate=builder
id.resolveStrategy=Closure.DELEGATE_ONLY
id()

println builder

This allows you to naturally enrich your interactions at will by adding methods in the delegate, instead of painfully adding elements to this map.