TL;DR

  • Create an interface whose methods represent keys in a properties file, map, resource bundle, or other source of configuration.
  • Annotate the methods of the interface as needed to designate property keys, default values, etc.
  • Create an instance of PropertyBinder, giving it the Class object of your interface.
  • Stamp out instances of your interface that are bound to specific configuration sources using bind().

Example

Given this properties file:

com.pholser.util.properties.examples.ExampleSchema.argsProperty = %d seconds to %tr
com.pholser.util.properties.examples.ExampleSchema.charArrayProperty = a,b,c
com.pholser.util.properties.examples.ExampleSchema.charListProperty = d,e,f
com.pholser.util.properties.examples.ExampleSchema.intProperty = 2
com.pholser.util.properties.examples.ExampleSchema.listOfEnumsWithSeparator =    YES  , NO,YES,    MAYBE
com.pholser.util.properties.examples.ExampleSchema.unadorned = no conversion
com.pholser.util.properties.examples.ExampleSchema.wrappedLongProperty = -1
date.property = 2010-02-14
unconverted.property = also no conversion

And this interface:

package com.pholser.util.properties.examples;

import java.math.BigDecimal;
import java.util.Date;
import java.util.List;

import com.pholser.util.properties.BoundProperty;
import com.pholser.util.properties.DefaultsTo;
import com.pholser.util.properties.ParsedAs;
import com.pholser.util.properties.ValuesSeparatedBy;
import com.pholser.util.properties.boundtypes.Ternary;

public interface ExampleSchema {
    String unadorned();

    @BoundProperty("unconverted.property")
    String annotated();

    int intProperty();

    Long wrappedLongProperty();

    char[] charArrayProperty();

    List<Character> charListProperty();

    @ValuesSeparatedBy(pattern = "\\s*,\\s*")
    List<Ternary> listOfEnumsWithSeparator();

    @DefaultsTo(value = "10")
    BigDecimal bigDecimalPropertyWithDefault();

    @BoundProperty("date.property")
    @ParsedAs({"MM/dd/yyyy", "yyyy-MM-dd"})
    Date dateProperty();

    String argsProperty(int quantity, Date time);
}

Then the following tests should pass:

package com.pholser.util.properties.examples;

import java.io.File;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

import static java.math.BigDecimal.*;
import static java.util.Arrays.*;

import com.pholser.util.properties.PropertyBinder;
import org.junit.Before;
import org.junit.Test;

import static com.pholser.util.properties.boundtypes.Ternary.*;
import static org.junit.Assert.*;

public class ExampleTest {
    private ExampleSchema bound;

    @Before public void initializeFixture() throws Exception {
        PropertyBinder<ExampleSchema> binder = PropertyBinder.forType(ExampleSchema.class);
        bound = binder.bind(new File("src/test/resources/example.properties"));
    }

    @Test public void bindingToPropertyWithMethodName() {
        assertEquals("no conversion", bound.unadorned());
    }

    @Test public void bindingToPropertyWithAnnotation() {
        assertEquals("also no conversion", bound.annotated());
    }

    @Test public void convertingPrimitivePropertyValues() {
        assertEquals(2, bound.intProperty());
    }

    @Test public void convertingWrappedPrimitivePropertyValues() {
        assertEquals(Long.valueOf(-1L), bound.wrappedLongProperty());
    }

    @Test public void convertingCommaSeparatedValuedPropertyToArray() {
        assertTrue(Arrays.equals(new char[] { 'a', 'b', 'c' }, bound.charArrayProperty()));
    }

    @Test public void convertingCommaSeparatedValuedPropertyToList() {
        assertEquals(asList('d', 'e', 'f'), bound.charListProperty());
    }

    @Test public void honoringDifferentSeparatorsForAggregateProperties() {
        assertEquals(asList(YES, NO, YES, MAYBE), bound.listOfEnumsWithSeparator());
    }

    @Test public void honoringDefaultValueIndicationWhenPropertyNotPresent() {
        assertEquals(TEN, bound.bigDecimalPropertyWithDefault());
    }

    @Test public void honoringDateFormatSpecificationsForDateProperties() throws Exception {
        assertEquals(MMddyyyy("02/14/2010"), bound.dateProperty());
    }

    @Test public void formattingPropertiesCorrespondingToMethodsWithArguments() throws Exception {
        assertEquals("10 seconds to 12:00:00 AM", bound.argsProperty(10, MMddyyyy("01/01/2011")));
    }

    private static Date MMddyyyy(String raw) throws ParseException {
        return new SimpleDateFormat("MM/dd/yyyy").parse(raw);
    }
}

So?

By presenting bits of configuration to your application as instances of a Java interface, you decouple the things that use the configuration from the means by which they're read/stored. You thereby enable easier testing of those pieces of your application that use the configuration -- supply mocks or stubs of the interface that answer different values for the configuration properties.

By letting Property Binder create instances of those interfaces for you, you relieve your application of the grunt work of converting configuration values to sensible Java types, supplying default values, and so forth.

Other Sources of Configuration

Property Binder admits properties files, resource bundles, and string-keyed maps out of the box. You can bind other types of string-keyed configuration by providing an implementation of interface PropertySource.

If you have an XML config file such as this:

<config>
    <timeout>5000</timeout>
    <output-file>/home/joeblow/out.txt</output-file>
</config>

You might adapt it for Property Binder's use like so:

package com.pholser.util.properties.examples;

import java.io.File;

import com.pholser.util.properties.BoundProperty;

public interface Config {
    @BoundProperty("/config/timeout")
    long timeout();

    @BoundProperty("/config/output-file")
    File outputFile();
}
package com.pholser.util.properties.examples;

import java.io.File;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import com.pholser.util.properties.BoundProperty;
import com.pholser.util.properties.PropertyBinder;
import com.pholser.util.properties.PropertySource;
import org.junit.Before;
import org.junit.Test;
import org.w3c.dom.Document;

import static org.junit.Assert.*;

public class XmlConfigTest {
    private Config config;

    @Before public final void initialize() throws Exception {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document document = builder.parse(getClass().getResourceAsStream("/config.xml"));

        PropertyBinder<Config> binder = PropertyBinder.forType(Config.class);
        config = binder.bind(new XmlConfigSource(document));
    }

    @Test public void retrievesTimeoutValue() {
        assertEquals(5000L, config.timeout());
    }

    @Test public void retrievesOutputFileValue() {
        assertEquals(new File("/home/joeblow/out.txt"), config.outputFile());
    }
}

class XmlConfigSource implements PropertySource {
    private final Document document;
    private final XPath xpath;

    XmlConfigSource(Document document) {
        this.document = document;
        xpath = XPathFactory.newInstance().newXPath();
    }

    @Override public Object propertyFor(BoundProperty key) {
        try {
            return xpath.compile(key.value()).evaluate(document);
        } catch (XPathExpressionException e) {
            throw new IllegalStateException(e);
        }
    }
}

You could even funnel a Configuration from Apache Commons Configuration into Property Binder:

package com.pholser.util.properties.examples;

import com.pholser.util.properties.BoundProperty;
import com.pholser.util.properties.PropertySource;
import org.apache.commons.configuration.Configuration;

class CommonsConfigPropertySource implements PropertySource {
    private final Configuration config;

    CommonsConfigPropertySource(Configuration config) {
        this.config = config;
    }

    @Override public Object propertyFor(BoundProperty key) {
        return config.getProperty(key.value());
    }
}