DIY Gradle Plugin Development

Plugging into Gradle Plugins - Part 3

 

Tasks

The apply() method shown above calls a method called applyTasks(). Let’s take a look at what this method does (listing 3). This example creates three new build tasks: update, up- dateSQL, and dbDoc (In Liquibase, update pushes new schema changes into a database, updateSQL performs a dry run of an update, and dbDoc builds JavaDoc-like documentation of a schema.). Since the Liquibase plugin is written in Groovy, we’re able to use a very Gradle-like syntax to declare new tasks; indeed, the code shown here would work verbatim inside a build file, apart from any plugin definition. Build masters don’t have to write plugins in Groovy, but it’s a rewarding choice due to its similarity to the Gradle build file syntax and its other advantages over Java as a language.

Listing 3

 

void applyTasks(Project project) {        [ 'update', 'updateSQL' ].each { taskName ->                 project.task(taskName, type: LiquibaseBaseTask) {                       group = 'Liquibase' 
                   command = taskName              }       }       project.task('dbDoc', type: LiquibaseDbDocTask) {               command = 'dbDoc' 
             group = 'Liquibase' 
           docDir = project.file("${project.buildDir}/database/docs")      }  }

 

Note in the dbDoc definition that a task property is set to a value derived from an existing Gradle Project property. This underscores the fact that the plugin is interacting with the build not just by adding functionality to it, but by examining its state as well. There is no practical limitation to the ways in which a plugin might interact with the project object.

All three of the newly created tasks have custom task types. For the sake of brevity, we’ll examine just one of these: LibasebaseBaseTask. A custom task type is simply a class that implements the org.gradle.api.Task interface, or more commonly extends the org.gradle.api.DefaultTask base class. The LiquibaseBaseTask provides a basic interface between the rest of the build and the core action of executing the Liquibase command-line driver through which all Liquibase operations are normally accessed. The properties of the LiquibaseBaseTask will become task configuration parameters when the plugin tasks are used in an actual build later on. To keep individual task configuration short and simple, the task code looks to the project object for configuration settings a build master might set globally (these are the workingDatabase and changelogs.main parameters). The code is shown in listing 4.

Listing 4

 

import org.gradle.api.DefaultTask  class LiquibaseBaseTask extends DefaultTask {          Database database 
     def changeLogFile 
     def command     def options = []        @TaskAction 
   def liquibaseAction() {                 // Default configuration from a convention variable             if(database == null) { 
                        database = project.workingDatabase              }               // Default configuration from a convention domain object collection             if(changeLogFile == null) { 
                   changeLogFile = project.changelogs.main.file            }               // process configuration inputs                 ...             // invoke Liquibase command-line driver                 ...     } }

 

All of this functionality can be incorporated into a build merely by applying the plugin. The tasks appear in the build for free, potentially with no further coding or configuration at all.

Conventions

Plugin conventions have a less-than-intuitive name, but are a very simple concept. In short, a convention is a class whose properties and methods are mixed in to the Project object of the build to which the plugin is applied. The class is usually a POJO, or in the case of our Groovy-authored plugin, a POGO. If you want a plugin to create a new domain object collection (like SourceSets, as described above) or a new project property (like groupId, defined by the Maven plugin), conventions are they way to go. You’d simply create a convention object – a POJO or POGO – that has those properties, and the Gradle mixes them into the project when the plugin is applied. You can also add globally available methods to the Project object in this same way. A simplified version of the Liquibase convention object is shown in listing 5.

Listing 5

 

class LiquibaseDatabaseConvention {
        final NamedDomainObjectContainer<Database> databases 
        final NamedDomainObjectContainer<ChangeLog> changelogs 
        Database workingDatabase

        def databases(Closure closure) { 
                databases.configure(closure)

        }
        def changelogs(Closure closure) {
                 changelogs.configure(closure)
        }
}

Based on what we’ve learned about conventions already, the purpose of the workingDatabase property is clear enough: it will become a top-level project property to be set in a build file outside of a task definition. We’ll see an example of this shortly. But first, we have to explore the NamedDomainObjectContainer class used as the type of the databases and changelogs properties. These also become top-level project properties to be set in the plugin users’s build file, but their structure is somewhat richer than a simple string. In fact, it is at this point that we first begin to see the real power of plugins as a means of extending the Gradle DSL to encompass entities in our domain that were not – and could not be – forseen by the developers of Gradle.

Since Liquibase is a database refactoring tool, it must connect to a database to do its work. As a feature of its implementation, it stores an ordered collection of database refactorings in a file called a ChangeLog. A build that wants to use Liquibase to manage database change might have a collection of databases it manages (e.g., development, staging, QA, production, etc.), and it might choose to organize its database changes into several ChangeLog files. Hence the build model must expand to describe these objects, and it must describe them in a way that makes sense in the context of the expanded build domain. It must model a collection of database and a collection of ChangeLogs. The NamedDomainObjectContainer class lets us do just that.

The best-known example of a domain object collection in Liquibase is the SourceSet.

As a plugin author, you can create collections of whatever domain objects exist in the scope of the plugin’s work. This is no mere feature of the plugin API. The ability to introduce new domain objects and their aggregations is essential to the spirit of Gradle as a tookit for creating build standards. Gra- dle itself knows nothing about databases, or how they might change, or how you might prefer to manage their change. Gradle with the Liquibase plugin has a robust definition of all three concepts.

 

Pages

Tim Berglund
Tim Berglund

What do you think?

JAX Magazine - 2014 - 03 Exclucively for iPad users JAX Magazine on Android

Comments

Latest opinions