A blog about software development and other software related matters

Blog Archive

Saturday, February 28, 2009

GroovyShell bindings validation

Groovy 1.6 got released not long a go with a load of new features, one of these is AST transformations which basically lets developers manipulate/access Groovy's AST before it gets loaded into the JVM.
This feature enables some really cool MOP options to mind however iv decided to try it in solving a non MOP-ish issue.

The GroovyShell is a very versatile and useful tool that enables us to evaluate any Groovy script file at runtime:


def bindings = [lastDay:2]
def script = """
import java.util.Calendar
Calendar cal = Calendar.instance
cal.set(2009,Calendar.FEBRUARY,2)
def ghday = cal.time
cal.set(2009,Calendar.MARCH,lastDay)
def spring = cal.time
def days = (ghday..spring).size()
println days
"""
def shell = new GroovyShell(new Binding(bindings))
shell.evaluate(script)

The script get its input parameters from the bindings hash, forgeting to pass an input variable (lastDay in this case) will cause a groovy.lang.MissingPropertyException to be thrown.

This is quite reasonable during development time but not in other scenarios in which the script is written in one machine & evaluated on a remote machine.
The exception gets thrown only post its evaluation which might be too late (deep in the execution flow), wouldn't it be nice if it was possible to validate this at will on the client side?

We could scan the script for suspect variables that might not have been initialised, such scanning might be possible by inspecting the script AST, looking after variables that weren't declared in the current script, Groovy code visitors to the rescue!

The following implementation matches the bill perfectly:

package com.jdftm.script.valid
import org.codehaus.groovy.ast.ClassCodeVisitorSupport
import org.codehaus.groovy.ast.expr.MethodCallExpression
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.ast.expr.VariableExpression
import org.codehaus.groovy.ast.expr.DeclarationExpression

class ClassCodeVisitorSupportImp extends ClassCodeVisitorSupport {

def declared = []

def existsByDefault=['context','args']

def bindings

def notInBindings = {name -> bindings?.keySet().find{it.equals(name)} == null}

def notDeclared = {name -> declared?.find{it.text.equals(name)} == null && !existsByDefault.contains(name)}

def notValid = {notDeclared(it.text) && !it.hasInitialExpression() && notInBindings(it.text)}

def suspectVariables = []

public void visitMethodCallExpression(MethodCallExpression methodCall) {
def variables=methodCall.arguments.expressions.findAll{(it instanceof VariableExpression)}
suspectVariables << variables.findAll{notValid(it)}
}

public void visitDeclarationExpression(final DeclarationExpression decleration){
declared << decleration.variableExpression
}

protected SourceUnit getSourceUnit() {
return null;
}
}

This visitor keeps a suspects list of all the variables that are either not in the declared variables or in the bindings hash.
The following class encapsulates the visitor usage:

package com.jdftm.script.valid
import org.codehaus.groovy.ast.ClassCodeVisitorSupport
import org.codehaus.groovy.ast.ModuleNode
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.control.CompilationUnit
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.control.Phases;

class GroovyScriptValidator {

def validate(script,bindings){
def ast=getAST(script)
def impl=new ClassCodeVisitorSupportImp()
impl.bindings=bindings
impl.visitClass((ClassNode)ast.getClasses().get(0));
impl.suspectVariables.flatten()
}

def ModuleNode getAST(String source) {
SourceUnit unit = SourceUnit.create("Test",source);
CompilationUnit compUnit = new CompilationUnit();
compUnit.addSource(unit);
compUnit.compile(Phases.SEMANTIC_ANALYSIS);
return unit.getAST();
}
}

Validating the script is now simple:
 
package com.jdftm.script.valid

/**
*
* @author ronen
*/
class InParamValidationTest {

public static void main(String[] args) {
def bindings = [lastDay:2]
def script = """
import java.util.Calendar

Calendar cal = Calendar.instance
cal.set(2009,Calendar.FEBRUARY,2)
def ghday = cal.time
cal.set(2009,Calendar.MARCH,lastDay)
def spring = cal.time
def days = (ghday..spring).size()
println days
"""
def validator = new GroovyScriptValidator()
def suspects = validator.validate(script,bindings)
suspects.each{println it.text}
if(suspects.size()==0){
def shell = new GroovyShell(new Binding(bindings))
shell.evaluate(script)
}
}
}

AST introspection is one cool feature to have!

No comments: