Loading...

June 27, 2016

Ratpacked: Using Groovy Configuration Scripts As Configuration Source

Ratpack has a lot of options to add configuration data to our application. We can use for example YAML and JSON files, properties, environment variables and Java system properties. Groovy has the ConfigSlurper class to parse Groovy script with configuration data. It even supports an environments block to set configuration value for a specific environment. If we want to support Groovy scripts as configuration definition we write a class that implements the ratpack.config.ConfigSource interface.

We create a new class ConfigSlurperConfigSource and implement the ConfigSource interface. We must implement the loadConfigData method in which we read the Groovy configuration and transform it to a ObjectNode so Ratpack can use it:

// File: src/main/groovy/mrhaki/ratpack/config/ConfigSlurperConfigSource.groovy
package mrhaki.ratpack.config

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ArrayNode
import com.fasterxml.jackson.databind.node.ObjectNode
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import ratpack.config.ConfigSource
import ratpack.file.FileSystemBinding

import java.nio.file.Path

@CompileStatic
class ConfigSlurperConfigSource implements ConfigSource {
    
    private final String configScript
    
    private final URL scriptUrl
    
    private final String environment
    
    ConfigSlurperConfigSource(final String configScript) {
        this(configScript, '')
    }

    ConfigSlurperConfigSource(final String configScript, final String environment) {
        this.configScript = configScript
        this.environment = environment
    }

    ConfigSlurperConfigSource(final Path configPath) {
        this(configPath, '')
    }

    ConfigSlurperConfigSource(final Path configPath, final String environment) {
        this(configPath.toUri(), environment)
    }

    ConfigSlurperConfigSource(final URI configUri) {
        this(configUri, '')
    }

    ConfigSlurperConfigSource(final URI configUri, final String environment) {
        this(configUri.toURL(), environment)
    }

    ConfigSlurperConfigSource(final URL configUrl) {
        this(configUrl, '')
    }

    ConfigSlurperConfigSource(final URL configUrl, final String environment) {
        this.scriptUrl = configUrl
        this.environment = environment
    }

    @Override
    ObjectNode loadConfigData(
            final ObjectMapper objectMapper, 
            final FileSystemBinding fileSystemBinding) throws Exception {

        // Create ConfigSlurper for given environment.
        final ConfigSlurper configSlurper = new ConfigSlurper(environment)

        // Read configuration.
        final ConfigObject configObject = 
                configScript ? 
                        configSlurper.parse(configScript) : 
                        configSlurper.parse(scriptUrl)
        
        // Transform configuration to node tree
        final ObjectNode rootNode = objectMapper.createObjectNode()
        populate(rootNode, configObject)
        return rootNode
    }

    @CompileDynamic
    private populate(final ObjectNode node, final ConfigObject config) {
        // Loop through configuration.
        // ConfigObject also implements Map interface,
        // so we can loop through key/value pairs.
        config.each { key, value ->
            // Value is another configuration,
            // this means the nested configuration
            // block.
            if (value instanceof Map) {
                populate(node.putObject(key), value)
            } else {
                // If value is a List we convert it to
                // an array node.
                if (value instanceof List) {
                    final ArrayNode listNode = node.putArray(key)
                    value.each { listValue ->
                        listNode.add(listValue)
                    }
                } else {
                    // Put key/value pair in node.
                    node.put(key, value)
                }
            }
        }
    }
}

We have several options to pass the Groovy configuration to the ConfigSlurperConfigSource class. We can use a String, URI, URL or Path reference. Let's create a file with some configuration data.

// File: src/ratpack/application.groovy
app {
    serverPort = 9000
}

environments {
    development {
        app {
            serverName = 'local'
        }
    }
    production {
        app {
            serverName = 'cloud'
            serverPort = 80
        }
    }
}

Next we create a Ratpack application using the Groovy DSL. In the serverConfig section we use our new ConfigSlurperConfigSource:

// File: src/ratpack/ratpack.groovy
import com.google.common.io.Resources
import com.mrhaki.config.ConfigSlurperConfigSource

import static groovy.json.JsonOutput.prettyPrint
import static groovy.json.JsonOutput.toJson
import static ratpack.groovy.Groovy.ratpack

//final Logger log = LoggerFactory.getLogger('ratpack')

ratpack {

    serverConfig {
        // Use Groovy configuration.
        add new ConfigSlurperConfigSource('''\
            app {
                message = 'Ratpack swings!'
            }''')

        // Use external Groovy configuration script file.
        add new ConfigSlurperConfigSource(
                Resources.getResource('application.groovy'), 'development')

        require '/app', SimpleConfig
    }

    handlers {
        get('configprops') { SimpleConfig config ->
            render(prettyPrint(toJson(config)))
        }
    }

}

// Simple configuration.
class SimpleConfig {
    String message
    String serverName
    Integer serverPort
}

Let's check the output of the configprops endpoint:

$ http -b localhost:5050/configprops
{
    "message": "Ratpack swings!",
    "serverName": "local",
    "serverPort": 9000
}

Now we set the environment to production in our Ratpack application:

// File: src/ratpack/ratpack.groovy
...

ratpack {

    serverConfig {
        ...

        // Use external Groovy configuration script file.
        add new ConfigSlurperConfigSource(
                Resources.getResource('application.groovy'), 'production')

       ...
    }

    ...
}

If we check configprops again we see different configuration values:

$ http -b localhost:5050/configprops
{
    "message": "Ratpack swings!",
    "serverName": "cloud",
    "serverPort": 80
}

Written with Ratpack 1.3.3.