Castor XML: Writing Custom FieldHandlers Documentation Author(s): Keith Visco
Introduction Writing a simple FieldHandler Writing a GeneralizedFieldHandler No Constructor, No Problem! Collections and FieldHandlers
Introduction Sometimes we need to deal with a data format that Castor doesn't support out-of-the-box, such as an unsupported Date/Time representation, or we want to wrap and unwrap fields in Wrapper objects to get the desired XML output without changing our object model. To handle these cases Castor allows specifying a custom FieldHandler which can do these varying conversions during calls to the fields setter and getter methods. The FieldHandler is the basic interface used by the Castor Framework when accessing field values or setting them. By specifying a custom FieldHandler in the mapping file we can basically intercept the calls to retreive or set a field's value and do whatever conversions are necessary. Writing a simple FieldHandler When a writing a FieldHandler handler we need to provide implementations of the various methods specified in the FieldHandler interface. The main two methods are the getValue and setValue methods which will basically handle all our conversion code. The other methods provide ways to create a new instance of the field's value or reset the field value.
Tip: | It's actually even easier to write custom field handlers if we use a GeneralizedFieldHandler. See more details in the next section. |
| Let's take a look at how to convert a date in the format YYYY-MM-DD using a custom FieldHandler. We want to marshal the following XML input file: test.xml<?xml version="1.0"?>
<root>2004-05-10</root> |
| The class we'll be marshalling from and unmarshalling to looks as follows: Root.java
import java.util.Date;
public class Root {
private Date _date = null;
public Root() {
super();
}
public Date getDate() {
return _date;
}
public void setDate(Date date) {
_date = date;
}
|
| So we need to write a custom FieldHandler that takes the input String and converts it into the proper java.util.Date instance: MyDateHandler.java
import org.exolab.castor.mapping.FieldHandler;
import org.exolab.castor.mapping.FieldDescriptor;
import org.exolab.castor.mapping.ValidityException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* The FieldHandler for the Date class
*
*/
public class MyDateHandler implements FieldHandler
{
private static final String FORMAT = "yyyy-MM-dd";
/**
* Creates a new MyDateHandler instance
*/
public MyDateHandler() {
super();
}
/**
* Returns the value of the field from the object.
*
* @param object The object
* @return The value of the field
* @throws IllegalStateException The Java object has changed and
* is no longer supported by this handler, or the handler is not
* compatiable with the Java object
*/
public Object getValue( Object object )
throws IllegalStateException
{
Root root = (Root)object;
Date value = root.getDate();
if (value == null) return null;
SimpleDateFormat formatter = new SimpleDateFormat(FORMAT);
Date date = (Date)value;
return formatter.format(date);
}
/**
* Sets the value of the field on the object.
*
* @param object The object
* @param value The new value
* @throws IllegalStateException The Java object has changed and
* is no longer supported by this handler, or the handler is not
* compatiable with the Java object
* @thorws IllegalArgumentException The value passed is not of
* a supported type
*/
public void setValue( Object object, Object value )
throws IllegalStateException, IllegalArgumentException
{
Root root = (Root)object;
SimpleDateFormat formatter = new SimpleDateFormat(FORMAT);
Date date = null;
try {
date = formatter.parse((String)value);
}
catch(ParseException px) {
throw new IllegalArgumentException(px.getMessage());
}
root.setDate(date);
}
/**
* Creates a new instance of the object described by this field.
*
* @param parent The object for which the field is created
* @return A new instance of the field's value
* @throws IllegalStateException This field is a simple type and
* cannot be instantiated
*/
public Object newInstance( Object parent )
throws IllegalStateException
{
//-- Since it's marked as a string...just return null,
//-- it's not needed.
return null;
}
/**
* Sets the value of the field to a default value.
*
* Reference fields are set to null, primitive fields are set to
* their default value, collection fields are emptied of all
* elements.
*
* @param object The object
* @throws IllegalStateException The Java object has changed and
* is no longer supported by this handler, or the handler is not
* compatiable with the Java object
*/
public void resetValue( Object object )
throws IllegalStateException, IllegalArgumentException
{
((Root)object).setDate(null);
}
/**
* @deprecated No longer supported
*/
public void checkValidity( Object object )
throws ValidityException, IllegalStateException
{
// do nothing
}
}
|
|
Tip: | The newInstance method should return null for immutable types. |
| There is also an AbstractFieldHandler that we can extend instead of implementing FieldHandler directly. Not only do we not have to implement deprecated methods, but we can also gain access to the FieldDescriptor used by Castor. In order to tell Castor that we want to use our Custom FieldHandler we must specify it in the mapping file: mapping.xml
<?xml version="1.0"?>
<mapping>
<class name="Root">
<field name="date" type="string" handler="MyDateHandler">
<bind-xml node="text"/>
</field>
</class>
</mapping>
|
| We can now use a simple Test class to unmarshal our XML document: Test.java
import java.io.*;
import org.exolab.castor.xml.*;
import org.exolab.castor.mapping.*;
public class Test {
public static void main(String[] args) {
try {
//--load mapping
Mapping mapping = new Mapping();
mapping.loadMapping("mapping.xml");
System.out.println("unmarshalling root instance:");
System.out.println();
Reader reader = new FileReader("test.xml");
Unmarshaller unmarshaller = new Unmarshaller(Root.class);
unmarshaller.setMapping(mapping);
Root root = (Root) unmarshaller.unmarshal(reader);
reader.close();
System.out.println("Root#getDate : " + root.getDate());
}
catch (Exception e) {
e.printStackTrace();
}
}
}
|
| Now simply compile the code and run!
% java Test
unmarshalling root instance:
Root#getDate : Mon May 10 00:00:00 CDT 2004
After running our test program we can see that Castor invoked our custom FieldHandler and we got our properly formatted date in our Root.class. Writing a GeneralizedFieldHandler A GeneraliedFieldHandler is an extension of FieldHandler interface where we simply write the conversion methods and Castor will automatically handle the underlying get/set operations. This allows us to re-use the same FieldHandler for fields from different classes that require the same conversion. Note: Currently the GeneralizedFieldHandler cannot be used from a binding-file for use with the SourceGenerator, an enhancement patch will be checked into CVS for this feature, shortly after 0.9.6 final is released. The same FieldHandler we used above can be written as a GeneralizedFieldHandler as such:
import org.exolab.castor.mapping.GeneralizedFieldHandler;
import org.exolab.castor.mapping.FieldDescriptor;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* The FieldHandler for the Date class
*
*/
public class MyDateHandler
extends GeneralizedFieldHandler
{
private static final String FORMAT = "yyyy-MM-dd";
/**
* Creates a new MyDateHandler instance
*/
public MyDateHandler() {
super();
}
/**
* This method is used to convert the value when the
* getValue method is called. The getValue method will
* obtain the actual field value from given 'parent' object.
* This convert method is then invoked with the field's
* value. The value returned from this method will be
* the actual value returned by getValue method.
*
* @param value the object value to convert after
* performing a get operation
* @return the converted value.
*/
public Object convertUponGet(Object value) {
if (value == null) return null;
SimpleDateFormat formatter = new SimpleDateFormat(FORMAT);
Date date = (Date)value;
return formatter.format(date);
}
/**
* This method is used to convert the value when the
* setValue method is called. The setValue method will
* call this method to obtain the converted value.
* The converted value will then be used as the value to
* set for the field.
*
* @param value the object value to convert before
* performing a set operation
* @return the converted value.
*/
public Object convertUponSet(Object value) {
SimpleDateFormat formatter = new SimpleDateFormat(FORMAT);
Date date = null;
try {
date = formatter.parse((String)value);
}
catch(ParseException px) {
throw new IllegalArgumentException(px.getMessage());
}
return date;
}
/**
* Returns the class type for the field that this
* GeneralizedFieldHandler converts to and from. This
* should be the type that is used in the
* object model.
*
* @return the class type of of the field
*/
public Class getFieldType() {
return Date.class;
}
/**
* Creates a new instance of the object described by
* this field.
*
* @param parent The object for which the field is created
* @return A new instance of the field's value
* @throws IllegalStateException This field is a simple
* type and cannot be instantiated
*/
public Object newInstance( Object parent )
throws IllegalStateException
{
//-- Since it's marked as a string...just return null,
//-- it's not needed.
return null;
}
}
|
| Everything else is the same. So we can re-run our test case using this GeneralizedFieldHandler and we'll get the same result. The main difference is that we implement the convertUponGet and convertUponSet methods. Notice that we never reference the Root class in our GeneralizedFieldHandler. This allows us to use the same exact FieldHandler for any field that requires this type of conversion. No Constructor, No Problem! A number of classes such as type-safe enum style classes have no constructor, but instead have some sort of static factory method used for converting a string value into an instance of the class. With a custom FieldHandler we can allow Castor to work nicely with these types of classes.
Tip: | Castor XML automatically supports these types of classes if they have a specific method: public static {Type} valueOf(String) |
| Note: We're working on the same support for Castor JDO Even though Castor XML supports the "valueOf" method type-safe enum style classes, we'll show you how to write a custom handler for these classes anyway since it's useful for any type of class regardless of the name of the factory method. Let's look at how to write a handler for the following type-safe enum style class, which was actually generated by Castor XML (javadoc removed for brevity):
import java.io.Serializable;
import java.util.Enumeration;
import java.util.Hashtable;
public class Color implements java.io.Serializable {
public static final int RED_TYPE = 0;
public static final Color RED = new Color(RED_TYPE, "red");
public static final int GREEN_TYPE = 1;
public static final Color GREEN = new Color(GREEN_TYPE, "green");
public static final int BLUE_TYPE = 2;
public static final Color BLUE = new Color(BLUE_TYPE, "blue");
private static java.util.Hashtable _memberTable = init();
private int type = -1;
private java.lang.String stringValue = null;
private Color(int type, java.lang.String value) {
super();
this.type = type;
this.stringValue = value;
} //-- test.types.Color(int, java.lang.String)
public static java.util.Enumeration enumerate()
{
return _memberTable.elements();
} //-- java.util.Enumeration enumerate()
public int getType()
{
return this.type;
} //-- int getType()
private static java.util.Hashtable init()
{
Hashtable members = new Hashtable();
members.put("red", RED);
members.put("green", GREEN);
members.put("blue", BLUE);
return members;
} //-- java.util.Hashtable init()
public java.lang.String toString()
{
return this.stringValue;
} //-- java.lang.String toString()
public static Color valueOf(java.lang.String string)
{
Object obj = null;
if (string != null) obj = _memberTable.get(string);
if (obj == null) {
String err = "'" + string + "' is not a valid Color";
throw new IllegalArgumentException(err);
}
return (Color) obj;
} //-- test.types.Color valueOf(java.lang.String)
}
|
| The GeneralizedFieldHandler for the above Color class is as follows (javadoc removed for brevity): ColorHandler.java
import org.exolab.castor.mapping.GeneralizedFieldHandler;
import org.exolab.castor.mapping.FieldDescriptor;
/**
* The FieldHandler for the Color class
**/
public class ColorHandler
extends GeneralizedFieldHandler
{
public ColorHandler() {
super();
}
public Object convertUponGet(Object value) {
if (value == null) return null;
Color color = (Color)value;
return color.toString();
}
public Object convertUponSet(Object value) {
return Color.valueOf((String)value);
}
public Class getFieldType() {
return Color.class;
}
public Object newInstance( Object parent )
throws IllegalStateException
{
//-- Since it's marked as a string...just return null,
//-- it's not needed.
return null;
}
}
|
| That's all there really is to it. Now we just need to hook this up to our mapping file and run a sample test. If we have a root class Foo as such: Foo.java
public class Foo {
private Color _color = null;
private int _size = 0;
private String _name = null;
public Foo() {
super();
}
public Color getColor() {
return _color;
}
public String getName() {
return _name;
}
public int getSize() {
return _size;
}
public void setColor(Color color) {
_color = color;
}
public void setName(String name) {
_name = name;
}
public void setSize(int size) {
_size = size;
}
}
|
| Our mapping file would be the following: mapping.xml
<?xml version="1.0"?>
<mapping>
<class name="Foo">
<field name="size" type="integer">
<bind-xml node="element"/>
</field>
<field name="name" type="string"/>
<field name="color" type="string" handler="ColorHandler"/>
</class>
</mapping>
|
| We can now use our custom FieldHandler to unmarshal the following xml input: test.xml
<?xml version="1.0"?>
<foo>
<name>MyFoo</name>
<size>345</size>
<color>blue</color>
</foo>
|
| A sample test class is as follows: Test.java
import java.io.*;
import org.exolab.castor.xml.*;
import org.exolab.castor.mapping.*;
public class Test {
public static void main(String[] args) {
try {
//--load mapping
Mapping mapping = new Mapping();
mapping.loadMapping("mapping.xml");
System.out.println("unmarshalling Foo:");
System.out.println();
Reader reader = new FileReader("test.xml");
Unmarshaller unmarshaller = new Unmarshaller(Foo.class);
unmarshaller.setMapping(mapping);
Foo foo = (Foo) unmarshaller.unmarshal(reader);
reader.close();
System.out.println("Foo#size : " + foo.getSize());
System.out.print("Foo#color: ");
if (foo.getColor() == null) {
System.out.println("null");
}
else {
System.out.println(foo.getColor().toString());
}
PrintWriter pw = new PrintWriter(System.out);
Marshaller marshaller = new Marshaller(pw);
marshaller.setMapping(mapping);
marshaller.marshal(foo);
pw.flush();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
|
| Collections and FieldHandlers The GeneralizedFieldHandler checked into the CVS version (main branch / 0.9.6 Final) of Castor automatically supports iterating over the items of a collection and passing them one-by-one to the convertUponGet. For backward compatibility or to handle the collection iteration yourself, simply add the following to the constructor of your GeneralizedFieldHandler implementation:
setCollectionIteration(false);
If you're going to be using custom field handlers for collection fields with a GeneralizedFieldHandler using versions of Castor prior to 0.9.6, then you'll need to handle the collection iteration yourself in the convertUponGet method. If you're not using a GeneralizedFieldHandler then you'll need to handle the collection iteration yourself in the FieldHandler#getValue() method.
Tip: | Since Castor incrementally adds items to collection fields, there usually is no need to handle collections directly in the convertUponSet method (or the setValue() for those not using GeneralizedFieldHandler). |
| |