Managing Configurations Using Digester


Managing Configurations Using Digester

In the past, configuration files have been stored in a Windows ini-type format, which defines sections with key value pairs. Many UNIX formats that predate the Windows ini-type format are similar. Regardless of the format types, the future of applications lies in the usage of XML files as configuration formats. XML is a standard, and it is easy to use. The Digester package is used to read XML-based configuration files.

In Chapter 5, we used the betwixt package to serialize objects to and from XML. The Digester package, which is a dependency of the betwixt package, is an XML serialization package. You may ask why we didn't cover the Digester package in Chapter 5. Well, from day one, the betwixt package was intended to serialize Java objects to XML, whereas the Digester package was originally intended for reading configuration files. However, either one could be used for either task. After all, reading a configuration file is nothing more than de-serializing a set of configuration objects.

Technical Details for the Digester Package

Tables 8.4 and 8.5 contain the abbreviated details necessary to use the Digester package.

Table 8.4: Repository details for the Digester package.

Item

Details

CVS repository

jakarta-commons

Directory within repository

digester

Main packages used

org.apache.commons.digester (contains the core classes); org.apache.commons.digester.xmlrules (contains the classes used to parse XML digester configuration files)

Table 8.5: Package and class details (legend: [digester] = org.apache.commons.digester).

Class/Interface

Details

[digester].Digester

The core class that manages the rules for the Digester package. You will use this class the most often.

[digester].Rule

An abstract class that might be extended when you attempt to define custom actions and rules.

[digester].*Rule

Core classes that implement the various actions and rules. When the Digester class is used to add rules, these classes are created automatically.

[digester].xmlrules.DigesterLoader

A core class used to instantiate the Digester class instance from an XML digester configuration file.

Reading a Simple Configuration

In this chapter, we will be using the class BeanToWrite , which originates from Chapter 5 and was also used in Chapter 7. As a quick refresher, the class BeanToWrite has two properties: integerValue and stringValue . In this chapter, the object is to be able to read a simple configuration file, as shown in Listing 8.12. The configuration file data is used to populate the class BeanToWrite .

Listing 8.12
start example
 <body> <element1>hello</element1>  <element2>1234</element2> </body> 
end example
 

The XML document in Listing 8.12 is processed by Listing 8.13.

Listing 8.13
start example
 Digester digester = new Digester(); digester.addObjectCreate( "body", BeanToWrite.class); digester.addCallMethod( "body/element1", "setStringValue", 0); digester.addBeanPropertySetter( "body/element2", "integerValue"); BeanToWrite bean = (BeanToWrite)digester.parse( generateSimpleConfiguration()); 
end example
 

In Listing 8.13, the main class used to process a configuration file is the class Digester. If we want to cross-reference the class BeanToWrite to individual XML tags in Listing 8.12, we create digester rules. The concept of a digester rule is that if a specific XML tag is encountered , some action should be taken. The action to be taken could be to create a class, call a method, or assign a bean property.

To create a rule, an XML tag is needed. In Listing 8.12, there are three XML tags. Therefore, in the simplest case, the rules could be defined as follows :

  • When the XML tag body is encountered, create a BeanToWrite class instance.

  • When the XML tag element1 is encountered, assign the value to the property stringValue .

  • When the XML tag element2 is encountered, assign the value to the property integerValue .

The definition of the rules is a bit vague, but a computer needs precision. The problem is that the definition of the tag is vague. Let's say there is an XML tag title. The XML tag title occurs at multiple places in the XML document. Using the rules defined in the previous list would cause problems because the same rule could be triggered even though the XML tag title might be located in different places. The resolution is to define the location of the XML tag by providing the context of the location. Therefore, we need to rewrite the rules with a more precise location and action, as shown in this list of rules:

  • When the XML tag body is encountered, call the method addObjectCreate that creates a BeanToWrite class instance.

  • When the XML tag body/element1 is encountered, call the method addCallMethod , where the method to call is setStringValue .

  • When the XML tag body/element2 is encountered, call the method addBeanPropertySetter , where the property to set is integerValue .

Once all of the rules have been defined, the XML file can be parsed using the method parse. Returned will be an object instance of the class BeanToWrite . Of course, this assumes that no errors occurred while the XML file was processed.

Defining the Matching Patterns

The matching patterns define a location where an action should occur. In Listing 8.13, three rules were defined when the Digester object was used to call a method that included the word add . The parameters of the methods are contexts that trigger a specific action. The way that the actions are triggered is related to how the XML configuration file is processed. The configuration file is processed using the Simple API for XML (SAX). The SAX processing model is based on events firing whenever an XML element is encountered. The events are used to provide a location definition using a stack (more about stacks soon). This is why a location of an XML element is defined in absolute terms. Listing 8.14 converts Listing 8.12 to a stack of locations from the Digester package's point of view.

Listing 8.14
start example
 <body> body <element1>hello</element1> body/element1  <element2>1234</element2> body/element2 </body> 
end example
 

The side effect of using absolute positions to define a location is that it can get tedious . For example, the location body/something/somewhere/and/more/elements is a bit long. This is why the Digester package is better as a configuration tool than a generic serialization framework like the betwixt package. The Digester assumes that the XML file is easy to read and understand.

There is one exception to the absolute path reference rule, which is shown in Listing 8.15.

Listing 8.15
start example
 */element1 
end example
 

In Listing 8.15, the asterisk represents a wildcard. This wildcard indicates that XML element element1 can be matched anywhere in the XML document. In contrast, the patterns in Listing 8.16 are not the same elements in a logical sense.

Listing 8.16
start example
 body/element1 body/element1/element1 body/elsewhere/child/element1 
end example
 

In Listing 8.16, the matched element is element1 , but it exists in three different locations. And for each of the locations, a different rule is created.

Defining the Rules

It's important to remember what the stack concept means. The stack concept involves pushing things on a stack, like one would put a piece of paper on top of a stack of paper. The top of the stack is always the last elements pushed . The rules created by the consumer have the ability to manipulate the stack. For example, if the matching pattern body is found, then the class BeanToWrite is instantiated. The action responsible for instantiating the class pushes the instantiated class onto a stack. Multiple actions can be executed for each matching pattern. This can result in multiple instantiated objects being pushed on the stack. However, due to the current architecture, only one object can be manipulated at a time.

To understand the fundamental premise of the action, Listing 8.17 shows a class that parses the configuration file defined in Listing 8.12.

Listing 8.17
start example
 class MyRule extends Rule { private String _bodyText; public void begin(String namespace, String name, Attributes attributes) throws Exception { if( name.compareTo( "body") == 0) { digester.push( new BeanToWrite()); } } public void body(String namespace, String name, String text) throws Exception { _bodyText = text.trim(); } public void end( String namespace, String name) throws Exception { if( name.compareTo( "element2") == 0) { BeanToWrite bean = (BeanToWrite)digester.peek(); bean.setIntegerValue( new Integer( _bodyText).intValue()); } else if( name.compareTo( "element1") == 0) { BeanToWrite bean = (BeanToWrite)digester.peek(); bean.setStringValue( _bodyText); } } } 
end example
 

In Listing 8.17, the class MyRule subclasses the class Rule . The abstract class Rule is the basis of all actions. When you're extending the class Rule , the methods that are implemented are based on the events that are captured. The events relate to the SAX XML event-processing model. Specifically, Listing 8.18 illustrates the events that are generated as per the XML document from Listing 8.12.

Listing 8.18
start example
 begin( "", "body", attributes); begin( "", "element1", attributes); body( "", "element1", "hello"); end( "", "element1"); begin( "", "element2", attributes); body( "", "element2", "1234"); end( "", "element2"); end( "", "body"); 
end example
 

In Listing 8.18, three major method calls can be considered as events: when an element begins ( begin ), the element body text ( body ), and when an element ends ( end ). The class in Listing 8.17 implements these methods and hence will capture these events. For example, in Listing 8.17, the first line is the begin method call, which instantiates the class BeanToWrite when cross-referenced to the method begin in Listing 8.17. In the implementation of the method begin in Listing 8.17, the instantiated class is pushed on the stack by the class method digester.push .

Then, to fill in the properties of the class BeanToWrite , the event when an element ends is used. This event is used because only this event will ensure that all configuration data has been captured. The configuration data is stored as an XML key value pair. This means that the configuration data triggers the event element body text, which is stored temporarily in the variable _bodyText of Listing 8.17. The BeanToWrite class instance, which was allocated and pushed onto the stack in a previous event, is peeked at from the stack using the method peek. In Listing 8.18, the events are then generated and the class of Listing 8.17 captures these events; when the two listings are assembled together they make it possible to populate a complex object hierarchy.

Listing 8.19 shows how to make use of the user -defined rule.

Listing 8.19
start example
 Digester digester = new Digester(); MyRule rule = new MyRule(); digester.addRule( "body", rule); digester.addRule( "body/element1", rule); digester.addRule( "body/element2", rule); BeanToWrite bean = (BeanToWrite)digester.parse( generateSimpleConfiguration()); 
end example
 

In Listing 8.19, the code has been written in an unusual manner. Instead of having a unique rule instance for every matched pattern, we use the same rule instance. The reason for this is simple, because the action class MyRule can process all elements of the configuration file. Of course, writing all applications using this manner is not that useful.

The Digester package defines a number of smaller rules, which are instantiated using methods on the Digester class. For example, the method addObjectCreate instantiates and adds the rule ObjectCreateRule . In the Digester package, all of the classes that end with the word "Rule" extend the Rule class. Following is a list of Digester class methods and what rules are instantiated:

  • addBeanPropertySetter : Instantiates the class BeanPropertySetterRule . This is used to assign a specific property on the current object in the Digester stack.

  • addCallMethod : Instantiates the class CallMethodRule . This is used to call a method on the current object in the Digester stack.

  • addCallParam : Instantiates the class CallParamRule . This is used in conjunction with the class CallMethodRule to establish a more complex method call that has parameters.

  • addFactoryCreate : Instantiates the class FactoryCreateRule . This is like the class ObjectCreateRule , except that a factory is used to instantiate the class instance that is pushed on the Digester stack.

  • addObjectCreate : Instantiates the class ObjectCreateRule . This class creates a class instance based on a class descriptor. The instantiated class is pushed on the Digester stack.

  • addSetNext : Instantiates the class SetNextRule . When executed, this class pops an object from the digester stack. The popped object is then assigned to the next available object on the Digester stack using a specified method.

  • addSetProperties : Instantiates the class SetPropertiesRule . This set of properties will be assigned to the current object on the stack.

  • addSetProperty : Instantiates the class SetPropertyRule . A single property is assigned to the current object on the stack.

  • addSetRoot : Instantiates the class SetRootRule . The implementation of this action is like the implementation of the method addSetNext . The difference is that, here, the child instance is assigned to the bottom object instance from the Digester stack.

  • addSetTop : Instantiates the class SetTopRule . This class is like the class SetNextRule except the order of assignment. This class does not pop anything from the Digester stack. The purpose of this class is to assign a parent to a child class. You create the relationship by retrieving the second-from-the-top object instance from the Digester stack and then assigning it using a specified method to the top object instance of the Digester stack.

Calling a Method with Parameters

In Listing 8.13, we used the method addCallMethod. In that case, the method called was a property method. There was a single parameter, which was a string. We used a method with multiple parameters because the Java configuration class will not always expose Java Bean properties that can easily be assigned. Instead, the state of the Java Configuration class needs to be assigned using a method. This would usually happen when the configuration file references Java classes that might not be a Java Bean or might be a business process class, or even a legacy class that cannot be rewritten. For example, maybe the contents of element1 and element2 of Listing 8.14 are parameters for a method, which usually are assigned to Java properties. The solution to this type of problem is shown in Listing 8.20. It is assumed that the XML configuration file in Listing 8.14 is attempted to be assigned to a class that exposes a method with two string parameters, for example, void assignBothValues ( String param1, String param2).

Listing 8.20
start example
 Digester digester = new Digester(); MyRule rule = new MyRule(); digester.addRule( "body", new ObjectCreateRule( BeanToWrite.class)); digester.addRule( "body", new CallMethodRule( "assignBothValues", 2)); digester.addRule( "body/element2", new CallParamRule(0)); digester.addRule( "body/element1", new CallParamRule(1)); BeanToWrite bean = (BeanToWrite)digester.parse( generateSimpleConfiguration()); 
end example
 

In Listing 8.20, the method addRule was used and the classes were instantiated directly. We chose to use this technique in this context to show how it would be done, and it allows a developer to extend the Digester -provided classes. To make a multiple parameter method call, the class CallMethodRule has to be associated with a pattern that is located at a higher level than the parameters. The class CallParamRule is used to associate an XML element value with a parameter of the method call. The integer passed to the constructor of the class CallParamRule indicates the order of the parameter in the method call. In Listing 8.20, this means that the first parameter is identified by the zero and the second parameter identified by the one. Listing 8.21 shows how to use the rules defined in Listing 8.20 (note that the events shown are the actual important events, and not the initialization of the individual classes).

Listing 8.21
start example
 ObjectCreateRule.begin( "", "body", attributes); CallParamRule.body( "", "element1", "hello"); CallParamRule.body( "", "element2", "1234"); CallMethodRule.end( "", "body"); 
end example
 

Notice in Listing 8.21 that the object is created correctly at the beginning of the events. Then, when the method body is called, the parameters are saved to a temporary location. Finally, the method end, which calls the method assignToBothValues , is called.

The method assignToBothValues is defined in Listing 8.22 (note that the class has been abbreviated for clarity).

Listing 8.22
start example
 public class BeanToWrite implements java.io.Serializable { private int _iValue; private String _strValue; public void assignBothValues( String ival, String sval) { _iValue = new Integer( ival).intValue(); _strValue = sval; } } 
end example
 

In Listing 8.22, the method assignBothValues has two methods, where both parameters are strings. This is the default format for all parameters when you make method calls without specifying the types. A variation of the Digester class method addCallMethod accepts an array of class descriptors as a parameter. The class descriptors define the data type of the individual parameters.

Establishing Parent-Child Relationships

Most configurations are not based on a single class. In most configuration cases, multiple objects are involved. This means that some parts of the configuration file will instantiate an object that needs to be added to a previously created object. Listing 8.23 is a more complicated configuration file than Listing 8.14.

Listing 8.23
start example
 <parent> <dataMember>hello</dataMember> <myReferenceToAnotherBean>  <stringValue>hello</stringValue>  <integerValue>1234</integerValue> </myReferenceToAnotherBean> </parent> 
end example
 

In Listing 8.23, there are two classes. The tag parent identifies the first class ParentBean, and the tag myReferenceToAnotherBean identifiers the other class, BeanToWrite. Listing 8.24 illustrates how to parse the multiple class configuration file.

Listing 8.24
start example
 Digester digester = new Digester(); digester.addObjectCreate( "parent", ParentBean.class); digester.addBeanPropertySetter( "parent/dataMember", "dataMember"); digester.addObjectCreate( "parent/myReferenceToAnotherBean", BeanToWrite.class); digester.addBeanPropertySetter( "parent/myReferenceToAnotherBean/stringValue", "stringValue"); digester.addBeanPropertySetter( "parent/myReferenceToAnotherBean/integerValue", "integerValue"); digester.addSetNext(  "parent/myReferenceToAnotherBean ", "setMyReferenceToAnotherBean"); ParentBean bean = (ParentBean)digester.parse( generateParentConfiguration()); 
end example
 

Listing 8.24 shows the essentials of how to string together multiple classes and assign Java properties. For every tag that has been identified as a class, which in our case are the tags parent and myReferenceToAnotherBean , there is an associated create object method call. The method addObjectCreate with the correct class descriptor is used to create the object. To assign the bean properties, the method addBeanPropertySetter is used.

To assign the child BeanToWrite class instance to the parent ParentBean class instance, the method addSetNext is used. The method addSetNext has two parameters. The first parameter is the pattern match, but the second parameter is the method that is used to assign the child class to the parent class. The method used to assign the child object in Listing 8.24 is a bean property. This is not a recommended practice because a configuration file may have multiple setMyReferenceToAnotherBean elements. In those cases, the method used to assign a child object to a parent object should be a method to add the object to a list. However, in this case, it is acceptable to use a bean property since there is only one setMyReferenceToAnotherBean element.

If you need to have a reference of the ParentBean class instance within the BeanToWrite class instance, you need to use the method addSetTop . Listing 8.25 is a sample implementation.

Listing 8.25
start example
 digester.addSetTop( "parent/myReferenceToAnotherBean", "setParent"); 
end example
 

Generally speaking, wherever the method addSetNext is used, the method addSetTop can be used.

Using XML Attributes

In all of the configuration examples so far in this chapter, we used XML configurations without any attributes. Digester does, however, allow the use of attributes, which can be assigned to bean properties like the method addBeanPropertySetter . Listing 8.26 is a modified configuration file that uses only attributes.

Listing 8.26
start example
 <body integerValue='1234' stringValue='hello' 
end example
 

Listing 8.27 is a program that parses the configuration into the object BeanToWrite .

Listing 8.27
start example
 Digester digester = new Digester(); digester.addObjectCreate( "body", BeanToWrite.class); digester.addSetProperties( "body", "integerValue", "integerValue"); digester.addSetProperties( "body", "stringValue", "stringValue"); BeanToWrite bean = (BeanToWrite)digester.parse( generateAttributeConfiguration()); 
end example
 

In Listing 8.27, the method addSetProperties is used to assign an attribute to a specific property. The method addSetProperties has three parameters. The first parameter is the pattern match, as in previous examples. The second parameter is the attribute identifier, where an attribute value is obtained. The third parameter represents the bean property that will be assigned with the obtained attribute value.

Using XML Digester Configuration Files

Thus far, we have specified all the rules using a piece of code. For added flexibility, you can assign the rules using an XML file. In effect, this is a configuration file that parses a configuration file. The advantage of using this approach is that you don't have to change the program if either the configuration file or the configuration objects change. We will convert Listing 8.23 into a configuration file, where the class ParentBean references the class BeanToWrite . Listing 8.28 is the implementation of the XML Digester configuration file.

Listing 8.28
start example
 <digester-rules> <pattern value="parent"> <object-create-rule  classname="com.devspace.jseng.serialization.ParentBean" /> <bean-property-setter-rule pattern="dataMember" name="dataMember" /> </pattern> <pattern value="parent/myReferenceToAnotherBean"> <object-create-rule classname="com.devspace.jseng.serialization.BeanToWrite" /> <call-method-rule pattern="stringValue" methodname="setStringValue" paramcount="0" /> <call-method-rule pattern="integerValue" methodname="setIntegerValue" paramcount="0" paramtypes="java.lang.Integer" /> <set-next-rule methodname="setMyReferenceToAnotherBean" /> </pattern> </digester-rules> 
end example
 

What should be very obvious from Listing 8.28 is that the element names are almost identical to the names of the classes used to execute a specific action. This is not accidental, because we use the Digester package to read Listing 8.28. The details are partially managed in the class DigesterRuleParser . The difference between the class names and the XML elements is that the XML elements are all lowercase words separated by hyphens.

One big advantage of using configuration files over a program that manually defines the Digester package rules is that it is simpler to make references because the XML elements are embedded. For example, Listing 8.28 contains the XML element pattern . The XML element pattern has an attribute value >, which has a value of parent . Notice that the child element bean-property-setter-rule pattern attribute is only dataMember and not parent/dataMember . This happens because the XML Digester parser can build an absolute path based on relative values. In fact, in Listing 8.28, we could have embedded the second XML tag pattern within the first XML tag pattern. In that case, the second XML element's pattern value attribute would be myReferenceToAnotherBean instead of parent/myReferenceToAnotherBean . The items within the second XML element pattern would not have to be adapted because they are already relative to the parent. However, if it is absolutely desired to use absolute paths through all elements, you can do so by removing the XML element pattern and referencing the paths absolutely . Note that the XML element object-create-rule would need an additional pattern attribute. In Listing 8.28, the lack of a pattern attribute means that you should use the current value.

Listing 8.29 is used to load and parse both configuration files and generate a set of class instances.

Listing 8.29
start example
 File rules = new File( getFilename()); Digester digester = DigesterLoader.createDigester( rules.toURL()); ParentBean bean = (ParentBean)digester.parse( generateParentConfiguration()); 
end example
 

In Listing 8.29, the class DigesterLoad , which is located in the package org.apache.commons.digester.xml.rules , is used. The class DigesterLoader is used to instantiate a Digester class instance based on the XML Digester configuration in Listing 8.28. There is a special catch, though. When the class DigesterLoad attempts to parse the XML Digester configuration file, a reference to the org.apache.commons.digester.xmlrules.digester-rules.dtd must exist on the class path. The returned Digester class instance can then be used to parse a configuration file, which instantiates a ParentBean class instance. Table 8.6 explains the available XML elements and what they mean.

Table 8.6: XML elements and the command line parsers implementations that can parse the arguments.

XML Element

Associated Class

Available Attributes

bean-property-setter-rule

BeanPropertySetterRule

pattern: path propertyname: The name of the bean property.

call-method-rule

CallMethodRule

pattern: path methodname (req): The name of the method that is called. paramcount: The number of parameters that the method expects. paramtypes: A comma-separated list of data types used to define the method type.

call-param-rule

CallParamRule

pattern: path paramnumber (req): The order of the parameter in the method call. attrname: The name of the attribute if the value is retrieved from an XML attribute. (Note that attrname and from-stack cannot be combined.) from-stack: Sets the value from the stack. (Note that attrname and from-stack cannot be combined.)

factory-create-rule

FactoryCreateRule

pattern: path classname (req): The name of class that supports the interface ObjectCreationFactory. ignore-exceptions: A true or false value that indicates whether or not exceptions in object instantiation should be ignored.

object-create-rule

ObjectCreateRule

pattern: path classname (req): The name of the class to instantiate.

set-properties-rule

SetPropertiesRule

A an attribute pattern (not usually used), but therefore child XML element alias, which has the following attributes: attr-name (req): The name of the XML attribute to retrieve the value from. prop-name: The name of the bean property to assign.

set-property-rule

SetPropertyRule

pattern: path name: The name of the XML attribute to retrieve the value from. value: The name of the bean property to assign.

set-top-rule

SetTopRule

pattern: path methodname (req): The name of the method to assign the parent. paramtype: The data type used to assign the parent.

set-next-rule

SetNextRule

pattern: path methodname (req): The name of the method to add the child. paramtype: The data type used to assign the child.

pattern
include N/A

N/A

value (req): The path in the XML document.class: The name of the class that includes a number of rules. path: The name of a file that includes a number of configuration rules.

Note that path means either a relative or absolute XML path in the configuration file, as per our explanation in the previous section. req means required; otherwise , the attributes are optional.




Applied Software Engineering Using Apache Jakarta Commons
Applied Software Engineering Using Apache Jakarta Commons (Charles River Media Computer Engineering)
ISBN: 1584502460
EAN: 2147483647
Year: 2002
Pages: 109

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net