Showing posts with label properties. Show all posts
Showing posts with label properties. Show all posts

2015-09-20

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.

2014-01-13

Properties from maven

Introduction

Sometimes your code should work in many environments using varaibles defined in many places. There could be default variable in code (when configuration is not defined), variable in configuration file or pom.xml for tests and maybe each developer should have own variable value...

Property reader code

All examples will be tests and will be use PropertyReader class which is defined below.
package com.blogspot.przybyszd.sandbox.propertyplaceholders;

import java.util.Properties;

public class PropertyReader{
    
    public static Properties readProperties(final String configurationFile){
        Properties properties = new Properties();
        try{
            properties.load(PropertyReader.class.getResourceAsStream(configurationFile));
        }catch(Exception e){
            System.err.println("Configuration file was not found");
        }
        return properties;
    }   
}
All properties files will be defined in src/test/resources.

Default properties in code

First, assume that properties file is not provided or property is not given:
package com.blogspot.przybyszd.sandbox.propertyplaceholders;

import static org.junit.Assert.assertEquals;
import java.util.Properties;
import org.junit.Test;

public class PropertyReaderTest{
    
    @Test
    public void shouldUseDefaultPropertyFromCode(){
        // given
        Properties properties = PropertyReader.readProperties("/no-existing-file.properties");
        // when
        String value = properties.getProperty("address", "localhost");
        // then
        assertEquals("localhost", value);
    }
}
But when property file exists and has this property then it will be used:
@Test
public void shouldUsePropertyFromFile(){
    // given
    Properties properties = PropertyReader.readProperties("/existing-file.properties");
    // when
    String value = properties.getProperty("address", "localhost");
    // then
    assertEquals("10.10.10.10", value);
}
Property in file existing-file.properties:
address=10.10.10.10

Default properties in pom.xml

Properties could be also defined in pom.xml and then placeholders could be used in property file. Test is going first:
@Test
public void shouldUsePropertyFromPom(){
    // given
    Properties properties = PropertyReader.readProperties("/file-with-placeholder.properties");
    // when
    String value = properties.getProperty("address", "localhost");
    // then
    assertEquals("trololo.com", value);
}
Instead of value in properties file there is only placeholder (file file-with-placeholder.properties):
address=${address}

Value of property is defined in pom.xml (filtering of test resources must be set to true and, optionally, resource filtering could be set to true):
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <groupId>property-placeholders</groupId>
 <artifactId>property-placeholders</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <name>PropertyPlaceholder</name>

 <properties>
  <address>google.com</address>
 </properties>

 <build>
  <resources>
   <resource>
    <directory>src/main/resources</directory>
    <filtering>true</filtering>
   </resource>
  </resources>
  <testResources>
   <testResource>
    <directory>src/test/resources</directory>
    <filtering>true</filtering>
   </testResource>
  </testResources>
 </build>

 <dependencies>
  <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>4.11</version>
   <scope>test</scope>
  </dependency>
 </dependencies>
</project>
Now variable could be also set from command line:
mvn -Daddress=my-address clean test

Default property in settings.xml

Each developer (or each environment) could define some properties in settings.xml in $HOME/.m2 directory and these properties overwrite properties defined in pom.xml.
Test first:
@Test
public void shouldUsePropertyFromSettingsXMl(){
    // given
    Properties properties = PropertyReader.readProperties("/file-with-placeholder.properties");
    // when
    String value = properties.getProperty("address2", "localhost");
    // then
    assertEquals("yahoo.com", value);
}
I add new property address2 so I have to add property placeholder in properties file and in pom.xml.
Properties file:
address=${address}
address2=${address2}
pom.xml:
<properties>
 <address>google.com</address>
 <address2>bing.com</address2>
</properties>
And now variable address2 will be redefined in settings.xml file :
<settings>
  <profiles>
    <profile>
      <id>property-placeholder</id>
      <activation>
        <activeByDefault>true</activeByDefault>
      </activation>
      <properties>
        <address2>yahoo.com</address2>
      </properties>
    </profile>
  </profiles>
</settings>
This profile is active by default so there is not need to use -P parameter.
Not the tests pass.

Conclusion

Use Maven and define properties anywhere.