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.
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:
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:
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.
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!
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!
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.