Rules Binder (new)

The Digester 3 design aims to eliminate all the Digester boilerplate without sacrificing maintainability.

With Digester 3, you implement modules, the DigesterLoader passes a RulesBinder to your module, and your module uses the binder to map patterns to Rules. We can break Digester's 3 architecture down into two distinct stages: startup and runtime. You build a DigesterLoader during startup and use it to obtain Digester instances at runtime.

Startup

You configure the Digester by implementing RulesModule. You pass DigesterLoader a module, the DigesterLoader passes your module a RulesBinder, and your module uses the binder to configure patterns/rules bindings. A binding most commonly consist of a mapping between a pattern and one or more Rule. For example:

class EmployeeModule
    implements RulesModule
{

    protected void configure( RulesBinder rulesBinder )
    {
        rulesBinder.forPattern( "employee" ).createObject().ofType( Employee.class );
        rulesBinder.forPattern( "employee/firstName" ).setBeanProperty();
        rulesBinder.forPattern( "employee/lastName" ).setBeanProperty();

        rulesBinder.forPattern( "employee/address" )
            .createObject().ofType( Address.class )
            .then()
            .setNext( "addAddress" );
        rulesBinder.forPattern( "employee/address/type" ).setBeanProperty();
        rulesBinder.forPattern( "employee/address/city" ).setBeanProperty();
        rulesBinder.forPattern( "employee/address/state" ).setBeanProperty();
    }

}

DRY (Don't Repeat Yourself): Repeating "rulesBinder" over and over for each binding can get a little tedious. The Digester package provides a module support class named AbstractRulesModule which implicitly gives you access to RulesBinder's methods. For example, we could extend AbstractRulesModule and rewrite the above binding as:

class EmployeeModule
    extends AbstractRulesModule
{

    @Override
    protected void configure()
    {
        forPattern( "employee" ).createObject().ofType( Employee.class );
        forPattern( "employee/firstName" ).setBeanProperty();
        forPattern( "employee/lastName" ).setBeanProperty();

        forPattern( "employee/address" )
            .createObject().ofType( Address.class )
            .then()
            .setNext( "addAddress" );
        forPattern( "employee/address/type" ).setBeanProperty();
        forPattern( "employee/address/city" ).setBeanProperty();
        forPattern( "employee/address/state" ).setBeanProperty();
    }

}

We'll use this syntax throughout the rest of the guide.

Creating a Digester entails the following steps:

  1. First, create an instance of your module and pass it to DigesterLoader.newLoader().
  2. The DigesterLoader creates a RulesBinder and passes it to your module.
  3. Your module uses the binder to define bindings.
  4. Set any desired configuration properties that will customize the operation of the Digester when you next initiate a parse operation.
  5. Based on the bindings you specified, DigesterLoader creates a Digester by invoking DigesterLoader.newDigester() and returns it to you.
  6. Optionally, push any desired initial object(s) onto the Digester's object stack.
  7. Call the digester.parse() method, passing a reference to the XML document to be parsed in one of a variety of forms. See the Digester.parse() documentation for details. Note that you will need to be prepared to catch any IOException or SAXException that is thrown by the parser, or any runtime expression that is thrown by one of the processing rules.
  8. Please remember that previously created Digester instances may be safely reused, as long as you have completed any previously requested parse, and you do not try to utilize a particular Digester instance from more than one thread at a time.

New Digester fluent APIs

The main difference between Digester 1.X, 2.X and 3.X is that the while the first follows the approach "given a Digester instance, then configure it", the new Digester instead follows the opposite approach "given one (or more) configuration(s), create multiple Digester instances" or "configure once, create everywhere".

Why? Even if both approaches sound complementary, the core concept is given by the assumption that every Digester instance is not thread-safe, that implies that in a multi-thread application users have often to reinstantiate the Digester and reconfigure it, i.e in a Servlet:

public class EmployeeServlet
  extends HttpServlet
{

    public void doGet(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException
    {
        Digester digester = new Digester();
        digester.setNamespaceAware( true );
        digester.setXIncludeAware( true );
        digester.addObjectCreate( "employee", Employee.class );
        digester.addCallMethod( "employee/firstName", "setFirstName", 0 );
        digester.addCallMethod( "employee/lastName", "setLastName", 0 );

        digester.addObjectCreate( "employee/address", Address.class );
        digester.addCallMethod( "employee/address/type", "setType", 0 );
        digester.addCallMethod( "employee/address/city", "setCity", 0 );
        digester.addCallMethod( "employee/address/state", "setState", 0 );
        digester.addSetNext( "employee/address", "addAddress" );

        Employee employee = digester.parse( openStream( req.getParameter( "employeeId" ) ) );
        ...
}

Nothing wrong with that approach but configuration is not reusable; the RuleSet interface fills in some way the reuse of configurations lack:

public class EmployeeRuleSet
  implements RuleSet
{

    public void addRuleInstances( Digester digester )
    {
        digester.addObjectCreate( "employee", Employee.class );
        digester.addCallMethod( "employee/firstName", "setFirstName", 0 );
        digester.addCallMethod( "employee/lastName", "setLastName", 0 );

        digester.addObjectCreate( "employee/address", Address.class );
        digester.addCallMethod( "employee/address/type", "setType", 0 );
        digester.addCallMethod( "employee/address/city", "setCity", 0 );
        digester.addCallMethod( "employee/address/state", "setState", 0 );
        digester.addSetNext( "employee/address", "addAddress" );
    }

}

then, in our sample servlet

public class EmployeeServlet
  extends HttpServlet
{

    private final RuleSet employeeRuleSet = new EmployeeRuleSet();

    public void doGet(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException
    {
        Digester digester = new Digester();
        digester.setNamespaceAware( true );
        digester.setXIncludeAware( true );

        employeeRuleSet.addRuleInstances( digester );

        Employee employee = digester.parse( openStream( req.getParameter( "employeeId" ) ) );
        ...
    }

}

Nothing wrong again, but:

  1. RuleSet is not really a configuration, it just sets rules to given Digester instance;
  2. Digester instance creation is totally delegated to clients;
  3. Rules that match to the same pattern, need to specify this last n times for how many rules match, that violates the DRY principle;
  4. Rules semantic is not intuitive, since their creation is strictly related to methods/constructors arguments.

In the new Digester, RuleSet has been suppressed in favor of RulesModule

class EmployeeModule
    extends AbstractRulesModule
{

    @Override
    protected void configure()
    {
        forPattern( "employee" ).createObject().ofType( Employee.class );
        forPattern( "employee/firstName" ).setBeanProperty();
        forPattern( "employee/lastName" ).setBeanProperty();

        forPattern( "employee/address" )
            .createObject().ofType( Address.class )
            .then()
            .setNext( "addAddress");
        forPattern( "employee/address/type" ).setBeanProperty();
        forPattern( "employee/address/city" ).setBeanProperty();
        forPattern( "employee/address/state" ).setBeanProperty();
    }

}

Then, our sample Servlet become:

public class EmployeeServlet
    extends HttpServlet
{

    private final DigesterLoader loader = newLoader( new EmployeeModule() )
        .setNamespaceAware( true )
        .setXIncludeAware( true );

    public void doGet(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException
    {
        Digester digester = loader.newDigester()

        Employee employee = digester.parse( openStream( req.getParameter("employeeId") ) );
        ...
    }

}

As you can notice, the RulesModule implements rules via fluent APIs, making rules semantic simpler, and the effort of configuration is moved to the startup; the DigesterLoader indeed will analyze all the RulesModule instances and will be ready to create new Digester instances with pre-filled rules.

One single configuration point, one single universal loader

As shown above, basic Digester2.X usage would be creating a Digester then setting the rules:

Digester digester = new Digester();
digester.addObjectCreate( "root", "org.apache.commons.digester.SimpleTestBean" );
digester.addBeanPropertySetter( "root", "alpha" );
digester.addBeanPropertySetter( "root/alpha", "beta" );
digester.addBeanPropertySetter( "root/delta", "delta" );

Alternatively, users can create the Rules instance, set the rules and pass it to the Digester:

ExtendedBaseRules rules = new ExtendedBaseRules();
rules.addRule( "root", new ObjectCreateRule( "org.apache.commons.digester.SimpleTestBean" ) );
rules.addRule( "root", new BeanPropertySetterRule( "alpha" ) );
rules.addRule( "root/alpha", new BeanPropertySetterRule( "beta" ) );
rules.addRule( "root/delta", new BeanPropertySetterRule( "delta" ) );

Digester digester = new Digester();
digester.setRules( rules );

Last, but not least, special loader classes have been created to gain more benefits from RuleSet: like the annotations package DigesterLoader, to avoid scanning class elements each time users want to create a new Digester instance to parse Channel type:

import org.apache.commons.digester.annotations.*;

DigesterLoader digesterLoader = new DigesterLoaderBuilder()
    .useDefaultAnnotationRuleProviderFactory()
    .useDefaultDigesterLoaderHandlerFactory();
Digester digester = digesterLoader.createDigester( Channel.class );

In Digester3 there is just one universal loader that aggregates all the power of the components described above, configurations are expressed via (Abstract)RulesModule

class SimpleTestBeanModule
  extends AbstractRulesModule
{

    @Override
    protected void configure()
    {
        forPattern( "root" )
            .createObject().ofType( "org.apache.commons.digester.SimpleTestBean" )
            .then()
            .setBeanProperty( "alpha" );
        forPattern( "root/alpha" ).setBeanProperty( "beta" );
        forPattern( "root/delta" ).setBeanProperty( "delta" );
    }

}

Users can simply create new Digester instances:

DigesterLoader loader = newLoader(new SimpleTestBeanModule());
...
Digester digester = loader.newDigester();

Users can create new Digester instances on top of different Rules types:

Digester digester = loader.newDigester(new ExtendedBaseRules());

An, by the nature of the universal loader, auxiliary optimizations are not needed:

DigesterLoader loader = newLoader( new FromAnnotationsRuleModule()
    {

        @Override
        protected void configureRules()
        {
            bindRulesFrom( Channel.class );
        }

    } );
...
Digester digester = loader.newDigester();
...
digester = loader.newDigester(); // Channel.class won't be analyzed again!

Extensions optimization

As shown above, the universal DigesterLoader introduces a set of optimizations not or partially introduced in the previous Digester releases: the FromXmlRuleSet, for example, parses the XML Digester rules each time the Digester creation is performed:

FromXmlRuleSet ruleSet = new FromXmlRuleSet( MyClass.class.getResource( "myrule.xml" ) );
Digester digester = new Digester();
ruleSet.addRuleInstances( digester ); // myrule.xml will be parsed
...
Digester newDigester = new Digester();
ruleSet.addRuleInstances( newDigester ); // myrule.xml will be parsed again!

In Digester3 there's only one RulesModules loading, so in the case of FromXmlRulesModule, the XML rules will be parsed only once:

DigesterLoader loader = newLoader( new FromXmlRulesModule()
    {

        @Override
        protected void loadRules()
        {
            loadXMLRulesFromText( MyClass.class.getResource( "myrule.xml" ) );
        }

    } );
...
Digester digester = loader.newDigester(); // myrule.xml already parsed
...
Digester newDigester = loader.newDigester(); // myrule.xml won't be parsed again!

Startup checks and improved error reporting

The new Digester tries as much as possible to check patterns/rules binding errors during the DigesterLoader bootstrap, avoiding exceptions during the parsing operations.

Let's suppose for example the following Digester

Digester digester = new Digester();
  digester.addObjectCreate( "root", "com.acme.InOtherClassLoader" );
  ....
  digester.addObjectCreate( "root/child", "foo.bar.DoesNotExist" );
  ...

is using a wrong ClassLoader to resolve types, or declared types are in the wrong package; a runtime error will be thrown as soon as the root pattern will match.

Let's suppose users debug their application and fix the ClassLoader problem, a new runtime error will be thrown as soon as the root/child pattern will match, and so on.

The new Digester tries to report all patterns/rules binding error in one single detailed report, i.e.

class SampleModule
    extends AbstractRulesModule
{

    @Override
    protected void configure()
    {
        forPattern( "root" ).createObject().ofType( "com.acme.InOtherClassLoader" );
        ...
        forPattern( "root/child" ).createObject().ofType( "foo.bar.DoesNotExist" );
        ...
    }

}

The DigesterLoader will report problems in the following way:

Exception in thread "XXX" org.apache.commons.digester3.DigesterLoadingException: Digester creation errors:

1) { forPattern( "root" ).createObject().ofType( String ) } class 'com.acme.InOtherClassLoader' cannot be load (SampleModule.java:5)

2) { forPattern( "root/child" ).createObject().ofType( String ) } class 'foo.bar.DoesNotExist' cannot be load (SampleModule.java:10)

2 errors

So, users have at least an overview to debug their applications.