Using Groovy AST to Add Common Properties to Grails Domain Classes

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;

import org.codehaus.groovy.transform.GroovyASTTransformationClass;
import java.lang.annotation.*;

@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.

5 thoughts on “Using Groovy AST to Add Common Properties to Grails Domain Classes”

  1. Hi
    love the article… finally understand a little more AST…
    i am trying todo what you do but i am getting an:

    Internal compiler error: java.lang.NullPointerException at
    org.codehaus.groovy.transform.ASTTransformationVisitor.visitClass(ASTTransformationVisitor.java:159)

    when i annotate my class

    i tried it in IntelliJ and STS… both have the same result…

    Any help would be appriciated

  2. @Chris,
    AST Transformations are really hard to debug since they’re done at compile time. I’ve seen that issue myself when I didn’t have things quite right. I’d try using some println statements and catching runtime exceptions (and then printing out the results) to figure out what’s happening. Other than that, it’s a bit hard to say without seeing more code…

  3. well the thing is that i dont even have anything in the ast transformation class… but as soon as i just add the annoation

    @GroovyASTTransformationClass({ “com..util.ast.AddDateASTTransformation” })

    it brings me the error… seems like there maybe an issue with groovy or something…

    maybe you have any tricks how to reset the groovy compilation? or do you maybe have an example project that i could try?

    this is what my annotation looks like:

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;

    import org.codehaus.groovy.transform.GroovyASTTransformationClass;

    @Target({ ElementType.TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @GroovyASTTransformationClass({ “com.util.ast.AddDateASTTransformation” })
    public @interface AddDate {
    }

    thanks for any help

    Chris

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>