Reading not XMLEncoded XML into Java Beans


For a small Java project is was searching for a standard JDK Class to read a XML configuration file into a Java bean. Important was that the structure of the XML file was maintainable by humans using external editors and not necessarily all elements in the XML file had to match attributes in the Java bean(s) wherein the contents of the XML file is stored. The XMLDecoder class in the java.beans package only can de-serialize objects encoded using the XMLEncoder class and therefore is less useful for his purpose.

xmlbeans

Based on Java's SAXParser and a relative simple handler extending DefaultHandler, i was able to parse an external editable XML into a Java bean holding (ArrayLists of) other beans as attributes, also capable of holding one or more Java Beans as attributes. There are some conventions which have to be followed for the XML file and Java Beans.

  • Classes require implementation of java.io.Serializable to be recognized as a bean.
  • Beans containing references to other beans require an attribute {element name}Beans and the put{Elementname}Bean method. (Capital E indicates that first char is capitalized to follow the java naming recommendations)
  • The first opening element name matching a bean named {Element_name}Bean, is returned as an object
  • A schema guarantees that content of elements matches the possible values assigned to an attribute

I always like the 'by example' explanations. Here an example using a shop inventory stored in the XML file SweetShop.xml which will be read in into the three beans shown in the object diagram using the XML2BeanHandler.

<?xml version="1.0" encoding="UTF-8"?>

<!--
    Document   : SweetShop.xml
    Created on : August 31, 2011, 10:30 PM
    Author     : petervannes
-->
<x2b:shop xmlns:x2b="http://www.petervannes.nl/xml2beanexample/1.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.petervannes.nl/xml2beanexample/1.0 file:/Users/petervannes/NetBeansProjects/XML2BeanExample/SweetShop.xsd">
    <x2b:shopdetails>
        <x2b:Shopname>Willy Wonka Chocolate Shop</x2b:Shopname>
        <x2b:Address>1445 Norwood Ave</x2b:Address>
        <x2b:Postalcode>IL 60143</x2b:Postalcode>
        <x2b:City>Itasca</x2b:City>
        <x2b:State>IL</x2b:State>
        <x2b:OfficePhone>555-1223</x2b:OfficePhone>
        <x2b:EmailAddress>willy.wonka@chocolate.com</x2b:EmailAddress>
    </x2b:shopdetails>
    <x2b:inventory>
        <x2b:category>
            <x2b:name>cakes</x2b:name>
            <x2b:products>
                <x2b:product>
                    <x2b:name>Coffee Cake</x2b:name>
                    <x2b:description>You can smell the arabica beans</x2b:description>
                    <x2b:pkgQuantity>4</x2b:pkgQuantity>
                    <x2b:price>6.45</x2b:price>
                </x2b:product>
                <x2b:product>
                    <x2b:name>Cupcakes</x2b:name>
                    <x2b:description>Assorted set</x2b:description>
                    <x2b:pkgQuantity>8</x2b:pkgQuantity>
                    <x2b:price>7.95</x2b:price>
                </x2b:product>
            </x2b:products>
        </x2b:category>
                <x2b:category>
            <x2b:name>pies</x2b:name>
            <x2b:products>
                <x2b:product>
                    <x2b:name>Apple Pie</x2b:name>
                    <x2b:description>Grandma's recipe</x2b:description>
                    <x2b:pkgQuantity>1</x2b:pkgQuantity>
                    <x2b:price>6.45</x2b:price>
                </x2b:product>
                <x2b:product>
                    <x2b:name>Peach and Cherry Pie</x2b:name>
                    <x2b:description>Home backed with fresh picked fruit</x2b:description>
                    <x2b:pkgQuantity>1</x2b:pkgQuantity>
                    <x2b:price>8.98</x2b:price>
                </x2b:product>                
            </x2b:products>
        </x2b:category>
        <x2b:category>
            <x2b:name>lollipops</x2b:name>
            <x2b:products>
                <x2b:product>
                    <x2b:name>Fruit Rock Lollipops</x2b:name>
                    <x2b:description>Fruit flavoured rock lollipops with a fruit shape in the center</x2b:description>
                    <x2b:pkgQuantity>tub of 150</x2b:pkgQuantity>
                    <x2b:price>19.98</x2b:price>
                </x2b:product>
                <x2b:product>
                    <x2b:name>Mega Strawberry Zoom Lollipops</x2b:name>
                    <x2b:description>This giant size, mega lollipop comes in a refreshing strawberry flavour</x2b:description>
                    <x2b:pkgQuantity>pack of 5</x2b:pkgQuantity>
                    <x2b:price>1.50</x2b:price>
                </x2b:product>
                <x2b:product>
                    <x2b:name>Lottalollies Tongue Painter</x2b:name>
                    <x2b:description>As the name suggest these paint your tongue</x2b:description>
                    <x2b:pkgQuantity>tub of 150</x2b:pkgQuantity>
                    <x2b:price>12.45</x2b:price>
                </x2b:product>
            </x2b:products>
        </x2b:category>
    </x2b:inventory>
</x2b:shop>

The XML2BeanHandler is responsible for;

  • Instantiating a new bean if an opening element matches a class named {Element_name}Bean.
  • Putting new instantiated beans on the 'stack'
  • Calling the method set{Element_name} for the current bean on the stack when if a closing element matches a method called set{Element_name} for the current bean on the stack
  • Removing instantiated beans from the stack when a closing element matches the classname of the object on the stack
  • Calling the method put{Element_name} on the parent object , adding the removed instance from the stack as an attribute of its parent. Otherwise when there is no parent object, the instantiated bean is the first instantiated bean and will be the 'root' bean which is returned as an object.

package xml2beanexample;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Stack;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 *
 * @author petervannes
 */
public class XML2BeanHandler extends DefaultHandler {
    
    private StringBuffer elementBuffer;
    private Stack<Object> beanStack = new Stack<Object>();
    private Object xmlObject = null ;

    @Override
    public void startElement(String namespaceURI, String sName, String qName,
            Attributes attrs) {

        elementBuffer = null;

        // Put object "{sName}Bean" on stack if available
        ClassLoader classLoader = this.getClass().getClassLoader();
        
        try {

            Class aClass = 
                    classLoader.loadClass(this.getClass().getPackage().getName() + 
                    "." + sName.substring(0, 1).toUpperCase() + 
                    sName.substring(1) + "Bean");
            if (aClass instanceof java.io.Serializable) {
                Object newObject = aClass.newInstance() ;
                beanStack.push(newObject);
            }
        } catch (Exception e) {
            // Do nothing
        }

    }

    @Override
    public void characters(char[] buf, int offset, int len) throws SAXException {
        String s = new String(buf, offset, len);

        if (elementBuffer == null) {
            elementBuffer = new StringBuffer(s);
        } else {
            elementBuffer.append(s);
        }
    }

    
    @Override
    public void endElement(String namespaceURI, String sName, 
        String qName) throws SAXException {

        if (!beanStack.isEmpty()) {
            // Call setter in bean if xml-element name matches a setter 
            // method of bean on top of the stack 
            if (Arrays.asList(getDeclaredMethodNames(beanStack.peek())).contains(
                    "set" + capFirstChar(sName))) {
                try { // some reflection
                    String methodName = "set" + capFirstChar(sName);
                    Method cfgBeanMethod = 
                            beanStack.peek().getClass().getDeclaredMethod(methodName, 
                            new Class[] {String.class});
                    cfgBeanMethod.invoke(beanStack.peek(), 
                            new Object[] {new String(elementBuffer)});
                } catch (Exception e) {
                    // Do nothing
                }
            } else { 
                // If xml-element name matches bean name on top of stack
                String elmtBeanName = capFirstChar(sName) + "Bean";

                if (beanStack.peek().getClass().getSimpleName().equals(elmtBeanName)) {
                    // remove the object from the stack
                    Object obj = beanStack.pop();

                    // If parent of popped object has 'put' method for popped-object 
                    // call the put method on object on stack for the popped-object
                    if (!beanStack.empty()) {
                        String methodName = "put" + capFirstChar(sName) + "Bean";
                        try { // call method using reflection
                            Method cfgBeanMethod = 
                                    beanStack.peek().getClass().getDeclaredMethod(methodName, 
                                    new Class[]{obj.getClass()});
                            cfgBeanMethod.invoke(beanStack.peek(), new Object[]{obj});
                        } catch (Exception e) {
                            // Do nothing
                        }
                    } else {
                        // beanstack empty, so root-object is closed in xml.
                        this.xmlObject = obj ;
                    } 
                }
            }
        } 
    }

    /**
     * Returns an array of Method names reflecting all the methods declared by
     * the class or interface represented by this Class object. 
     * @param object
     * @return the array of String objects representing all of the declared 
     * methods names of this class
     */
    private String[] getDeclaredMethodNames(Object object) {

        Method[] methods = object.getClass().getDeclaredMethods();
        String[] declaredMethodNames = new String[methods.length];

        int x = 0;
        for (Method m : object.getClass().getDeclaredMethods()) {
            declaredMethodNames[x] = methods[x].getName();
            x++;
        }
        return declaredMethodNames;
    }
    
    public Object getBeanObject() {
        return this.xmlObject ;
    }
    
    public static String capFirstChar(String sName) {
        return sName.substring(0, 1).toUpperCase() + sName.substring(1);
    }
}

After de-serializing the SweetShop.xml using the XML2BeanHandler, the attributes of the 'root' bean will be according to the output shown below. Note that the bean does not have the e-mail address of the shop included in the xml file because there is no EmailAddress attribute defined in the bean.

+ShopBean
| shopname; Willy Wonka Chocolate Shop
| address; 1445 Norwood Ave
| postalcode; IL 60143
| city; Itasca
| state; IL
| officePhone; 555-1223
| Categories; 
|            +CategoryBean
|            | name; cakes
|            | products; 
|            |          +ProductBean
|            |          | name; Coffee Cake
|            |          | description; You can smell the arabica beans
|            |          | pkgQuantity; 4
|            |          | price; 6.45
|            |          +ProductBean
|            |          | name; Cupcakes
|            |          | description; Assorted set
|            |          | pkgQuantity; 8
|            |          | price; 7.95
|            +CategoryBean
|            | name; pies
|            | products; 
|            |          +ProductBean
|            |          | name; Apple Pie
|            |          | description; Grandma's recipe
|            |          | pkgQuantity; 1
|            |          | price; 6.45
|            |          +ProductBean
|            |          | name; Peach and Cherry Pie
|            |          | description; Home backed with fresh picked fruit
|            |          | pkgQuantity; 1
|            |          | price; 8.98
|            +CategoryBean
|            | name; lollipops
|            | products; 
|            |          +ProductBean
|            |          | name; Fruit Rock Lollipops
|            |          | description; Fruit flavoured rock lollipops with a fruit shape in the center
|            |          | pkgQuantity; tub of 150
|            |          | price; 19.98
|            |          +ProductBean
|            |          | name; Mega Strawberry Zoom Lollipops
|            |          | description; This giant size, mega lollipop comes in a refreshing strawberry flavour
|            |          | pkgQuantity; pack of 5
|            |          | price; 1.50
|            |          +ProductBean
|            |          | name; Lottalollies Tongue Painter
|            |          | description; As the name suggest these paint your tongue
|            |          | pkgQuantity; tub of 150
|            |          | price; 12.45

A working demo in a Netbeans 7 project can be downloaded here

blog comments powered by Disqus