File-based Configurations
Often configuration properties are stored in files on the user's hard
disk, e.g. in .properties files or as XML documents. In order to
access this data, functionality is needed to select the configuration
files, load them into memory, and write changes back to disk. The
following sections describe how this can be done.
FileBasedConfigurationBuilder
In Commons Configuration a specialized
configuration
builder implementation is responsible for the creation of
file-based configuration objects and the management of their
associated data files:
FileBasedConfigurationBuilder
. Usage of this class follows
the typical pattern for configuration builders, i.e. a builder
instance is created providing the class of the Configuration
object to be created, the configure()
method is called
with initialization parameters, and finally getConfiguration()
returns an initialized instance of the configuration class. When
configuring the builder the file to be loaded can be specified; if this
was done, the Configuration
object returned by the
builder contains all properties read from the underlying file.
In order to define the file to be loaded, a parameters object
implementing the
FileBasedBuilderProperties
interface can be passed to the
builder's configure()
method. Using this interface the
location of the file to be loaded can be provided in multiple ways:
- With the
setFile()
method the data file can be
specified as a java.io.File
object.
- The
setURL()
method takes a java.net.URL
as argument; the file will be loaded from this URL.
- The methods
setFileName()
and setBasePath()
allow specifying the path of the data file. The base path is
important if relative paths are to be resolved based on this file.
- With
setPath()
an absolute path to the file to be
loaded can be provided.
A parameters object for file-based configurations is typically obtained
from a
Parameters
instance. Here the fileBased()
method
or one of the methods returning parameter objects derived from
FileBasedBuilderProperties
can be used. In addition to
the properties that define the location of the file to be loaded, the
parameters object support a couple of other properties, too, which
are mainly related to way how the file is resolved. This is described
later on in this chapter.
As an example for using a file-based configuration builder, the
following code fragment shows how a properties file can be read whose
location is specified using a File
object:
Parameters params = new Parameters();
// Read data from this file
File propertiesFile = new File("config.properties");
FileBasedConfigurationBuilder<FileBasedConfiguration> builder =
new FileBasedConfigurationBuilder<FileBasedConfiguration>(PropertiesConfiguration.class)
.configure(params.fileBased()
.setFile(propertiesFile));
try
{
Configuration config = builder.getConfiguration();
// config contains all properties read from the file
}
catch(ConfigurationException cex)
{
// loading of the configuration file failed
}
In this example a parameters object for file-based configurations
is obtained from a Parameters
instance. We could of
course also have used a derived parameters class - when loading a
properties file a parameters object for properties configurations
would have been a logic choice. Here only a single parameter, the
file to be loaded, is set; but remember that all other initialization
parameters common to all configuration classes are available as well.
A configuration instance created this way stays connected to its
builder. Especially, the builder stores the location of the
underlying configuration file. This comes in handy if changes on
the configuration object are to be written back to disk. For this
purpose, FileBasedConfigurationBuilder
provides a
convenient save()
method. Calling this method stores
the current content of the associated configuration into its original
configuration file, overwriting the existing file on disk. This is
demonstrated in the following code fragment which continues from the
previous example:
// Some manipulations on the configuration object
config.addProperty("newProperty", "new");
config.setProperty("updateProperty", "changedValue");
// Make changes persistent
try
{
builder.save();
}
catch(ConfigurationException cex)
{
// saving of the configuration file failed
}
Note that the save()
method of the builder does not
expect a configuration object as parameter. It always operates on
the instance managed by this builder. Because of this relationship
it is typically better to store the builder object rather than the
configuration. The configuration can always be obtained via the
builder's getConfiguration()
method, but operations
related to the configuration file are only available through the
builder.
In addition to the save()
method,
FileBasedConfigurationBuilder
offers functionality for
automatically saving changes on its managed configuration. This can
be used to ensure that every modification of a configuration object is
immediately written to disk. This feature is enabled via the
setAutoSave()
method as shown in the following example:
FileBasedConfigurationBuilder<FileBasedConfiguration> builder =
new FileBasedConfigurationBuilder<FileBasedConfiguration>(PropertiesConfiguration.class)
.configure(params.fileBased()
.setFile(new File("config.properties")));
// enable auto save mode
builder.setAutoSave(true);
Configuration config = builder.getConfiguration();
config.setProperty("colors.background", "#000000"); // the configuration is saved after this call
Be careful with this mode when you have many updates on your
configuration. This will lead to many I/O operations, too. Behind
the scenes, automatic saving is implemented via the
event notification mechanism available
for all configuration objects. A specialized event listener is
registered at the builder's managed configuration object which triggers
the save()
method every time an update event is received.
Making it easier
The code fragments presented so far in this chapter and the previous
one show that the fluent API offered by configuration builders in many
cases allows the creation and initialization of a configuration
builder in a single expression. Nevertheless, especially in simple
cases when no complex initialization is required, this approach tends
to become verbose. For instance, if just a configuration is to be
loaded from a file, you always have to create a file-based
parameters object, initialize it, create a builder, and pass the
parameters to its configure()
method.
To support this frequent use case in a more convenient way, the
Configurations
class exists. This class contains a bunch
of convenience methods that simplify the creation of many standard
configurations from different sources like files or URLs. Using
this class, the code for the creation of a configuration builder can
be reduced. The example for loading a properties configuration
presented above becomes now:
Configurations configs = new Configurations();
// Read data from this file
File propertiesFile = new File("config.properties");
FileBasedConfigurationBuilder<PropertiesConfiguration> builder =
configs.propertiesBuilder(propertiesFile);
From this builder the properties configuration can be obtained in the
usual way. It is even possible to by-pass the builder at all:
Configurations configs = new Configurations();
// Read data from this file
File propertiesFile = new File("config.properties");
PropertiesConfiguration config = configs.properties(propertiesFile);
Here behind the scenes a configuration builder is created and
initialized; then its managed configuration is queried and returned
to the caller. A ConfigurationException
is thrown if
an error occurs. Skipping the configuration builder and accessing the
configuration directly is recommended only for simple use cases. A
builder typically offers more flexibility for the handling and
management of configuration objects.
In these examples, a java.io.File
object was used to
access configuration data. There are overloaded methods for
specifying the data to be loaded in alternative ways: using URLs or
file names/paths. In addition to properties configurations, the
Configurations
class supports a couple of other
frequently used configuration formats. For instance, the methods
xml()
and xmlBuilder()
provide easy access
to XML documents.
Even if there is no direct support for a specific configuration
implementation, with the generic fileBased()
or
fileBasedBuilder()
methods, access to all kinds of
file-based configurations can be simplified. We take the
PropertyListConfiguration
class as an example for which no
specific access methods exist. The code fragment below shows how a
builder for such a configuration can be constructed using a generic
method:
Configurations configs = new Configurations();
// Read data from this URL
URL sourceURL = ...;
FileBasedConfigurationBuilder<PropertyListConfiguration> builder =
configs.fileBasedBuilder(PropertyListConfiguration.class, sourceURL);
PropertyListConfiguration config = builder.getConfiguration();
Configurations
instances are thread-safe and can be stored
centrally by an application. So they can be used as a central configuration
factory - of course, with limited flexibility; this is the price to
be payed for simplicity. However, these restrictions can be partly
circumvented by making use of
default
initialization parameters. An instance is associated with a
Parameters
object which is used to construct parameter
objects for the created configuration builders. By assigning default
parameters to this object the default settings used for the created
builders can be tweaked. Note however, that the class typically
creates only generic parameter objects; file-based parameters rather
than, say, specialized parameters for properties configurations. This
makes default settings only possible for basic parameters.
File Operations on Configurations
With FileBasedConfigurationBuilder
a single configuration
file is assigned to a configuration instance. For some use cases a
more flexible approach is required. For instance, a modified
configuration is to be stored in another file, or multiple configuration
files should be loaded into the same instance. To achieve this, the
underlying mechanisms for dealing with files have to be used.
I/O operations on files are controlled by the
FileHandler
class. Basically, this class connects a location
of a configuration file (and some other meta information like the
file's encoding) with an object which can read data from or write data
to this location. FileHandler
defines the typical
properties for defining the file to be loaded, i.e. the location can be
specified as a URL, a File, an absolute path, etc.
The object which actually reads and writes the data is represented by the
FileBased
interface. This is a pretty lean interface
consisting of only two methods for reading data from a Reader and
writing data to a Writer. All configuration implementations that can
be initialized from configuration files implement this interface; but
in theory the FileHandler
could interact with other
objects implementing FileBased
as well.
FileHandler
has the two methods load()
and
save()
. They work as follows:
- The location of the managed file is evaluated, and a corresponding
stream is opened. Depending on the way the location was specified,
this could mean opening a connection on a URL, opening a stream to
a
File
or an absolute path name, resolving relative
file names, etc.
- The resulting stream is then passed to the associated
FileBased
's read()
or write()
method.
Next to these simple load()
and save()
methods a number of overloaded methods exists which expect additional
parameters defining the source or target of the operation. For
instance, there is a load(URL)
method which reads data
directly from the passed in URL ignoring the location stored in the
FileHandler
instance. In fact, there are overloaded
methods for all the supported variants for defining a file. When
making use of these methods the following points have to be kept in
mind:
- The location stored in the
FileHandler
instance is
not changed; it is completely by-passed by these methods. Only
explicit calls to the various setter methods modify the location.
- The
load()
methods eventually call the target
object's read()
method, no matter if it has already been
called before. For configuration objects as target this means that
the configuration is not cleared before new data is loaded.
(Actually a FileHandler
is not aware which kind of
target object it is serving; so it has no chance to clear it first.)
This behavior makes it easy to construct union configurations by
simply executing multiple load operations. But if you want to reuse
a configuration object and load a different file, remember to call the
clear()
method first to ensure that old properties are
wiped out.
When constructing a FileHandler
instance the
FileBased
object it operates on has to be passed to the
constructor. With this information we are now able to look at a
concrete example. The goal is to create a configuration for a
properties file, read in another properties file (so that a union of
the properties is constructed), and finally write the resulting
configuration to a new file. The code can look as follows (the
handling of exceptions has been omitted):
// Read first file directly via the builder
FileBasedConfigurationBuilder<PropertiesConfiguration> builder =
new FileBasedConfigurationBuilder<PropertiesConfiguration>(PropertiesConfiguration.class)
.configure(params.fileBased()
.setFile(new File("config.properties")));
PropertiesConfiguration config = builder.getConfiguration();
// Create a file handler and associate it with the configuration
FileHandler handler = new FileHandler(config);
// Load another configuration source, for instance from a relative path
handler.load("user.properties");
// Store the resulting configuration in a new file
File out = new File("union.properties");
handler.save(out);
The FileHandler
class is thread-safe; it is no problem
for instance to define a file location in one thread and then call
load()
on another thread. It is also possible to have
multiple FileHandler
objects associated with the same
target object. Here concurrent I/O operations could cause problems.
Therefore, FileHandler
checks whether the target object
implements the
SynchronizerSupport
interface. If this is the case, proper
synchronization for load and save operations can be performed. Because
all configuration implementations implement SynchronizerSupport
they can safely be used together with FileHandler
.
Another important class related to file access is
FileLocator
. An instance stores all information
required for resolving a file to be accessed. FileHandler
uses a FileLocator
instance to maintain this part of
file-related information. If you need to customize the access to
configuration files, you sometimes have to deal with
FileLocator
objects because the files to be operated on are
described in terms of such objects.
Customizing File Access
When working with file-based configurations application code has multiple
ways to specify the location of the file to be loaded. If a URL
is provided, the source file to be loaded is defined in a pretty
unambiguous way. If relative file names or paths are used, situation
is less obvious.
Commons Configuration provides two mechanisms to customize the
way configuration files are accessed:
- File systems
- File location strategies
They are described in the following sub sections.
File Systems
In its default mode of operation Commons Configuration supports retrieving and storing
configuration files either on a local file system or via http. However, Commons
Configuration provides support for allowing other File System adapters. All file
access is accomplished through the
FileSystem
class so accessing files using other mechanisms is possible.
Commons Configuration also provides a second FileSystem
implementation which allows retrieval using
Apache Commons VFS. As of this writing
Commons VFS supports 18 protocols for manipulating files.
The FileSystem
used by Commons Configuration can be set in
the builder's parameter object, together with other properties defining
the file to be loaded. When working with
CombinedConfigurationBuilder
it is also possible to
define the file system in the configuration definition file to be
processed by the builder - in both a global way and for each referenced
sub configuration. The following listing shows a configuration definition
file for a combined builder making use of this functionality. Per
default, the
VFSFileSystem
is used, but the included XML
configuration is loaded via a
DefaultFileSystem
instance:
<configuration>
<header>
<fileSystem config-class="org.apache.commons.configuration2.io.VFSFileSystem"/>
</header>
<override>
<xml fileName="settings.xml" config-name="xml">
<fileSystem config-class="org.apache.commons.configuration2.io.DefaultFileSystem"/>
</xml>
<!-- Other sources omitted -->
</override>
</configuration>
Commons VFS allows options to the underlying file systems being used. Commons Configuration
allows applications to provide these by implementing the
FileOptionsProvider
interface
and registering the provider with the FileSystem
. FileOptionsProvider
has a single method that must be implemented, getOptions()
, which returns a Map
containing the keys and values that the FileSystem
might use. The getOptions()
method is called as each configuration uses VFS to create a FileOjbect
to
access the file. The map returned does not have to contain the same keys and/or values
each time it is called. For example, the value of the currentUser
key can be
set to the id of the currently logged in user to allow a WebDAV save to record the userid
as a file attribute.
File Location Strategies
Before a file can be accessed it has to be located first. In the 1.x
versions of Commons Configuration, there was a hard-coded
algorithm for looking up configuration files defined by a file name
and an optional base path in various places. Starting with version 2.0,
it is now possible to adapt this algorithm. The key to this is the
FileLocationStrategy
interface. The interface defines
a single method:
URL locate(FileSystem fileSystem, FileLocator locator);
The purpose of this method is to resolve a file described by the passed
in
FileLocator
object and return a URL for it. If
required, the provided FileSystem
can be used. The URL
yielded by a successful locate operation is directly used to access
the affected file. If the file could not be resolved, a
FileLocationStrategy
implementation should not throw an
exception, but return null instead. This allows multiple
strategies to be chained so that different locations can be searched for
the file one after the other.
Commons Configuration ships with a set of standard
FileLocationStrategy
implementations. They are pretty
specialized, meaning that a single implementation focuses on a very
specific search algorithm. The true power lies in combining these
strategies in a way suitable for an application or use case. The
following table describes the available FileLocationStrategy
implementations:
Location Strategy class |
Description |
ProvidedURLLocationStrategy
|
Directly returns the URL stored in the passed in
FileLocator . Unless an application needs some
special URL transformation, a file locator's URL - if defined -
can typically be used directly to access a file. So it makes
sense to use this strategy at the very beginning of your chain
of strategies.
|
FileSystemLocationStrategy
|
Passes the base path and the file name stored in the passed in
FileLocator to the locateFromURL()
method of the current FileSystem . This gives the file
system the opportunity to perform a special resolution.
|
AbsoluteNameLocationStrategy
|
Checks whether the file name stored in the passed in
FileLocator is actually an absolute path name
pointing to an existing file. If this is the case, the URL to
this file is returned.
|
BasePathLocationStrategy
|
This strategy creates a concatenation of the base path and file
name stored in the passed in FileLocator (of course,
only if both are defined). If this results in a path pointing to
an existing file, this file's URL is returned.
|
HomeDirectoryLocationStrategy
|
Searches for the referenced file in the current system user's home
directory. It is also possible to specify a different directory
in which the strategy should search; the path to the target
directory can be passed to the constructor.
|
ClasspathLocationStrategy
|
Interprets the file name stored in the passed in
FileLocator as a resource name and tries to look it
up on the current classpath.
|
CombinedLocationStrategy
|
This is a kind of meta strategy which allows combining an arbitrary
number of other FileLocationStrategy objects. At
construction time a collection with sub strategies has to be
passed in. In its implementation of the locate()
method, the strategy iterates over all its sub strategies (in the
order they were passed to the constructor) until one returns a
non null URL. This URL is returned.
|
As an example, consider that an application wants configuration files
to be looked up (in this order)
- by their URL
- by the file system (which will evaluate base path and file name)
- on the classpath
Then a concrete location strategy could be constructed as follows:
List<FileLocationStrategy> subs = Arrays.asList(
new ProvidedURLLocationStrategy(),
new FileSystemLocationStrategy(),
new ClasspathLocationStrategy());
FileLocationStrategy strategy = new CombinedLocationStrategy(subs);
This strategy can now be passed to a file-based configuration builder.
If no strategy is passed to a builder, a default one is used. This
default strategy is almost identical to the hard-coded search algorithm
that was used in earlier versions of Commons Configuration.
In fact, the pre-defined basic FileLocationStrategy
implementations were extracted from this algorithm.
Because the FileLocationStrategy
interface is very simple
it should be easy to create a custom implementation. The specific
search algorithm just has to be coded into the locate()
method. Then this custom strategy implementation can be combined with
other standard strategies by making use of a
CombinedLocationStrategy
.