Enonic Resource Tool: Building a custom gradle plugin

Posted by


For some time, people working with Enonic CMS sites have asked for a easier way of handling resources (xslt, css, js etc) using a Version Control System, and across multiple installations. As an example, it would be nice to be able to check out resources from a VCS to my local file system, edit files, push changes to a server and then commit them back into the VCS. The ert-gradle-plugin is an attempt to ease this and other tasks.

The result of this labs-project is available on GitHub

What and how

First, I drafted a few requirements:

  • Backup an installation
  • Copy selected or all resources between installations, and between e.g local filesystem and an installation.
  • See diff and synchronize between installations
  • Let the user easily define tasks, e.g "diff test prod"

Early in the process, I decided to use Gradle. This would enable the users to easily build the custom task.
The resources in Enonic CMS are manipulated through WebDAV, so I needed way do this communication. I decided to create a custom gradle plugin with a java client using a simple, lightweight WebDAV library. This turned out to be a lot more complicated than planned. Most open source WebDAV-projects where either too simple, too advanced or too dead. After trying several options, I decided to opt for the upcoming Apache VFS 2.0 which was still under development and depended on the Jackrabbit WebDAV Library.

There are then 3 separate parts in work to get the plugin to work:

  1. The client implementation (Java) - This is responsible for doing all the dirty work; copy, delete, create, diff, sync, etc.
  2. The custom plugin implementation (Groovy) - This is the link between the Java client and Gradle
  3. The build-script using the plugin - This is where the user can define resource locations, tasks etc.

1 - The Java client

The Java part of the plugin implements a "Resource Client", which are actually doing the copying files and folders, creating diffs and keeping two locations in sync.

Based on a user given location URL, Apache VFS is used to locate a org.apache.commons.vfs2.FileObject, which is an abstract file-representation for a resource or a folder across different file-systems. This is nice since I get a lot of filesystems out of the box. I wont go into details about how this is implemented, everything is available on GitHub The main consideration, it that the Resource Client implements the methods needed by the plugin.

2 - The Custom plugin

The plugin should enable the user to invoke the client to do different tasks. This part was written in Groovy, since this is the most straight forward approach inheriting the way of doing things from Gradle.

To create a custom plugin, you need to write an implementation of org.gradle.api.Plugin. This interface implements one method; apply - which applies the plugin to the given target (e.g you build-script project)

To pass information from the gradle-script to your plugin, "Conventions" from the Gradle plugin API is at you disposal. I defined some simple properties; cache-url and backup-url in my main build.gradle. These will enable the user to defined where to put backups, and also where to cache the state of a remote server locally:

ert {
    cacheUrl = "file://Users/rmy/Dev/Workspace/Java/Enonic/git/ert-runner/.cache"
    backupUrl = "file://Users/rmy/Dev/Workspace/Java/Enonic/git/ert-runner/.backup"

These options are grouped inside the ert-closure, and could be fetched defining a "PropertyConvention":

When you start a gradle script, you will be passed a "Project" object, and than object has a Convention object that holds a Map with all defined project conventions. During the execution phase, Gradle will look for Plugin conventions that accepts the properties defined in the build script. So, the first line adds the PropertyConvention to the Map of conventions, and the PropertyConvention class accepts the parameters "cacheUrl" and "backupUrl". More details about using Conventions can be found in this article
The resource locations should also be passed to the plugin, this is a bit more complex, but follows pretty much the same approach; A closure location with different locations and properties for each one:

locations {
    dev {
        url = "file://Users/rmy/Dev/Workspace/Java/Enonic/git/ert-runner/ert-local-data"
        excludes = excludeStatics
    test {
        url = "webdav://admin:password@vtnode1:8280/cms-commando-stable-packages/dav"
        cacheLocal = true
        excludes = excludeStatics
    prod {
        url = "webdav://user:password@prodserver:8180/myweb/dav"
        cacheLocal = true

The location url is a valid Apache VFS url, and there are also a number of other options.

Then, its time to define the tasks that the plugin implement for usage in the buildscript.

The ERTCopy task is extending the org.gradle.api.Default task, and the @TaskActions defines the method to execute when task is executed. The ResourceClient is created by the ResourceClientFactory, and the copyFile(String filename) is invoked on the client.

Development setup creating custom gradle plugins

When creating gradle plugins, its most convenient to develop the plugin directly under the main project to avoid round-trips deploying the plugin to repository etc. This is easily achieved putting the plugin-code under a {{buildSrc}}-folder, and thus the plugin will be available for the main buildscript. So I use the following setup during the initial development:

  |_ buildSrc
        |_ build.gradle
        |_ src
            |_ main
                |_ groovy
                |… java

The topmost build.gradle file, is the file where tasks and resource locations should be defined.
The build.gradle file in the buildSrc is the build-file for the custom plugin.  

 3 - The user build script

The user build script is the way for the user to interact with the ResourceClient and execute task. Some of the configuration are already mentioned when we considered the custom plugin implementation, but here is a full example of a build script when the plugin is not locally present but fetch from the enonic repository:

The first line instructs the script to apply our custom made plugin. The next block is the buildscript declaration which defines the classpath needed to include external liberaries. In our case, we define the enonic public repository and defines a dependency to the ert-gradle-plugin. Then, some properties and then the locations is defined. All locations follow the The tasks then defines its type as one of the plugin defined task-types.

For documentation on how to configure and use the ert-gradle-plugin, see ert-gradle-plugin at GitHub