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 Name | Method Name |
| name | setName |
| Name | setName |
| firstName | setFirstName |
| FirstName | setFirstName |
| first-name | setFirstName |
| first.name | setFirstName |
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:
- the org.sapia.xml.ConfixProcessor system property
- if defined and accessible;
- the value of the property org.sapia.xml.ConfixProcessor of
the file $JAVA_HOME/jre/lib/sapia.properties if it exists;
- 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.
|