ORIGINAL DRAFT
The overlapping domain between Java and XML is fertile ground for innovation, but many results are still exploratory and sometimes appear to be overly complicated. In a practical sense, developers are typically looking to XML as a data exchange medium. XML is to data what Java is to behavior. Everybody’s talking about it. So why yet another article on a saturated subject?
It’s clear that XML is a powerful medium for exchanging data. In Java, one of the best ways to expose meaningful data is to use the JavaBeans model, drawing clear boundaries between internal and external access. Property accessors tell you everything you need to know about external access while allowing you to encapsulate implementation details. Clearly, the properties expose all the data that should be exchanged.
The approach we’ll cover in this article is not a panacea. But it hasn’t been explored much at the time of this writing. So I think it’s important to demonstrate how this could be done. We’ll implement a few classes that make the process of exporting and importing JavaBean properties, to and from XML, as transparent as possible, with little or no effort outside the normal JavaBean development process. I think you’ll find this solution especially elegant and applicable in a wide variety of circumstances.
Other Solutions
Let’s take a quick look at other existing solutions to keep things in context. There are several approaches that already exist in a variety of implementations, each with its own spin on a particular design.
Generators create Java classes from DTDs (sometimes even from XML examples), generating a set of source files to be used in your application. There are a few obvious benefits to this approach, not the least of which is that the classes can implement good type-safety and constraint-satisfaction features. Unfortunately, these representations are commonly inefficient, involving many layers of indirection, which typically result in poor internal structures and bad performance characteristics. Some produce better results, but typically trade off ease-of-use in the process.
You could, of course, use XML/DOM as the internal representation, exchanging data without modification, but there are serious performance penalties for doing this as well. They manifest themselves as soon as you start working with anything other than trivial data structures. It’s typically more efficient, at least at the application level, to export and import data structures to and from XML than it is to use DOM as your internal model.
To put this in perspective, consider what it would mean to implement a Swing TableModel using DOM as the underlying data repository. You can’t easily map a two dimensional grid onto the DOM interface directly and generated classes would still have to be glued to your TableModel in some way, while still retaining an inefficient hierarchical structure. Better to go the other way, crafting the TableModel so that it performs well, and exporting/importing XML for other purposes.
There are introspection solutions that map instance variables onto XML. These involve looking at the internal structure of your classes and mapping them onto XML in one form or another. Most of these require publicly declared instance variables to allow introspection access, a potentially dangerous security issue, and a strike against effective encapsulation.
Sun has recently put forward a serialization proposals that captures Java class in their entirety. This may be a good idea for doing mobile code, or component persistence, but in cases where only the data needs to change hands, the XML overhead is prohibitive.
JavaBeans
Our implementation is based on introspection, but it uses the JavaBeans model and is targeted at systems that allow you to define internal data structures as JavaBean implementations. Fortunately, these include JSP, EJB and other JavaBean components, covering a broad range of potential applications.
Our objective is to enable JavaBeans to be easily translated to and from an XML document and for the conversion process to be as transparent as possible. To accomplish this, we’ll expose two key methods:
public static Document getDocument(Object obj);
public static Object getObject(Document doc);
These methods provide a high-level interface that make the process virtually transparent. To use it, you simply call XMLBeanManager.getInstance() and pass a JavaBean instance to the getDocument method. The resulting DOM (Document Object Model) can be fed to getObject to get a new instance with all the JavaBean properties intact. Naturally, this is much more interesting if something happens in between, such as transmission over TCP/IP, file storage or additional processing and intermediate conversions.
We’ll use the Java Introspector class, which is designed to inspect JavaBean components, producing a BeanInfo instance. BeanInfo classes can be inferred or hand-generated, depending on the component’s origin. Since we use the mechanisms provided by the Introspector, any explicitly defined BeanInfo class is automatically applied, making it possible to map to and from virtually any JavaBean object transparently.
Overview
Our solution supports properties that expose both read and write accessors. This is worth repeating. If property accessors are read-only or write-only, they will be ignored in the XML translation process. This is a design choice based on the notion that exporting is only useful if you can import the data somewhere. You can always modify the code to remove this restriction if you need to.
Arrays require accessors that allow reading and writing the whole array. This doesn’t preclude the existence of indexed accessor methods, but full array accessors must exist if they are to be converted to and from XML. This is important because the JavaBeans specification doesn’t provide any mechanism for getting the size of an array, other than having a reference to it, so the indexed accessors are insufficient.
You’ll also need to work with property values that are either primitive data types (boolean, char, byte, short, int, long, float or double), wrappers (Boolean, Character, Byte, Short, Integer, Long, Float, Double or String), other JavaBeans, and arrays or collections (List, Set and Map) of these data types. Because, beans, arrays and collections can be nested arbitrarily, we can accommodate virtually any compound data structure without having to change any code.
To run this code, you’ll need a suitable XML infrastructure. This implementation relies on JDOM, which in turn relies on Xerces, the Apache XML library. With both of these on your class path, everything should be functional and all you have to do to convert to an XML Document is call the getDocument method with a suitable JavaBean instance. To recover the object, pass the Document to the getObject method and a new object is created with exactly the same properties.
For this code to work across JVMs, you’ll need to have access to the same JavaBean code base. This is no worse than having to keep RMI stubs and skeletons in sync, or keeping CORBA and/or DCOM classes compatible, but don’t expect the code to be captured in XML. This is not an XML serialization mechanism. Only bean properties are preserved in the XML representation.
Implementation
There are seven key classes in this project, as seen in Figure 1. The XMLBeanManager is the main class. It makes use of an XMLBeanImporter and an XMLBeanExporter, which in turn use the XMLBeanUtil (with a few static utility methods), XMLBeanMapEntry (to support importing and exporting Map entries), and the XMLBeanInfo class. XMLBeanInfo contains a collection of XMLBeanProperty instances, describing each visible property. Each encapsulates the property name, class type, whether it’s an array or not, and the read and write methods required to get and set a property value.
Most of the work is done in the XMLBeanExporter and XMLBeanImporter classes. In each case, we have to deal with several data types and their properties. Naturally, the primitive data types are terminals and need no further processing, and the wrappers are similar. Much of the work centers on handling arrays and collection instances.
The Code
Lets look at the code for the XMLBeanExporter and XMLBeanImporter classes first and then tie things together with the XMLBeanManager class.
The XMLBeanExporter class is responsible for turning a JavaBean instance into an XML Document. You can see the code for this class in Listing 1. The getElement method is the main entry point. We first create a JDOM Element object and set the class name as an attribute named "class". After that, we start handing the different data types with conditional statements.
If we’re dealing with a wrapper class (Integer, Double, etc.), we add the value as XML content, by calling the toString method, otherwise we’re dealing with a JavaBean instance that may have multiple properties. We can get all the properties that are relevant to this code by creating an instance of XMLBeanInfo, passing the JavaBean instance in the contructor. The next step is to walk through the list of resulting XMLBeanProperty objects, handling each as appropriate.
For each bean property, we get the name, type, array flag and method reference. We adjust the type name along the way. While this isn’t strictly necessary, I didn’t much like the cryptic abbreviations and wanted the XML data types to be readable. Java identifies a boolean as “[”, for example, and prefixes array names with the “[” symbols. The last thing we do is call invoke on the Method to get the property value to be processed.
With all this information in hand, we proceed to handle each data type on its own merit. Null properties use the null keyword, arrays are delegated to the addArrayElements method, collections are handled by the addCollectionElements method, and individual elements are delegated to the addIndividualElement method. The getElement method returns the JDOM Element we just constructed, which will become the root element in the Document object returned by XMLBeanManager.
The methods in XMLBeanExporter are fairly self-explanatory. To handle Collection elements, we walk through each object with an iterator and call getElement to create children recursively. The Map handling requires a JavaBean to represent Entry objects, so I’ve created an XMLBeanMapEntry object to handle these special cases. XMLBeanMapEntry captures the key and value and uses standard JavaBean accessors, where the Map.Entry objects do not.
To handle array elements, we use the introspection Array class to remain type-agnostic. This reduces the code considerably from what it would have to be if we were handling each data type individually. You’ll notice we reset the data type if we’re dealing with a primitive type because the introspection Method invocation always returns a wrapper class (Integer instead of int, for example). Handling individual elements is trivial but requires the same adjustment.
The XMLBeanImporter implementation requires a little more work. You can take a look at the code in Listing 2. The main entry point is the getObject method, which takes a JDOM Element object and returns an instance of the object it describes. We throw an illegal argument exception if the element doesn’t have the expected "OBJECT" tag.
After getting the class name, we start processing the different data types, starting with the wrapper classes. The getWrapperObject method handles the creation of individual wrapper object instances, with the XML Element content converted to appropriate values.
For other data types, we create an instance using the Class forName and newInstance methods and then create an XMLBeanInfo class that reflects the class name, type, array flag and methods for each property. We can get the children for any XML Element by using JDOM’s getChildren method, after which each entry is processed individually.
For each bean property, we make sure we have the right tag name, get a few values from the Element and then deal with various cases. The first case is a null value, so we call the set method on the JavaBean and set the value to null. We handle arrays by calling addArrayObjects, collection instances by calling the addCollectionObjects method and individual elements with the addIndividualObject method.
The addCollectionObjects method uses two supporting methods to handle the population of elements in lists and maps. Both List and Set classes extend Collection and support the add method, so they are handled in the same way. Map instances need to reverse the XMLBeanMapEntry process we used in the XMLBeanExporter class.
For each collection object, we create an instance in the addCollectionObjects method and call either the setListValues method for List and Set instances or the setMapValues method for Map objects. The setListValues is straight forward. The setMapValues method uses the Map interface’s put method and adds each XMLBeanMapEntry key and value explicitly.
The addArrayObjects method deals with array instances and uses the introspection Array utilities to deal with arrays of non-explicit types. We use the Array newInstance method to create a new array and then call the setArrayValue method to deal with each type of object explicitly before setting the property value with the invoke method from the introspection Method object. Our setArrayValue method deals with primitives explicitly and other object generically.
Finally, we handle individual objects in the addIndividualObject method, which is fairly simple, setting the property using the appropriate accessor method. To get the object instance, we call the getArgument method, which translates text values to primitive instances where applicable and deals with other object types generically, by calling getObject recursively.
Listing 3 shows the code for XMLBeanManager, which is very simple, given that most of the work is done by the XMLBeanExporter and XMLBeanImporter classes. We declare the constructor as protected and use a singleton pattern, instantiating only one static copy on the initial call to getInstance.
There are two instance variables that get initiated during object creation to assign the exporter and importer instances. The getDocument method merely uses the exporter’s getElement method and wraps the results in a JDOM Document instance when called. The getObject method does the reverse and calls the importer’s getObject method on the root element for the Document object.
Testing
When you download the code for from www.java-pro.com, you’ll find a set of test beans to verify the integrity of our implementation. It’s important to provide this kind of verification if we’re going to rely on the code. The beans provide accessors for all the primitive and wrapper objects, both alone and in arrays, as well as Collections. The TestCompoundBean class provides accessors for each of the other test bean classes, excersizing all of the variants we support.
The TestXMLBeanManager class uses TestBean and prints out the original object string, the XML representation and the duplicate object string to make sure everything is performing as expected. This requires that each of the beans provide a good toString implementation so that we can view the results and compare them visually. Naturally, you can also compare the strings from the original and duplicate beans to avoid any human errors.
These tests are indicative but do no guarantee that the code is completely bug-free. If you find problems, feel free to write me and let me know. If you can diagnose the exact nature of the bug, that would be useful. If you have a fix to propose, you can send that along as well. At the time this article was written, there are no bugs that I know of, but the code hasn’t been in commercial use.
Exception handling deserves a little additional attention. Looming publishing deadlines precluded my spending sufficient time on this. A lot of the methods simply declare "throws Exception", rather than specific exceptions. This is a shortcut that needs to be cleaned up before the code could be considered sufficiently robust. What’s more, the exception handling should really be capable of helping you understand what went wrong, enough so that the problem can at least be corrected. Some kind of versioning scheme would also be nice.
Summary
XMLBeanManager solves a number of common problem with relative ease. It squarely addresses the need to export and import JavaBean properties in the form of XML documents, which can then be stored, transmitted or modified as needed. This approach is applicable to any JavaBean, such as JSP beans, Enterprise JavaBeans, component beans, or any class that implements bean accessors by convention.
If you can take the time to write, I’d like to hear from you about how this solution compares to others you may have used. If there’s enough interest, I’d be willing to release this code to the open source community, so long as there’s a reputable champion who wants to spearhead the effort. My own experience suggests that this is a great way to deal with a wide spectrum of common issue in an area that is still being explored rather aggressively, without apparent consensus at this stage.
Listing 1
import java.util.*;
import java.beans.*;
import java.lang.reflect.*;
import org.jdom.*;
public class XMLBeanExporter
{
public Element getElement(Object obj)
throws Exception
{
Element xmlClass = new Element("OBJECT");
String className = obj.getClass().getName();
xmlClass.addAttribute("class",
XMLBeanUtil.adjustClassName(className));
if (XMLBeanUtil.isWrapper(className))
{
xmlClass.addContent(obj.toString());
}
else
{
XMLBeanInfo info = new XMLBeanInfo(obj);
for (int i = 0; i < info.getProperties().size(); i++)
{
XMLBeanProperty beanProperty =
(XMLBeanProperty)info.getProperties().get(i);
// Initialize a few useful variables.
String name = beanProperty.getName();
Class type = beanProperty.getType();
boolean isArray = beanProperty.isArray();
String typeName =
XMLBeanUtil.adjustClassName(type.getName());
// Create a property element and set attributes.
Element xmlProperty = new Element("PROPERTY");
xmlProperty.addAttribute("name", name);
xmlProperty.addAttribute("type",
typeName + (isArray ? "[]" : ""));
// Use reflections to invoke the get method.
Object objProperty = beanProperty.getGetMethod().
invoke(obj, new Object[] {});
if (objProperty == null)
{
xmlProperty.addContent("null");
}
// Add child elements for the property object.
else if (isArray)
{
addArrayElements(xmlProperty, objProperty, typeName);
}
else
{
if (XMLBeanUtil.isCollection(typeName))
{
addCollectionElements(xmlProperty, objProperty);
}
else
{
addIndividuaElement(xmlProperty, objProperty, type);
}
}
xmlClass.addContent(xmlProperty);
}
}
return xmlClass;
}
protected void addCollectionElements(
Element parent, Object obj)
throws Exception
{
if (obj instanceof List)
{
List list = (List)obj;
Iterator iterator = list.iterator();
while (iterator.hasNext())
{
Object item = iterator.next();
Element element = getElement(item);
parent.addContent(element);
}
}
if (obj instanceof Set)
{
Set set = (Set)obj;
Iterator iterator = set.iterator();
while (iterator.hasNext())
{
Object item = iterator.next();
Element element = getElement(item);
parent.addContent(element);
}
}
if (obj instanceof Map)
{
Set set = ((Map)obj).entrySet();
Iterator iterator = set.iterator();
while (iterator.hasNext())
{
Object item = iterator.next();
Object key = ((Map.Entry)item).getKey();
Object val = ((Map.Entry)item).getValue();
item = new XMLBeanMapEntry(key, val);
// NOTE: Map.Entry is not a good bean
// so we need to handle this better
Element element = getElement(item);
parent.addContent(element);
}
}
}
protected void addArrayElements(
Element parent, Object obj, String typeName)
throws Exception
{
// Deal with property arrays using recursion.
int length = Array.getLength(obj);
for (int j = 0; j < length; j++)
{
Object item = Array.get(obj, j);
Element element = getElement(item);
// Method.invoke results are always wrapped, so
// we reset primitive element class attributes.
if (XMLBeanUtil.isPrimitive(typeName))
{
element.removeAttribute("class");
element.addAttribute("class", typeName);
}
parent.addContent(element);
}
}
protected void addIndividuaElement(
Element parent, Object obj, Class type)
throws Exception
{
Element element = getElement(obj);
// Method.invoke results are always wrapped, so
// we reset primitive element class attributes.
if (type.isPrimitive())
{
element.removeAttribute("class");
element.addAttribute("class", type.getName());
}
parent.addContent(element);
}
}
Listing 2
import java.util.*;
import java.beans.*;
import java.lang.reflect.*;
import org.jdom.*;
public class XMLBeanImporter
{
public Object getObject(Element xmlClass)
throws Exception
{
if (!xmlClass.getName().equals("OBJECT"))
throw new IllegalArgumentException("Expecting OBJECT tag");
String className = xmlClass.getAttributeValue("class");
if (XMLBeanUtil.isWrapper(className))
{
return getWrapperObject(xmlClass);
}
Object objClass = Class.forName(className).newInstance();
XMLBeanInfo info = new XMLBeanInfo(objClass);
List children = xmlClass.getChildren();
for (int i = 0; i < children.size(); i++)
{
XMLBeanProperty beanProperty =
(XMLBeanProperty)info.getProperties().get(i);
Element xmlProperty = (Element)children.get(i);
if (!xmlProperty.getName().equals("PROPERTY"))
throw new IllegalArgumentException("Expecting PROPERTY tag");
String xmlName = xmlProperty.getAttributeValue("name");
String xmlType = xmlProperty.getAttributeValue("type");
String xmlValue = xmlProperty.getText();
if (xmlValue.equals("null"))
{
Method method = beanProperty.getSetMethod();
method.invoke(objClass, new Object[] { null });
}
else
if (xmlType.endsWith("[]"))
{
addArrayObjects(objClass, xmlProperty, beanProperty);
}
else
{
if (XMLBeanUtil.isCollection(xmlType))
{
addCollectionObjects(objClass, xmlProperty, beanProperty);
}
else
{
addIndividualObject(objClass, xmlProperty, beanProperty);
}
}
}
return objClass;
}
public Object getWrapperObject(Element element)
throws ClassNotFoundException
{
String className = element.getAttributeValue("class");
Class type = Class.forName(className);
String text = element.getText();
if (type == Boolean.class)
return Boolean.valueOf(text);
if (type == Character.class)
return new Character(text.charAt(0));
if (type == Byte.class)
return Byte.valueOf(text);
if (type == Short.class)
return Short.valueOf(text);
if (type == Integer.class)
return Integer.valueOf(text);
if (type == Long.class)
return Long.valueOf(text);
if (type == Float.class)
return Float.valueOf(text);
if (type == Double.class)
return Double.valueOf(text);
if (type == String.class)
return new String(text);
return null;
}
protected void addCollectionObjects(
Object parent, Element xmlProperty,
XMLBeanProperty beanProperty)
throws Exception
{
Method method = beanProperty.getSetMethod();
String xmlType = xmlProperty.getAttributeValue("type");
Class objType = XMLBeanUtil.getClassForName(xmlType);
List children = xmlProperty.getChildren();
if (objType == List.class) setListValues(
parent, method, children, new ArrayList());
if (objType == Vector.class) setListValues(
parent, method, children, new Vector());
if (objType == ArrayList.class) setListValues(
parent, method, children, new ArrayList());
if (objType == LinkedList.class) setListValues(
parent, method, children, new LinkedList());
if (objType == Set.class) setListValues(
parent, method, children, new HashSet());
if (objType == HashSet.class) setListValues(
parent, method, children, new HashSet());
if (objType == TreeSet.class) setListValues(
parent, method, children, new TreeSet());
if (objType == Map.class) setMapValues(
parent, method, children, new HashMap());
if (objType == HashMap.class) setMapValues(
parent, method, children, new HashMap());
if (objType == Hashtable.class) setMapValues(
parent, method, children, new Hashtable());
}
protected void setListValues(Object parent,
Method method, List children, Collection output)
throws Exception
{
int count = children.size();
for (int i = 0; i < count; i++)
{
output.add(getObject((Element)children.get(i)));
}
method.invoke(parent, new Object[] { output });
}
protected void setMapValues(Object parent,
Method method, List children, Map output)
throws Exception
{
int count = children.size();
for (int i = 0; i < count; i++)
{
XMLBeanMapEntry entry = (XMLBeanMapEntry)
getObject((Element)children.get(i));
output.put(entry.getKey(), entry.getValue());
}
method.invoke(parent, new Object[] { output });
}
protected void addArrayObjects(
Object parent, Element xmlProperty,
XMLBeanProperty beanProperty)
throws Exception
{
String xmlType = xmlProperty.getAttributeValue("type");
xmlType = xmlType.substring(0, xmlType.length() - 2);
Class objType = XMLBeanUtil.getClassForName(xmlType);
List props = xmlProperty.getChildren();
int count = props.size();
Object array = Array.newInstance(objType, count);
for (int j = 0; j < count; j++)
{
setArrayValue(array, j, (Element)props.get(j));
}
Method method = beanProperty.getSetMethod();
method.invoke(parent, new Object[] { array });
}
public void setArrayValue(
Object array, int index, Element element)
throws Exception
{
String type = element.getAttributeValue("class");
if (XMLBeanUtil.isPrimitive(type))
{
String text = element.getText();
if (type.equals("boolean")) Array.setBoolean(
array, index, text.equalsIgnoreCase("true"));
if (type.equals("char")) Array.setChar(
array, index, text.charAt(0));
if (type.equals("byte")) Array.setByte(
array, index, Byte.parseByte(text));
if (type.equals("short")) Array.setShort(
array, index, Short.parseShort(text));
if (type.equals("int")) Array.setInt(
array, index, Integer.parseInt(text));
if (type.equals("long")) Array.setLong(
array, index, Long.parseLong(text));
if (type.equals("float")) Array.setFloat(
array, index, Float.parseFloat(text));
if (type.equals("double")) Array.setDouble(
array, index, Double.parseDouble(text));
}
else
{
Array.set(array, index, getObject(element));
}
}
protected void addIndividualObject(
Object parent, Element xmlProperty,
XMLBeanProperty beanProperty)
throws Exception
{
Element child = xmlProperty.getChild("OBJECT");
Object object = getArgument(child);
Method method = beanProperty.getSetMethod();
method.invoke(parent, new Object[] { object });
}
public Object getArgument(Element element)
throws Exception
{
String type = element.getAttributeValue("class");
String text = element.getText();
if (type.equals("boolean"))
return Boolean.valueOf(text);
if (type.equals("char"))
return new Character(text.charAt(0));
if (type.equals("byte"))
return Byte.valueOf(text);
if (type.equals("short"))
return Short.valueOf(text);
if (type.equals("int"))
return Integer.valueOf(text);
if (type.equals("long"))
return Long.valueOf(text);
if (type.equals("float"))
return Float.valueOf(text);
if (type.equals("double"))
return Double.valueOf(text);
return getObject(element);
}
}
Listing 3
import java.util.*;
import org.jdom.*;
import org.jdom.output.*;
public class TestXMLBeanManager
{
public static void main(String[] args)
throws Exception
{
TestCompoundBean original = new TestCompoundBean();
original.initValues();
XMLBeanManager manager = XMLBeanManager.getInstance();
// Create an XML document from the object.
Document doc = manager.getDocument(original);
// Prety print the document on the console.
XMLOutputter outputter = new XMLOutputter(" ", true);
outputter.output(doc, System.out);
// Recreate an object from the XML content.
Object duplicate = manager.getObject(doc);
System.out.println("Original Object: " + original);
System.out.println("Duplicate Object: " + duplicate);
if (original.toString().equals(duplicate.toString()))
System.out.println("TEST SUCCESSFUL!");
else
System.out.println("TEST FAILED!");
}
}