Using Groovy AST to Add Common Properties to Grails Domain Classes

November 9, 2011 - 4 minute read -
groovy meta-programming grails

Groovy offers a lot of runtime meta-programming capabilities that allow you to add reusable functionality in a shared fashion. Grails plugins make use of this ability to enhance your project. One of the things that you can't do with runtime meta-programming in Grails is to add persistent Hibernate properties to your domain classes. If you want to add a persistent property in a plugin (or otherwise using meta-programming) for your Grails project you have to make use of "compile-time" meta-programming. In Groovy this is done with AST Transformations.

(If you are unfamiliar with the concept of the Abstract Syntax Tree, see the Wikipedia article on Abstract Syntax Tree.)

AST Transformations are made up of two parts: (1) An annotation and (2) an ASTTransformation implementation. During compilation the Groovy compiler finds all of the Annotations and calls the ASTTransformation implementation for the annotation passing in information about.

To create your own Transformation you start by creating an Annotation. The key to the annotation working is that your annotation has to itself be annotated with @GroovyASTTransformationClass. The values passed to the GroovyASTTransformationClass define the Transformation that will be called on classes, methods or other code prior to it being compiled.

Example Annotation

package net.zorched.grails.effectivity;</p>
<p>import org.codehaus.groovy.transform.GroovyASTTransformationClass;
import java.lang.annotation.*;</p>
<p>@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@GroovyASTTransformationClass({"net.zorched.grails.effectivity.EffectivizeASTTransformation"})
public @interface Effectivize {
}

Notice the reference to net.zorched.grails.effectivity.EffectivizeASTTransformation. That's the important part because it defines the class that will be used to perform the transformation.

Example Transformation

package net.zorched.grails.effectivity;
import org.codehaus.groovy.ast.*;
import org.codehaus.groovy.ast.builder.AstBuilder;
import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.stmt.*;
import org.codehaus.groovy.control.*;
import org.codehaus.groovy.transform.*;
import java.util.*;
import static org.springframework.asm.Opcodes.*;
@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
public class EffectivizeASTTransformation implements ASTTransformation {
      // This is the main method to implement from ASTTransformation that is called by the compiler
    public void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
        if (null == nodes) return;
        if (null == nodes[0]) return;
        if (null == nodes[1]) return;
        if (!(nodes[0] instanceof AnnotationNode)) return;
          ClassNode cNode = (ClassNode) nodes[1];
        addProperty(cNode, "effectiveStart", Date.class, createGenerateStartMethodCall())
        addProperty(cNode, "effectiveEnd", Date.class, createGenerateEndMethodCall())
      }
      // This method returns an expression that is used to initialize the newly created property
    private Expression createGenerateStartMethodCall() {
        return new ConstructorCallExpression(new ClassNode(Date.class), ArgumentListExpression.EMPTY_ARGUMENTS);
    }
      private Expression createGenerateEndMethodCall() {
        return new MethodCallExpression(
                new ConstructorCallExpression(new ClassNode(Date.class), ArgumentListExpression.EMPTY_ARGUMENTS),
                "parse",
                new ArgumentListExpression(new ConstantExpression("yyyy/MM/dd"), new ConstantExpression("2099/12/31")));
    }
      // This method adds a new property to the class. Groovy automatically handles adding the getters and setters so you
    // don't have to create special methods for those
    private void addProperty(ClassNode cNode, String propertyName, Class propertyType, Expression initialValue) {
        FieldNode field = new FieldNode(
                propertyName,
                ACC_PRIVATE,
                new ClassNode(propertyType),
                new ClassNode(cNode.getClass()),
                initialValue
        );
          cNode.addProperty(new PropertyNode(field, ACC_PUBLIC, null, null));
    }
}

This example code gets called for each annotated class and adds two new Date properties called effectiveStart and effectiveEnd to it. Those properties are seen by Grails and Hibernate and will become persistent and behave the same as if you typed them directly in your Domain.

It's a lot of work to add a simple property to a class, but if you're looking to consistently add properties and constraints across many Grails Domain classes, this is the way to do it.