http://www.developer.com/

Back to article

Introducing Betwixt


September 30, 2005

Betwixt is a Jakarta Commons component that provides XML introspection routines for mapping JavaBeans to XML and vice versa.. It uses concepts similar to the JavaBeans Introspector and BeanInfo to decipher information about XML data and map it to JavaBeans. Digester rules are used to create JavaBeans from XML internally.

Betwixt lies in the category commonly classed as data-binding tools Betwixt is a feature rich product that provides a lot of flexibility in its operation. This excerpt from Manning's Jakarta Commons Online Bookshelf gives an introduction to this component. Before we delve into Betwixt proper, let's take a brief look at data-binding concepts.

Understanding data binding

Data binding is the process by which data is transferred from one form to another. Although it's primarily used in conjunction with XML, data binding is a more generic concept and can be used to describe data in any format. The transformation from one form to another is inherently considered to be bidirectional.

Why is it called data binding? Well, one explanation is, because the two forms of data are considered bound to each other. Thus, technically, a change in the data in one form should automatically be reflected in the second form. Another explanation is that in business processes, the form of the data isn't important, what is important is the data itself, and data binding provides a mechanism for different forms of data to be seamlessly accessible.

Whatever the reasons for the name, the concept is straightforward. Data binding is the process of two-way transformations between data in different formats, with the emphasis on the actual data without regard to the structure of the data itself. Since we're concerned here with Java- and XML-based objects, we can further refine this concept. For our purposes, data binding is the two-way transformation between Java objects (JavaBeans) and XML data. The process by which Java objects are converted to corresponding XML representation is called marshalling, and the reverse conversion from XML to Java objects is called un-marshalling.

Finally, as we said before, although we're only concerned with Java and XML data binding, the concept can be extended to other formats as well. Thus, bidirectional transformations between XML data and a relational database tables/columns is also called data binding.

Betwixt structure

Betwixt closely follows the JavaBean structure for maintaining, accessing, and modifying information about a JavaBean and translating this information back and forth between its XML representations. The classes in the Sun package java.beans provide interfaces and concrete classes that give you information about a bean. Betwixt uses a similar mechanism: Specifically, it uses classes called XMLIntrospector and XMLBeanInfo that mirror the work done by the Introspector and BeanInfo classes from the java.beans package. Because these classes are so similar, let's look at those from the java.beans package first. This will help you better understand the process behind the Betwixt classes.

Java's BeanInfo and Introspector classes

Introspector is a concrete class in the java.beans package. It provides a standard way to learn more about a class, be it a JavaBean or not. If you call the static method getBeanInfo(Class targetClass), passing it the class that you want to learn more about, it returns a BeanInfo object that contains information about the target class.

Introspector works by first looking for an explicit BeanInfo class for your target class. Thus, as a conscientious developer, you may decide to provide a BeanInfo class for your target class that tells anyone who cares details about your target class over and above what the introspector can gather by itself using lowlevel reflection. If the introspector can locate such an explicit BeanInfo class about your target class, it merges this information with its own implicit reflection analysis on your target class and returns concrete BeanInfo information combining the two.

So, BeanInfo is information about your JavaBean packaged in a convenient class. However, BeanInfo is only an interface; if you decide to provide a BeanInfo class for your target class, you'll either need to implement this interface or use the SimpleBeanInfo class, extend it, and override the methods you want. The information about your target class should be provided using the MethodDescriptor, PropertyDescriptor, EventDescriptor, or BeanDescriptor class. Each of these class names should be self-describing. We recommend that you look up the java.beans package in the Java API for more information about these classes.

Figure 1 shows the relationship between the classes we just discussed.

Figure 1: Relationships between important classes of the java.beans package

Now that you know more about these base Java classes, let's look at how Betwixt uses the same principle in its XMLBeanInfo and XMLIntrospector classes to inspect information about Java objects.

Reading objects using XMLBeanInfo and XMLIntrospector

If we were to break down the whole bean --> XML Betwixt process into concrete steps, the first step would involve the XMLIntrospector class gathering information about the target class by searching for any java.beans.BeanInfo information, the second step would use this information to create an XMLBeanInfo object, and the final step would write this information out as XML using the BeanWriter and AbstractBeanWriter classes. These steps are shown in figure 2.

Figure 2: Steps in the bean --> XML process using Betwixt

In this section, we'll discuss the first two steps. The next section will look at the classes of step 3.

The XMLIntrospector class uses the java.beans.Introspector class to analyze the target class before it's written out as XML. The default introspection using java.beans.Introspector can be overridden by providing a .betwixt file, which contains information about the target class. This file must be named the same name as the target class (and an extension of .betwixt) and must be available in the CLASSPATH.

The XMLBeanInfo class uses org.apache.commons.betwixt.NodeDescriptor-based classes to encapsulate information. The org.apache.commons.betwixt.ElementDescriptor class defines information about the elements to be present in the XML, and org.apache.commons.betwixt. AttributeDescriptor defines information about the attributes. Both these classes derive from the NodeDescriptor class and are similar to the way MethodDescriptor and other classes define information about a JavaBean.

There is an internal process for caching preprocessed classes. Before a target class is introspected (a potentially time-consuming process), an internal registry is consulted. If this registry, defined by the interface org.apache.commons.betwixt.registry.XMLBeanInfoRegistry and implemented using org.apache.commons.betwixt.registry.DefaultXMLBeanInfoRegistry, doesn't contain XMLBeanInfo information about the target class, only then is the introspection continued. After the introspection is performed, this information is stored in the registry using the target class as the key. You can turn off this caching by using org.apache.commons.betwixt.registry. NoCacheRegistry instead of the default registry. This registry-indeed, any registry you create-can be set on the XMLIntrospector by calling setRegistry(XMLBeanInfoRegistry registry) method.

Even before the target class is looked up in the cache, the target class is normalized. Normalization is the process by which you can make changes to the target class before it's introspected. These changes can range from making changes to the superclass of your target class that should be introspected to supplying interface information that a class implements. This process allows you to make changes in the target class, thus restricting or enabling the class information before it's written out.

The default normalization, implemented by the class org.apache.commons.betwixt.strategy.ClassNormalizer, makes no changes to your class and returns it as it is. You can extend this class and enable your own normalization procedure by setting this class as the normalizer Betwixt should use by calling setClassNormalizer(ClassNormalizer classNormalizer) on the XMLIntrospector. Betwixt comes with another normalizer called ListedClassNormalizer, which allows you to substitute a target class for another class by calling the method addSubstitute(Class targetClass, Class substituteClass). This normalizer is useful for cases when you may want to restrict introspection of your classes to a standard set.

Betwixt writes out XML representations by using the classes AbstractBeanWriter and BeanWriter, which appear in the third step of the Betwixt bean --> XML process shown in figure 2. The next section looks at these classes.

Writing objects using AbstractBeanWriter and BeanWriter

AbstractBeanWriter is the superclass of BeanWriter. It contains concrete code to process the target class, whereas BeanWriter contains code to write the output as an XML stream. These classes, and the corresponding reader class called BeanReader, are in the org.apache.commons.betwixt.io package.

When Betwixt is writing your target bean as XML, it uses the org.apache.commons. betwixt.BindingConfiguration class to resolve three runtime issues. This class is a placeholder for resolving these issues and contains methods to set and get these issues as properties. This class can be set with the correct properties and attached to the AbstractBeanWriter (or the BeanReader, because the same configuration can be used for both reading and writing) by calling the method setBindingConfiguration(BindingConfiguration config).

The three issues in question are as follows:

  • Matching cyclic elements
  • Overriding the bean type
  • Object to String conversions

The first issue involves using an ID for matching cyclic elements. When an XML file is being written out, elements that reference each other, leading to a cyclic check, can be referenced using ID and IDREF attributes. This is the default behavior of Betwixt. However, you can change it by setting setMapIDs(boolean mapIDs) to false in the BindingConfiguration class. As a side note, theUclasses in the package org.apache.commons.betwixt.io.id can be used to customize the value of the ID and IDREF attributes. By default, Betwixt uses a sequential ID generator, SequentialIDGenerator, which starts at zero. An ID generator must implement the org.apache.commons.betwixt.io.IDGenerator interface or extend the org.apache.commons.betwixt.io.id.AbstractIDGenerator class. Another ID generator, besides the sequential one, is RandomIDGenerator.

The second issue deals with the class name attribute used to override the type of a bean. By default, Betwixt uses className. You can set it to your actual class name, or to its superclass name, or to anything you desire. Note that you must use the same name (or the same BindingConfiguration class) when reading the XML back in, to ensure that your object is created exactly as it was originally.

The final issue that BindingConfiguration allows you to handle is the class that will be used for Object to String conversions. Such conversions, as may be obvious, are used to represent String versions of Objects. However, if you're scratching your head wondering how it's different from a toString() operation, hold on to your hats: We're talking about a round-trip conversion during which Objects are converted to String representations and String representations are converted to their original Object types. Betwixt's default mechanism for doing this uses the ConvertUtils package of the BeanUtils component.

So far, we've concentrated on writing beans as XML. In the next section, we'll cover the XML --> bean process, which is based on the Digester component.

Converting XML to beans

On the other end of the bean --> XML spectrum is the XML --> bean process. It relies on the org.apache.commons.betwixt.io.BeanReader class, which is the primary class for reading an XML document and converting it to a tree of beans. BeanReader uses a subset of the Digester component to perform its functionality, and the BeanReader class extends the org.apache.commons.digester.Digester class. It makes sense, because Digester provides functionality to create beans read from an XML file. The Digester subset classes are placed in the package org.apache.commons.betwixt.digester.

You need to register the top-level element in the XML document as a JavaBean class with the BeanReader before you can parse a document. You can do this either by calling registerBeanClass(Class clazz) or by calling registerBeanClass(String path, Class clazz). The second method allows you to match the top-level element in case it differs from the name of the actual class, or if the top-level element isn't the element that you want created. In this case, the path is an XPath traversal to the actual element within the document. This rounds out our coverage of the Betwixt structure. It's time to look at some code and see Betwixt at work.

Betwixt in action

We'll start working with Betwixt by looking at a simple round-trip operation of converting a JavaBean to an XML representation and then back from that XML representation to the original JavaBean.

Note: Before you try any of these examples, make sure you have the commons-collections, commonslogging, commons-digester, and commons-beanutils libraries in your CLASSPATH, in addition to the Betwixt library.

Simple round trip

This example of using Betwixt will show how you can convert a bean --> XML and then use the same Betwixt-created XML representation to arrive at the original bean. For use within this example and the examples to follow, Listing 1 shows a simple class that models an ingredient for meal, Listing 2 shows a Meal class that uses the Ingredient class and finally Listing 3 shows a MealPlan class that uses the Meal class to model a meal plan for a week.

Listing 1. Ingredient class

package com.manning.commons.chapter05;
import java.util.Collection;
public class Ingredient {
   private String key;
   private String name;
   private String alternate;
   public String getKey() {
      return this.key;
   }
   public void setKey(String key) {
      this.key = key;
   }
   public String getName() {
      return this.name;
   }
   public void setName(String name) {
      this.name = name;
   }
   public String getAlternate() {
      return this.alternate;
   }
   public void setAlternate(String alternate) {
      this.alternate = alternate;
   }
   public Ingredient() {
      this("Empty", "Empty", "Empty");
   }
   public Ingredient(String key, String name, String alternate) {
      this.key = key;
      this.name = name;
      this.alternate = alternate;
   }
   public String toString() {
      return "[Key=" + this.key + ", Name=" + this.name + ",
               Alternate=" + this.alternate + "]";
   }
}

Listing 2. A Meal Class

package com.manning.commons.chapter05;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
public class Meal {
   private int weekday;
   private int mealType;
   private String mealName;
   private Map ingredients;
   /** 0 - Sunday, 1 - Monday ..... 6 - Saturday */
   public int getWeekday() {
      return this.weekday;
   }
   public void setWeekday(int weekday) {
      this.weekday = weekday;
   }
   /** 0 - breakfast, 1 - lunch, 2 - dinner, 3 - other */
   public int getMealType() {
      return this.mealType;
   }
   public void setMealType(int mealType) {
      this.mealType = mealType;
   }
   public String getMealName() {
      return this.mealName;
   }
   public void setMealName(String mealName) {
      this.mealName = mealName;
   }
   public Map getIngredients() {
      return this.ingredients;
   }
   public void addIngredient(Ingredient ingredient) {
      addIngredient(ingredient.getKey(), ingredient);
   }
   public void addIngredient(String key, Ingredient ingredient) {
      ingredients.put(key, ingredient);
   }
   public Meal() {
      this(0, 0, "No meal");
   }
   public Meal(int weekday, int mealType, String mealName) {
      this.weekday = weekday;
      this.mealType = mealType;
      this.mealName = mealName;
      ingredients = new HashMap();
   }
   public String toString() {
      StringBuffer buffer = new StringBuffer(
         "Meal name: " + this.mealName + ", Meal Type: "
                       + this.mealType + " Meal Day: "
                       + this.weekday);
      buffer.append("rnIngredients:rn");
      Iterator itr = ingredients.values().iterator();
      while(itr.hasNext()) {
         buffer.append(itr.next() + "rn");
      }
      return buffer.toString();
   }
}

Listing 3. A MealPlan class

package com.manning.commons.chapter05;
import java.util.Map;
import java.util.Date;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Calendar;
import java.util.ArrayList;
import java.util.Collection;
import java.util.GregorianCalendar;
public class MealPlan {
   private Date startDate;
   private Date endDate;
   private Collection meals;
   private Meal snack;
   public Date getStartDate() {
      return this.startDate;
   }
   public void setStartDate(Date startDate) {
      this.startDate = startDate;
   }
   public Date getEndDate() {
      return this.endDate;
   }
   public void setEndDate(Date endDate) {
      this.endDate = endDate;
   }
   public Collection getMeals() {
      return this.meals;
   }
   public Meal getSnack() {
      return this.snack;
   }
   public void setSnack(Meal snack) {
      this.snack = snack;
   }
   public void addMeal(Meal meal) {
      if(meal == null)
         throw new IllegalArgumentException("Meal cannot be added
                                             null");
      meals.add(meal);
   }
   /** If this is called, it creates a MealPlan starting from now
     * till end of week */
   public MealPlan() {
      this(null, null);
   }
   public MealPlan(Date startDate, Date endDate) {
      if(startDate == null && endDate == null) {
         startDate = new Date();
         Calendar calendar = new GregorianCalendar();
         calendar.setTime(startDate);
         calendar.add(Calendar.WEEK_OF_YEAR, 1);
         endDate = calendar.getTime();
      }
      if(startDate == null || endDate
                   == null || startDate.after(endDate))
         throw new IllegalArgumentException("Please check the dates");
      this.startDate = startDate;
      this.endDate = endDate;
      meals = new ArrayList();
   }
   public String toString() {
      StringBuffer buffer =
         new StringBuffer(" **** Your Meal Plan **** ");
      buffer.append("rnStart Date: " + this.startDate);
      buffer.append("rnEnd Date: " + this.endDate + "rnrn");
      Iterator itr = meals.iterator();
      while(itr.hasNext()) {
         buffer.append(itr.next() + "rn");
      }
      buffer.append
         (" ----------------------------------------------rn");
      return buffer.toString();
   }
}

Listing 4. Simple round-trip conversion example

package com.manning.commons.chapter05;
import java.io.File;
import java.io.FileWriter;
import org.apache.commons.betwixt.io.BeanReader;
import org.apache.commons.betwixt.io.BeanWriter;
public class SimpleRoundTrip {
public static void main(String args[]) throws Exception {
FileWriter fWriter = new FileWriter("output.xml");
BeanWriter bWriter = new BeanWriter(fWriter);
Ingredient ingredient = new Ingredient("POT", "Potato", "None");
bWriter.write("ingredient", ingredient);
bWriter.flush();
BeanReader reader = new BeanReader();
reader.registerBeanClass("ingredient", Ingredient.class);
Ingredient ingReadFromFile =
(Ingredient)reader.parse(new File("output.xml"));
System.err.println(ingReadFromFile);
}
}

To convert a JavaBean to XML, you need an instance of the BeanWriter class. This class contains four separate constructors, allowing you to use an OutputStream (or one with a different encoding than the platform default), a Writer, or a default empty constructor that defaults to using System.out for its output. In this case, we've used a FileWriter, which outputs the results of the conversion to a file called output.xml.

The Ingredient class is created here with dummy data. Note that contrary to what we mentioned earlier, JavaBean-to-XML conversion doesn't require the presence of an empty constructor. Once we're ready with the bean we want to write, we write it out using the write method to the underlying stream. The first argument is the top-level element name, and the second argument is the bean to write. Finally, although Betwixt is configured to flush automatically for OutputStreams, it's a good idea to do so yourself in all cases.

To start the reverse process, we use the BeanReader class, which is really a wrapper around the Digester component. Before the XML document can be read in, Betwixt needs to know the toplevel element and the class it maps to. This is done using the registerBeanClass method. By registering the class, Betwixt creates an ObjectCreateRule behind the scenes for the internal Digester instance.

All that's left is for the BeanReader class to read in the XML representation. This is again delegated internally to the Digester instance. The resulting object that is created is an exact representation of the original Ingredient object.

With this example behind us, let's look at how you can read information about a target bean from an external file.

Using a .betwixt file

A .betwixt file is used to provide information about a target class. If Betwixt finds a .betwixt file in the CLASSPATH that matches the target class name, it doesn't use the default java.beans. Introspector and instead relies on the information about the bean in the XML file. Note that this file can be used in both bean --> XML and XML --> bean transformation. Any properties that you set in the code are overwritten if matching properties are found in this file. This implies that the .betwixt file has the final say in how Betwixt's transformation for a target class is configured. In this section, you'll see how to use this file.

The .betwixt file should be described in XML. This means that there must either be a schema or a Document Type Definition (DTD) for the valid structure of this file. Listing 5 shows the relevant DTD, which you can find in the resources section of the Betwixt source code.

Listing 5. DTD for .betwixt file

<?xml version='1.0' encoding='UTF-8'?>
<!-- $Id: dotbetwixt.dtd,v 1.7 2003/08/24 16:58:54 rdonkin Exp $ -->
<!-- The DTD for .betwixt files -->
<!ELEMENT attribute EMPTY>
<!ATTLIST attribute
name CDATA #IMPLIED
type CDATA #IMPLIED
uri CDATA #IMPLIED
value CDATA #IMPLIED
property CDATA #IMPLIED
>
<!ELEMENT addDefaults EMPTY>
<!ELEMENT element (attribute|addDefaults|element|text)*>
<!ATTLIST element
name CDATA #REQUIRED
type CDATA #IMPLIED
uri CDATA #IMPLIED
value CDATA #IMPLIED
property CDATA #IMPLIED
updater CDATA #IMPLIED
class CDATA #IMPLIED
>
<!ELEMENT text EMPTY>
<!-- If you specify value, you cannot specify type or property -->
<!ATTLIST text
type CDATA #IMPLIED
value CDATA #IMPLIED
property CDATA #IMPLIED
>
<!ELEMENT hide EMPTY>
<!ATTLIST hide
property CDATA #REQUIRED
>
<!ELEMENT info (hide|element)*>
<!ATTLIST info
primitiveTypes (element|attribute) "attribute"
>

The root element of the .betwixt file, even though it may not be obvious from the DTD, should be the info element; it has only one attribute, primitiveTypes. This attribute specifies whether primitive data types like String and int in the target class should be written out as elements or attributes.

Potential bug: Even though the DTD specifies that the default value for the primitiveTypes attribute is attribute, in reality the default seems to be element. By the time this is published, this bug may have been resolved.

Let's start by creating a .betwixt file for our Ingredient class. We'll use the same code as in the previous section, and we'll drop this ingredient.betwixt file in the CLASSPATH. This will ensure that it's read by Betwixt and used for writing output.xml from listing 4 (and for reading and creating an Ingredient object). Listing 6 shows the first draft of this file.

Listing 6 First draft of ingredient.betwixt

<?xml version='1.0' encoding='UTF-8' ?>
<info primitiveTypes="attribute">
<element name='ingredient'>
<addDefaults/>
</element>
</info>

The root element is info; it contains a single attribute called primitiveTypes. The root element in the output.xml file must be named ingredient. And, Betwixt must use java.beans.Introspector to do the rest of the work of looking at the Ingredient bean.

If you place this file in the CLASSPATH and rerun the code in listing 4, your output.xml file will look like this:

<ingredient alternate="None" key="POT" name="Potato"/>

The output is a single line with attributes for all the properties and the root element is ingredient.

To stop the printing of an element/attribute, use the element hide in the .betwixt file and specify the property that you want to suppress. This is shown in the second draft, in listing 7.

Listing 7. Stopping the output of an element in ingredient.betwixt using the hide element

<?xml version='1.0' encoding='UTF-8' ?>
<info primitiveTypes="attribute">
<hide property="alternate" />
<element name='ingredient'>
<addDefaults/>
</element>
</info>

You can use the hide element to suppress the printing of undesired properties. Note that the list of hide elements should appear before the element element. The output of using this file now looks like this:

<ingredient key="POT" name="Potato"/>

This effectively suppresses the alternate property. The result would be similar if primitiveTypes was set to element instead of attribute: Both key and name would be printed as elements, and alternate would be suppressed.

If you don't want default matching for your properties and elements/attributes, you can specify your own. You can specify as many properties you want to match and leave the rest to the default introspector by using the addDefaults element. Until now, we've left the matching of all the elements to the default introspector. Let's change this: listing 8 shows how.

Listing 8. Overriding the default matching behavior

<?xml version='1.0' encoding='UTF-8' ?>
<info primitiveTypes="element">
<element name='ingredient'>
<element name="key" property="key"/>
<element name="name" property="name"/>
<element name="alternate" property="alternate"/>
</element>
</info>

As you can see, the elements in the output.xml file are now explicitly matched to their respective properties in the Ingredient class. You could change this behavior to match these properties to any element that you wanted; this works in both directions of a transformation. You can also specify the method to use for the setting of these properties. Thus, instead of the default setXXX method, you can use any method that you wish to call, by using the updater attribute. For example, suppose you have a method calledJsetDifferentName instead of setName, and you want to use this method to set the name property. You change this in listing 8 as shown here:

<element name="name" property="name" updater="setDifferentName"/>

Now, each time Betwixt encounters the name element in the XML file, it will map that to the name property of the Ingredient object using the setDifferentName method. You can take this behavior one step further by using the attribute class, which allows you to use a different class than the primitive class for the instantiation of your property. Thus, if you want your String properties to be instantiated using a modified version of the String class called ModifiedString, you can use the following:

<element name="name" property="name" class="ModifiedString"/>

This is especially useful in cases where the type of a property is specified using an interface or an abstract class, and you want to use a specific implementation to create your object.

Working with multiple values

To show how Betwixt works with collections and maps, consider listing 9. It shows the code for converting a Meal object to XML and vice versa. Note that the Meal class contains the ingredients as a HashMap.

Listing 9. Converting a Meal object to XML and back

package com.manning.commons.chapter05;
import java.io.File;
import java.io.FileWriter;
import org.apache.commons.betwixt.io.BeanReader;
import org.apache.commons.betwixt.io.BeanWriter;
public class MultipleValues {
public static void main(String args[]) throws Exception {
FileWriter fWriter = new FileWriter("output.xml");
BeanWriter bWriter = new BeanWriter(fWriter);
Meal meal = new Meal(1, 1, "Potato Bake");
Ingredient ingredient1 = new Ingredient("POT", "Potato", "None");
Ingredient ingredient2 = new Ingredient("CRM", "Cream", "None");
meal.addIngredient(ingredient1);
meal.addIngredient(ingredient2);
bWriter.enablePrettyPrint();
bWriter.write("meal", meal);
bWriter.flush();
BeanReader reader = new BeanReader();
reader.registerBeanClass("meal", Meal.class);
Meal mealReadFromFile =
(Meal)reader.parse(new File("output.xml"));
System.err.println(mealReadFromFile);
}
}

Most of this code doesn't change from listing 4.The output.xml file created by running this code is shown in listing 10.

Listing 10. output.xml for multiple values represented as a HashMap

<meal>
<ingredients>
<entry>
<key>POT</key>
<value>
<key>POT</key>
<name>Potato</name>
<alternate>None</alternate>
</value>
</entry>
<entry>
<key>CRM</key>
<value>
<key>CRM</key>
<name>Cream</name>
<alternate>None</alternate>
</value>
</entry>
</ingredients>
<mealName>Potato Bake</mealName>
<mealType>1</mealType>
<weekday>1</weekday>
</meal>

As expected, the meal root element contains an ingredients element that represents the map. Each value in the map is entered in the XML as a separate entry element. Each entry element contains two elements: the key element, representing the key; and the value element, representing the value associated with that key (in this case, an Ingredient object with its own properties marked as elements). The rest of the meal properties are listed at the end.

If, instead of a Map, we were dealing with a collection-based object, the output would be similar. But instead of the entry element, we'd have the actual name of the object (); and, of course, there would be no key element.

The reverse process relies on the presence of an addXXX method in the original object. Thus, the Meal object is faithfully re-created with the right ingredients, because Betwixt recognizes the presence of an addIngredient method in the Meal class. You can plug in your own method by specifying the update attribute in a .betwixt file, for the matching element. For example:

<element name="ingredients" property="ingredients"
         updater="customAddMethod" />

You now know how to read and write multiple values using Betwixt. In the next section, we'll consider the custom conversion process, which lest you specify specially built patterns for String --> Object and Object --> String conversions.

Custom conversions

Betwixt uses the ConvertUtils component of Commons BeanUtils for converting Objects to Strings and vice versa. However, at times, this default behavior may not be what you want. For example, you may want the dates in your output to be printed in a particular format (the default format prints the date as EEE MMM dd HH:mm:ss zzz yyyy, using the UK as its locale). You can do this by using a custom implementation of the ObjectStringConverter class and installing it as the default on the BindingConfiguration class.

Listing 11 shows an implementation of the ObjectStringConverter class for converting dates in a format that is different from the default implementation. Note that this class only extends the objectToString method, which enables bean --> XML conversion, and not the stringToObject method, which you would have to override for XML --> bean conversion.

Listing 11. Converting dates in a special format

package com.manning.commons.chapter05;
import java.text.SimpleDateFormat;
import org.apache.commons.betwixt.expression.Context;
import org.apache.commons.betwixt.strategy.ObjectStringConverter;
public class DateConvertor extends ObjectStringConverter {
private static final SimpleDateFormat formatter
= new SimpleDateFormat("dd-MMM-yyyy");
public String objectToString(
Object object, Class type, String flavor,
Context context) {
if(object != null) {
if(object instanceof java.util.Date) {
return formatter.format( (java.util.Date) object );
}
}
return super.objectToString(object, type, flavor, context);
}
}

To use this special converter, we need to install it on Betwixt using the BindingConfiguration class, as shown in listing 12.

Listing 12. Installing and using the custom converter

package com.manning.commons.chapter05;
import java.io.File;
import java.io.FileWriter;
import org.apache.commons.betwixt.io.BeanReader;
import org.apache.commons.betwixt.io.BeanWriter;
import org.apache.commons.betwixt.BindingConfiguration;
public class CustomConversion {
public static void main(String args[]) throws Exception {
FileWriter fWriter = new FileWriter("output.xml");
BeanWriter bWriter = new BeanWriter(fWriter);
MealPlan plan = new MealPlan();
Meal meal = new Meal(1, 1, "Potato Bake");
Ingredient ingredient1 = new Ingredient("POT", "Potato", "None");
Ingredient ingredient2 = new Ingredient("CRM", "Cream", "None");
meal.addIngredient(ingredient1);
meal.addIngredient(ingredient2);
plan.addMeal(meal);
bWriter.enablePrettyPrint();
BindingConfiguration bConfig =
bWriter.getBindingConfiguration();
bConfig.setObjectStringConverter(new DateConvertor());
bWriter.setBindingConfiguration(bConfig);
bWriter.write("mealplan", plan);
bWriter.flush();
}
}

The output of running the code in listing 12 is as shown in listing 13. Notice that the dates are written in the special format dd-MMM-yyyy. If you run the code in listing 12 without installing the special converter, the dates will be printed in the default format EEE MMM dd HH:mm:ss zzz yyyy.

Listing 13. output.xml, using a special converter on the MealPlan object

<mealplan>
<endDate>>23-Apr-2004</endDate>
<meals>
<meal>
<ingredients>
<entry>
<key>POT</key>
<value>
<key>POT</key>
<name>Potato</name>
<alternate>None</alternate>
</value>
</entry>
<entry>
<key>CRM</key>
<value>
<key>CRM</key>
<name>Cream</name>
<alternate>None</alternate>
</value>
</entry>
</ingredients>
<mealName>Potato Bake</mealName>
<mealType>1</mealType>
<weekday>1</weekday>
</meal>
</meals>
<startDate>16-Apr-2004</startDate>
</mealplan>

While reading this XML file, we can't use the default converter: It expects the date in the default format, but the date here is in the special format. To make a complete round trip, the stringToObject method also needs to be overwritten in listing 8. We'll leave that as an exercise for you!

Summary

Betwixt is a utility tool for converting JavaBeans to XML and vice versa. It's a flexible tool that uses Digester rules to create objects from XML files. It also converts JavaBeans to XML in a manner that can be customized to a high degree.

About the Author

Vikram Goyal is a Java Developer by day and a technical writer by night. Vikram has worked with Java since its inception and remembers the time when Tomcat meant an animal and not a servlet engine. Vikram regularly writes how-to articles on open source projects and his series of articles on Jakarta Commons was the first such effort to make some sense out of the chaotic world that Jakarta Commons is. This article series is still reflected in the official main entry page of Jakarta Commons as the only online series covering these components.

Vikram enjoys working with Open Source technologies, Team Leading and Civilization 3. He lives in Brisbane, Australia, with his wife.

Source of This Material

Jakarta Commons Online Bookshelf
By Vikram Goyal



Published: March, 2005, 402 pages
Published by Manning Publications
PDF ebook: $39.95
Jakarta Commons Online Bookshelf provides detailed technical information about 18 components from Jakarta Commons Proper and 1 component from the Commons Sandbox (Chain, module 13). Modules can be purchased individually from the Manning web site at www.manning.com/goyal. This material is from Module 5 of the book.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date