Saturday, April 9, 2011

Gradle, Groovy, and MethodMissing

I've created a few of these, but I think my latest is the cleanest implementation I've done yet. I've created a new ProjectTemplate builder for laying out new projects from scratch. Why, you ask? Because I seem to be always creating new projects and importing them into my IDE. Or, I use the IDE to create them and then wind up with a bunch of IDE files stuck in source control. Personally, I prefer the former.

Normally creating the initial project structure isn't that big of a deal, it's just a few simple *nix commands really:
mkdir -p MyProject/src/
cd MyProject/src
mkdir -p main/java main/resources test/java test/resources

I could just create a small bash script to handle this for me, but I'd have to change it around all the time to handle the project name, or if I wanted to use Groovy instead of Java, etc... I could also just make the bash script handle user input, but I'd still want to set the project up with some sort of build system, and it'd be cool if the build system could do this for me.

As I've been a fan of Gradle for a while now, I wanted to see if I could do something to get Gradle to do this for me. I know Maven can do this already with archetypes, but as I said, I wanted Gradle to be able to do this for me.

Without getting into the Gradle bit too much just yet, because I want to do that properly, and it's a whole other ball of wax, I'll focus on the Groovy bits first.

So, the goal is to have a small-ish Gradle build script that'll let me create a directory structure for a new Groovy project. A pretty standard directory structure looks like this:
[project root]/
   src/
      main/
         groovy/
         resources/
      test/
         groovy/
         resources/
Let's start with a basic Gradle build script: build.gradle
task "create-groovy-project" << {
   String projectName = System.console().readLine("> Project Name: ")
   println "Creating directory structure for new Groovy project: ${projectName}"
}

Running 'gradle create-groovy-project' now results in us being asked for our new project's name, and then it being echoed back at us.

Now for a little Groovy fun. Here's a shortened version of my ProjectTemplate class. This should just be pasted into the top of your build.gradle file.
class ProjectTemplate {
   
   private File parent
   
   private ProjectTemplate() {}
   
   static void root(String path, Closure closure) {
      new ProjectTemplate().d(path, closure)
   }

   void d(String name, Closure closure = {}) {
      File oldParent = parent
      if (parent) {
         parent = new File(parent, name)
      } else {
         parent = new File(name)
      }
      parent.mkdirs()
      closure.delegate = this
      closure()
      parent = oldParent
   }
   void f(Map args = [:], String name) {
      File file
      if (parent) {
         file = new File(parent, name)
      } else {
         file = new File(name)
      }
      file.exists() ?: file.createNewFile()
      if (args.content) {
         def content = args.content.stripIndent()
         if (args.append) {
            file.append(content)
         } else {
            file.text = content
         }
      }
   }
}
Nothing too spiffy about it. You use it by calling the static 'root' method and then declaring your directories and files. Let's add it to our 'create-groovy-project' task:
task "create-groovy-project" << {
   String projectName = System.console().readLine("> Project Name: ")
   println "Creating directory structure for new Groovy project: ${projectName}"
   ProjectTemplate.root(projectName) {
      d("src") {
         d("main") {
            d("groovy")
            d("resources")
         }
         d("test") {
            d("groovy")
            d("resources")
         }
      }
      f("LICENSE.txt", content: "// LICENSE GOES HERE")
      f("README.txt", content: "// What is this project all about, and what's needed to build it?")
   }
}
Running 'gradle create-groovy-project' now, and giving the name "MyProject" results in a new directory called "MyProject" which contains a directory structure like:
MyProject/
   src/
      main/
         groovy/
         resources/
      test/
         groovy/
         resources/
   LICENSE.txt
   README.txt
Ok, so far, so good. We can now create basic directory structures for a new Groovy project, and it wouldn't be too hard at all to create an 'create-java-project', or 'create-scala-project' task now to create directory structures for those types of projects.

But the ProjectTemplate usage is still a little noisy for me. We can get rid of the parenthesis, but it's still not enough.

Let's add a methodMissing method to the ProjectTemplate class to make things cleaner:
def methodMissing(String name, def args) {
      if (args) {
         def arg = args[0]
         if (arg instanceof Closure) {
            d(name, arg)
         } else if (arg instanceof Map) {
            f(arg, name)
         } else if (arg instanceof String || arg instanceof GString) {
            f([content: arg], name)
         } else {
            println "Couldn't figure out what to do. name: ${name}, arg: ${arg}, type: ${arg.getClass()}"
         }
      }
   }
What does this buy us? Well, now we don't need to specify the method name anymore. That's right kiddies, no more d's or f's. :)

We can change our usage to this now:
ProjectTemplate.root(projectName) {
      "src" {
         "main" {
            "groovy" {}
            "resources" {}
         }
         "test" {
            "groovy" {}
            "resources" {}
         }
      }
      "LICENSE.txt" "// LICENSE GOES HERE"
      "README.txt" "// What is this project all about, and what's needed to build it?"
   }
This is a lot cleaner, and easier to read to me.

I am actually working on a Gradle plugin which I hope to make available very soon, but for now It's hosted at LaunchPad

You can download the source using Bazaar:
bzr branch lp:gradle-templates
I've uploaded the complete build.gradle file on TellurianRing.com (1).

I'll blog more about my Gradle plugin endeavors as they happen.

Cheers,
Eric

(1) build.gradle (right click and save as)