Easy configuration usage with ConfigSlurper

What's the problem?

We have to deal with properties in almost every projects that we write. Properties class, which we use in these cases, is just mapping key to value. Sometimes it is fine, but in many cases properties look like tree. Example of properties file is shown below:
systemName=test
endpoint.first.protocol=http
endpoint.first.address=localhost
endpoint.first.port=8080
endpoint.first.path=test
endpoint.second.protocol=ftp
endpoint.second.address=localhost
endpoint.second.port=21
endpoint.second.user=admin
endpoint.second.password=pass
Here we have simple properties like systemName and also complex endpoints definition (all properties which start with endpoint) and single endpoints definition (each endpoint properties starts with endpoint.<ENDPOINT_NAME>). How simple could it be to treat this properties like a tree and simply extract subset of them? The answer is using ConfigSlurper.

ConfigSlurper from properties

To use ConfigSlurper just parse properties object:
def 'should import configuration from properties'() {
    given:
        Properties p = new Properties()
        p.load(ConfigSlurperTest.getResourceAsStream('/configuration.properties'))
    expect:
        new ConfigSlurper().parse(p).systemName as String == 'test'
}
Parse method returns ConfigObject which is just very clever map Map. Now you could get property using dot notation:
def 'should get nested property'() {
    expect:
        fromProperties.endpoint.first.protocol == 'http'
}
But there is a deal. If you use ConfigObject then you cannot use it like normal Properties and get property with dots.
def 'should not used nested property as one string'() {
    expect:
        fromProperties.'endpoint.first.protocol' != 'http'
}
ConfigObject allows you to extract subtree as Properties:
def 'should get first endpoint info from properties'() {
    expect:
        fromProperties.endpoint.first.toProperties() == [
            protocol: 'http',
            address : 'localhost',
            port    : '8080',
            path    : 'test'
        ]
}
and even:
def 'should allow for nested property as one string when toProperties called'() {
    expect:
        fromProperties.endpoint.toProperties()['first.protocol'] == 'http'
}
If you want to know how many endpoint you have and how they are named you could use keySet method:
def 'should get list of endpoints'() {
    expect:
        fromProperties.endpoint.keySet() == ['first', 'second'] as Set
}
ConfigSlurper do not return null even if property is not found, so you could get nested property without fear:
def 'should not throw exception when missing property'() {
    expect:
        fromProperties.endpoint.third.port.toProperties() == [:] as Properties
}
You have only to be careful, when have property named like begining of another property:
def 'should throw exception when asking for too nested property'() {
    when:
        fromProperties.endpoint.first.port.test
    then:
        thrown(MissingPropertyException)
}
fromProperties.endpoint.first.port returns String and do not have test property. You could also print properties from ConfigObject:
println fromProperties.prettyPrint()
The output looks like this:
endpoint {
    first {
        path='test'
        port='8080'
        protocol='http'
        address='localhost'
    }
    second {
        password='pass'
        protocol='ftp'
        address='localhost'
        port='21'
        user='admin'
    }
}
systemName='test'
Hmm... It looks like DSL. Why do not keep your configuration in this manner?

ConfigSlurper from script

Your configuration could be a groovy script.
systemName = 'test'
endpoint {
    first {
        path = 'test'
        port = 8080
        protocol = 'http'
        address = 'localhost'
    }
    second {
        password = 'pass'
        protocol = 'ftp'
        address = 'localhost'
        port = 21
        user = 'admin'
    }
}
test.key = ['really': 'nested?'] as Properties
You could pass such configuration as resource stream or file content:
def 'should get config from script as url'() {
    given:
        ConfigObject config = new ConfigSlurper().parse(ConfigSlurperTest.getResource('/configuration.groovy'))
    expect:
        config.systemName == 'test'
}

def 'should get config from script as string'() {
    given:
        ConfigObject config = new ConfigSlurper().parse(ConfigSlurperTest.getResource('/configuration.groovy').text)
    expect:
        config.systemName == 'test'
}
What interesting all your properties do not have to be strings. It could be any object: String, long, int, etc.
def 'should get nested properties from script as int'() {
    expect:
        fromScript.endpoint.first.port == 8080
}

def 'should get really nested properties from script and continue digging'() {
    expect:
        fromScript.test.key.really == 'nested?'
}

Conclusion

You could deal with properties like simple Map, but why if you could instead use it like tree of properties? Sources are available here.

3 thoughts on “Easy configuration usage with ConfigSlurper”

  1. environments {
    pub{
    mtp.defaultDs=’mtp_infrastructure_dataSourceJNDI’
    mtp.logs.defaultDs=’mtp_logs_infrastructure_dataSourceJNDI’
    mtp.dingtalk.propertiesFilePath=’/data/dingtalk/dingtalk.properties’
    log4j.rootLogger=’ERROR,CONSOLE,R’
    log4j.logger.org.springframework=’ERROR’
    log4j.logger.com.kungeek.mtp.api=’ERROR’
    log4j.logger.org.apache.ibatis=’ERROR’
    log4j.logger.org.mybatis=’ERROR’
    log4j.logger.java.sql=’ERROR’
    //log4j.logger.com.kungeek.mtp.api.infrastructure.service.FileService=’ERROR’
    mtp.api.version=’${PRODUCT_VERSION}’
    mtp.api.versionTime=’${BUILD_TIME}’
    }
    }

  2. environments {
    pub{
    mtp.defaultDs=’mtp_infrastructure_dataSourceJNDI’
    mtp.logs.defaultDs=’mtp_logs_infrastructure_dataSourceJNDI’
    mtp.dingtalk.propertiesFilePath=’/data/dingtalk/dingtalk.properties’
    log4j.rootLogger=’ERROR,CONSOLE,R’
    log4j.logger.org.springframework=’ERROR’
    log4j.logger.com.kungeek.mtp.api=’ERROR’
    log4j.logger.org.apache.ibatis=’ERROR’
    log4j.logger.org.mybatis=’ERROR’
    log4j.logger.java.sql=’ERROR’
    }
    }

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.