Quantcast
Channel: Baeldung
Viewing all articles
Browse latest Browse all 4535

Intro to Apache Commons Configuration Project

$
0
0
start here featured

1. Overview

At deployment time we may need to provide some configuration to the application. This can be from multiple external sources.

Apache Commons Configuration provides a unified approach to manage configuration from different sources.

In this tutorial, we’ll explore how Apache Commons Configuration can help us configure our application.

2. Introduction to Apache Commons Configuration

The Apache Commons Configuration provides an interface for Java applications to access and use configuration data from varied sources. Through configuration builders, it offers typed access to both single and multi-valued characteristics.

It handles properties consistently across several sources, including files, databases, and hierarchical documents such as XML.

2.1. Maven Dependency

Let’s start by adding the latest versions of the configurations library and bean utils to the pom.xml:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-configuration2</artifactId>
    <version>2.10.0</version>
</dependency>
<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.9.4</version>
</dependency>

2.2. Setup

Let’s define some common configuration files we may encounter.

We’ll create a flat-file format – a .properties file:

db.host=baeldung.com
db.port=9999
db.user=admin
db.password=bXlTZWNyZXRTdHJpbmc=
db.url=${db.host}:${db.port}
db.username=${sys:user.name}
db.external-service=${const:com.baeldung.commons.configuration.ExternalServices.BAELDUNG_WEBSITE}

Let’s create another in a hierarchical XML format:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration SYSTEM "validation-sample.dtd">
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>Pattern1</pattern>
            <pattern>Pattern2</pattern>
        </encoder>
    </appender>
    <root>
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

3. Configurations Helper Class

Apache Commons Configuration provides the Configurations utility class to read the same from different sources for a quick start with standard options. This is a thread-safe class, which helps us create varied configuration objects with default parameters.

Additionally, we can also provide custom parameters passing the Parameters instance.

3.1. Reading a Properties File

We’ll read the properties file via the Configurations class and access it via the Configuration class. There are multiple ways to read a file or get the test resources folder. We can read or cast the properties to numbers or a List of certain object types. Finally, we can also provide default values.

Let’s try to access the configurations from the properties file:

Configurations configs = new Configurations();
Configuration config = configs.properties(new File("src/test/resources/configuration/file.properties"));
String dbHost = config.getString("db.host");
int dbPort = config.getInt("db.port");
String dbUser = config.getString("db.user");
String dbPassword = config.getString("undefinedKey", "defaultValue");
assertEquals("baeldung.com", dbHost);
assertEquals(9999, dbPort);
assertEquals("admin", dbUser);
assertEquals("defaultValue", dbPassword);

3.2. Reading an XML file

We’ll use the XMLConfiguration class to access the properties from the XML file, which extends the Configuration class:

Configurations configs = new Configurations();
XMLConfiguration config = configs.xml(new File("src/test/resources/configuration/hierarchical.xml"));
String appender = config.getString("appender[@name]");
List<String> encoderPatterns = config.getList(String.class, "appender.encoder.pattern");
String pattern1 = config.getString("appender.encoder.pattern(0)");

The traversal of the different elements is via the dot ‘.’ notation which enables accessing the hierarchical nature of the input file.

4. Configuration From Properties File

In addition to using the Configurations class, the Apache Commons Configuration provides support to read/access this format with additional features. The configuration object for the properties file is instantiated using the FileBasedConfigurationBuilder.

Let’s look at an example of how we can use this builder to access the properties file:

Parameters params = new Parameters();
FileBasedConfigurationBuilder<FileBasedConfiguration> builder = 
  new FileBasedConfigurationBuilder<FileBasedConfiguration>(PropertiesConfiguration.class)
    .configure(params.properties()
    .setFileName("src/test/resources/configuration/file1.properties"));

Subsequently, we can use the standard methods to access the attributes. Additionally, we can provide custom implementations for IO operations on the property file by extending the PropertiesReader or PropertiesWriter classes of the PropertiesConfiguration class.

It’s also possible to link further property files via the include and includeOptional flag by specifying the file name as the value. The difference between these two flags is that if the property file isn’t found, then the include flag throws ConfigurationException.

First, we create a new property file named file1.properties. This file includes the initial properties file using include and also a non-existent file using includeOptional:

db.host=baeldung.com
include=file.properties
includeOptional=file2.properties

Now, let’s verify that we can read from both property files:

Configuration config = builder.getConfiguration();
String dbHost = config.getString("db.host");
int dbPort = config.getInt("db.port");

5. Configuration From an XML

Configuration via XML is also a common practice during application development. The library provides XMLConfiguration to access attributes from an XML file.

A standard requirement with XML files is that of validating the same to ensure there are no discrepancies. XMLConfiguration provides two flags to validate the structure and content of the file. We can set the validating flag to enable a validating parser or set the schemaValidation flag to true, to enable validating against a schema in addition to normal validation.

Let’s define a schema for the XML file we’ve defined previously:

<!ELEMENT configuration (appender+, root)>
<!ELEMENT appender (encoder?)>
<!ATTLIST appender
    name CDATA #REQUIRED
    class CDATA #REQUIRED
    >
<!ELEMENT encoder (pattern+)>
<!ELEMENT pattern (#PCDATA)>
<!ELEMENT root (appender-ref+)>
<!ELEMENT appender-ref EMPTY>
<!ATTLIST appender-ref
    ref CDATA #REQUIRED
    >

Now let’s run a test with schemaValidation true to verify the behavior:

Parameters params = new Parameters();
FileBasedConfigurationBuilder<XMLConfiguration> builder = new FileBasedConfigurationBuilder<>(XMLConfiguration.class)
  .configure(params.xml()
  .setFileName("src/test/resources/configuration/hierarchical.xml")
  .setValidating(true));
XMLConfiguration config = builder.getConfiguration();
String appender = config.getString("appender[@name]");
List<String> encoderPatterns = config.getList(String.class, "appender.encoder.pattern");
assertEquals("STDOUT", appender);
assertEquals(2, encoderPatterns.size());

6. Multi-Tenant Configurations

In a multi-tenant application setup, multiple clients share a common code base and are differentiated by the configuration properties for each client. The library provides support to handle this scenario with MultiFileConfigurationBuilder.

We need to pass a file pattern for the properties file which contains a client-identifying parameter. Finally, this parameter can be resolved using interpolation which is then resolved as the configuration name:

System.setProperty("tenant", "A");
String filePattern = "src/test/resources/configuration/tenant-${sys:tenant}.properties";
MultiFileConfigurationBuilder<PropertiesConfiguration> builder = new MultiFileConfigurationBuilder<>(
  PropertiesConfiguration.class)
    .configure(new Parameters()
      .multiFile()
      .setFilePattern(filePattern)
      .setPrefixLookups(ConfigurationInterpolator.getDefaultPrefixLookups()));
Configuration config = builder.getConfiguration();
String tenantAName = config.getString("name");
assertEquals("Tenant A", tenantAName);

We’ve defined a file pattern; for this example, we’ve provided the tenant value from the System properties.

We’ve also provided the DefaultPrefixLookups which will be used to instantiate the ConfigurationInterpolator for the MultiFileConfigurationBuilder.

7. Handling Different Data Types

The library supports the handling of various data types. Let’s take a look at a few scenarios in the following sub-sections.

7.1. Missing Properties

It’s possible to try to access properties not present in the configuration. In such cases, if the return value is an object type, then a null is returned.

However, if the return value is a primitive type, then a NoSuchElementException is thrown. We can override this behavior by passing a default value to be returned by the method:

PropertiesConfiguration propertiesConfig = new PropertiesConfiguration();
String objectProperty = propertiesConfig.getString("anyProperty");
int primitiveProperty = propertiesConfig.getInt("anyProperty", 1);
assertNull(objectProperty);
assertEquals(1, primitiveProperty);

In the example above, we’ve provided the default value 1 to the primitive int property. Now let’s verify the exception is thrown if we don’t provide the default value:

assertThrows(NoSuchElementException.class, () -> propertiesConfig.getInt("anyProperty"));

7.2. Handling Lists and Arrays

Apache Commons Configuration supports the handling of properties with multiple values. We need to define a delimiter to identify and convert multi-value properties. We can achieve this by setting the ListDelimiterHandler in the configuration.

Let’s write a test to set the delimiter on the configuration, and then read the multi-value properties:

PropertiesConfiguration propertiesConfig = new PropertiesConfiguration();
propertiesConfig.setListDelimiterHandler(new DefaultListDelimiterHandler(';'));
propertiesConfig.addProperty("delimitedProperty", "admin;read-only;read-write");
propertiesConfig.addProperty("arrayProperty", "value1;value2");
List<Object> delimitedProperties = propertiesConfig.getList("delimitedProperty");
String[] arrayProperties = propertiesConfig.getStringArray("arrayProperty");

In the snippet above, we can extract the properties as List or Array based on the set delimiter.

Also, if we extract the multi-value property as a String, we  get the first value of the same:

assertEquals("value1", propertiesConfig.getString("arrayProperty"));

8. Interpolation and Expressions

At times we may need to use existing configurations from the underlying system. The library supports resolving configuration via interpolation or expressions as well.

8.1. Interpolation

The library supports interpolation, which allows us to define placeholders in the configuration, and the properties are resolved at runtime. We can access system properties, environment properties, or a constant member of a class:

System.setProperty("user.name", "Baeldung");
String dbUrl = config.getString("db.url");
String userName = config.getString("db.username");
String externalService = config.getString("db.external-service");

8.2. Using Expressions

In addition to the interpolation options we saw in the previous section, the library also provides lookups using expressions. We can perform string lookups via the Apache Commons Jexl library.

Let’s include this dependency and then take a look at an example of how we can use it in our configurations:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-jexl</artifactId>
    <version>2.1.1</version>
</dependency>

Let’s define the property containing the expression:

db.data-dump-location=${expr:System.getProperty("user.home")}/dump.dat

Initially, we set the System property for the test case. Then, we access the DefaultPrefixLookups to add the expr lookup. Next, to resolve the System variable in our expression, we’ve mapped the same to the appropriate class and added a default ConfigurationInterpolator:

System.setProperty("user.home", "/usr/lib");
Map<String, Lookup> lookups = new HashMap<>(ConfigurationInterpolator.getDefaultPrefixLookups());
ExprLookup.Variables variables = new ExprLookup.Variables();
variables.add(new ExprLookup.Variable("System", "Class:java.lang.System"));
ExprLookup exprLookup = new ExprLookup(variables);
exprLookup.setInterpolator(new ConfigurationInterpolator());
lookups.put("expr", exprLookup);

Finally, we’ve resolved the configuration expression after adding the updated lookups to the Configuration:

FileBasedConfigurationBuilder<FileBasedConfiguration> builder = 
  new FileBasedConfigurationBuilder<FileBasedConfiguration>(
    PropertiesConfiguration.class).configure(params.properties()
      .setFileName("src/test/resources/configuration/file1.properties")
      .setPrefixLookups(lookups));

9. Data Type Conversion and Decoding

Another common scenario is converting properties from one data type to another or configuring encoded secrets. Let’s explore how the Apache Commons Configuration library handles these scenarios.

9.1. Data Type Conversions

The library supports out-of-the-box data type conversion and tries to convert the property value based on the called method. If a value cannot be converted, the underlying implementation throws a ConversionException.

Let’s see the data type conversion for different data types:

config.addProperty("stringProperty", "This is a string");
config.addProperty("numericProperty", "9999");
config.addProperty("booleanProperty", "true");
assertEquals("This is a string", config.getString("stringProperty"));
assertEquals(9999, config.getInt("numericProperty"));
assertTrue(config.getBoolean("booleanProperty"));

Now let’s assert that an exception is thrown when the data type conversion cannot happen:

config.addProperty("numericProperty", "9999a");
assertThrows(ConversionException.class,()->config.getInt("numericProperty"));

9.2. Encoded Properties

It’s common to have encoded secrets or credentials in application properties and the library supports reading these encoded properties. It exposes the interface ConfigurationDecoder which has a decode() method. This method expects an encoded string and we can provide our custom implementation to decode the same.

We can use this custom implementation with the Configuration to decode the encoded properties.

Let’s define a simple implementation of the ConfigurationDecoder interface:

public class CustomDecoder implements ConfigurationDecoder {
    @Override
    public String decode(String encodedValue) {
        return new String(Base64.decodeBase64(encodedValue));
    }
}

Now let’s use it to decode an encoded property:

((AbstractConfiguration) config).setConfigurationDecoder(new CustomDecoder());
assertEquals("mySecretString", config.getEncodedString("db.password"));

10. Copying Configurations

We can copy or append one configuration to another. For flat configurations, the append() or copy() methods of the AbstractConfiguration class allow us to make a copy of the configuration. However, the copy() method overrides existing properties.

Let’s take a look at a couple of examples that demonstrate the same.

10.1. copy() Method

We use the copy() method to copy the configuration from an existing one:

Configuration baseConfig = configs.properties(new File("src/test/resources/configuration/file.properties"));
Configuration subConfig = new PropertiesConfiguration();
subConfig.addProperty("db.host","baeldung");
((AbstractConfiguration) subConfig).copy(baseConfig);
String dbHost = subConfig.getString("db.host");
assertEquals("baeldung.com", dbHost);

10.2. append() Method

In the example below, let’s use the append() method to copy the configuration. However, it doesn’t override the existing properties:

Configuration baseConfig = configs.properties(new File("src/test/resources/configuration/file.properties"));
Configuration subConfig = new PropertiesConfiguration();
subConfig.addProperty("db.host","baeldung");
((AbstractConfiguration) subConfig).append(baseConfig);
String dbHost = subConfig.getString("db.host");
assertEquals("baeldung", dbHost);

10.3. Copying Hierarchical Configurations

For hierarchical configurations, using the append() or copy() methods doesn’t preserve the hierarchical nature of the configurations. In this case, we can either use the clone() method or pass the existing configuration to the constructor of the new one.

Let’s look at how we can make a copy of the same:

XMLConfiguration baseConfig = configs.xml(new File("src/test/resources/configuration/hierarchical.xml"));
XMLConfiguration subConfig = new XMLConfiguration();
subConfig = (XMLConfiguration) baseConfig.clone();
//subConfig = new XMLConfiguration(baseConfig);

The effect remains the same if we switch from the clone() method to the constructor initialization.

11. Conclusion

In this article, first, we explored a quick way to start with Apache Commons configuration with the Configuration class.

Then, we saw how to read and access varied configuration files with more specific implementations. Next, we also explored multi-tenant configuration scenarios.

Finally,  we went over some of the additional features available within the library for common scenarios.

As always, the code can be found over on GitHub.

       

Viewing all articles
Browse latest Browse all 4535

Trending Articles