Loading...

February 19, 2016

Gradle Goodness: Using Nested Domain Object Containers

In a previous post we learned how to use the NamedDomainObjectContainer class. We could create new objects using a nice DSL in Gradle. But what if we want to use DSL syntax to create objects within objects? We can use the same mechanism to achieve this by nesting NamedDomainObjectContainer objects.

We want to support the following DSL to create a collection of Server objects, where each server can have multiple Node objects:

// File: build.gradle
apply plugin: com.mrhaki.gradle.DeploymentPlugin

deployments {
    aws {
        url = 'http://aws.address'

        nodes {
            node1 {
                port = 9000
            }
            node2 {
                port = 80
            }
        }
    }

    cf {
        url = 'http://cf.address'

        nodes {
            test {
                port = 10001
            }
            acceptanceTest {
                port = 10002
            }
        }
    }
}

This would create two Server objects with then names aws and cf. Each server has Node objects with names like node1, node2, test and acceptanceTest. Let's look at the Server class where we have added a nodes property of type NamedDomainObjectContainer as the nested object container. Also notice the nodes method so we can use the DSL syntax to create Node objects.

// File: buildSrc/src/main/groovy/com/mrhaki/gradle/Server.groovy
package com.mrhaki.gradle

import org.gradle.api.NamedDomainObjectContainer

class Server {

    /**
     * An instance is created in the plugin class, because
     * there we have access to the container() method
     * of the Project object.
     */
    NamedDomainObjectContainer<Node> nodes

    String url

    String name

    /**
     * We need this constructor so Gradle can create an instance
     * from the DSL.
     */
    Server(String name) {
        this.name = name
    }

    /**
     * Inside the DSL this method is invoked. We use
     * the configure method of the NamedDomainObjectContainer to
     * automatically create Node instances.
     * Notice this is a method not a property assignment.
     * <pre>
     * server1 {
     *     url = 'http://server1'
     *     nodes { // This is the nodes() method we define here.
     *         port = 9000
     *     }
     * }
     * </pre>
     */
    def nodes(final Closure configureClosure) {
        nodes.configure(configureClosure)
    }

}

And the Node class:

// File: buildSrc/src/main/groovy/com/mrhaki/gradle/Node.groovy
package com.mrhaki.gradle

class Node {

    String name

    Integer port

    /**
      * We need this constructor so Gradle can create an instance
      * from the DSL.
      */
    Node(String name) {
        this.name = name
    }
}

To make the DSL work we use a custom plugin so we can add the DSL for creating the objects to our project:

// File: buildSrc/src/main/groovy/com/mrhaki/gradle/DeploymentPlugin.groovy
package com.mrhaki.gradle

import org.gradle.api.Project
import org.gradle.api.Plugin
import org.gradle.api.NamedDomainObjectContainer

class DeploymentPlugin implements Plugin<Project> {

    public static final String EXTENSION_NAME = 'deployments'

    private static final String DEPLOY_TASK_PATTERN = 'deployOn%sTo%s'

    private static final String REPORTING_TASK_NAME = 'reportDeployments'

    private static final String TASK_GROUP_NAME = 'Deployment'

    void apply(final Project project) {
        setupExtension(project)
        createDeploymentTasks(project)
        createReportTask(project)
    }

    /**
     * Create extension on the project for handling the deployments
     * definition DSL with servers and nodes. This allows the following DSL
     * in our build script:
     * <pre>
     * deployments {
     *     server1 {
     *         url = 'http://server'
     *         nodes {
     *             node1 {
     *                 port = 9000
     *             }
     *         }
     *     }
     * }
     * </pre>
     */
    private void setupExtension(final Project project) {

        // Create NamedDomainObjectContainer for Server objects.
        // We must use the container() method of the Project class
        // to create an instance. New Server instances are
        // automatically created, because we have String argument
        // constructor that will get the name we use in the DSL.
        final NamedDomainObjectContainer<Server> servers =
            project.container(Server)

        servers.all {
            // Here we have access to the project object, so we
            // can use the container() method to create a
            // NamedDomainObjectContainer for Node objects.
            nodes = project.container(Node)
        }

        // Use deployments as name in the build script to define
        // servers and nodes.
        project.extensions.add(EXTENSION_NAME, servers)
    }

    /**
     * Create a new deployment task for each node.
     */
    private void createDeploymentTasks(final Project project) {
        def servers = project.extensions.getByName(EXTENSION_NAME)

        servers.all {
            // Actual Server instance is the delegate
            // of this closure. We assign it to a variable
            // so we can use it again inside the
            // closure for nodes.all() method.
            def serverInfo = delegate

            nodes.all {
                // Assign this closure's delegate to
                // variable so we can use it in the task
                // configuration closure.
                def nodeInfo = delegate

                // Make node and server names pretty
                // for use in task name.
                def taskName =
                    String.format(
                        DEPLOY_TASK_PATTERN,
                        name.capitalize(),
                        serverInfo.name.capitalize())

                // Create new task for this node.
                project.task(taskName, type: DeploymentTask) {
                    description = "Deploy to '${nodeInfo.name}' on '${serverInfo.name}'"
                    group = TASK_GROUP_NAME

                    server = serverInfo
                    node = nodeInfo
                }
            }
        }
    }

    /**
     * Add reporting task to project.
     */
    private void createReportTask(final Project project) {
        project.task(REPORTING_TASK_NAME, type: DeploymentReportTask) {
            description = 'Show configuration of servers and nodes'
            group = TASK_GROUP_NAME
        }
    }
}

We also have two custom tasks that use the Server and Node instances that are created by the DSL in our build file. The DeploymentTask is configured from the plugin where the server and node properties are set:

// File: buildSrc/src/main/groovy/com/mrhaki/gradle/DeploymentTask.groovy
package com.mrhaki.gradle

import org.gradle.api.tasks.TaskAction
import org.gradle.api.DefaultTask

class DeploymentTask extends DefaultTask {

    Server server

    Node node

    /**
     * Simple implementation to show we can
     * access the Server and Node instances created
     * from the DSL.
     */
    @TaskAction
    void deploy() {
        println "Deploying to ${server.url}:${node.port}"
    }

}

The DeploymentReportTask references the project extensions to get a hold of the Server and Node objects:

// File: buildSrc/src/main/groovy/com/mrhaki/gradle/DeploymentReportTask.groovy
package com.mrhaki.gradle

import org.gradle.api.tasks.TaskAction
import org.gradle.api.DefaultTask

class DeploymentReportTask extends DefaultTask {

    /**
     * Simple task to show we can access the
     * Server and Node instances also via the
     * project extension.
     */
    @TaskAction
    void report() {
        def servers = project.extensions.getByName(DeploymentPlugin.EXTENSION_NAME)

        servers.all {
            println "Server '${name}' with url '${url}':"

            nodes.all {
                println "\tNode '${name}' using port ${port}"
            }
        }
    }

}

Let's run the tasks task first to see which tasks are added by the plugin. Next we invoke some tasks:

$ gradle -q tasks
...
Deployment tasks
----------------
deployOnAcceptanceTestToCf - Deploy to 'acceptanceTest' on 'cf'
deployOnNode1ToAws - Deploy to 'node1' on 'aws'
deployOnNode2ToAws - Deploy to 'node2' on 'aws'
deployOnTestToCf - Deploy to 'test' on 'cf'
reportDeployments - Show configuration of servers and nodes
...
$ gradle -q deployOnNode2ToAws

Deploying to http://aws.address:80
$ gradle -q reportDeployments

Server 'aws' with url 'http://aws.address':
 Node 'node1' using port 9000
 Node 'node2' using port 80
Server 'cf' with url 'http://cf.address':
 Node 'acceptanceTest' using port 10002
 Node 'test' using port 10001
$

Written with Gradle 2.11.