Saturday, November 13, 2010

XML Data Binding

Cross-posted (in Portuguese) at Technè - Blog de Tecnologia do C.E.S.A.R.

It is an easy task to load XML data as Java Objects if you are using SE or EE versions. You can use, for example, JAXB or Castor. Is it possible to do it as easily on Java ME? Yes, let me show you how.

The first restriction we have to face is the reduced number of reflection features available on CLDC. It is not possible to use constructors with parameters or call methods. And we should avoid using Class.forName, because we would need to add exceptions to the obfuscator.

This proposal uses two classes to implement the Data Binding Unmarshall. One to represent each tag found and the other to deal with XML parsing details. The code of the first classe is:


public class XMLTag {
// if you do not have enough memory, use lazy
// instantiation on these attributes
private Hashtable attributes = new Hashtable();
private Vector childs = new Vector();

public void setAttributeValue(String attribute, String value) {
if (attribute != null && value != null) {
attributes.put(attribute, value);
}
}
public String getAttributeValue (String attribute) {
return (String) attributes.get(attribute);
}

public void addChild (XMLTag child) {
childs.addElement(child);
}
public Enumeration getChilds () {
return childs.elements();
}
public XMLTag getChildAt (int index) {
return (XMLTag) childs.elementAt(index);
}
}

Lets take an RSS version 2.0 XML sample:

<rss version="2.0">
<channel>
<item>
<title>item title</title>
<link>http://site.com</link>
</item>
<item>
<title>item title 2</title>
<link>http://site.com/2</link>
</item>
</channel>
</rss>

Below are the necessary classes to deal with above tags:

class RSS extends XMLTag {
Channel channel;
public void addChild(XMLTag child) {
if (child instanceof Channel) {
channel = (Channel) child;
}
}
}
class Channel extends XMLTag {
public void addChild(XMLTag child) {
if (child instanceof Item) {
super.addChild(child);
}
}
}
class Item extends XMLTag {
}

We do not need to create classes for title and link tags because these tags are considered attributes of item. Their values will be stored at XMLTag.attributes.

To prevent the use of Class.forName we use a map with tags and classes. The key is a String with the tag name and the value is a Class that extends XMLTag. This map will be passed as parameter to the class responsible for the XML parsing. Below is our map:


Hashtable map = new Hashtable();
map.put("rss", RSS.class);
map.put("channel", Channel.class);
map.put("item", Item.class);

The chosen parser is SAX from JSR 172. It notifies a Handler every time it finds an XML snippet. At the tag begin it calls startElement, at the tag end it calls endElement and when it finds text it calls characters.

A Stack is used to pile each tag found during the parsing. This way, when rss tag is found an RSS instance is added to the Stack. Below are the Stack changes until we find item tag:

As title tag does not have a corresponding Class it is treated as an attribute of the tag at the top of the Stack. The value we receive at characters method is saved in a buffer until we find the end of title tag. When we reach the end of title tag we store the value of buffer using XMLTag.setAttributeValue.

When we reach the end of a mapped tag we remove it from the Stack and add it as a child of the new Stack top.

At the end of the XML data the Stack will be empty and all instances will be related:

Below is the source code of the second class:


class XMLBinder extends org.xml.sax.helpers.DefaultHandler {

private Hashtable map = new Hashtable();
private Stack stack = new Stack();
private XMLTag rootElement;

private String attribute;
private StringBuffer value = new StringBuffer();

/**
* @param map with String keys and XMLTag values
*/
public XMLBinder(Hashtable map) {
Enumeration e = map.keys();
while (e.hasMoreElements()) {
Object key = e.nextElement();
Object tag = map.get(key);
if (validateMapping(key, tag)) {
this.map.put(key, tag);
} else {
throw new IllegalArgumentException("key " + key);
}
}
}
private boolean validateMapping (Object key, Object tag) {
return key instanceof String
&& tag instanceof Class
&& XMLTag.class.isAssignableFrom((Class) tag);
}

public XMLTag unmarshall (InputStream in) throws IOException {
try {
SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
parser.parse(in, this);
return rootElement;
} catch (Exception ex) {
throw new IOException("caused by " + ex);
}
}

public void startElement(String uri, String localName, String qName,
Attributes attributes) throws SAXException {
Class tag = (Class) map.get(qName);
if (tag != null) {
try {
XMLTag newTag = (XMLTag) tag.newInstance();
addAttributesToXMLTag(attributes, newTag);
stack.push(newTag);
} catch (Exception e) {
throw new SAXException("caused by " + e);
}
} else {
attribute = qName;
}
}
private void addAttributesToXMLTag (Attributes attributes, XMLTag newTag) {
if (attributes != null) {
for (int i = attributes.getLength() - 1; i >= 0; i--) {
String attrName = attributes.getQName(i);
String attrValue = attributes.getValue(i);
newTag.setAttributeValue(attrName, attrValue);
}
}
}

public void characters(char[] ch, int start, int length) {
if (attribute != null) {
value.append(ch, start, length);
}
}

public void endElement(String uri, String localName, String qName)
throws SAXException {
if (stack.isEmpty()) {
throw new SAXException("no mapping for " + qName);
}
if (attribute != null && attribute.equals(qName)) {
XMLTag parent = (XMLTag) stack.peek();
parent.setAttributeValue(attribute, value.toString());
attribute = null;
value.setLength(0);
} else {
XMLTag child = (XMLTag) stack.pop();
if (stack.isEmpty() == false) {
XMLTag parent = (XMLTag) stack.peek();
parent.addChild(child);
} else {
rootElement = (XMLTag) child;
}
}
}
}

For example, if we want to read RSS feed from Technè – Blog de Tecnologia do c.e.s.a.r – we use the following code:

String url = "http://techne.cesar.org.br/feed/?lang=en";
InputStream in = Connector.openInputStream(url);
XMLBinder binder = new XMLBinder(map);

rss = (RSS) binder.unmarshall(in);
Enumeration e = rss.channel.getChilds();
while (e.hasMoreElements()) {
Item i = (Item) e.nextElement();
// i.getAttributeValue("title")
// i.getAttributeValue("link")
}

Hope this helps.

No comments: