How to implement a Groovy global AST transformation in a Grails plugin?
Asked Answered
L

1

4

I'd like to modify some of my Grails domain classes at compilation time. I initially thought this was a job for Groovy's global ASTTransformation since I don't want to annotate my domain classes (which local transformers require). What's the best way to do this?

I also tried mimicking DefaultGrailsDomainClassInjector.java by creating my own class in the same package, implementing the same interfaces, but I probably just didn't know how to package it up in the right place because I never saw my methods get invoked.

On the other hand I was able to manually create a JAR which contained a compiled AST transformation class, along with the META-INF/services artifacts that plain Groovy global transformations require. I threw that JAR into my project's "lib" dir and visit() was successfully invoked. Obviously this was a sloppy job because I am hoping to have the source code of my AST transformation in a Grails plugin and not require a separate JAR artifact if I don't have to, plus I couldn't get this approach to work by having the JAR in my Grails plugin's "lib" but had to put it into the Grails app's "lib" instead.

This post helped a bit too: Grails 2.1.1 - How to develop a plugin with an AstTransformer?

Lykins answered 24/11, 2014 at 22:22 Comment(0)
B
7

The thing about global transforms the transform code should be available when the compilation starts. Having the transformer in a jar was what i did first! But as you said it is a sloppy job. What you want to do is have your ast transforming class compile before others gets to the compilation phase. Here is what you do!

Preparing the transformer

Create a directory called precompiled in src folder! and add the Transformation class and the classes (such as annotations) the transformer uses in this directory with the correct packaging structure.

Then create a file called org.codehaus.groovy.transform.ASTTransformation in called precompiled/META-INF/services and you will have the following structure.

precompiled
--amanu
----LoggingASTTransformation.groovy
--META-INF
----services
------org.codehaus.groovy.transform.ASTTransformation

Then write the fully qualified name of the transformer in the org.codehaus.groovy.transform.ASTTransformation file, for the example above the fully qualified name would be amanu.LoggingASTTransformation

Implementation

package amanu 

import org.codehaus.groovy.transform.GroovyASTTransformation 
import org.codehaus.groovy.transform.ASTTransformation 
import org.codehaus.groovy.control.CompilePhase 
import org.codehaus.groovy.ast.ASTNode 
import org.codehaus.groovy.control.SourceUnit 

@GroovyASTTransformation(phase=CompilePhase.CANONICALIZATION) 
class TeamDomainASTTransformation implements ASTTransformation{ 

   public void visit(ASTNode[] nodes, SourceUnit sourceUnit) { 
       println ("*********************** VISIT ************")
       source.getAST()?.getClasses()?.each { classNode -> 
          //Class node is a class that is contained in the file being compiled
          classNode.addProperty("filed", ClassNode.ACC_PUBLIC, new ClassNode(Class.forName("java.lang.String")), null, null, null)
       }
   } 
} 

Compilation

After implementing this you can go off in two ways! The first approach is to put it in a jar, like you did! and the other is to use a groovy script to compile it before others. To do this in grails, we use _Events.groovy script.

You can do this from a plugin or the main project, it doesn't matter. If it doesn't exist create a file called _Events.groovy and add the following content.

The code is copied from reinhard-seiler.blogspot.com with modifications

eventCompileStart = {target ->  
    ...
    compileAST(pluginBasedir, classesDirPath)  
    ...
  }  
  def compileAST(def srcBaseDir, def destDir) {  
   ant.sequential {  
      echo "Precompiling AST Transformations ..."  
      echo "src ${srcBaseDir} ${destDir}"  
      path id: "grails.compile.classpath", compileClasspath  
      def classpathId = "grails.compile.classpath"  
      mkdir dir: destDir  
      groovyc(destdir: destDir,  
          srcDir: "$srcBaseDir/src/precompiled",  
          classpathref: classpathId,  
          stacktrace: "yes",  
          encoding: "UTF-8")
     copy(toDir:"$destDir/META-INF"){
        fileset(dir:"$srcBaseDir/src/precompiled/META-INF")
     }  
      echo "done precompiling AST Transformations"  
    }  
}  

the previous script will compile the transformer before others are compiled! This enable the transformer to be available for transforming your domain classes.

Don't forget

If you use any class other than those added in your classpath, you will have to precompile those too. The above script will compile everything in the precompiled directory and you can also add classes that don't need ast, but are needed for it in that directory!

If you want to use domain classes in transformation, You might want to do the precompilation in evenCompileEnd block! But this will make things slower!

Update

@Douglas Mendes mentioned there is a simple way to cause pre compilation. Which is more concise.

eventCompileStart = { 
   target -> projectCompiler.srcDirectories.add(0, "./src/precompiled") 
}
Brosy answered 2/1, 2015 at 7:26 Comment(2)
There's a simpler way to compile the AST source folder first also using the _Events.groovy: eventCompileStart = { target -> projectCompiler.srcDirectories.add(0, "./src/precompiled") }Darreldarrell
The link is dead, alive one: blog.rseiler.at/2011/09/…Alverta

© 2022 - 2024 — McMap. All rights reserved.