|
|||||||||
PREV PACKAGE NEXT PACKAGE | FRAMES NO FRAMES |
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. |
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.
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.
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:
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.
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:
Combining these two algorithms, we see that the full search path for the property value associated with the property name "key" will be:
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:
If the key were "test.key", the full XPaths would be:
Alternately, the final component in the key name can be an attribute rather than an element, expanding the search algorithm to:
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.
At the XML nodes located in the search path, the configuration utility looks in several locations for the property value:
value
" attribute of the element whose name
matches the last part of the key.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.
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.
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 ${file.separator}
.${:}
: For ${path.separator}
.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.
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);
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.
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.
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
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:
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
".
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.
disable.child.config=true
disable.parent.config=true
(this option take precedence over the above).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.
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.
|
|||||||||
PREV PACKAGE NEXT PACKAGE | FRAMES NO FRAMES |