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.

Tuesday, November 9, 2010

Check for updates

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

Nowadays it is very common to have a "Check for updates" feature on mobile apps. Specially if they are Android or iPhone apps, because their platform already provide this. And what about Java ME apps? Here is a way.

First we need an official site and a public file in it. For example, http://mysite.com/myapp.txt. This file will have the application latest version number and the url from where it can be downloaded. The following lines will have the description of what have changed:

"[version] [url]

[description]"

Then we add an UI option for this feature, say, "Check for updates". When user selects this option the app downloads the file content with:


InputStream in = Connector.openInputStream("http://mysite.com/myapp.txt");
ByteArrayOutputStream out = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) >= 0) {
out.write(i);
}
String content = new String(out.toByteArray());

Current application version may be stored in a constant (final) attribute, but can be read dinamically with MIDlet.getAppProperty(“MIDlet-Version”). To trigger an update the versions need only to be different.

IMPORTANT: description must be used. This clarifies to the user the benefits of updating.

If the user selects to go on with the update the application only need to call
MIDlet.platformRequest(url).
This will open the handset browser to present the url.

As most users do not care about manually checking for new versions we might also add an automatic verifier. If the application is connected this verification can be done in between other connections. If the application is offline it can check how much time has passed since the last check.

Add the following auxiliar methods:


private long byteArrayToLong(byte [] buf) throws IOException {
ByteArrayInputStream in = new ByteArrayInputStream(buf);
DataInputStream dis = new DataInputStream(in);
return dis.readLong();
}
private byte[] longToByteArray(long value) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(out);
dos.writeLong(value);
return out.toByteArray();
}

And the following code on MIDlet.startApp():


RecordStore rs = RecordStore.openRecordStore("lastUpdateCheck", true);
long now = System.currentTimeMillis();
if (rs.getNumRecords() == 0) {
// first application execution
byte[] data = longToByteArray(now);
rs.addRecord(data, 0, data.length);
} else {
final long MONTH = 30*24*60*60*1000;
long before = byteArrayToLong(rs.getRecord(1));
if (now - before > MONTH) {
// show "Check for Update" Alert
// save current time
byte[] data = longToByteArray(now);
rs.setRecord(1, data, 0, data.length);
}
}
rs.closeRecordStore();

When developing an update remember to keep the same RecordStore names and how they are used. Users do not like to loose their configurations. To make sure that this will happen use the same MIDlet-Name and MIDlet-Vendor on jad file.

Combined with Crash Reports, continuous updates add credibility to the application and the development team.
Hope this helps.

Related topics:

Sunday, November 7, 2010

Adding Crash Report

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

Using only Twitter API for Java ME it is possible to add Crash Report to a Java ME application.

Crash Report is a set of information with details of an issue that made an application stop working.
Operating Systems like Microsoft Windows, Mac OS and Linux distros have this feature.
We browsers like Mozilla Firefox and Google Chrome also do. Android too since version 2.2 .

It is recommended that the application has its own Twitter account. This way all development team can follow application reports.

What should go inside a Crash Report?


  1. Application name and version: MIDlet-Name e Version

  2. Name and value of attributes and variables

  3. Exception that caused the issue

  4. Handset model running the app: microedition.platform



Below is a class that will handle all this info:

public class CrashReport {
private StringBuffer report = new StringBuffer();
private CrashReport (String content) {
report.append(content);
}
public CrashReport (MIDlet midlet, String details, Throwable e) {
if (midlet != null) {
report.append(midlet.getAppProperty("MIDlet-Name"));
report.append(' ');
report.append(midlet.getAppProperty("MIDlet-Version"));
report.append(' ');
}
if (details != null) {
report.append(details).append(' ');
}
if (e != null) {
report.append(e).append(' ');
}
String platform = System.getProperty("microedition.platform");
if (platform != null) {
report.append(platform);
}
}
public String toString () {
return report.toString();
}
public byte [] toByteArray () {
return toString().getBytes();
}
public static CrashReport fromByteArray (byte[] data) {
return new CrashReport(new String(data));
}
}


As soon as we get an unexpected exception we show a dialog stating that there was an error as asking if the details can be sent to the development team. If the user answers no, the report is discarded. If the answer is yes, the app tries to send the message right away.

Below is the class responsible to send the report:


public class CrashReportTwitterSender {
private TweetER tweetEr;
public CrashReportTwitterSender () throws IOException {
// check previous post on how to get a credential instance
UserAccountManager accountManager = UserAccountManager.getInstance(credential);
try {
if (accountManager.verifyCredential()) {
tweetEr = TweetER.getInstance(accountManager);
} else {
throw new IOException("Could not verify credential");
}
} catch (LimitExceededException ex) {
throw new IOException("caused by " + ex.getClass().getName());
}
}
public boolean sendReport (CrashReport report) {
try {
tweetEr.post(new Tweet(report.toString()));
return true;
} catch (Exception ex) {
return false;
}
}
}


If an error happens when the app is trying to send the report we ask the user to save it and send later.
If the user answers no, the report is discarded. If the answer is yes, the app saves the report for future usage.

Below is the class responsible to save CrashReport instances at RecordStore:


public class CrashReportStore {
private RecordStore reports;
public CrashReportStore () throws IOException {
try {
reports = RecordStore.openRecordStore("reports", true);
} catch (RecordStoreException ex) {
throw new IOException("caused by " + ex.getClass().getName());
}
}
public void addReport (CrashReport report) throws IOException {
byte [] data = report.toByteArray();
try {
reports.addRecord(data, 0, data.length);
} catch (RecordStoreException ex) {
throw new IOException("caused by " + ex);
}
}
public boolean hasReports () {
try {
return this.reports.getNumRecords() > 0;
} catch (RecordStoreNotOpenException ex) {
return false;
}
}
public void close() throws IOException {
try {
this.reports.closeRecordStore();
} catch (RecordStoreException ex) {
throw new IOException("caused by " + ex);
}
}
public void sendAllReports (CrashReportTwitterSender sender) throws IOException {
try {
RecordEnumeration re = this.reports.enumerateRecords(null, null, false);
while (re.hasNextElement()) {
sendReportAndDeleteRecord(sender, re.nextRecordId());
}
} catch (Exception ex) {
throw new IOException("caused by " + ex);
}
}
private void sendReportAndDeleteRecord (CrashReportTwitterSender sender, int id)
throws RecordStoreException
{
byte [] data = this.reports.getRecord(id);
CrashReport report = CrashReport.fromByteArray(data);
if (sender.sendReport(report)) {
this.reports.deleteRecord(id);
}
}
}


Next time the application is started it can check if CrashReportStore.hasReports().
If there is none we do not need to bother. If there is some, we ask the user to send the reports now or later.

Below is a sample code to test what we wrote here. It should be pasted at some class that extends MIDlet.


CrashReport report = new CrashReport(this, "crash report test", null);
CrashReportTwitterSender sender = new CrashReportTwitterSender();
if (sender.sendReport(report) == false) {
CrashReportStore store = new CrashReportStore();
store.addReport(report);
store.close();
}


Related topics: