by Jason Menard
This is the third in a three part series discussing elements of the Jakarta Commons project. The first part discussed the Commons BeanUtils package, the second part discussed the Commons Digester package, and the final part demonstrates using these packages together to build a framework for dynamically generating data transfer objects.
In the first two parts of this series we learned how to create and use DynaBeans and how to use Digester to map XML to objects. This article assumes that you have already read the previous two parts of this series and builds on that material. This time around we are going to combine what we learned and use the Digester and BeanUtils packages to create something truly useful - a framework for dynamic data transfer objects, or DTOs. I'll refer to these as DynaDTOs.
A DTO is a serializable object which represents the state of a business object. More specifically, a DTO is an object containing a business object's data which may easily be transported throughout the system. By "easily transported" I mean that it is serializable and does not contain extraneous business logic. These are pretty lightweight objects. A DTO may be something as simple as a Map, but often it is a JavaBean.
Creating a whole slew of JavaBeans for this purpose could certainly be tedious. Wouldn't it be nice if there were an easier way? Instead of having to hand code all those JavaBeans, it might be easier if we could simply define them using XML, similar to how one may define DynaActionForms in Struts. The combination of the BeanUtils and Digester packages will allow us to do precisely this.
I first need to determine how I want to configure the DTOs in XML. Assume I want to define a DTO "book", which has properties of "title", "author", "isbn", "datePublished", and "numberOfPages". With this in mind, I chose to go with the following representation:
<dto name="book"> <property name="title" type="java.lang.String" /> <property name="author" type="java.lang.Object" /> <property name="isbn" type="java.lang.String" /> <property name="datePublished" type="java.util.Date" /> <property name="numberOfPages" type="java.lang.Integer" /> </dto> |
I'm still missing something here. I have already stated that I'm going to create DynaBeans for these DTOs, but I never specified whether I would use a BasicDynaBean, a DynaBean of my own creation, or whether I would keep things a bit flexible and allow DTOs to use any implementation of DynaBean. It's probably best to keep things flexible, so I'll add an optional "type" attribute to the <dto> element to facilitate this.
<dto name="book" type="org.apache.commons.beanutils.BasicDynaBean"> |
If you look at the above XML, you'll notice that I declared "author" to be an Object. That's because my business object Book has another business object Author assigned to its "author" property. Remember, DTOs represent data from business objects, so the structure of our DTOs will often mirror the structure of the business objects whose data they will contain. What this tells me is that I also need a DTO for "author", whose properties happen to be "lastName" and "firstName".
<dto name="author"> <property name="lastName" type="java.lang.String" /> <property name="firstName" type="java.lang.String" /> </dto> |
Now that I have defined more than one DTO, I need to add another element to my XML to contain these DTO configurations.
<dto-config> <dto name="book" type="org.apache.commons.beanutils.BasicDynaBean" /> <property name="title" type="java.lang.String" /> <property name="author" type="java.lang.Object" /> <property name="isbn" type="java.lang.String" /> <property name="datePublished" type="java.util.Date" /> <property name="numberOfPages" type="java.lang.Integer" /> </dto> <dto name="author"> <property name="lastName" type="java.lang.String" /> <property name="firstName" type="java.lang.String" /> </dto> </dto-config> |
Here is a DTD for the dto-config.
<!ELEMENT dto-config (dto+)> <!ELEMENT dto (property+)> <!ATTLIST dto name CDATA #REQUIRED type CDATA #IMPLIED> <!ELEMENT property EMPTY> <!ATTLIST property name CDATA #REQUIRED type CDATA #REQUIRED> |
See dto-config.xml and dynaDTO.dtd for the complete version of each file.
Now that we have a handle on what our XML will look like, the next problems to address are 1) how is the programmer going to get access to his DTOs, and 2) how are we going to digest the XML. Keep in mind that we've already stated up front that we are going to use the Commons Digester package to do much of the grunt work, so that gives us a starting point.
I made the determination to have a factory that will provide new instances of DTOs, which I call DynaDTOFactory. The DynaDTOFactory will be responsible for parsing the dto-config file and returning new instances of DTOs specified in the dto-config file. Take a look at DynaDTOFactory.java to view the entire class.
Looking at the class there are a couple of things to note. The DynaDTOFactory assumes by default that our dto-config file will be named "dto-config.xml", although I have provided a constructor which takes the name of a config file if you wish to call it something else. We can also see that a new DTO may be instantiated via a newInstance(String name), where name is the "name" attribute of a given <dto> element. There is a Map which will be used to hold the DynaClass objects which will be needed to instantiate each DTO, keyed by the "name" attribute of the <dto> element. DynaClasses are added to the Map via the addDynaClass(DynaClass dynaClass) method. Here's what we have so far:
public class DynaDTOFactory { private Map dynaClassMap = new HashMap(); private String config = "dto-config.xml"; public DynaDTOFactory() throws FileNotFoundException, IOException, SAXException { init(); } public DynaDTOFactory(String config) throws FileNotFoundException, IOException, SAXException { this.config = config; init(); } public DynaBean newInstance(String name) throws InstantiationException, IllegalAccessException { DynaBean dynaBean = null; DynaClass dynaClass = (DynaClass) dynaClassMap.get(name); if (dynaClass != null) { dynaBean = dynaClass.newInstance(); } return dynaBean; } void addDynaClass(DynaClass dynaClass) { dynaClassMap.put(dynaClass.getName(), dynaClass); } } |
You'll notice that the constructors call an init() method. This is the method that will set up our Digester to parse the XML.
private void init() throws FileNotFoundException, IOException, SAXException { InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(config); if (is == null) { throw new FileNotFoundException(config + " file not found."); } Digester digester = new Digester(); digester.setNamespaceAware(true); digester.setValidating(true); digester.setUseContextClassLoader(true); digester.addRuleSet(new DynaDTORuleSet()); digester.push(this); digester.parse(is); is.close(); } |
The init() method loads the dto-config.xml file, instantiates and sets up an instance of Digester, pushes this instance of DynaDTOFactory onto the digester's stack, and parses the dto-config. Having already read my article on Digester, you likely astutely noticed that there are no processing rules present. The Digester package allows pluggable rule sets, and we have taken advantage of that here. Digester's addRuleSet() method takes as a an instance of the RuleSet interface, which contains the actual processing rules. parameter This allows the ability to not only separate out the processing rules, but also to dynamically specify a set of processing rules an instance of Digester uses to parse the XML. In this particular case, we are using an instance of RuleSet to provide a clean separation. In our init() method, take notice of how we do this:
digester.addRuleSet(new DynaDTORuleSet()); |
DynaDTORuleSet is the custom implementation of the RuleSet interface that will be used to contain our processing rules.
The custom implementation of RuleSet, DynaDTORuleSet, is where most of the magic takes place. This class will contain the processing rules necessary to turn our XML into actual DynaBeans. The Digester package provides an abstract implementation of the RuleSet interface called RuleSetBase, which DynaDTORuleSet will extend. The abstract method addRuleInstances(Digester digester) must be implemented, and this is where we stick the actual processing rules.
Looking back at our XML, let's see what we actually need to do before we start writing our processing rules:
Examining item #1, this sounds like it should be an ObjectCreateRule. In the previous article, we used Digester's addObjectCreate() method to register a default ObjectCreateRule. Unfortunately, you can only use that rule to create objects that have no-argument constructors, and DynaClass objects require arguments in order to be constructed. Digester provides another type of Rule which is exactly what we are looking for though: the FactoryCreateRule. The FactoryCreateRule is to be used where there is a need to create objects whose constructors require arguments, or where some kind of set-up is needed prior to construction. We're actually going to kill two birds with one stone here, because not only does will we create an object with this rule, but the object we create will be popped off the stack when the ending tag is processed, thus satisfying item #4 above.
A FactoryCreateRule requires an implementation of the ObjectCreationFactory interface, and this is what we will need to implement. The particular implementation of ObjectCreationFactory for this purpose will be called DynaClassFactory. With that in mind, here's what our RuleSet should look like up to this point:
public class DynaDTORuleSet extends RuleSetBase { public void addRuleInstances(Digester digester) { digester.addFactoryCreate("dto-config/dto", new DynaClassFactory()); } } |
We'll get back to the specific implementation of DynaClassFactory later. For now let's figure out what other processing rules we need.
Item #2 on our list tells us that we need a rule to create DynaProperty objects. While the first instinct might be to think of writing another FactoryCreateRule, that wouldn't be quite what we're looking for. The FactoryCreateRule would pop our DynaProperty off of the stack after it is created, but the behavior we desire in this case is to leave it on the stack for now. Therefore what we really need to create is just a generic rule via Digester's addRule() method. The addRule() method requires an argument of type Rule, which we will need to implement, and we will call it AddPropertyRule(). Here's our RuleSet after this step:
public class DynaDTORuleSet extends RuleSetBase { public void addRuleInstances(Digester digester) { digester.addFactoryCreate("dto-config/dto", new DynaClassFactory()); digester.addRule("dto-config/dto/property", new AddPropertyRule()); } } |
Looking at item #3, it becomes evident that we will have to create yet another custom rule, which we'll call AddPropertiesRule(). That's the last rule we need though, so we can finish off our RuleSet.
public class DynaDTORuleSet extends RuleSetBase { public void addRuleInstances(Digester digester) { digester.addFactoryCreate("dto-config/dto", new DynaClassFactory()); digester.addRule("dto-config/dto/property", new AddPropertyRule()); digester.addRule("dto-config/dto", new AddPropertiesRule()); } } |
There really isn't much involved with the actual RuleSet. It gets a little more involved though when we need to provide the specific implementations we've identified.
AddPropertyRule is the simplest, so we'll start there. We've already identified what it is we want to accomplish with this rule in item #2 above, so be sure to keep this in mind. We're going to extend the Rule class, and since we want our behavior to occur when the beginning of the specified element is encountered, we need to override the begin() method.
final class AddPropertyRule extends Rule { public void begin(String namespace, String name, Attributes attributes) throws Exception { String propertyName = attributes.getValue("name"); Class type = Class.forName(attributes.getValue("type")); digester.push(new DynaProperty(propertyName, type)); } } |
The Attributes object gives us access to the attributes we specified in the XML, which in this case are the "name" and "type" attributes. A DynaProperty requires a property name and a class in order to be constructed, so it's simple enough to take the information that was set in the XML and construct a DynaProperty. Then we just need to push the DynaProperty onto the stack and we're done.
Previously it was stated that when the ending
This should be straightforward enough, but we need to decide what implementation of DynaClass to add the DynaProperty objects to. BasicDynaClass has a setProperties(DynaProperty[] properties) method which would be useful, but unfortunately it's protected. This means that we need to provide our own implementation of the DynaClass interface. I've decided to extend BasicDynaClass and provide the needed functionality in the DynaDTODynaClass class. See DynaDTODynaClass.java for the code.
Now all that remains is to actually implement the AddPropertiesRule class. Keep in mind that the DynaClass will be popped off the stack by the FactoryCreateRule, so we need to leave it on the stack for now.
final class AddPropertiesRule extends Rule { public void end(String namespace, String name) { ArrayList list = new ArrayList(); while (digester.peek() instanceof DynaProperty) { list.add(digester.pop()); } int size = list.size(); DynaProperty[] properties = new DynaProperty[size]; // Maintain property order specified in dto-config.xml. // The last element of list is the first property that was // added to the stack. for (int i = 0; i < size; i++) { properties[i] = (DynaProperty) list.get(size - i - 1); } // all the properties have been popped off leaving a DynaClass // on top of the stack DynaDTODynaClass dynaClass = (DynaDTODynaClass) digester.peek(); dynaClass.setProperties(properties); // The DynaDTOFactory is one level beneath the DynaClass on the stack DynaDTOFactory factory = (DynaDTOFactory) digester.peek(1); factory.addDynaClass(dynaClass); } } |
Please go back and re-read what our goals for this class are. When the beginning
A decision needs to be made regarding what types of DynaBeans are going to be instantiated by our DynaClasses. We could simply go with BasicDynaBean classes, but that's a little bit limiting. BasicDynaBean does not provide proper equals(), hashCode(), and toString() methods, and these are something that might come in handy for our DTOs. To remedy this, let's just extend BasicDynaBean into a class that we'll call DynaDTODynaBean. See DynaDTODynaBean.java for the code.
Since we are extending AbstractObjectCreationFactory, creating our implementation of ObjectCreationFactory is as simple as overriding the createObject() method. We use the Java Reflection API to dynamically create the specific DynaClass objects we need.
final class DynaClassFactory extends AbstractObjectCreationFactory { private final String DEFAULT_DYNA_BEAN = "com.javaranch.dynadto.DynaDTODynaBean"; private final String DYNA_BEAN = "org.apache.commons.beanutils.DynaBean"; public Object createObject(Attributes attributes) throws Exception { String className = attributes.getValue("type"); String name = attributes.getValue("name"); if (className == null) { className = DEFAULT_DYNA_BEAN; } Class c = Class.forName(className); if(!(Class.forName(DYNA_BEAN)).isAssignableFrom(c)) { throw new Exception(className + " does not implement " + DYNA_BEAN); } Class[] constructorClassArgs = {java.lang.String.class, java.lang.Class.class}; Constructor constructor = DynaDTODynaClass.class.getConstructor(constructorClassArgs); Object[] constructorArgs = {name, c}; Object dynaClass = constructor.newInstance(constructorArgs); return dynaClass; } } |
That's really all there is to it. Take a look at DynaDTORuleSet.java for the code for DynaDTORuleSet as well as the three Rule implementations. Here's a simple driver (DynaDTOFactoryDriver.java) that demonstrates our DynaDTOs in action.
public class DynaDTOFactoryDriver { public static void main(String[] args) { try { DynaDTOFactory dtoFactory = new DynaDTOFactory(); DynaBean book = dtoFactory.newInstance("book"); book.set("title", "Jess In Action"); DynaBean author = dtoFactory.newInstance("author"); author.set("lastName", "Friedman-Hill"); author.set("firstName", "Ernest"); book.set("author", author); System.out.println(book.get("title")); System.out.println(book.get("author")); } catch (Exception e) { e.printStackTrace(); } } } |
The Commons BeanUtils and Digester packages are extremely useful by themselves, but as I hope I've shown here, when they are combined together, they can work some pretty powerful mojo.