Confix

Overview

Confix stands for "configuration" and "xml". Indeed, the Confix API intends to spare developers from the tedious task of "manually" parsing XML configuration files, replacing this approach with a direct translation of XML configurations into object format. Trying the API is to become addicted; you will never want to manually process XML configuration files ever again.

Features

To wet your appetite, let us give you an insight into the API's features:

  • translates XML configuration files into objects;
  • uses Java's Reflection API to instantiate and initialize the objects corresponding to the configuration;
  • XML elements are converted into objects;
  • supports XML namespace definitions;
  • XML attributes are mapped to "setters";
  • automatic type conversion: XML attributes are automatically converted to the type specified by their corresponding setters (int, boolean, long, etc.)
  • implements the factory pattern: separates XML traversal from object creation - the latter is delegated to "object factories".
  • allows parts of the XML to bypass the conversion mechanism: the XML will remain as such if some objects are interested in manipulating the XML themselves;
  • flexible, interface-based XML-to-object conversion strategies, suitable to solve most configuration pecularities.

Architecture

As was mentioned above, the API allows to convert XML configurations into objects, a cumbersome task that we have all done at some point. At Sapia, we became so fed up with it that we have decided to implement a toolkit that does it for us. The Confix architecture cleanly separates XML traversal from object creation. The architecture is based on the following entities:

  • The Confix Processor: the processor traverses an XML configuration file - in fact, it does not traverse the "file" per say, but rather some representation of it (to give you an insight, there is a SAX processor and a JDOM processor). The processor deals with an object factory to retrieve objects corresponding to the XML elements in the configuration - see next item for more on object factories. The processor is also in charge of assigning the values that belong to each created object's corresponding XML element - the attribute values are assigned on the created objects.
  • The Object Factory: every time an XML element is encountered by the processor, the latter calls its object factory to instantiate an object that corresponds to the element that was encountered. An object factory implements a specific strategy to instantiate objects - one could use Java's Reflection API, another hard-coded if/then logic, etc. By using a factory, the processor is abstracted from instantiation of objects corresponding to the XML elements.

Learning by Example

To quickly learn an API, nothing matches the hands-on approach. This section provides a step-by-step Confix how-to.

Creating a Configuration

For the sake of complicity, we will create a simple configuration. The configuration shown below models a family:

<family name="Simpson">
  <father   name="Homer" age="45"/>
  <mother   name="Marge" age="42"/>
  <boy      name="Bart"  age="10"/>
  <girl     name="Lisa"  age="10"/>
  <dog      name="Santa's Little Helper"
            age="2"
            animal="true"/>
</family>

Creating the Object Model

Now that we have a configuration, we will create the classes that correspond to each of the XML element found in it. First, we create a "Being" class - father, mother, boy, girl, and dog are all beings:

package org.sapia.util.xml.confix.examples;

public class Being{

  private boolean _animal;
  private String  _name;
  private int     _age;

  public void setAnimal(boolean animal){
    _animal = animal;
  }

  public void setName(String name){
    _name = name;
  }

  public void setAge(int age){
    _age = age;
  }
}

As you can see, there must be one setter for each XML attribute specified in the configuration. Confix will coerce the attribute values to the appropriate Java type. Attribute values must correspond to Java's primitive types, to wrapper object of the primitive type, or to String. The following table illustrates how attribute names are converted into method names:

Attribute NameMethod Name
namesetName
NamesetName
firstNamesetFirstName
FirstNamesetFirstName
first-namesetFirstName
first.namesetFirstName

Now, onto the Family class:

package org.sapia.util.xml.confix.examples;
import java.util.*;

public class Family{

  private Being   _father, _mother;
  private List    _boys  = new ArrayList()
  private List    _girls = new ArrayList()
  private List    _dogs  = new ArrayList()
  private String  _name;

  public void setName(String name){
    _name = name;
  }

  public void setMother(Being mother){
    if(_mother != null){
      throw new IllegalArgumentException(
       "Mother has already been assigned"
      );
    }
    _mother = mother;
  }

  public void setFather(Being father){
    if(_father != null){
      throw new IllegalArgumentException(
       "Father has already been assigned"
      );
    }
    _father = father;
  }

  public void addBoy(Being boy){
    _boys.add(boy);
  }

  public void addGirl(Being girl){
    _girls.add(girl);
  }

  public void addDog(Being dog){
    _dogs.add(dog);
  }
}

As you can see, setter and adder methods match the corresponding elements in our configuration. What the processor does upon encountering elements is the following:

  • It calls its object factory so that the latter creates an object corresponding to the encountered element.
  • It tries to assign this new object to its parent, if the parent is not null - the only case when null occurs is when processing the root element.

Assignment of the new object to its parent obeys the following rules:

  • The processor tries to find a setter on the parent object that matches the XML element name corresponding to the new object;
  • if a setter is found, it is invoked - the type of the new object must match the type of the parameter that is accepted by the setter;
  • if no setter is found, an adder is searched;
  • if an adder is found, it is invoked;
  • if no setter or adder is found, an exception is thrown.
Setters and adders are searched in a manner similar as for attributes; the same method naming patterns are taken into account - see table above.

Thus, the processor resolves elements to objects in a recursive manner, following a dept-first algorithm - a child is assigned to its parent only once it has itself completely been initialized. At the end of the process, one obtains a complete object graph.

Creating the Object Factory

We have our configuration and our object model. Now we must implement the factory that will instantiate the objects corresponding to our configuration. To do so, we implement the ObjectFactoryIF interface.

package org.sapia.util.xml.confix.examples;

import org.sapia.util.xml.confix.CreationStatus;
import org.sapia.util.xml.confix.ObjectCreationException;
import org.sapia.util.xml.confix.ObjectFactoryIF;
import java.util.*;

public class FamilyObjectFactory
             implements ObjectFactoryIF{

  private static Set _beings = new HashSet();

  static{
    _beings.add("father");
    _beings.add("mother");
    _beings.add("boy");
    _beings.add("girl");
    _beings.add("dog");
  }

  CreationStatus newObjectFor(String aPrefix,
                              String aNamespaceURI,
                              String anElementName,
                              Object aParent)
                   throws ObjectCreationException{
    if(_beings.contains(anElementName){
      return CreationStatus.create(new Being());
    }
    else if (anElementName.equals("family")){
      return CreationStatus.create(new Family());
    }
    else{
      throw new ObjectCreationException(
       "Element not recognized: " + anElementName
      );
    }
  }
}

As can be seen, implementing an object factory is quite straightforward; one only needs implementing the newObjectFor(...) factory method, and returning a CreationStatus instance that contains the created object - we will see more about the "creation status" further below.

The creational method takes many arguments: the first three arguments pertain to the XML configuration itself; the last argument is the parent object of the one that is to be created by the method. The parent object is null if no object has been created yet - meaning that the element name passed in corresponds to the name of the root element in the configuration.

Using the Processor

Now that we have our factory, we are ready to rock. As seen earlier, the Confix processor is in charge of traversing an XML resource and to create an object representation of that XML. The interface ConfixProcessorIF is in charge of that and it has only one method:

public Object process(InputStream is)
                                  throws ProcessingException;

The code below shows how a Family instance is created. You will see that there are some steps involved before being able to get our Family object. This example shows a factory method that creates Family objects from an input stream that contains the XML family configuration.

package org.sapia.util.xml.confix.examples;

import java.io.InputStream;
import org.sapia.util.xml.confix.ConfixProcessorFactory;
import org.sapia.util.xml.confix.ConfixProcessorIF;
import org.sapia.util.xml.confix.ObjectFactoryIF;

public class FamilyFactory {

  /**
   * Factory method that creates a Family instance from
   * the stream over the XML passed in. It return null
   * if an error occurs.
   */
  public static Family
    createFamily(InputStream anXmlStream) {

    Family aFamily = null;

    try {

      // Creating an instance of our
      // family object factory
      ObjectFactoryIF anObjectFactory =
          new FamilyObjectFactory();

      // Creating a new processor factory instance
      ConfixProcessorFactory aProcessorFactory =
          ConfixProcessorFactory.newFactory();

      // Getting a Confix processor for
      // our object factory
      ConfixProcessorIF aProcessor =
          aProcessorFactory.createProcessor(
            anObjectFactory);

      // Finally, we process the input stream of
      // the configuration
      aFamily = (Family)
          aProcessor.process(anXmlStream);

    } catch (Exception e) {
      System.err.println(
       "Error while processing the family configuration");
      e.printStackTrace();
    }

    return aFamily;
  }
}

Getting a Processor Factory

To use a Confix processor, we first need to get a ConfixProcessorFactory. This factory is responsible for creating the processor that we will use. Since there are multiple processor implementations, the factory prevents us from tying our code directly to the implementation. We create a processor factory by invoking the newFactory() static method.

Getting a Processor Instance

JAXP-like Discovery

Now that we have the processor factory, we can create a ConfixProcessorIF using the createProcessor(ObjectFactoryIF) method . This method takes as an argument the ObjectFactoryIF to be used by the created processor.

How does the processor factory know which implementation of the Confix processor to create? Well, the factory uses a mechanism similar to Sun's JAXP specification. It looks at different places for the name of the ConfixProcessorIF class for which an instance should be created. Once it finds a class name, it looks for a constructor that takes has only argument an ObjectFactoryIF. The algorithm that searches for the class name is defined below; the factory looks for:

  1. the org.sapia.xml.ConfixProcessor system property - if defined and accessible;
  2. the value of the property org.sapia.xml.ConfixProcessor of the file $JAVA_HOME/jre/lib/sapia.properties if it exists;
  3. the Jar Service Provider discovery mechanism specified in the Jar File Specification. A jar file can have a resource with the name META-INF/services/org.sapia.xml.ConfixProcessor containing the name of the concrete class to instantiate.

The above algorithm stops as soon as a processor can be determined; if no processor class can be found, the fallback default implementation (SAXProcessor) is used. There are other processor implementations that come with Confix, based on JDOM and DOM4J - see below.

The Manual way

You can get hold of a ConfixProcessorIF by instantiating it yourself. There are three implementations:

The above respectively use SAX, JDOM and DOM4J to create objects from XML. You will need the appropriate dependencies in your classpath at runtime, of course.

Processing the XML

Finally, we now have a Confix processor that uses our custom object factory. The last step is to call the process(InputStream) method, passing in the input stream that contains our XML document. The result of that call is either a Family instance returned (in our specific example) or a ProcessingException - thrown if an error occured while processing the XML.

Advanced Issues

XML Element Content

The previous example showed a simple configuration file where XML elements contain only attributes or nested elements. What about content of XML element? Imagine the following configuration file:

<family>
  <name>Simpson</name>
  <father>
    <name>Homer</name>
    <age>45</age>
  </father>
  <mother>
    <name>Marge</name>
    <age>42</age>
  </mother>
</family>

As you can see, this new configuration represents the same content as the one seen earlier. Instead of using attributes to define the values, it uses the content of the element. This encoding style is similar to the SOAP encoding defined by the Simple Object Access Protocol specification. How will the Confix processor react when encountering such an encoding? There are two mechanisms put in place in the processor, and each occurs in a specific scenario: when the object factory is not able to create an object for the encountered element, and when the factory creates one.

When the Confix processor asks the object factory to create an object for an XML element and the factory can't do it, the processor as a fall-back mechanism that tries to assign the content of the current element, for which no object was created, to the parent object using the current element name. For instance, when processing the XML configuration above, the processor would first create a Family instance and then it would ask the object factory to create an object for the XML element name. Assuming we are using the same object factory as in the previous example, no object would be created by the factory. The processor then uses the fallback mechanism and tries to call the method setName() on the Family instance, passing in the content of the element (Simpson).

The second mechanism is simpler and occurs when the content of an XML element is encountered, and the object factory could create an object for the given element. In that scenario, the content of the XML element is assigned to its target object looking for a setText() or addText() method - as if it was an XML attribute/element named text. As an example, imagine that the object factory that creates family objects was modified to create an object for the name element. The processor would then call a setText() or addText() method one the Name instance, passing the value Simpson to the method.

As an extension of the behavior described previously, the JDOMProcessor also supports passing complex objects to methods that are represented by an XML element, as in the following:

<family>
  <name>Simpson</name>
  <father>
    <being>
      <name>Homer</name>
      <age>45</age>
    </being>
  </father>
</family>

The above is quite similar to what you have already seen, except that this time, the "father" element has been decoupled from the Being object. What the JDOMProcessor expects here then is a setFather or addFather method, and it tries to acquire an object from the underlying factory, using the being element - this would of course require a modification to our factory class, which would create Being instances when given "being" as an element. Note that the last requirement that must be met for the processor to behave this way is to have, within the element that represents the method to call (in this case, "father"), a single root element (corresponding to the object to pass to the method).

Built-In Factories

The API comes bundled with object factory implementations: CompositeObjectFactory and ReflectionFactory. The former, as its name implies, is in fact an aggregation of other factories. Each object factories added to the CompositeObjectFactory is associated to a XML namespace URI or namespace prefix. When the newObjectFor(...) method of the composite factory is called, it looks for the factory that was registered for the namespace URI or namespace prefix of the encountered element. It then delegates the creation logic to that object factory. The mapping logic is either done on the namespace URI or on the namespace prefix. By default, the factory uses the namespace URI has the identifier of the factory to use, but that behavior can be changed using the setMapToPrefix() method - see the javadoc for more information.

The composite factory is convenient when one wants to create configuration files that correspond to objects categorized on a per-namespace basis. To give you an example, one could build a task-based system configured through XML (such as Ant), where tasks in fact correspond to classes, and for which instance are created when their corresponding elements are encountered:

...
<tasks xmlns:somePrefix="http://acme.org/task"
       xmlns:someOtherPrefix="http://myuri.net">
  <somePrefix:copy
    fromDir="c:/website/html"
    toDir="ftp.somehost.com/public_html" />

  <someOtherPrefix:copy
    fromDir="c:/website/html"
    toDir="g:/backup/website/html" />
</tasks>
...

As you can see, with such a pattern, two tasks can have the same local name, yet confusion is avoided by relating each task to its own namespace. Such a model could allow for the extension of the task system by different contributors, with each contributor being attributed its own namespace - therefore avoiding name collision.

The other object factory implementation that comes with Confix creates objects using Java reflection. It proceeds as follows:

  • First, it will try to find a createXXXX() or addXXXX() no-args method that returns an object, and whose name matches the received XML element's name - users familiar with implementing Ant tasks certainly know about this pattern. If such a method is found, it is invoked, and the object that was created is returned in a CreationStatus instance.
  • if the above fails, it will attempt to create an object by trying to match the element name it receives to a class name. Indeed, if you look at the constructor of the class, you will see that it takes an array of strings; these strings are assumed to be package names. When receiving a newObjectFor(...) call, the factory iterates through its package names, concatenating the passed in element name to each package name - this gives the fully qualified name of the potential class to create an instance from. It tries to create an instance of that class using a Class.forName(...).newInstance() call; if the call succeeds, the created object is returned - within a CreationStatus instance. Ultimately, if no object could be created, an ObjectCreationException is thrown.

The first creational method used deserves a bit more explanation - or illustration. To that end, have a peek at the code below:

package org.sapia.util.xml.confix.examples;
import java.util.*;

public class Family{

  private Being   _father, _mother;
  private List    _boys  = new ArrayList()
  private List    _girls = new ArrayList()
  private List    _dogs  = new ArrayList()
  private String  _name;

  public void setName(String name){
    _name = name;
  }

  public Being createMother(){
    if(_mother != null){
      throw new IllegalArgumentException(
       "Mother has already been assigned"
      );
    }
    return _mother = new Being();
  }

  public Being createFather(){
    if(_father != null){
      throw new IllegalArgumentException(
       "Father has already been assigned"
      );
    }
    return _father = new Being();
  }

  public Being addBoy(){
    Being boy = new Being();
    _boys.add(boy);
    return boy;
  }

  public Being addGirl(){
    Being girl = new Being();
    _girls.add(girl);
    return girl;
  }

  public Being addDog(){
    Being dog = new Being();
    _dogs.add(dog);
    return dog;
  }
}

See how the Family class thus becomes itself a factory for its own objects? The ReflectionFactory interprets the adder an creator methods appropiately, by matching them with the corresponding XML elements. Furthermore, it is important to note that the objects thus created are automatically assigned to their parent (the family); this means that the processor should not later on try to assign the created object through corresponding setters or adders (as is normally the case).

How does the processor know that it should not do this? Well, this is what the CreationStatus is about. The class has a flag (a boolean value) that is intended to tell to the processor if the created object has already been assigned to its parent; if the flag is set to true, then the processor will not try to assign the child to its parent. The wasAssigned() method allows the processor to introspect the flag; from within the factory, it is set to true through code similar to the following:

return CreationStatus.
              create(theCreatedInstance).
                assigned(true)

Now, to be consistent with the definition of our new Family, we revise our factory:

package org.sapia.util.xml.confix.examples;

import org.sapia.util.xml.confix.CreationStatus;
import org.sapia.util.xml.confix.ObjectCreationException;
import org.sapia.util.xml.confix.ReflectionFactory;
import java.util.*;

public class FamilyObjectFactory
             extends ReflectionFactory{

  CreationStatus newObjectFor(String aPrefix,
                              String aNamespaceURI,
                              String anElementName,
                              Object aParent)
                   throws ObjectCreationException{
    if (anElementName.equals("family")){
      return CreationStatus.create(new Family());
    }
    else{
      return super.newObjectFor(aPrefix,
                                aNamespaceURI,
                                anElementName,
                                aParent);
    }
  }
}

Our class now extends ReflectionFactory; as such, it benefits from the addXXXX and createXXXX pattern.

Object Wrappers

Imagine for a minute that we have a framework that allows to dynamically configure Java software components through XML: basically, we have some engine that instantiates components based on an XML configuration file. An excerpt of such a file is given below:

<components>
  <!--
      This component polls a server at a given
      URL to see if it is running and sends an
      alert email to the configured address if
      it isn't.
  -->
  <component class="org.acmesoft.StatusMonitor"
             url="http://www.acmesoft.net/someService"
             email="admin@acmesoft.net" />
</components>

What basically happens is that the XML file is processed by an application container; the container intantiates the components that make up the application dynamically. To process the XML and create objects from it, the Confix API is used - of course. In this case, we have a class that matches the "component" element in the above configuration. An instance of this class has a setClass() method that takes the name of the class of the component that is to be instantiated. In the configuration, we have a matching "class" attribute; all other attributes "belong" to the component that is to be instantiated. How does the Confix processor know that the "class" attribute belongs to the class that corresponds to the object that will represent our "component" element, and that the other attributes belong to the object that will be created using the specified class name? The code snippets below will help us answer that question:

import org.sapia.util.xml.confix.ObjectWrapperIF

public class ComponentElement implements ObjectWrapperIF{

  private Component _component;

  public void setClass(String className)
              throws Exception{
    _component = (Component)Class.forName(className)
                      .newInstance();
  }

  public void start(){
    if(_component == null){
      throw new IllegalStateException("'class' " +
       "attribute not set; " +
       "must be set before other attributes");
    }
    _component.start();
  }

  public Object getWrappedObject(){
    if(_component == null){
      throw new IllegalStateException("'class' " +
       "attribute not set; " +
       "must be set before other attributes");
    }
    return _component;
  }
}

Now, the StatusMonitor:

package net.acmesoft;

public class StatusMonitor implements Component{

  private String _url, _email;

  public void setEmail(String email){
    _email = email;
  }

  public void setUrl(String url){
    _email = email;
  }

  /**
   * Let`s say that this method is imposed by
   * the fictious "Component" interface that
   * this class implements...
   */
  public void start(){
     // ... monitoring logic here ...
  }

}

When the Confix processor encounters an attribute, it tries to call the corresponding setter; if it works, fine. If not, an exception is thrown... Except in the above case: if the object that was assumed to be the parent of the attribute is an instance of the ObjectWrapperIF interface and the search for a setter fails, the processor calls the getWrappedObject() method on the wrapper, an then tries to resolve the attribute on the wrapped instance. In our case, the ComponentElement class creates its wrapped object upon its setClass() method being called. Since the class does not have setters corresponding to the "url" and "email" attributes, the getWrappedObject() method will be called by the processor - to be robust, the class ensures at that point that its wrapped object has indeed been created. The processor then tries to find the setters (for "url" and "email") on the wrapped instance.

Object Handlers

In some cases, it might not be possible for a class to have an adder/setter for some XML elements, or we might not wish it - we might want to be able to process add-hoc elements, for which we do not want to define any getter or setter. In our case, imagine that we want to nest components - notice the nested "component" element:

<components>
  <component class="org.acmesoft.StatusMonitor"
             url="http://www.acmesoft.net/someService"
             email="admin@www.acmesoft.net">

             <component class="org.acmesoft.Logger"
                        file="./logs/log.txt" />
  </component>

</components>

We revise our code accordingly - notice the handleObject() method:

import org.sapia.util.xml.confix.ObjectWrapperIF;
import org.sapia.util.xml.confix.ObjectHandlerIF;
import org.sapia.util.xml.confix.ConfigurationException;

import java.util.*;

public class ComponentElement
             implements ObjectWrapperIF,
                        ObjectHandlerIF{

  private Object _component;
  private List   _children = new ArrayList();

  public void setClass(String className)
              throws Exception{
    _component = Class.forName(className)
                      .newInstance();
  }

  public void start(){
    if(_component == null){
      throw new IllegalStateException("'class' " +
       "attribute not set; " +
       "must be set before other attributes");
    }
    _component.start();

    ComponentElement current;

    for(int i = 0; i < _children.size; i++){
      current = (ComponentElement)_children.get(i);
      current.start();
    }

  }

  public Object getWrappedObject(){
    if(_component == null){
      throw new IllegalStateException("'class' " +
       "attribute not set; " +
       "must be set before other attributes");
    }
    return _component;
  }

  public void handleObject(String elementName,
                           Object toHandle)
                throws ConfigurationException{
    if(toHandle instanceof ComponentElement){
      _children.add(toHandle);
    }
    else{
      throw new ConfigurationException(
       "ComponentElement instance expected"
      );
    }
  }
}

As you can see, in this case, our ComponentElement class now also implements the ObjectHandlerIF interface. In our case, we do not specify an adder or setter method for nested "component" elements. Rather, we handle the objects corresponding to these elements in the handleObject() method. The Confix processor "knows" about the ObjectHandlerIF; if it cannot find any setter or adder for an object that corresponds to a given XML element, it will check if the current object is a ObjectWrapperIF; if the check fails, it will then check to see if the current object is a ObjectHandlerIF. If that is the case, it will call the handleObject() method, passing to the latter the object to assign.

The ObjectCreationCallback Interface

In some cases, it is not possible to map an XML element to a bean-like class (that has a no-arg constructor and javabean-like methods). For example, take the java.net.URL class; it could not be instantiated dynamically by Confix, and it has no setter/adder methods. Yet, you could need to create a URL instance dynamically. Well, guess what, the Confix API offers the ObjectCreationCallback interface to handle these type of situations: when the Confix runtime receives objects from object factories, it checks if these objects implement the above-mentioned interface; if so, a cast is performed, and the onCreate() method is called. The method returns an object which is used from then on - just as if it had been created by an object factory. Using our example then, here is how URL instances could be created:

public class URLTag 
  implements ObjectCreationCallback{

  private _link;
  
  public void setLink(String link){
    _link = link;
  }

  public Object onCreate() 
    throws ConfigurationException{
    
    if(_link == null){
      throw new 
        ConfigurationException("'link' not " +
          "specified for URL");
    }
    try{
      return new java.net.URL(_link);
    }catch(MalformedURLException e){
      throw new ConfigurationException("Invalid value " +
        "for 'link' attribute of URL", e);
    }
  }
}

The NullObject Interface

Imagine that you want some objects created by Confix to be ignored, based on given conditions. For example, taken our URL example above, imagine that you do not want to throw an exception if the link attribute has not been set; you just want Confix to forget about the URL. Well then, use the NullObject interface in that case:

public class URLTag 
  implements ObjectCreationCallback{

  private _link;
  
  public void setLink(String link){
    _link = link;
  }

  public Object onCreate() 
    throws ConfigurationException{
    
    if(_link == null){
      return new NullObject(){};
    }
    try{
      return new java.net.URL(_link);
    }catch(MalformedURLException e){
      throw new ConfigurationException("Invalid value " +
        "for 'link' attribute of URL", e);
    }
  }
}

When Confix encounters an NullObject, it just ignores it, an treats it just as if it had never been created.

Consuming Raw XML

The XMLConsumer interface

Imagine that in our framework, we want our components to be able to receive an XML configuration, in the form of a SAX InputSource. To do so, have the appropriate classes in your configuration object model implement the XMLConsumer interface. Instances of this interface are "recognized" by the processors, and raw XML is handled by them to the implementations. The code below introduces the Configuration class to demonstrate how this is done:

import org.sapia.util.xml.confix.XMLConsumer;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.xml.sax.InputSource;

public class Configuration implements XMLConsumer{

  private Element _conf;

  /**
   * @see XMLConsumer
   */
  public void consume(InputSource is) throws Exception{
    SAXReader reader = new SAXReader();
    _conf = reader.read(is).getRootElement();
  }

  protected Element getConfiguration(){
    if(_conf == null){
      throw new IllegalStateException("Configuration not specified");
    }
    return _conf;
  }

}

Now, we modify our ComponentElement class accordingly - notice the createConfiguration() and init() methods:

import org.sapia.util.xml.confix.ObjectWrapperIF;
import org.sapia.util.xml.confix.ObjectHandlerIF;
import org.sapia.util.xml.confix.ConfigurationException;

import java.util.*;

public class ComponentElement
             implements ObjectWrapperIF,
                        ObjectHandlerIF{

  private Object _component;
  private List   _children = new ArrayList();
  private Configuration _conf = new Configuration();

  public void setClass(String className)
              throws Exception{
    _component = Class.forName(className)
                      .newInstance();
  }

  public Configuration createConfiguration(){
    return _conf;
  }

  public void init() throws InitException{
    if(_component == null){
      throw new IllegalStateException("'class' " +
       "attribute not set; " +
       "must be set before other attributes");
    }
    _component.init(_conf.getConfiguration());

    ComponentElement current;

    for(int i = 0; i < _children.size; i++){
      current = (ComponentElement)_children.get(i);
      current.init();
    }
  }

  public void start(){
    if(_component == null){
      throw new IllegalStateException("'class' " +
       "attribute not set; " +
       "must be set before other attributes");
    }
    _component.start();

    ComponentElement current;

    for(int i = 0; i < _children.size; i++){
      current = (ComponentElement)_children.get(i);
      current.start();
    }

  }

  public Object getWrappedObject(){
    if(_component == null){
      throw new IllegalStateException("'class' " +
       "attribute not set; " +
       "must be set before other attributes");
    }
    return _component;
  }

  public void handleObject(String elementName,
                           Object toHandle)
                throws ConfigurationException{
    if(toHandle instanceof ComponentElement){
      _children.add(toHandle);
    }
    else{
      throw new ConfigurationException(
       "ComponentElement instance expected"
      );
    }
  }
}

Now, our revised status monitor - notice the init() method:

package net.acmesoft;

import org.jdom.Element;

public class StatusMonitor implements Component{

  private String _url, _email;

  public void setEmail(String email){
    _email = email;
  }

  public void setUrl(String url){
    _email = email;
  }

  public void init(Element conf){
     // ... process config here ...
  }

  public void start(){
     // ... monitoring logic here ...
  }
}

And, finally, the updated configuration - notice the nested configuration element:

<components>
  <component class="org.acmesoft.StatusMonitor"
             url="http://www.acmesoft.net/someService"
             email="admin@www.acmesoft.net">

             <configuration>
               <message>The service is down!!!</message>
             </configuration>

             <component class="org.acmesoft.Logger"
                        file="./logs/log.txt" />
  </component>

</components>

In addition, nothing stops your handler from also specifying its own attributes.

The XMLConsumer interface is only recognized by the JDOMProcessor and the Dom4jProcessor classes.

Using the SAXProcessor

The SAXProcessor uses a SAX parser to navigate through the XML. Similarly to the JDOMProcessor and Dom4jProcessor classes that allow accessing XML "as is", directly, the SAXProcessor allows receiving the SAX events generated by the underlying SAX Parser. To manipulate these events, you need to implements the HandlerStateIF interface. For the ones familiar with the SAX API, you will see that this interface is similar to the SAXHanlder interface. In fact, the SAXProcessor uses a small framework based on the state pattern that gives the ability to define parsing logic by element. Each piece of logic represents a "state" that knows how to handle given SAX events. This approach has the advantage of breaking the necessary logic to parse complex XML documents into small pieces. When parsing an XML document, a HandlerContextIF instance is passed. This context gives you the possibility to change the state of the parser, passing in another HandlerContextIF. For example the Configuration class defined previously to handle raw XML input could be rewritten as follows:

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.sapia.util.xml.parser.HandlerContextIF;
import org.sapia.util.xml.parser.HandlerStateIF;

public class Configuration implements HandlerStateIF {

  private StringBuffer _message;

  protected String getMessage() {
    return _message;
  }

  public void startElement(
      HandlerContextIF aContext, String anUri,
      String aLocalName, String aQualifiedName,
      Attributes someAttributes)
      throws SAXException {
    if (aLocalName.equals("message")) {
      _message = new StringBuffer();
    }
  }

  public void endElement(
      HandlerContextIF aContext, String anUri,
      String aLocalName, String aQualifiedName)
      throws SAXException {
    // Tell the context we are done parsing the XML
    // putting back the previous state of the parser
    aContext.removeCurrentState(anUri,
                                aLocalName,
                                aQualifiedName);
  }

  public void characters(
      HandlerContextIF aContext, char[] someChars,
      int anOffset, int length) throws SAXException {
    if (_message != null) {
      _message.append(someChars, anOffset, length);
    }
  }

  public void ignorableWhitespace(
      HandlerContextIF aContext, char[] someChars,
      int anOffset, int aLength) throws SAXException {
  }
}

The HandlerStateIF interface is only recognized by the SAXProcessor.

Conclusion

Use Confix if, like us, you are fed up of processing XML configuration files manually. Confix also offers various hooks that allow you to bypass its normal behavior, making it a very flexible tool. After you'll have used it once, you will become addicted; you will compulsively make your stuff configurable. Moreover, you'll dump resorting to JDOM and DOM4J directly as means to process XML configuration files.