March 11, 2005

Using a factory in commons-digester

commons-digester is one of the more useful jakarta commons libraries. Yeah, it's verbose and it might take a little while to grok if you're not used to stack processing and other xml-isms. But once grokked it's applicable many places, especially since lots of systems are passing around XML these days. Parsing a message-based document (short) into an object seems to be pretty quick too and since you're using rules it's pretty easy to extend once you get the basics down -- this article has a practical application of it.

The idea behind the digester is that you have events that fire based on element paths. A common scenario: when you hit a path create a new object, then set its attributes and child elements as properties, something like:

  // Process the address from a message like:
  // <customer first="foo" last="bar">
  //   <address 
  //       street="1234 Main Street" city="Wilmerding" state="PA" />
  // </customer>
 
  Digest d = new Digester();
  ...
  // push a new object on the stack...
  d.addObjectCreate( "customer/address", Address.class );
  
  // this will call on the topmost stack object 
  // 'setStreet()', 'setCity()', 'setState()'
  d.addSetProperties( "customer/address" );
 
  // ...or you can do it explicitly
  d.addSetProperties( "customer/address", "street", "street" );
  d.addSetProperties( "customer/address", "city", "city" );
  d.addSetProperties( "customer/address", "state", "state" );
 
  // add the topmost stack object to the previous one
  d.addSetNext( "customer/address" );

Verbose, but not so bad. What happens when you have different types of addresses?

  // <customer first="foo" last="bar">
  //   <address country="USA"
  //       street="1234 Main Street" city="Wilmerding" state="PA" />
  //   <address country="Canada"
  //       street="1234 Main Street" city="Toronto" province="ON" />
  // </customer>

Assuming e've got a parent class 'Address' and subclasses 'CanadaAddress', 'UnitedStatesAddress', etc. How do we tell digester to create the right object? A factory, of course:

  // just change the 'addObjectCreate()' to:
  d.addFactoryCreate( "customer/address", AddressFactory.class );

The factory class is cake:

// 'AbstractObjectCreationFactory' is from commons-digester
public class AddressFactory extends AbstractObjectCreationFactory
{
    public Object createObject( Attributes attributes )
        throws Exception
    {
        String country = attributes.getValue( "country);
        if ( "USA".equals( country ) ) {
            return new UnitedStatesAddress();
        }
        else if ( "Canada".equals( country ) ) {
            return new CanadaAddress();
        }
        ...
Next: If you don't agree with me I don't want to hear it...
Previous: Hyperproductive, then not so much...