Package org.chwf.config

A package for retrieving configuration data from properties and XML files.

See:
          Description

Interface Summary
Config A class for retrieving configuration data from properties and XML files.
RawConfig An interface exposing raw configuration data.
 

Class Summary
ConfigFactory  
ConfigMap Adapter map for Config objects.
PolymorphicConfig A subclass of Config that adds the superclass configuration to the configuration search path.
 

Exception Summary
ConfigurationException Exception for configuration.
 

Package org.chwf.config Description

A package for retrieving configuration data from properties and XML files. Its primary class is Config. This class uses a hierarchal search algorithm to retrieve configuration data from various locations. This is to support a flexible development process:

This utility uses the Java classpath to search for configuration files. This makes it easier to manage the distribution of configuration files.

Retrieving Configuration Data

When retrieving a configuration value in code, the value must be associated with a resource and a key.

Typically the resource will be the Java class where the configuration value will be used, referred to as the context class. The key is the string that identifies the configuration value. Actual retrieval can be accomplished as follows.

  Config config = Config.getConfig(<Class>);
  String value = config.get("<key>");

Alternately, the resource can be a file path, expressed as a string:

  Config config = Config.getConfig(<resource-file-path>);
  String value = config.get("<key>");

Typically, configuration values are used to initialize constants:

  public class Example {
    private static Config CONFIG = Config.getConfig(Example.class);
    public static final TEMP_DIR = CONFIG.get("temp.dir");

    // More code ...
  }

This approach gives the best performance: configuration information only needs to be retrieved once, during class loading. Thereafter, the constants can be used for fast access.

Property Files

The Config class searches a variety of locations for configuration files, based on the context class. It starts first with a configuration file whose location matches the context class's name. It searches next for a package configuration file (chrysalis.properties), then for the containing package's configuration file and so forth, up to the default package. For example, if a class's name is "com.domain.Example", its configuration data is retrieved from the following files, in order of precedence:

  1. com/domain/Example.properties
  2. com/domain/chrysalis.properties
  3. com/chrysalis.properties
  4. chrysalis.properties

The motivation for this search path is to allow configuration data initially to be located in class-specific configuration files, and then moved up to package-level configuration files once the configuration data stabilizes.

The search algorithm uses the Java classpath to locate the configuration file. More specifically, it uses the getResourceAsStream() method of the ClassLoader to load individual files. To determine which ClassLoader to use, the Config class uses the getContextClassLoader() method of the current thread, which in most cases will be the same ClassLoader used to load the context class.

In-File Key Search

Within package configuration files, the Config class searches for the property value in more than one place, based on its search context. The search context is the difference between the resource name and the location of the package configuration file. For the com.domain.Example class and the configuration file for the com package ("/com/chrysalis.properties"), the search context is "domain.Example".

Within the property file, the search context is added to the beginning of the property key to retrieve the value. If the property value is not found under this context-key, the search continues by stripping off one name from the end of the search context. Continuing the above example, the Config class will check the following property names within the "/com/chrysalis.properties" file:

  1. domain.Example.key
  2. domain.key
  3. key

Combining these two algorithms, we see that the full search path for the property value associated with the property name "key" will be:

  1. /com/domain/Example.properties: key
  2. /com/domain/chrysalis.properties: Example.key
  3. /com/domain/chrysalis.properties: key
  4. /com/chrysalis.properties: domain.Example.key
  5. /com/chrysalis.properties: domain.key
  6. /com/chrysalis.properties: key
  7. /chrysalis.properties: com.domain.Example.key
  8. /chrysalis.properties: com.domain.key
  9. /chrysalis.properties: com.key
  10. /chrysalis.properties: key

XML Configuration Files

Configuration data can be stored in an XML file instead of a property file. XML configuration file names follows the rules as property files, but use the file extension ".xml" instead. Within the XML file, property names are parsed like XPath expressions, converting all "." into "/". The root tag for XML configuration files is always "<config>", and XPath search is always prefaced with this root tag.

For example, for the XML configuration file for the com package ("/com/chrysalis.xml"), the property value's for the Example class above would be retrieved using the XPaths:

  1. /config/domain/Example/key
  2. /config/domain/key
  3. /config/key

If the key were "test.key", the full XPaths would be:

  1. /config/domain/Example/test/key
  2. /config/domain/test/key
  3. /config/test/key

Alternately, the final component in the key name can be an attribute rather than an element, expanding the search algorithm to:

  1. /config/domain/Example/test/@key
  2. /config/domain/Example/test/key
  3. /config/domain/test/@key
  4. /config/domain/test/key
  5. /config/test/@key
  6. /config/test/key

Note: The above discussion is only intended to make the search algorithm clear. The XPath expressions above are not actually evaluated when retrieving configuration values. In practice, configuration data is pre-loaded and cached in memory for greater efficiency.

XML Property Values

At the XML nodes located in the search path, the configuration utility looks in several locations for the property value:

This gives several alternatives for encoding property values in the XML file. First, as an attribute named after the last value in the key (test.key):

  <config>
    <test key="the value" />
  </config>

Second, using a value attribute of a sub-element.

  <config>
    <test>
      <key value="the value" />
    </test>
  </config>

Third, as the text content of a sub-element.

  <config>
    <test>
      <key>the value</key>
    </test>
  </config>

The first option is preferred, because it is the most concise. The only situation where this option cannot be used is when the key itself must terminate with "name" or "value" (which have special interpretations as configuration attributes). If multiple options present, they take precedence in the order given above.

Named XML Elements

In addition to specifying search path elements via the tag name, the configuration utility looks for a "name" attribute in each tag. If there is a name attribute, it takes precedence over the tag name in the search path. The following are equivalent (and both correspond to the property key "foo").

  <foo value="the value" />
  <bar name="foo" value="the value" />

The motivation for this convention to make it easier to define common element names, simplifying DTD definitions for the XML configuration file. Suppose, for example, you had a "maxsize" configuration value that applied to several classes in the same package. If the configuration data were in the chrysalis.xml file, it might look like the following.

  <config>
    <ExampleClass1 maxsize="12" />
    <ExampleClass2 maxsize="14" />
    <ExampleClass3 maxsize="6" />
  </config>

It would be difficult to define a DTD for the above XML. Consider an alternative using elements with name attributes.

  <config>
    <class name="ExampleClass1" maxsize="12" />
    <class name="ExampleClass2" maxsize="14" />
    <class name="ExampleClass3" maxsize="6" />
  </config>

In this case, defining a flexible DTD is much easier.

  <!-- DTD for above XML -->
  <!ELEMENT config (class*)>
  <!ELEMENT class (EMPTY)>
    <!ATTLIST class name CDATA #REQUIRED>
    <!ATTLIST class maxsize CDATA #REQUIRED>

The above DTD will still work as more classes are added. The tag name "class" is arbitrary, and has no effect on the property search path, since it is overridden by the name attribute of each tag.

Property Expansion

Individual property values can be embedded in other property values using a syntax similar to variable expansion in Unix shell scripts, Java security policy files and Ant build scripts. When a string of the form ${key} appears in a property value, it is expanded and replaced with the property value associated with that key.

For example, consider this fragment of a property file.

  foo.key=foo
  bar.key=${foo.key} embedded in bar

The method call config.get("bar.key") will return the string "foo embedded in bar".

When expanding properties, the Config class first looks for a configuration value in the same file. If no such property is found, it looks for a System property value, using the System.getProperty() method. It will not resolve properties from other configuration files. [In the author's opinion, this would cause too much confusion].

When expanding System properties, the following abbreviations may be used:

For example, to specify the location of a temporary directory under the user's home directory:

  temp.dir=${user.home}${/}temp

The property expansion terminates after a certain number of iterations to prevent endless recursion.

Typed and Missing Values

The Config class has various utility methods for retrieving typed values:

  String value = config.get("String.property");
  int value = config.getInt("int.property");
  double value = config.getDouble("double.property");
  boolean value = config.getBoolean("boolean.property");

These methods convert property strings to the appropriate data type. These methods throw a ConfigurationException if the value is not found or is not the correct type.

There are alternate versions of each of the getter methods that allow you to specify a default value. These alternate method returns the default value if the value is not found or is not the correct type.

  String value = config.get("int.property", "default");
  int value = config.getInt("int.property", 13);
  double value = config.getDouble("double.property", 13.0);
  boolean value = config.getBoolean("boolean.property", false);

Maps and Lists

The Config class has utility methods for retrieving maps and lists. Both these operations retrieve all the properties whose key begin with the given search key:

  Map map = config.getMap("map.keys");
  String[] list = config.getList("map.keys");

  # In the property file
  map.keys.1=value 1
  map.keys.2=value 2
  map.keys.3=value 3

If there are no properties beginning with the given key, the getMap() returns an empty map. The getList() method, however, never returns an empty array; it throws a ConfigurationException instead.

The map is actually a SortedMap, so that its entries are arranged in order. The list is simply the map values [map.keys()], as an array of strings. Therefore, it retains the alphabetical ordering specified by the keys. Map keys are sorted as strings; for lists with more than 10 entries, you must choose the keys with care if ordering is important:

  # In the property file
  map.keys.01=value 1
  map.keys.02=value 2
  map.keys.03=value 3
  ...
  map.keys.09=value 9
  map.keys.10=value 10
  map.keys.11=value 11
  ...

If there is only one entry and its key matches the search key exactly, the single property value is parsed as a comma-delimited list.

  String[] list = config.getList("list.keys");

  # In the property file
  list.keys=value 1,value 2

Finally, the Config algorithm will walk up the file hierarchy to search for map and list data, but it will only use the data from the first file containing matching keys. In particular, map and list data is never derived from more than one configuration file.

Maps and Lists in XML

The map and list rules described above also apply to maps and lists in XML configuration files:

  Map map = config.getMap("map.key");
  String[] list = config.getList("map.key");

  <config>
    <map>
      <key>
        <v1>value 1</v1>
        <v2>value 1</v2>
        <v3>value 1</v3>
      </key>
    </map>
  </config>

In addition, duplicate entries in XML are assigned dummy keys, so that all XML entries will appear in the list:

  String[] list = config.getList("map.key");

  <config>
    <map>
      <key>value 1</key>
      <key>value 2</key>
      <key>value 3</key>
    </map>
  </config>

The dummy keys are in string ordering, so the ordering of the XML tags is retained.

Finally, named configuration values in XML files can be retrieved in sequential order using the getOrderedMap() method:

  Map map = config.getOrderedMap("xml-ordered-map");

  <config>
    <xml-ordered-map>
      <item name="C">Item 1</item>
      <item name="B">Item 2</item>
      <item name="A">Item 3</item>
    </xml-ordered-map>
  </config>

In the above example, the map will have three values, with the keys "C", "B" and "A", in that order. The getOrderedMap() method only functions correctly for XML configuration data, not data in property files.

Maps and Lists with Wildcards

You can use "*" wildcards in the keys used to retrieve maps and lists:

  Map map = config.getMap("map.*.key");
  String[] list = config.getList("map.*.key");

  # In the property file
  map.pickle.key=value 1
  map.pear.key=value 2
  map.apple.key.plus=value 3

For lists, it will retrieve all values that begin with the wildcard pattern. For maps, it does the same, but the keys in the new map will be derived from the wildcard values. In the example above, the map keys will be:

  pickle
  pear
  apple.plus

Internationalization

The Config class has some support for internationalization, and can be used as an alternative to the java.util.ResourceBundle. There is an additional factory method for the Config class that specifies a locale:

  Config.getConfig(<class>, <locale>);
  Config.getConfig(Example.class, Locale.CANADA_FRENCH);

If this factory method is used, the configuration search algorithm adds a locale suffix to the file names, identical to the locale suffix used by the ResourceBundle. The Config class searches all country-specific files first, up to the root directory, then language-specific files, then unqualified files:

  1. com/domain/Example_fr_CA.xml/properties
  2. com/domain/chrysalis_fr_CA.xml/properties
  3. com/chrysalis_fr_CA.xml/properties
  4. chrysalis_fr_CA.xml/properties
  5. com/domain/Example_fr.xml/properties
  6. com/domain/chrysalis_fr.xml/properties
  7. com/chrysalis_fr.xml/properties
  8. chrysalis_fr.xml/properties
  9. com/domain/Example.xml/properties
  10. com/domain/chrysalis.xml/properties
  11. com/chrysalis.xml/properties
  12. chrysalis.xml/properties

Configuration Management

The idea behind this library is to make it easier to manage configuration values during application development. For rapid development, it is easiest to define configuration values on a per-class basis, giving each class its own property file.

  public class Example1 {
    private Config config = Config.getConfig(Example1.class); 
    public static final EXAMPLE_VALUE = config.get("example.key");
  }

  # Example1.properties
  example.key=A value

Later, configuration values can be moved into the package's chrysalis.properties file to unify all configuration data into a single location. Configuration keys can be qualified by class names to eliminate property naming conflicts.

  # chrysalis.properties
  Example1.example.key=A value
  Example2.example.key=A value
  Example3.example.key=A different value

Common configuration values can be unified by eliminating the class name qualifiers. Classes with specialized values can retain their qualifiers.

  # chrysalis.properties
  example.key=A value
  Example3.example.key=A different value

Ultimately, the property files can be converted to XML for cleaner syntax. The XML can be assigned DTDs or Schemas for better validation of configuration files.

  <!-- chrysalis.xml -->
  <config>
    <example key="Example value" />
    <!-- Overridden property for Example3 class -->
    <class name="Example3">
      <example key="A different value" />
    </class>
  </config>

To prepare for XML conversion,  you should use a reverse ordering for subcomponents of property keys. For example, if you have a group of message properties, you should name their keys "message.*" rather than "*.message".

Configuration Search Controls

During development, the flexibility of reorganizing configuration data is very helpful. During production, it can cause serious maintenance problems, because it can be difficult to determine exactly where a configuration value is located. There are two special configuration values that disable the normal search algorithm.

The above configuration options can be used to restrict the configuration files included in the search path to make it less likely that values will be retrieved from unexpected places. There are equivalent values for XML files:

  <config>
    <disable child.config="true" />
    <disable parent.config="true" />
  </config>

In some cases, you will want to put extra configuration information in a special location (such as the root directory). You can specify an "extra.xml.config" property in any configuration file in the search path to load additional configuration information. This file must be XML, and it is retrieved from the classpath.

  extra.xml.config=<file location>
  extra.xml.config=ChrysalisConfig.xml

You must also be careful of the Java classpath. If a configuration file appears more than once in the classpath, the first version of that file in the classpath will take precedence. This is especially an issue with J2EE servers that have complex classloading operations.

Design Notes

When designing a hierarchical configuration system, there were a couple possibilities I considered for the property search path. I eventually settled on using the package hierarchy because it was simple and has worked well in other situations (e.g. Log4J).

The other obvious alternative was to use the inheritance hierarchy. I used this approach in an early draft of this library, but I eventually abandoned it because I found it too confusing. Also, the inheritance of configuration values in subclasses can be accomplished through normal OO inheritance, so putting it into the configuration search seemed redundant. If you need this behavior, though, you can use the PolymorphicConfig instead of the normal config.

The search algorithm goes all the way up to the package configuration file for the default package, but I do not suggest that you actually define configuration data that globally. Each application or library should put its own global configuration data in its root package. For example, if this mechanism were used to define the configuration information for the Tomcat application, global configuration information for the Chrysalis library is in the org/chwf/chrysalis.properties file.

Finally, it is worth taking a moment and describing what this utility does not cover:

This library is only for managing configuration data that must be initialized from a file exactly once, when the application is started, and will remain fixed throughout the entire run of the application.



Copyright © 2002-2004, Paul Strack. All Rights Reserved.