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 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.
* server1 {
* url = 'http://server1'
* nodes { // This is the nodes() method we define here.
* port = 9000
* }
* }
*/
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 {
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:
* deployments {
* server1 {
* url = 'http://server'
* nodes {
* node1 {
* port = 9000
* }
* }
* }
* }
*/
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 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.