Jason's random musings on Java, the Universe, and Everything

Categories : All | Articles | Fire & Rescue | Personal | Programming | Reviews

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


One thing I'm having a real issue with is BeanUtils using a registered converter in both directions.
I registered a DateLocaleConverter as mentioned here to convert with the format "yyyy-MM-dd". When copying a String to a Date there are no problems. BeanUtils uses my converter and set the Date on my bean. However, going the other way from Date to String, BeanUtils always seems to use a StringConverter and just does a toString() on my date, ignoring the format I've defined in my converter.

Is there something I'm missing here?

Why will BeanUtils not use my registered converter in both directions?
Create another converter for strings. If the type of the value object being converted is of type date then convert it to the format you want. Hope this helps, beat my head on it all night.
Thank you, you made an excellent overview on Digester and the underlying Converter. I found another piece in the Date-Time-Conversion puzzle useful: In XML schema, Dates are formatted using ISO 8601. Being too lazy to write a parser myself, I stumbled upon Joda. Simply wrap this into your own Converter. Then, construct a Joda DateTime object from the String and convert it to your desired type of Date representation.
Forcing the developer to register a single converter which gets used everywhere is a poor design choice on Apache's part.

What I'd really want is the ability to control conversion on a digester-by-digester basis -- or, ideally, down to the level of an individual element or attribute. For example, I might have two different representations of a date/time string in different parts of my XML, and they would require two separate DateFormat instances to parse properly.

Having a single "default" converter for a type is not bad ... but give us the ability to override it.



Add a comment

Title
Body
HTML : b, i, blockquote, br, p, pre, a href="", ul, ol, li
Math Quiz 9 + 7 = (Helps stop blog spam)
Name
E-mail address
Website
Remember me Yes  No 

E-mail addresses are not publicly displayed, so please only leave your e-mail address if you would like to be notified when new comments are added to this blog entry (you can opt-out later).

TrackBack to http://radio.javaranch.com/jason/addTrackBack.action?entry=1086797433000