ORIGINAL DRAFT

An increasing number of applications are making use of XML as a common representation for configuration files. Besides the notion of an open standard, there is a great deal of flexibility made possible by this kind of implementation. By assuming that the collection of configuration elements will be small enough to encapsulate in a Document Object Model, we can readily access hierarchically organized values, loading and saving them on-demand.

In this article we’ll develop a simple XML configuration infrastructure that provides access to values by using tag paths and optional attribute names. To maximize flexibility we’ll provide a callback mechanism for interpreting lists of elements, so that you can use custom structures to represent more complex objects.

One of our objectives is to handle arbitrary XML configuration files. By this, I mean that a DTD may not be present and in some cases configuration paths or values may not be present in the file. What typically occurs when values are not present depends on the application. Some choose to add a new value, prompting the user or adding a default, while others return a default value without changing the content of the file. We’ll make sure both these choice are available.

Figure 1: JConfig Classes.

Figure 1: JConfig Classes.

Figure 1 shows the class relationship for the building blocks we’ll be using. The XMLConfig class is the main class. A ConfigPath class provides a path representation, including optional attributes. We’ll take a closer look at this momentarily. The ConfigValue class provides a way of representing return values such that they can be easily converted to various data types, with support for default values when appropriate.

The ListConverter interface is a vehicle for retrieving objects lists by translating element lists at a given path. We’ll provide a ListConverterExample that shows you how to handle points and rectangle objects to illustrate this technique. The ListConverter interface is simple and expects a list of JDOM Element objects. The output List can contain any type of object you need.

public interface ListConverter
{
  public List convert(List elements);
}

Before we look at the ListConverterExample, let’s take a look at the ConfigPath and XMLConfig files. But first, we’ll mention the classes you won’t see listed here. You can download these classes from www.xmlmag.com.

Along with the classes is an example configuration file called “ConfigExample.xml”. The XMLConfigTest file uses “ConfigExample.xml” to shows a number of variants for paths, attributes and lists of compound values. We demonstrate access to integer values, lookups of existing and non-existent values, additions to the hirerarchy, etc.

ConfigValue is a class that stores a string value and provides accessors for retrieving the string in various forms. This implementation converts to integer and double values but can easily be extended to support any primitive type, leaving compound objects to the ListConverter interface. Each accessors you find in ConfigValue has two variants, the second one offering the ability to provide an optional default value.

The first class we’ll look at is ConfigPath, in Listing 1. This is a class that represents a logical path to a specific node. It’s worth mentioning that something like XPath could have been used here, but I intentionally avoided doing this for a few reasons. The first is that XPath is much more powerful than what’s needed here and likely to cause more confusion than clarity.

The second reason I avoided using XPath is that I wanted to support paths with configurable delimiters. For example, a path might be specified as either “root.node.leaf” or “root/node/leaf”. Using XPath would have constrained this to using the second form only. What’s more, it seemed more intuitive to me to allow a path with an attribute to be represented as “root/node/leaf@attribute” but XPath expects to see “root/node/leaf/@attribute” which didn’t fit my model of what configuration paths should look like.

ConfigPath expects a string argument in the constructor and provides protected methods for tokenizing and parsing this path into a set of nodes. A Node is an internal representation of a tag name in the XML file. To facilitate working with paths, however, the nodes are parsed out when the ConfigPath object is instantiated.

The ConfigPath class has two internal classes. A Node class represents each segment in a path and has a name associated with the tag it should match. We provide a toString method that prints that name and a constructor that expects a name argument. There is also a specialization of a Node in the form of an Attr inner class, which is used to represent an attribute name.

Each of the nodes are stored in a sequence as a List. If there is an attribute, we expect it to be the last element. You’ll notice that we provide utility methods that allow you to set both the node and attribute delimiter characters as well as a getDepth method that returns the path depth in Nodes (excluding any attribute node) and a hasAttribute method that lets us easily figure out if this is a path that includes an attribute.

Two accessor methods allow us to retrieve a Node by index value or the attribute where applicable. The getNode method expects an index value between zero and getDepth minus one. If an attribute is part of the path, the last element, the getDepth method will return the size of the list minus one. If you call the getAttr method on a path with no attribute, you’ll get a null value. The only other method in this class is the toString method, which glues the path back together for presentation purposes.

The XMLConfig class in Listing 2 is the main class you’ll use to access configuration files. It’s intended for use as a factory that returns the same configuration file reference whenever you call the getInstance method. You can load the file or save it explicitly. Once loaded, you have full access to path elements. The getValue and setValue methods get and set a value at a given path, respectively. The path mechanism handles attributes transparently.

To support this functionality, there are two protected methods, makeElement and findElement. The first method will create an element and any parent elements if they are not presented and is used by the setValue method. The second method, findElement, will return a null value if any node along the path does not exist, or the Element at the end of the path if it does.

The getValue method always returns a ConfigValue object, which can be used to ask for the value in different ways. Some methods in the ConfigValue class handle null values by using a default value argument, returned if no explicit value is available. Other methods, without default arguments, may return a null value if none is available.

The last method in XMLConfig is the getList method, which expects a ListConverter argument. If this argument is null, the Element list returned directly. You should avoid that condition if possible. Normally, you’ll want to use a ListConverter instance to convert the Element list to something more useful, typically a list of Java objects that have more a specific meaning.

Let’s take a look at the ListConverterExample class, in Listing 3, to show how you can handle collections of compound objects. I’ve chosen to handle Point and Rectangle object but any object can be represented in similar ways. I’ve also chosen to use attributes to store coordinate values but these could just as well have been handled with child XML tags and values stored as content for those tags. How you chose to represent objects is entirely under your control.

There is only one method in the ListConverterExample, implementing the ListConverter interface. We expect to process a list of JDOM Element objects, so after creating a new List for output, we walk through the list and cast each entry as an Element object. We then branch based on the name of the element, handling points and rectangles slightly differently.

In the case of a POINT tag, we look for an x and y attribute and cast the values to integers. In a production system, you’ll probably want to do better error handling, telling users when there’s something wrong with the configuration file content. When we handle a RECT tag, we expect the x, y, w and h attributes to be present, and again each value is expected to be an integer. The last step in each case is to create an instance of the target object, adding it to the output list.

You’ll notice that this technique gives you considerable flexibility. You can handle collections of similar objects at a specified node, even though the path to each object cannot be specified by a unique string. You can also filter objects, ignoring those you are not interested in on a given pass, using different ListConverter instances to retrieve them in separate calls to getList. Finally, you can return the Element list as-is and do something else with it entirely if you like.

There’s no question that XMLConfig is a solution that could be handle in other ways. You could use Property files or the new Preferences API in Java 1.4. In practice, however, properties files break down as your application gets more complex and the Preferences API (which uses the platform’s hierarchical configuration storage) is difficult for users to edit by hand. While it is good to protect users from manual configuration changes, I’ve yet to see an application in which it wasn’t necessary at some point to do exactly that.

The XMLConfig infrastructure is fairly simple yet powerful enough to provide you with the tools you need to use XML as the basis for your configuration files. The ListConverter interface provides a powerful mechanism for handling more complex Java object representations while retaining the readability and editability of XML files. I won’t profess that this is a perfect solution to configuration handling, but it is a small step toward a standard that more and more projects are moving toward.

Listing 1

import java.util.*;

public class ConfigPath
{
  public static final String NODE_DELIMITER = "/";
  public static final String ATTR_DELIMITER = "@";
  
  protected String nodeDelimiter = NODE_DELIMITER;
  protected String attrDelimiter = ATTR_DELIMITER;
  protected List nodes;

  public ConfigPath(String path)
  {
    nodes = parse(tokenize(path));
  }
  
  public void setNodeDelimiter(char chr)
  {
    nodeDelimiter = "" + chr;
  }
  
  public void setAttrDelimiter(char chr)
  {
    attrDelimiter = "" + chr;
  }
  
  public int getDepth()
  {
    int depth = nodes.size();
    if (hasAttribute()) depth -= 1;
    return depth;
  }
  
  public boolean hasAttribute()
  {
    int last = nodes.size() - 1;
    Node node = getNode(last);
    return node instanceof Attr;
  }
  
  public Node getNode(int index)
  {
    return (Node)nodes.get(index);
  }
  
  public Attr getAttr()
  {
    int last = nodes.size() - 1;
    Node node = getNode(last);
    if (node instanceof Attr)
    {
      return (Attr)node;
    }
    else
    {
      return null;
    }
  }

  protected String[] tokenize(String path)
  {
    StringTokenizer tokenizer = new StringTokenizer(
      path, nodeDelimiter + attrDelimiter, true);
    int count = tokenizer.countTokens();
    String[] tokens = new String[count];
    for (int i = 0; i < count; i++)
    {
      tokens[i] = tokenizer.nextToken();
    }
    return tokens;
  }
  
  protected List parse(String[] tokens)
  {
    List list = new ArrayList();
    for (int i = 0; i < tokens.length; i++)
    {
      if (!tokens[i].equals(nodeDelimiter) &&
          !tokens[i].equals(attrDelimiter))
      {
        if (i > 0 && tokens[i - 1].equals(attrDelimiter))
        {
          list.add(new Attr(tokens[i]));
        }
        else
        {
          list.add(new Node(tokens[i]));
        }
      }
    }
    return list;
  }
  
  public String toString()
  {
    StringBuffer buffer = new StringBuffer();
    for (int i = 0; i < nodes.size(); i++)
    {
      buffer.append(nodes.get(i).toString());
    }
    return buffer.toString();
  }
  
  public static class Node
  {
    protected String name;
    
    public Node(String name)
    {
      this.name = name;
    }
    
    public String getName()
    {
      return name;
    }
    
    public String toString()
    {
      return "" + NODE_DELIMITER + name;
    }
  }

  public static class Attr extends Node
  {
    public Attr(String name)
    {
      super(name);
    }
    
    public String toString()
    {
      return "" + ATTR_DELIMITER + name;
    }
  }
  
  public static void main(String[] args)
  {
    String text = "/this/is/a/test@home";
    ConfigPath path = new ConfigPath(text);
    System.out.println(path);
  }
}

Listing 2

import java.io.*;
import java.util.*;

import org.jdom.*;
import org.jdom.input.*;
import org.jdom.output.*;

public class XMLConfig
{
  protected static Map map = new HashMap();
  
  protected Document doc;
  protected File file;
  
  private XMLConfig(File file)
  {
    this.file = file;
  }
  
  public static XMLConfig getInstance(File file)
  {
    if (!map.containsKey(file))
    {
      map.put(file, new XMLConfig(file));
    }
    return (XMLConfig)map.get(file);
  }

  public void load() throws JDOMException
  {
    DOMBuilder builder = new DOMBuilder();
    doc = builder.build(file);
  }

  public void save() throws IOException
  {
    XMLOutputter outputter = new XMLOutputter(" ", true);
    FileWriter writer = new FileWriter(file);
    outputter.output(doc, writer);
    writer.close();
  }

  protected Element makeElement(ConfigPath nodes)
  {
    Element child = doc.getRootElement();
    String name = nodes.getNode(0).getName();
    if (!child.getName().equals(name))
    {
      throw new IllegalArgumentException(
        "Root node must be CONFIG");
    }
    int depth = nodes.getDepth();
    for (int i = 1; i < depth; i++)
    {
      name = nodes.getNode(i).getName();
      Element parent = child;
      child = child.getChild(name);
      if (child == null)
      {
        child = new Element(name);
        parent.addContent(child);
      }
    }
    return child;
  }
  
  protected Element findElement(ConfigPath nodes)
  {
    Element child = doc.getRootElement();
    String name = nodes.getNode(0).getName();
    if (!child.getName().equals(name))
    {
      return null;
    }
    int depth = nodes.getDepth();
    for (int i = 1; i < depth; i++)
    {
      name = nodes.getNode(i).getName();
      child = child.getChild(name);
      if (child == null) return null;
    }
    return child;
  }
  
  public void setValue(String path, ConfigValue value)
  {
    setValue(path, value.toString());
  }
  
  public void setValue(String path, String value)
  {
    ConfigPath nodes = new ConfigPath(path);
    Element child = makeElement(nodes);
    if (nodes.hasAttribute())
    {
      String attr = nodes.getAttr().getName();
      if (child.getAttribute(attr) != null)
      {
        child.removeAttribute(attr);
      }
      child.addAttribute(attr, value);
    }
    else
    {
      child.setText(value);
    }
  }
  
  public ConfigValue getValue(String path)
  {
    ConfigPath nodes = new ConfigPath(path);
    Element child = findElement(nodes);
    if (child == null)
    {
      return new ConfigValue(null);
    }
    if (nodes.hasAttribute())
    {
      String attr = nodes.getAttr().getName();
      String value = child.getAttributeValue(attr);
      return new ConfigValue(value);
    }
    else
    {
      String value = child.getText();
      return new ConfigValue(value);
    }
  }
  
  public List getList(String path, ListConverter converter)
  {
    ConfigPath nodes = new ConfigPath(path);
    Element child = findElement(nodes);
    if (child == null) return null;
    List list = child.getChildren();
    if (converter == null) return list;
    return converter.convert(list);
  }
}

Listing 3

import org.jdom.*;

import java.awt.Point;
import java.awt.Rectangle;
import java.util.*;

public class ListConverterExample
  implements ListConverter
{
  public List convert(List elements)
  {
    List list = new ArrayList();
    for (int i = 0; i < elements.size(); i++)
    {
      Element elem = (Element)elements.get(i);
      String name = elem.getName();
      if (name.equalsIgnoreCase("POINT"))
      {
        int x = Integer.parseInt(
          elem.getAttributeValue("x"));
        int y = Integer.parseInt(
          elem.getAttributeValue("y"));
        list.add(new Point(x, y));
      }
      if (name.equalsIgnoreCase("RECT"))
      {
        int x = Integer.parseInt(
          elem.getAttributeValue("x"));
        int y = Integer.parseInt(
          elem.getAttributeValue("y"));
        int w = Integer.parseInt(
          elem.getAttributeValue("w"));
        int h = Integer.parseInt(
          elem.getAttributeValue("h"));
        list.add(new Rectangle(x, y, w, h));
      }
    }
    return list;
  }
}