XmlConfigFile Tutorialby Maik SchmidtVersion 0.9.0IntroductionA lot of modern software can be customized by configuration files and more and more applications use XML as the format for these configuration files. This makes sense because of at least the following reasons:
So, creating and reading XML configuration files seems to be easy, but what about accessing the content of such a file? Many applications use "DOM tree traversing" or convert the XML document into a simpler internal structure (e.g. Hashes). Some advantages of XML get lost by doing so. If you want to change and write back a configuration, for example, you will have to write code that converts your internal structure back to XML. But there is a better, easier, and standardized way for accessing elements in an XML document: XPath. With XmlConfigFile you can access configuration parameters via XPath expressions. This tutorial will show you how to do this. InstallationYou can download XmlConfigFile here. XmlConfigFile depends on REXML, so you will have to install it first. Then run ruby install.rb config ruby install.rb setup ruby install.rb install A Simple ExampleFor our first example, we assume, that we have a configuration file called example.xml that looks like this:
<!-- A sample configuration file. --> <?xml version="1.0" encoding="iso-8859-1"?> <config> <version>1.7</version> <splash-screen enabled='yes' delay='5000' /> <greeting lang="en">Hello, world!</greeting> <greeting lang="de">Hallo, Welt!</greeting> <base-dir>${BASEDIR}</base-dir> <db env="test"> Standard connection. <name>addresses</name> <user role="admin">scott</user> <pwd>tiger</pwd> <host>example.com</host> <driver> <vendor>MySql</vendor> <version>3.23</version> </driver> </db> <db env="prod"> <name>addresses</name> <user>production</user> <pwd>secret</pwd> <host>example.com</host> <driver> <vendor>Oracle</vendor> <version>8.1</version> </driver> </db> </config> To load and parse this file, you have to do the following: require 'xmlconfigfile' config = XmlConfigFile.new('example.xml') Now you can access all the configuration file's entries via XPath. To get the content of the version element as a String, simply call version = config.get_string('/config/version') # -> '1.7'or even shorter version = config['/config/version'] # -> '1.7' To get the version element as float value, call version = config.get_float('/config/version') # -> 1.7This works similar for integer values: splash_delay = config.get_int('/config/splash-screen/@delay') # -> 5000Of course, all Ruby literals for integer and float values (hex, octal, exponential notation, etc.) are supported. Boolean values are a bit different. The following table shows, which values by default mean true respectively false in configuration files handled by XmlConfigFile:
splash_enabled = config.get_boolean('/config/splash-screen/@enabled') # -> true If you want to provide your own values for true and false, just do this: config.true_values = ["HIja'", "HISlaH"] config.false_values = ["ghobe'"]Now, the Klingon phrases HIja' and HISlaH mean true and ghobe' means false. Of course, whitespace and case will still be ignored. But keep in mind, please, that truth is always better than falseness, i.e., if true_values and false_values share common elements, the meaning of these elements is true. Default ValuesAll the get methods described above allow you to provide a default value that will be returned in case the parameter requested does not exist: config['/config/version', '1.0'] # -> '1.7' config['/unknown/parameter', 'frodo'] # -> 'frodo' config.get_int('/unknown/parameter', 42) # -> 42If you do not provide a default value, nil will be returned: config['/unknown/parameter'] # -> nil Handling Sets of Configuration ParametersConfiguration files do often contain different variants of configuration parameters, for example for different countries, for different languages, or for different environments. With XPath, it's simple to keep them all in a single configuration file. If you want to get the german version of our friendly greeting element, just call greeting = config["/config/greeting[@lang='de']"] # -> 'Hallo, Welt!' To get the name of your production database user, call user = config["/config/db[@env='prod']/user"] # -> 'production' You will often need a bunch of related configuration parameters at the same time. XmlConfigFile offers different ways to achieve this. The simplest way is to use a so called path prefix, that will be put in front of each XPath, i.e., instead of calling name = config["/config/db[@env='prod']/name"] # -> 'addresses' user = config["/config/db[@env='prod']/user"] # -> 'production' pwd = config["/config/db[@env='prod']/pwd"] # -> 'secret'you could use the slightly simpler version config.path_prefix = "/config/db[@env='prod']/" name = config["name"] # -> 'addresses' user = config["user"] # -> 'production' pwd = config["pwd"] # -> 'secret' But what, if you need a set of parameters as a whole? Therefore the get_parameters method does exist. It converts a node list into a Hash. The keys of this Hash are the paths to the single elements, where the tag names are separated by the '.' character by default. The root element (config in our case) will be excluded. So, to get all database configuration parameters for your test environment, you have to call dbParams = config.get_parameters("/config/db[@env='test']/*") The resulting Hash looks like this: dbParams = { db.driver.vendor => 'MySql', db.driver.version => '3.23', db.host => 'example.com', db.pwd => 'tiger', db.user => 'scott', db.name => 'addresses' } If you want to expand attributes, too, you have to do the following: config.expand_attributes = trueNow dbParams = config.get_parameters("/config/db[@env='test']/*")
returns:
dbParams = { db.driver.vendor => 'MySql', db.driver.version => '3.23', db.host => 'example.com', db.pwd => 'tiger', db.user => 'scott', db.user.role => 'admin', # Yes! Attributes will be returned, too! db.name => 'addresses' } By default, the single path elements will be separated by a '.' character. If you want to, you can specify an arbitrary string as path separator: dbParams = config.get_parameters("/config/db[@env='test']/*", "-silly-") This will result in the following Hash: dbParams = { db-silly-driver-silly-vendor => 'MySql', db-silly-driver-silly-version => '3.23', db-silly-host => 'example.com', db-silly-pwd => 'tiger', db-silly-user => 'scott', db-silly-user-silly-role => 'admin', db-silly-name => 'addresses' } To convert your whole configuration file into a Hash call: hashConfig = config.get_parameters('//*')But be careful: In the example configuration file above, many elements have the same 'path name', e.g. there are two elements that will be converted to 'db.user'. Only the last entry will survive! Also note, that it is not possible to access "orphaned" text nodes, e.g. the text 'Standard connection.' in the db element of our example configuration file will be ignored. If you really need access to a bunch of elements sharing the same name, you should try the following: dbParams = config.get_string_array("/config/db")The result looks like this: dbParams = [ { 'db.name' => 'addresses', 'db.user' => 'scott', 'db.pwd' => 'tiger', 'db.host' => 'example.com', 'db.driver.vendor' => 'MySql', 'db.driver.version' => '3.23' }, { 'db.name' => 'addresses', 'db.user' => 'production', 'db.pwd' => 'secret', 'db.host' => 'example.com', 'db.driver.vendor' => 'Oracle', 'db.driver.version' => '8.1' } ] Advanced FeaturesIn addition to the features described in the last section, you will find some advanced features, like referencing environment variables from your configuration files or an automatic reloading and observing mechanism.Using Environment VariablesIt is often useful to combine the usage of configuration files and environment variables. XmlConfigFile makes this task easy: You can put references to environment variables into your configuration files and they will get expanded to their actual values as the file is loaded. The syntax for such references is ${Name of environment variable} So, if you have an environment variable, that specifies a base directory as defined in our example configuration file, you can use it like this: baseDir = config['/config/base-dir'] # -> Current value of $BASEDIR Reloading the Configuration PeriodicallyIt is often convenient, if you can reconfigure a running system without stopping and restarting it. XmlConfigFile supports such a mechanism. Simply provide the length of the reload period (measured in seconds) while creating a new object of class XmlConfigFile: config = XmlConfigFile.new('example.xml', 300)The configuration file 'example.xml' will be checked for changes every five minutes now. If the file's modification timestamp has changed, it will be reloaded automatically. If the modified file is invalid or does not exist any longer, the last working version will be used and an error message will be sent to $stderr. Every time a configuration file is reloaded, references to environment variables will be replaced by their actual values. Please note, that a configuration file only will be reloaded, if the modification timestamp of the file was changed. So, if you only change the environment, nothing will happen until you touch the file.
If you no longer need the reloading thread, you should be a good
citizen and call Observing a Configuration FileIn many cases it will not be sufficient to reload a configuration file periodically. Probably some parts of your application need to be informed immediately about such changes. E.g. you have to re-initialize a database connection, if the according configuration parameters have changed. Therefore it is possible to add so called observers to an instance of class XmlConfigFile: class Orwell def initialize(config) config.add_observer(self) @config = config end def update(*args) filename = args[0] puts "Configuration file #{filename} has changed." sample = @config['/just/another/xpath'] end end config = XmlConfigFile.new('config.xml', 300) orwell = Orwell.new(config)In the example above, the configuration file 'config.xml' will be checked for changes every five minutes. If something has changed, the instance of class Orwell will be informed, i.e., its update method will
be called with the configuration file's name. So, the two things you have
to do to become an observer are:
The observers of a configuration file will also be notified, if the
configuration was changed using the Changing and Storing a Configuration FileThe ability to change and store a configuration file turns class XmlConfigFile into a preferences package. Currently it is only possible to change existing parameters, but future versions of XmlConfigFile will provide the possibility to create new nodes automatically, if it is necessary. To change an existing attribute or element, you have to do the following: config.set_parameter('/config/splash-screen/@enabled', 'no')The statement above will set the enabled attribute of element splash-screen to false, i.e. it disables the splash screen. Of course, it would be very useful to make this user decision persistent: config.storeThis will overwrite the original configuration file with the current configuration held in memory. If you want to store the current configuration into another file, you have to provide a filename: config.store('another_file.xml') Further ReadingIf you are interested in absolute truth, you will have to look at the source code or the API docs. AcknowledgementsA big "Thank you!" (in no particular order) goes to
ContactIf you have any suggestions or want to report bugs, please contact me (contact@maik-schmidt.de). |