BeanUtils, Digester, and Type Conversion

by Jason Menard

The following assumes some level of existing knowledge with the Commons BeanUtils and Commons Digester packages. See Resources for further information.

Brett Bell asked on JavaRanch recently if Digester can convert a String held in an XML attribute to a java.util.Date or int. This brings up a little understood (and poorly documented, IMHO) topic relating to the Commons Digester and BeanUtils packages - that of type conversion.

The first thing to realize is that Digester does not handle the type conversions itself. Digester makes heavy use of the BeanUtils package to work its magic, and this includes using that package for type conversions. So in order to answer Brett's questions, we need to take a closer look at a class in BeanUtils called ConvertUtils.

It is the ConvertUtils class that handles type conversion for BeanUtils. This class's convert() methods allow conversions from Strings to Objects of a given class, from Objects of a given class to Strings, and from a String[] of values to an Object[] of a given class. In order to do this, ConvertUtils must have registered with it an implementation of the Converter interface for each type of conversion it wishes to perform.

The default configuration for ConvertUtils will handle conversions of the following primitives and classes: java.lang.BigDecimal, java.lang.BigInteger, boolean and java.lang.Boolean, byte and java.lang.Byte, char and java.lang.Character, java.lang.Class, double and java.lang.Double, float and java.lang.Float, int and java.lang.Integer, long and java.lang.Long, short and java.lang.Short, java.lang.String, java.sql.Date, java.sql.Time, and java.sql.Timestamp. You can add custom converters to handle anything not covered by the default configuration merely by registering your own implementation of the Converter interface using the ConvertUtils.register() method.

Let's look at some code. Assume we have the following XML document, "testdoc.xml", which we wish to map to an instance of a Thing class.

<thing>
  <number>3</number>
  <date>06/07/04</date>
  <point>3,5</point>
</thing>

Here is our Thing class.

package commonstests;

import java.awt.Point;
import java.util.*;
import java.text.*;

public class Thing {
    private int number;
    private Date date;
    private Point point;

    public Thing() {
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }
    
    public Point getPoint() {
        return point;
    }
    
    public void setPoint(Point point) {
        this.point = point;
    }
    
    public String toString() {
        String dateString = null;
        if (date != null) {
            dateString = DateFormat.getDateInstance().format(date);
        }
        StringBuffer sb = new StringBuffer();
        sb.append("[Thing number=");
        sb.append(number);
        sb.append(" date=");
        sb.append(dateString);
        sb.append(" point=");
        if (point != null) {
            sb.append(point.toString());
        } else {
            sb.append("null");
        }
        sb.append("]");
        return sb.toString();
    }
}

Looking at the XML and the Thing class we can see that we want to map the XML String data to an int, a java.util.Date, and a java.awt.Point. Assuming an instance of Thing is at the root of our digester stack, we would use the following processing rules.

digester.addBeanPropertySetter("thing/number");
digester.addBeanPropertySetter("thing/date");
digester.addBeanPropertySetter("thing/point");

Knowing that Digester uses BeanUtils to handle the type conversion, and knowing that an int is handled by the default configuration of ConvertUtils, we should have no problem processing the <number> element. Unfortunately, things will come to a screeching hault as soon as Digester tries to process the <date> element. You might think that this would be a good time to implement a custom Converter. Luckily, we don't actually have to.

The BeanUtils package comes with a number of concrete Converter implementations for use with locale-sensitive classes, such as java.util.Date. These locale-sensitive converters may be found in the org.apache.commons.beanutils.locale.converters package. Included in this package is DateLocaleConverter, which will handle conversion for java.util.Date. We can just go ahead and use this Converter instead of implementing our own.

At this point I should quickly mention that the org.apache.commons.beanutils.locale package contains locale-sensitive LocaleBeanUtils and LocaleConvertUtils classes which you can use in place of BeanUtils and ConvertUtils respectively, if you are doing a lot of locale-dependant population of your Java Beans. Digester does not use these however so it's nothing we need to worry about right now.

In order to configure a DateLocaleConverter for our needs, we need to provide it with a Locale and a pattern for our date format. DateLocaleConverter uses an instance of java.text.SimpleDateFormat to handle the formatting, so a quick look at the API for that class will allow us to come up with a suitable pattern for our date. Once we configure an instance of DateLocaleConverter then we must simply register it with ConvertUtils, also supplying the class type we wish ConvertUtils to use this Converter for, which is of course java.util.Date in this case. We'll also set our instance of DateLocaleConverter to be lenient in its parsing, by using the setLenient() method.

String pattern = "MM/dd/yy";
Locale locale = Locale.getDefault();
DateLocaleConverter converter = new DateLocaleConverter(locale, pattern);
converter.setLenient(true);
ConvertUtils.register(converter, java.util.Date.class);

Now if we use Digester to process our XML it will have no problem handling the date conversion. Unfortunately, this time it will error out when it tries to process the <point> element. We need to write a custom implementation of Converter to handle java.awt.Point.

Writing an implementation of Converter really isn't all that involved, and the only method we need to provide is convert(java.lang.Class type, java.lang.Object value). This method throws a ConversionException if a problem is encountered during a Conversion. One thing to keep in mind when writing your converter is what should happen when an empty element exists in your XML, such as <point/>. Should it populate your Object with null or should it use some default value? With that in mind, it's not a bad idea to allow your converter the option of providing a default value. Here's a converter that will handle java.awt.Point.

package commonstests;

import java.awt.Point;
import org.apache.commons.beanutils.Converter;
import org.apache.commons.beanutils.ConversionException;

public final class PointConverter implements Converter {
    private Object defaultValue = null;
    private boolean useDefault = true;

    public PointConverter() {
        this.defaultValue = null;
        this.useDefault = false;
    }

    public PointConverter(Object defaultValue) {
        this.defaultValue = defaultValue;
        this.useDefault = true;
    }

    public Object convert(Class type, Object value) throws Conversion Exception {
        if (value == null) {
            if (useDefault) {
                return (defaultValue);
            } else {
                throw new ConversionException("No value specified");
            }
        }

        if (value instanceof Point) {
            return (value);
        }

        Point point = null;
        if (value instanceof String) {
            try {
                point = parsePoint((String)value);
            }
            catch (Exception e) {
                if (useDefault) {
                    return (defaultValue);
                } else {
                    throw new ConversionException(e);
                }
            }
        } else {
            throw new ConversionException("Input value not of correct type");
        }

        return point;
    }

    private Point parsePoint(String s) throws ConversionException {
        if (s == null) {
            throw new ConversionException("No value specified");
        }
        if (s.length() < 1) {
            return null;
        }
        String noSpaces = s;
        if (s.indexOf(' ') > -1) {
            noSpaces = s.replaceAll(" ", "");
        }
        String[] coords = noSpaces.split(",");
        if (coords.length != 2) {
            throw new ConversionException("Value not in proper format: x,y");
        }
        int x = 0;
        int y = 0;
        try {
            x = Integer.parseInt(coords[0]);
            y = Integer.parseInt(coords[1]);
        } catch (NumberFormatException ex) {
            throw new ConversionException("Value not in proper format: x,y");
        }
        Point point = new Point(x, y);
        return point;
    }
}

There's not really all that much to it. Of course, we also have to remember to register our PointConverter with ConvertUtils.

ConvertUtils.register(new PointConverter(), java.awt.Point.class);

Now we should have no problem with Digester performing the proper mappings. Here's a driver you may run which demonstrates this.

package commonstests;

import java.io.*;
import java.util.*;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.locale.converters.DateLocaleConverter;
import org.apache.commons.digester.*;

public class TestDigester {
    private Digester digester;

    public TestDigester() {
    }

    public void run() {
        configureConverter();
        digester = new Digester();
        Thing thing = null;
        File file = new File("testdoc.xml");
        digester.setValidating(false);
        digester.push(new Thing());
        addRules();
        try {
            thing = (Thing)digester.parse(file);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(thing);
    }

    private void addRules() {
        digester.addBeanPropertySetter("thing/number");
        digester.addBeanPropertySetter("thing/date");
        digester.addBeanPropertySetter("thing/point");
    }

    private void configureConverter() {
        String pattern = "MM/dd/yy";
        Locale locale = Locale.getDefault();
        DateLocaleConverter converter = new DateLocaleConverter(locale, pattern);
        converter.setLenient(true);
        ConvertUtils.register(converter, java.util.Date.class);
        ConvertUtils.register(new PointConverter(), java.awt.Point.class);
    }

    public static void main(String[] args) {
        TestDigester td = new TestDigester();
        td.run();
    }
}

Happy converting!

Resources